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.token.*;
025import com.nimbusds.oauth2.sdk.util.CollectionUtils;
026import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
027import com.nimbusds.oauth2.sdk.util.StringUtils;
028import com.nimbusds.oauth2.sdk.util.URLUtils;
029import net.jcip.annotations.Immutable;
030
031import java.net.URI;
032import java.util.*;
033
034
035/**
036 * Token introspection request. Used by a protected resource to get the
037 * authorisation for a received access token. May also be used by clients to
038 * get the authorisation for a refresh token.
039 *
040 * <p>The caller may be required to authenticate itself with a
041 * {@link ClientAuthentication client authentication} method, such as
042 * {@link com.nimbusds.oauth2.sdk.auth.ClientSecretBasic client_secret_basic},
043 * or to present a dedicated {@link AccessToken access token}.
044 *
045 * <p>Example token introspection request, where the protected resource
046 * authenticates with a secret (the token type is also hinted):
047 *
048 * <pre>
049 * POST /introspect HTTP/1.1
050 * Host: server.example.com
051 * Accept: application/json
052 * Content-Type: application/x-www-form-urlencoded
053 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
054 *
055 * token=mF_9.B5f-4.1JqM&amp;token_type_hint=access_token
056 * </pre>
057 *
058 * <p>Example token introspection request, where the protected resource
059 * presents a bearer token:
060 *
061 * <pre>
062 * POST /introspect HTTP/1.1
063 * Host: server.example.com
064 * Accept: application/json
065 * Content-Type: application/x-www-form-urlencoded
066 * Authorization: Bearer 23410913-abewfq.123483
067 *
068 * token=2YotnFZFEjr1zCsicMWpAA
069 * </pre>
070 *
071 * <p>Related specifications:
072 *
073 * <ul>
074 *     <li>OAuth 2.0 Token Introspection (RFC 7662)
075 * </ul>
076 */
077@Immutable
078public class TokenIntrospectionRequest extends AbstractOptionallyAuthenticatedRequest {
079
080
081        /**
082         * The token to introspect.
083         */
084        private final Token token;
085
086
087        /**
088         * Optional access token to authorise the submitter.
089         */
090        private final AccessToken clientAuthz;
091
092
093        /**
094         * Optional additional parameters.
095         */
096        private final Map<String,List<String>> customParams;
097
098
099        /**
100         * Creates a new token introspection request.
101         *
102         * @param endpoint The URI of the token introspection endpoint. May be
103         *                 {@code null} if the {@link #toHTTPRequest} method is
104         *                 not going to be used.
105         * @param token    The access or refresh token to introspect. Must not
106         *                 be {@code null}.
107         */
108        public TokenIntrospectionRequest(final URI endpoint,
109                                         final Token token) {
110
111                this(endpoint, token, null);
112        }
113
114
115        /**
116         * Creates a new token introspection request.
117         *
118         * @param endpoint     The URI of the token introspection endpoint. May
119         *                     be {@code null} if the {@link #toHTTPRequest}
120         *                     method is not going to be used.
121         * @param token        The access or refresh token to introspect. Must
122         *                     not be {@code null}.
123         * @param customParams Optional custom parameters, {@code null} if
124         *                     none.
125         */
126        public TokenIntrospectionRequest(final URI endpoint,
127                                         final Token token,
128                                         final Map<String,List<String>> customParams) {
129
130                super(endpoint, (ClientAuthentication) null);
131                this.token = Objects.requireNonNull(token);
132                this.clientAuthz = null;
133                this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap();
134        }
135
136
137        /**
138         * Creates a new token introspection request, including a client
139         * authentication for the caller.
140         *
141         * @param endpoint   The URI of the token introspection endpoint. May
142         *                   be {@code null} if the {@link #toHTTPRequest}
143         *                   method is not going to be used.
144         * @param clientAuth The client authentication, {@code null} if none.
145         * @param token      The access or refresh token to introspect. Must
146         *                   not be {@code null}.
147         */
148        public TokenIntrospectionRequest(final URI endpoint,
149                                         final ClientAuthentication clientAuth,
150                                         final Token token) {
151
152                this(endpoint, clientAuth, token, null);
153        }
154
155
156        /**
157         * Creates a new token introspection request, including a client
158         * authentication for the caller.
159         *
160         * @param endpoint     The URI of the token introspection endpoint. May
161         *                     be {@code null} if the {@link #toHTTPRequest}
162         *                     method is not going to be used.
163         * @param clientAuth   The client authentication, {@code null} if none.
164         * @param token        The access or refresh token to introspect. Must
165         *                     not be {@code null}.
166         * @param customParams Optional custom parameters, {@code null} if
167         *                     none.
168         */
169        public TokenIntrospectionRequest(final URI endpoint,
170                                         final ClientAuthentication clientAuth,
171                                         final Token token,
172                                         final Map<String,List<String>> customParams) {
173
174                this(endpoint, clientAuth != null ? Collections.singletonList(clientAuth) : null, token, customParams);
175        }
176
177
178        /**
179         * Creates a new token introspection request, including client
180         * authentication candidates for the caller.
181         *
182         * @param endpoint             The URI of the token introspection
183         *                             endpoint. May be {@code null} if the
184         *                             {@link #toHTTPRequest} method is not
185         *                             going to be used.
186         * @param clientAuthCandidates The client authentication candidates.
187         *                             Must not be {@code null}.
188         * @param token                The access or refresh token to
189         *                             introspect. Must not be {@code null}.
190         * @param customParams         Optional custom parameters, {@code null}
191         *                             if none.
192         */
193        public TokenIntrospectionRequest(final URI endpoint,
194                                         final List<ClientAuthentication> clientAuthCandidates,
195                                         final Token token,
196                                         final Map<String,List<String>> customParams) {
197
198                super(endpoint, clientAuthCandidates);
199                this.token = Objects.requireNonNull(token);
200                this.clientAuthz = null;
201                this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap();
202        }
203
204
205        /**
206         * Creates a new token introspection request, including an access token
207         * to authorise the caller.
208         *
209         * @param endpoint    The URI of the token introspection endpoint. May
210         *                    be {@code null} if the {@link #toHTTPRequest}
211         *                    method is not going to be used.
212         * @param clientAuthz The client authorisation, {@code null} if none.
213         * @param token       The access or refresh token to introspect. Must
214         *                    not be {@code null}.
215         */
216        public TokenIntrospectionRequest(final URI endpoint,
217                                         final AccessToken clientAuthz,
218                                         final Token token) {
219
220                this(endpoint, clientAuthz, token, null);
221        }
222
223
224        /**
225         * Creates a new token introspection request, including an access token
226         * to authorise the caller.
227         *
228         * @param endpoint     The URI of the token introspection endpoint. May
229         *                     be {@code null} if the {@link #toHTTPRequest}
230         *                     method is not going to be used.
231         * @param clientAuthz  The client authorisation, {@code null} if none.
232         * @param token        The access or refresh token to introspect. Must
233         *                     not be {@code null}.
234         * @param customParams Optional custom parameters, {@code null} if
235         *                     none.
236         */
237        public TokenIntrospectionRequest(final URI endpoint,
238                                         final AccessToken clientAuthz,
239                                         final Token token,
240                                         final Map<String,List<String>> customParams) {
241
242                super(endpoint, (ClientAuthentication) null);
243                this.token = Objects.requireNonNull(token);
244                this.clientAuthz = clientAuthz;
245                this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap();
246        }
247
248
249        /**
250         * Returns the client authorisation.
251         *
252         * @return The client authorisation as an access token, {@code null} if
253         *         none.
254         */
255        public AccessToken getClientAuthorization() {
256
257                return clientAuthz;
258        }
259
260
261        /**
262         * Returns the token to introspect. The {@code instanceof} operator can
263         * be used to infer the token type. If it's neither
264         * {@link com.nimbusds.oauth2.sdk.token.AccessToken} nor
265         * {@link com.nimbusds.oauth2.sdk.token.RefreshToken} the
266         * {@code token_type_hint} has not been provided as part of the token
267         * revocation request.
268         *
269         * @return The token.
270         */
271        public Token getToken() {
272
273                return token;
274        }
275
276
277        /**
278         * Returns the custom request parameters.
279         *
280         * @return The custom request parameters, empty map if none.
281         */
282        public Map<String,List<String>> getCustomParameters() {
283
284                return customParams;
285        }
286        
287
288        @Override
289        public HTTPRequest toHTTPRequest() {
290
291                if (getEndpointURI() == null)
292                        throw new SerializeException("The endpoint URI is not specified");
293
294                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
295                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
296
297                Map<String,List<String>> params = new HashMap<>();
298                params.put("token", Collections.singletonList(token.getValue()));
299
300                if (token instanceof AccessToken) {
301                        params.put("token_type_hint", Collections.singletonList("access_token"));
302                } else if (token instanceof RefreshToken) {
303                        params.put("token_type_hint", Collections.singletonList("refresh_token"));
304                }
305
306                params.putAll(customParams);
307
308                httpRequest.setBody(URLUtils.serializeParameters(params));
309
310                if (getClientAuthentication() != null) {
311                        for (ClientAuthentication ca: getClientAuthenticationCandidates()) {
312                                ca.applyTo(httpRequest);
313                        }
314                }
315
316                if (clientAuthz != null) {
317                        httpRequest.setAuthorization(clientAuthz.toAuthorizationHeader());
318                }
319
320                return httpRequest;
321        }
322
323
324        /**
325         * Parses a token introspection request from the specified HTTP
326         * request.
327         *
328         * @param httpRequest The HTTP request. Must not be {@code null}.
329         *
330         * @return The token introspection request.
331         *
332         * @throws ParseException If the HTTP request couldn't be parsed to a
333         *                        token introspection request.
334         */
335        public static TokenIntrospectionRequest parse(final HTTPRequest httpRequest)
336                throws ParseException {
337
338                // Only HTTP POST accepted
339                httpRequest.ensureMethod(HTTPRequest.Method.POST);
340                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
341
342                Map<String, List<String>> params = httpRequest.getBodyAsFormParameters();
343
344                final String tokenValue = MultivaluedMapUtils.removeAndReturnFirstValue(params, "token");
345
346                if (StringUtils.isBlank(tokenValue)) {
347                        throw new ParseException("Missing required token parameter");
348                }
349
350                // Detect the token type
351                final String tokenTypeHint = MultivaluedMapUtils.removeAndReturnFirstValue(params, "token_type_hint");
352
353                Token token;
354                if ("access_token".equals(tokenTypeHint)) {
355                        token = new TypelessAccessToken(tokenValue);
356                } else if ("refresh_token".equals(tokenTypeHint)) {
357                        token = new RefreshToken(tokenValue);
358                } else {
359                        // Unknown or missing token_type_hint
360                        //
361                        // https://datatracker.ietf.org/doc/html/rfc7662#section-2.1
362                        //    token_type_hint
363                        //      OPTIONAL.  ... An authorization server MAY
364                        //      ignore this parameter.
365                        token = new TypelessToken(tokenValue);
366                }
367
368                // Important: auth methods mutually exclusive!
369
370                // Parse client authentication candidates, if any
371                List<ClientAuthentication> clientAuthCandidates;
372                try {
373                        clientAuthCandidates = ClientAuthentication.parseCandidates(httpRequest);
374                } catch (ParseException e) {
375                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
376                }
377
378                if (CollectionUtils.isNotEmpty(clientAuthCandidates)) {
379                        for (ClientAuthentication ca: clientAuthCandidates) {
380                                for (String formParam: ca.getFormParameterNames()) {
381                                        MultivaluedMapUtils.removeAndReturnFirstValue(params, formParam);
382                                }
383                        }
384                }
385
386                // Parse optional client authz (token)
387                AccessToken clientAuthz = null;
388
389                if (clientAuthCandidates == null && httpRequest.getAuthorization() != null) {
390                        clientAuthz = AccessToken.parse(httpRequest.getAuthorization(), AccessTokenType.BEARER);
391                }
392
393                URI uri = httpRequest.getURI();
394
395                if (clientAuthz != null) {
396                        return new TokenIntrospectionRequest(uri, clientAuthz, token, params);
397                } else {
398                        return new TokenIntrospectionRequest(uri, clientAuthCandidates, token, params);
399                }
400        }
401}