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