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