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