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