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