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