001package com.nimbusds.openid.connect.sdk.op; 002 003 004import java.io.IOException; 005import java.net.MalformedURLException; 006import java.util.Collections; 007import java.util.HashMap; 008import java.util.Map; 009 010import com.nimbusds.jose.JOSEException; 011import com.nimbusds.jose.proc.BadJOSEException; 012import com.nimbusds.jose.proc.SecurityContext; 013import com.nimbusds.jwt.JWT; 014import com.nimbusds.jwt.JWTClaimsSet; 015import com.nimbusds.jwt.JWTParser; 016import com.nimbusds.jwt.proc.JWTProcessor; 017import com.nimbusds.oauth2.sdk.ParseException; 018import com.nimbusds.oauth2.sdk.http.ResourceRetriever; 019import com.nimbusds.openid.connect.sdk.AuthenticationRequest; 020import com.nimbusds.openid.connect.sdk.OIDCError; 021import net.jcip.annotations.ThreadSafe; 022 023 024/** 025 * Resolves the final OpenID Connect authentication request by superseding its 026 * parameters with those found in the optional OpenID Connect request object. 027 * The request object is encoded as a JSON Web Token (JWT) and can be specified 028 * directly (inline) using the {@code request} parameter, or by URL using the 029 * {@code request_uri} parameter. 030 * 031 * <p>To process signed and optionally encrypted request objects a 032 * {@link JWTProcessor JWT processor} for the expected JWS / JWE algorithms 033 * must be provided at construction time. 034 * 035 * <p>To fetch OpenID Connect request objects specified by URL a 036 * {@link ResourceRetriever JWT retriever} must be provided, otherwise only 037 * inlined request objects can be processed. 038 * 039 * <p>Related specifications: 040 * 041 * <ul> 042 * <li>OpenID Connect Core 1.0, section 6. 043 * </ul> 044 */ 045@ThreadSafe 046public class AuthenticationRequestResolver<C extends SecurityContext> { 047 048 049 /** 050 * The JWT processor. 051 */ 052 private final JWTProcessor<C> jwtProcessor; 053 054 055 /** 056 * Optional retriever for request objects passed by URL. 057 */ 058 private final ResourceRetriever jwtRetriever; 059 060 061 /** 062 * Creates a new minimal OpenID Connect authentication request 063 * resolver. It will not process OpenID Connect request objects and 064 * will throw a {@link ResolveException} if the authentication request 065 * includes a {@code request} or {@code request_uri} parameter. 066 */ 067 public AuthenticationRequestResolver() { 068 jwtProcessor = null; 069 jwtRetriever = null; 070 } 071 072 073 /** 074 * Creates a new OpenID Connect authentication request resolver that 075 * supports OpenID Connect request objects passed by value (using the 076 * authentication {@code request} parameter). It will throw a 077 * {@link ResolveException} if the authentication request includes a 078 * {@code request_uri} parameter. 079 * 080 * @param jwtProcessor A configured JWT processor providing JWS 081 * validation and optional JWE decryption of the 082 * request objects. Must not be {@code null}. 083 */ 084 public AuthenticationRequestResolver(final JWTProcessor<C> jwtProcessor) { 085 if (jwtProcessor == null) 086 throw new IllegalArgumentException("The JWT processor must not be null"); 087 this.jwtProcessor = jwtProcessor; 088 jwtRetriever = null; 089 } 090 091 092 /** 093 * Creates a new OpenID Connect request object resolver that supports 094 * OpenID Connect request objects passed by value (using the 095 * authentication {@code request} parameter) or by reference (using the 096 * authentication {@code request_uri} parameter). 097 * 098 * @param jwtProcessor A configured JWT processor providing JWS 099 * validation and optional JWE decryption of the 100 * request objects. Must not be {@code null}. 101 * @param jwtRetriever A configured JWT retriever for OpenID Connect 102 * request objects passed by URI. Must not be 103 * {@code null}. 104 */ 105 public AuthenticationRequestResolver(final JWTProcessor<C> jwtProcessor, 106 final ResourceRetriever jwtRetriever) { 107 if (jwtProcessor == null) 108 throw new IllegalArgumentException("The JWT processor must not be null"); 109 this.jwtProcessor = jwtProcessor; 110 111 if (jwtRetriever == null) 112 throw new IllegalArgumentException("The JWT retriever must not be null"); 113 this.jwtRetriever = jwtRetriever; 114 } 115 116 117 /** 118 * Returns the JWT processor. 119 * 120 * @return The JWT processor, {@code null} if not specified. 121 */ 122 public JWTProcessor<C> getJWTProcessor() { 123 124 return jwtProcessor; 125 } 126 127 128 /** 129 * Returns the JWT retriever. 130 * 131 * @return The JWT retriever, {@code null} if not specified. 132 */ 133 public ResourceRetriever getJWTRetriever() { 134 135 return jwtRetriever; 136 } 137 138 139 /** 140 * Reformats the specified JWT claims set to a 141 * {@literal java.util.Map&<String,String>} instance. 142 * 143 * @param claimsSet The JWT claims set to reformat. Must not be 144 * {@code null}. 145 * 146 * @return The JWT claims set as an unmodifiable map of string keys / 147 * string values. 148 */ 149 public static Map<String,String> reformatClaims(final JWTClaimsSet claimsSet) { 150 151 Map<String,Object> claims = claimsSet.getClaims(); 152 153 // Reformat all claim values as strings 154 Map<String,String> reformattedClaims = new HashMap<>(); 155 156 for (Map.Entry<String,Object> entry: claims.entrySet()) { 157 158 if (entry.getValue() == null) { 159 continue; // skip 160 } 161 162 reformattedClaims.put(entry.getKey(), entry.getValue().toString()); 163 } 164 165 return Collections.unmodifiableMap(reformattedClaims); 166 } 167 168 169 /** 170 * Resolves the specified OpenID Connect authentication request by 171 * superseding its parameters with those found in the optional OpenID 172 * Connect request object (if any). 173 * 174 * @param request The OpenID Connect authentication request. 175 * Must not be {@code null}. 176 * @param securityContext Optional security context to pass to the JWT 177 * processor, {@code null} if not specified. 178 * 179 * @return The resolved authentication request, or the original 180 * unmodified request if no OpenID Connect request object was 181 * specified. 182 * 183 * @throws ResolveException If the request couldn't be resolved. 184 */ 185 public AuthenticationRequest resolve(final AuthenticationRequest request, 186 final C securityContext) 187 throws ResolveException, JOSEException { 188 189 if (! request.specifiesRequestObject()) { 190 // Return unmodified 191 return request; 192 } 193 194 final JWT jwt; 195 196 if (request.getRequestURI() != null) { 197 198 // Check if request_uri is supported 199 if (jwtRetriever == null || jwtProcessor == null) { 200 throw new ResolveException(OIDCError.REQUEST_URI_NOT_SUPPORTED, request); 201 } 202 203 // Download request object 204 try { 205 jwt = JWTParser.parse(jwtRetriever.retrieveResource(request.getRequestURI().toURL()).getContent()); 206 } catch (MalformedURLException e) { 207 throw new ResolveException(OIDCError.INVALID_REQUEST_URI.setDescription("Malformed URL"), request); 208 } catch (IOException e) { 209 // Most likely client problem, possible causes: bad URL, timeout, network down 210 throw new ResolveException("Couldn't retrieve request_uri: " + e.getMessage(), 211 "Network error, check the request_uri", // error_description for client, hide details 212 request, e); 213 } catch (java.text.ParseException e) { 214 throw new ResolveException(OIDCError.INVALID_REQUEST_URI.setDescription("Invalid JWT"), request); 215 } 216 217 } else { 218 // Check if request by value is supported 219 if (jwtProcessor == null) { 220 throw new ResolveException(OIDCError.REQUEST_NOT_SUPPORTED, request); 221 } 222 223 // Request object inlined 224 jwt = request.getRequestObject(); 225 } 226 227 final JWTClaimsSet jwtClaims; 228 229 try { 230 jwtClaims = jwtProcessor.process(jwt, securityContext); 231 } catch (BadJOSEException e) { 232 throw new ResolveException("Invalid request object: " + e.getMessage(), 233 "Bad JWT / signature / HMAC / encryption", // error_description for client, hide details 234 request, e); 235 } 236 237 Map<String,String> finalParams = new HashMap<>(); 238 finalParams.putAll(request.toParameters()); 239 finalParams.putAll(reformatClaims(jwtClaims)); // Merge params from request object 240 finalParams.remove("request"); // make sure request object is deleted 241 finalParams.remove("request_uri"); // make sure request_uri is deleted 242 243 // Create new updated OpenID auth request 244 try { 245 return AuthenticationRequest.parse(request.getEndpointURI(), finalParams); 246 } catch (ParseException e) { 247 // E.g. missing OIDC required redirect_uri 248 throw new ResolveException("Couldn't create final OpenID authentication request: " + e.getMessage(), 249 "Invalid request object parameter(s): " + e.getMessage(), // error_description for client 250 request, e); 251 } 252 } 253}