001package com.nimbusds.openid.connect.sdk.op; 002 003 004import java.io.IOException; 005import java.net.MalformedURLException; 006import java.net.URL; 007import java.util.Collections; 008import java.util.HashMap; 009import java.util.Map; 010 011import net.jcip.annotations.ThreadSafe; 012 013import net.minidev.json.JSONObject; 014 015import com.nimbusds.jose.JOSEException; 016import com.nimbusds.jwt.JWT; 017import com.nimbusds.jwt.JWTParser; 018import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; 019 020import com.nimbusds.oauth2.sdk.ErrorObject; 021import com.nimbusds.oauth2.sdk.ParseException; 022import com.nimbusds.oauth2.sdk.SerializeException; 023 024import com.nimbusds.openid.connect.sdk.AuthenticationRequest; 025import com.nimbusds.openid.connect.sdk.OIDCError; 026import com.nimbusds.openid.connect.sdk.util.JWTDecoder; 027import com.nimbusds.openid.connect.sdk.util.Resource; 028import com.nimbusds.openid.connect.sdk.util.ResourceRetriever; 029 030 031/** 032 * Resolves the final OpenID Connect authentication request by superseding its 033 * parameters with those found in the optional OpenID Connect request object. 034 * The request object is encoded as a JSON Web Token (JWT) and can be specified 035 * directly (inline) using the {@code request} parameter, or by URL using the 036 * {@code request_uri} parameter. 037 * 038 * <p>To process signed (JWS) and optionally encrypted (JWE) request object 039 * JWTs a {@link com.nimbusds.openid.connect.sdk.util.JWTDecoder JWT decoder} 040 * for the expected JWS / JWE algorithms must be provided at construction time. 041 * 042 * <p>To fetch OpenID Connect request objects specified by URL a 043 * {@link com.nimbusds.openid.connect.sdk.util.ResourceRetriever JWT retriever} 044 * must be provided, otherwise only inlined request objects can be processed. 045 * 046 * <p>Related specifications: 047 * 048 * <ul> 049 * <li>OpenID Connect Core 1.0, section 6. 050 * </ul> 051 */ 052@ThreadSafe 053public class AuthenticationRequestResolver { 054 055 056 /** 057 * The JWT decoder. 058 */ 059 private final JWTDecoder jwtDecoder; 060 061 062 /** 063 * Optional retriever for JWTs passed by URL. 064 */ 065 private final ResourceRetriever jwtRetriever; 066 067 068 /** 069 * Creates a new minimal OpenID Connect authentication request 070 * resolver. It will not process OpenID Connect request objects and 071 * will throw a {@link ResolveException} if the authentication request 072 * includes a {@code request} or {@code request_uri} parameter. 073 */ 074 public AuthenticationRequestResolver() { 075 076 jwtDecoder = null; 077 jwtRetriever = null; 078 } 079 080 081 /** 082 * Creates a new OpenID Connect authentication request resolver that 083 * supports OpenID Connect request objects passed by value (using the 084 * authentication {@code request} parameter). It will throw a 085 * {@link ResolveException} if the authentication request includes a 086 * {@code request_uri} parameter. 087 * 088 * @param jwtDecoder A configured JWT decoder providing JWS validation 089 * and optional JWE decryption of the request 090 * objects. Must not be {@code null}. 091 */ 092 public AuthenticationRequestResolver(final JWTDecoder jwtDecoder) { 093 094 if (jwtDecoder == null) 095 throw new IllegalArgumentException("The JWT decoder must not be null"); 096 097 this.jwtDecoder = jwtDecoder; 098 099 jwtRetriever = null; 100 } 101 102 103 /** 104 * Creates a new OpenID Connect request object resolver that supports 105 * OpenID Connect request objects passed by value (using the 106 * authentication {@code request} parameter) or by reference (using the 107 * authentication {@code request_uri} parameter). 108 * 109 * @param jwtDecoder A configured JWT decoder providing JWS 110 * validation and optional JWE decryption of the 111 * request objects. Must not be {@code null}. 112 * @param jwtRetriever A configured JWT retriever for OpenID Connect 113 * request objects passed by URI. Must not be 114 * {@code null}. 115 */ 116 public AuthenticationRequestResolver(final JWTDecoder jwtDecoder, 117 final ResourceRetriever jwtRetriever) { 118 119 if (jwtDecoder == null) 120 throw new IllegalArgumentException("The JWT decoder must not be null"); 121 122 this.jwtDecoder = jwtDecoder; 123 124 125 if (jwtRetriever == null) 126 throw new IllegalArgumentException("The JWT retriever must not be null"); 127 128 this.jwtRetriever = jwtRetriever; 129 } 130 131 132 /** 133 * Gets the JWT decoder. 134 * 135 * @return The JWT decoder, {@code null} if not specified. 136 */ 137 public JWTDecoder getJWTDecoder() { 138 139 return jwtDecoder; 140 } 141 142 143 /** 144 * Gets the JWT retriever. 145 * 146 * @return The JWT retriever, {@code null} if not specified. 147 */ 148 public ResourceRetriever getJWTRetriever() { 149 150 return jwtRetriever; 151 } 152 153 154 /** 155 * Retrieves a JWT from the specified URL. The content type of the URL 156 * resource is not checked. 157 * 158 * @param url The URL of the JWT. Must not be {@code null}. 159 * 160 * @return The retrieved JWT. 161 * 162 * @throws ResolveException If no JWT retriever is configured, if the 163 * resource couldn't be retrieved, or parsed 164 * to a JWT. 165 */ 166 private JWT retrieveRequestObject(final URL url) 167 throws ResolveException { 168 169 if (jwtRetriever == null) { 170 171 throw new ResolveException("OpenID Connect request object cannot be resolved: No JWT retriever is configured"); 172 } 173 174 Resource resource; 175 176 try { 177 resource = jwtRetriever.retrieveResource(url); 178 179 } catch (IOException e) { 180 181 throw new ResolveException("Couldn't retrieve OpenID Connect request object: " + e.getMessage(), e); 182 } 183 184 try { 185 return JWTParser.parse(resource.getContent()); 186 187 } catch (java.text.ParseException e) { 188 189 throw new ResolveException("Couldn't parse OpenID Connect request object: " + e.getMessage(), e); 190 } 191 } 192 193 194 /** 195 * Decodes the specified OpenID Connect request object, and if it's 196 * secured performs additional JWS signature validation and JWE 197 * decryption. 198 * 199 * @param requestObject The OpenID Connect request object to decode. 200 * Must not be {@code null}. 201 * 202 * @return The extracted JWT claims of the OpenID Connect request 203 * object. 204 * 205 * @throws ResolveException If no JWT decoder is configured, if JWT 206 * decoding, JWS validation or JWE decryption 207 * failed. 208 */ 209 private ReadOnlyJWTClaimsSet decodeRequestObject(final JWT requestObject) 210 throws ResolveException { 211 212 if (jwtDecoder == null) { 213 214 throw new ResolveException("OpenID Connect request object cannot be decoded: No JWT decoder is configured"); 215 } 216 217 try { 218 return jwtDecoder.decodeJWT(requestObject); 219 220 } catch (JOSEException e) { 221 222 throw new ResolveException("Couldn't decode OpenID Connect request object JWT: " + e.getMessage(), e); 223 224 } catch (java.text.ParseException e) { 225 226 throw new ResolveException("Couldn't parse OpenID Connect request object JWT claims: " + e.getMessage(), e); 227 } 228 } 229 230 231 /** 232 * Reformats the specified JWT claims set to a 233 * {@literal java.util.Map&<String,String>} instance. 234 * 235 * @param claimsSet The JWT claims set to reformat. Must not be 236 * {@code null}. 237 * 238 * @return The JWT claims set as an unmodifiable map of string keys / 239 * string values. 240 * 241 * @throws ResolveException If reformatting of the JWT claims set 242 * failed. 243 */ 244 public static Map<String,String> reformatClaims(final ReadOnlyJWTClaimsSet claimsSet) 245 throws ResolveException { 246 247 Map<String,Object> claims = claimsSet.getAllClaims(); 248 249 // Reformat all claim values as strings 250 Map<String,String> reformattedClaims = new HashMap<>(); 251 252 for (Map.Entry<String,Object> entry: claims.entrySet()) { 253 254 Object value = entry.getValue(); 255 256 if (value instanceof String) { 257 258 reformattedClaims.put(entry.getKey(), (String)value); 259 260 } else if (value instanceof Boolean) { 261 262 Boolean bool = (Boolean)value; 263 reformattedClaims.put(entry.getKey(), bool.toString()); 264 265 } else if (value instanceof Number) { 266 267 Number number = (Number)value; 268 reformattedClaims.put(entry.getKey(), number.toString()); 269 270 } else if (value instanceof JSONObject) { 271 272 JSONObject jsonObject = (JSONObject)value; 273 reformattedClaims.put(entry.getKey(), jsonObject.toString()); 274 275 } else { 276 277 throw new ResolveException("Couldn't process JWT claim \"" + entry.getKey() + "\": Unsupported type"); 278 } 279 } 280 281 return Collections.unmodifiableMap(reformattedClaims); 282 } 283 284 285 /** 286 * Resolves the specified OpenID Connect authentication request by 287 * superseding its parameters with those found in the optional OpenID 288 * Connect request object (if any). 289 * 290 * @param request The OpenID Connect authentication request. Must not be 291 * {@code null}. 292 * 293 * @return The resolved authentication request, or the original 294 * unmodified request if no OpenID Connect request object was 295 * specified. 296 * 297 * @throws ResolveException If the request couldn't be resolved. 298 */ 299 public AuthenticationRequest resolve(final AuthenticationRequest request) 300 throws ResolveException { 301 302 if (! request.specifiesRequestObject()) { 303 // Return the same request 304 return request; 305 } 306 307 try { 308 JWT jwt; 309 310 if (request.getRequestURI() != null) { 311 312 // Download request object 313 URL requestURL; 314 315 try { 316 requestURL = request.getRequestURI().toURL(); 317 318 } catch (MalformedURLException e) { 319 320 throw new ResolveException(e.getMessage(), e); 321 } 322 323 jwt = retrieveRequestObject(requestURL); 324 } else { 325 // Request object inlined 326 jwt = request.getRequestObject(); 327 } 328 329 ReadOnlyJWTClaimsSet jwtClaims = decodeRequestObject(jwt); 330 331 Map<String, String> requestObjectParams = reformatClaims(jwtClaims); 332 333 Map<String, String> finalParams = new HashMap<>(); 334 335 try { 336 finalParams.putAll(request.toParameters()); 337 338 } catch (SerializeException e) { 339 340 throw new ResolveException("Couldn't resolve final OpenID Connect authentication request: " + e.getMessage(), e); 341 } 342 343 // Merge params from request object 344 finalParams.putAll(requestObjectParams); 345 346 347 // Parse again 348 AuthenticationRequest finalAuthRequest; 349 350 try { 351 finalAuthRequest = AuthenticationRequest.parse(request.getEndpointURI(), finalParams); 352 353 } catch (ParseException e) { 354 355 throw new ResolveException("Couldn't create final OpenID Connect authentication request: " + e.getMessage(), e); 356 } 357 358 return new AuthenticationRequest( 359 finalAuthRequest.getEndpointURI(), 360 finalAuthRequest.getResponseType(), 361 finalAuthRequest.getScope(), 362 finalAuthRequest.getClientID(), 363 finalAuthRequest.getRedirectionURI(), 364 finalAuthRequest.getState(), 365 finalAuthRequest.getNonce(), 366 finalAuthRequest.getDisplay(), 367 finalAuthRequest.getPrompt(), 368 finalAuthRequest.getMaxAge(), 369 finalAuthRequest.getUILocales(), 370 finalAuthRequest.getClaimsLocales(), 371 finalAuthRequest.getIDTokenHint(), 372 finalAuthRequest.getLoginHint(), 373 finalAuthRequest.getACRValues(), 374 finalAuthRequest.getClaims(), 375 null, // request object 376 null); // request URI 377 378 } catch (ResolveException e) { 379 380 // Repackage exception with redirection URI, state, error object 381 382 ErrorObject err; 383 384 if (request.getRequestURI() != null) 385 err = OIDCError.INVALID_REQUEST_URI; 386 else 387 err = OIDCError.INVALID_REQUEST_OBJECT; 388 389 throw new ResolveException( 390 e.getMessage(), 391 err, 392 request.getClientID(), 393 request.getRedirectionURI(), 394 request.getState(), 395 e.getCause()); 396 } 397 } 398}