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