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