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