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