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&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}