001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk;
019
020
021import com.nimbusds.common.contenttype.ContentType;
022import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
023import com.nimbusds.oauth2.sdk.http.HTTPRequest;
024import com.nimbusds.oauth2.sdk.id.ClientID;
025import com.nimbusds.oauth2.sdk.token.*;
026import com.nimbusds.oauth2.sdk.util.CollectionUtils;
027import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
028import com.nimbusds.oauth2.sdk.util.StringUtils;
029import com.nimbusds.oauth2.sdk.util.URLUtils;
030import com.nimbusds.openid.connect.sdk.nativesso.DeviceSecret;
031import com.nimbusds.openid.connect.sdk.nativesso.DeviceSecretToken;
032import net.jcip.annotations.Immutable;
033
034import java.net.URI;
035import java.util.*;
036
037
038/**
039 * Token revocation request. Used to revoke an issued access token, refresh
040 * token or device secret.
041 *
042 * <p>Example token revocation request for a confidential client:
043 *
044 * <pre>
045 * POST /revoke HTTP/1.1
046 * Host: server.example.com
047 * Content-Type: application/x-www-form-urlencoded
048 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
049 *
050 * token=45ghiukldjahdnhzdauz&amp;token_type_hint=refresh_token
051 * </pre>
052 *
053 * <p>Example token revocation request for a public client:
054 *
055 * <pre>
056 * POST /revoke HTTP/1.1
057 * Host: server.example.com
058 * Content-Type: application/x-www-form-urlencoded
059 *
060 * token=45ghiukldjahdnhzdauz&amp;token_type_hint=refresh_token&amp;client_id=123456
061 * </pre>
062 *
063 * <p>Related specifications:
064 *
065 * <ul>
066 *     <li>OAuth 2.0 Token Revocation (RFC 7009)
067 * </ul>
068 */
069@Immutable
070public final class TokenRevocationRequest extends AbstractOptionallyIdentifiedRequest {
071
072
073        /**
074         * The token to revoke.
075         */
076        private final Token token;
077
078
079        /**
080         * Optional additional parameters.
081         */
082        private final Map<String,List<String>> customParams;
083
084
085        /**
086         * Creates a new token revocation request for a confidential client.
087         *
088         * @param endpoint   The URI of the token revocation endpoint. May be
089         *                   {@code null} if the {@link #toHTTPRequest} method
090         *                   is not going to be used.
091         * @param clientAuth The client authentication. Must not be
092         *                   {@code null}.
093         * @param token      The access token, refresh token or device secret
094         *                   to revoke. Must not be {@code null}.
095         */
096        public TokenRevocationRequest(final URI endpoint,
097                                      final ClientAuthentication clientAuth,
098                                      final Token token) {
099
100                this(endpoint, Collections.singletonList(Objects.requireNonNull(clientAuth)), token);
101        }
102
103
104        /**
105         * Creates a new token revocation request for a confidential client.
106         *
107         * @param endpoint             The URI of the token revocation
108         *                             endpoint. May be {@code null} if the
109         *                             {@link #toHTTPRequest} method is not
110         *                             going to be used.
111         * @param clientAuthCandidates The client authentication candidates.
112         *                             Must not be empty.
113         * @param token                The access token, refresh token or
114         *                             device secret to revoke. Must not be
115         *                             {@code null}.
116         */
117        public TokenRevocationRequest(final URI endpoint,
118                                      final List<ClientAuthentication> clientAuthCandidates,
119                                      final Token token) {
120
121                this(endpoint, clientAuthCandidates, token, null);
122        }
123
124
125        /**
126         * Creates a new token revocation request for a confidential client.
127         *
128         * @param endpoint             The URI of the token revocation
129         *                             endpoint. May be {@code null} if the
130         *                             {@link #toHTTPRequest} method is not
131         *                             going to be used.
132         * @param clientAuthCandidates The client authentication candidates.
133         *                             Must not be empty.
134         * @param token                The access token, refresh token or
135         *                             device secret to revoke. Must not be
136         *                             {@code null}.
137         * @param customParams         Optional custom parameters, {@code null}
138         *                             if none.
139         */
140        public TokenRevocationRequest(final URI endpoint,
141                                      final List<ClientAuthentication> clientAuthCandidates,
142                                      final Token token,
143                                      final Map<String,List<String>> customParams) {
144
145                super(endpoint, clientAuthCandidates);
146                if (clientAuthCandidates.isEmpty()) {
147                        throw new IllegalArgumentException("The client authentication candidates must not be empty");
148                }
149                this.token = Objects.requireNonNull(token);
150                this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap();
151        }
152
153
154        /**
155         * Creates a new token revocation request for a public client.
156         *
157         * @param endpoint The URI of the token revocation endpoint. May be
158         *                 {@code null} if the {@link #toHTTPRequest} method
159         *                 is not going to be used.
160         * @param clientID The client ID. Must not be {@code null}.
161         * @param token    The access token, refresh token or device secret to
162         *                 revoke. Must not be {@code null}.
163         */
164        public TokenRevocationRequest(final URI endpoint,
165                                      final ClientID clientID,
166                                      final Token token) {
167
168                this(endpoint, clientID, token, null);
169        }
170
171
172        /**
173         * Creates a new token revocation request for a public client.
174         *
175         * @param endpoint     The URI of the token revocation endpoint. May be
176         *                     {@code null} if the {@link #toHTTPRequest}
177         *                     method is not going to be used.
178         * @param clientID     The client ID. Must not be {@code null}.
179         * @param token        The access token, refresh token or device secret
180         *                     to revoke. Must not be {@code null}.
181         * @param customParams Optional custom parameters, {@code null} if
182         *                     none.
183         */
184        public TokenRevocationRequest(final URI endpoint,
185                                      final ClientID clientID,
186                                      final Token token,
187                                      final Map<String,List<String>> customParams) {
188
189                super(endpoint, Objects.requireNonNull(clientID));
190                this.token = Objects.requireNonNull(token);
191                this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap();
192        }
193
194
195        /**
196         * Returns the token to revoke. The {@code instanceof} operator can be
197         * used to infer the token type. If it's neither
198         * {@link com.nimbusds.oauth2.sdk.token.AccessToken} nor
199         * {@link com.nimbusds.oauth2.sdk.token.RefreshToken} or
200         * {@link com.nimbusds.openid.connect.sdk.nativesso.DeviceSecretToken}
201         * the {@code token_type_hint} has not been provided as part of the
202         * token revocation request.
203         *
204         * @return The token.
205         */
206        public Token getToken() {
207
208                return token;
209        }
210
211
212        /**
213         * Returns the custom request parameters.
214         *
215         * @return The custom request parameters, empty map if none.
216         */
217        public Map<String,List<String>> getCustomParameters() {
218
219                return customParams;
220        }
221
222
223        @Override
224        public HTTPRequest toHTTPRequest() {
225
226                if (getEndpointURI() == null)
227                        throw new SerializeException("The endpoint URI is not specified");
228
229                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
230                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
231
232                Map<String,List<String>> params = new HashMap<>();
233
234                if (getClientID() != null) {
235                        // public client
236                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
237                }
238
239                params.put("token", Collections.singletonList(token.getValue()));
240
241                if (token instanceof AccessToken) {
242                        params.put("token_type_hint", Collections.singletonList("access_token"));
243                } else if (token instanceof RefreshToken) {
244                        params.put("token_type_hint", Collections.singletonList("refresh_token"));
245                } else if (token instanceof DeviceSecretToken) {
246                        params.put("token_type_hint", Collections.singletonList("device_secret"));
247                }
248
249                params.putAll(customParams);
250
251                httpRequest.setBody(URLUtils.serializeParameters(params));
252
253                if (getClientAuthentication() != null) {
254                        // confidential client
255                        for (ClientAuthentication ca: getClientAuthenticationCandidates()) {
256                                ca.applyTo(httpRequest);
257                        }
258                }
259
260                return httpRequest;
261        }
262
263
264        /**
265         * Parses a token revocation request from the specified HTTP request.
266         *
267         * @param httpRequest The HTTP request. Must not be {@code null}.
268         *
269         * @return The token revocation request.
270         *
271         * @throws ParseException If the HTTP request couldn't be parsed to a
272         *                        token revocation request.
273         */
274        public static TokenRevocationRequest parse(final HTTPRequest httpRequest)
275                throws ParseException {
276
277                // Only HTTP POST accepted
278                httpRequest.ensureMethod(HTTPRequest.Method.POST);
279                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
280
281                Map<String,List<String>> params = httpRequest.getBodyAsFormParameters();
282
283                final String tokenValue = MultivaluedMapUtils.removeAndReturnFirstValue(params,"token");
284
285                if (StringUtils.isBlank(tokenValue)) {
286                        throw new ParseException("Missing required token parameter");
287                }
288
289                // Detect the token type
290                final String tokenTypeHint = MultivaluedMapUtils.removeAndReturnFirstValue(params,"token_type_hint");
291
292                Token token;
293                if ("access_token".equals(tokenTypeHint)) {
294                        token = new TypelessAccessToken(tokenValue);
295                } else if ("refresh_token".equals(tokenTypeHint)) {
296                        token = new RefreshToken(tokenValue);
297                } else if ("device_secret".equals(tokenTypeHint)) {
298                        token = new DeviceSecretToken(new DeviceSecret(tokenValue));
299                } else {
300                        // Any token
301                        token = new TypelessToken(tokenValue);
302                }
303
304                URI uri = httpRequest.getURI();
305
306                // Parse client authentication candidates, if any
307                List<ClientAuthentication> clientAuthCandidates;
308                try {
309                        clientAuthCandidates = ClientAuthentication.parseCandidates(httpRequest);
310                } catch (ParseException e) {
311                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
312                }
313
314                if (CollectionUtils.isNotEmpty(clientAuthCandidates)) {
315
316                        for (ClientAuthentication ca: clientAuthCandidates) {
317                                for (String formParam: ca.getFormParameterNames()) {
318                                        MultivaluedMapUtils.removeAndReturnFirstValue(params, formParam);
319                                }
320                        }
321
322                        return new TokenRevocationRequest(uri, clientAuthCandidates, token, params);
323                }
324
325                // Public client
326                final String clientIDString = MultivaluedMapUtils.removeAndReturnFirstValue(params, "client_id");
327
328                if (StringUtils.isBlank(clientIDString)) {
329                        throw new ParseException("No client authentication or client_id parameter found");
330                }
331
332                return new TokenRevocationRequest(uri, new ClientID(clientIDString), token, params);
333        }
334}