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