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