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