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