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