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 */ 202 public AuthenticationRequest resolve(final AuthenticationRequest request, 203 final C securityContext) 204 throws ResolveException, JOSEException { 205 206 if (! request.specifiesRequestObject()) { 207 // Return unmodified 208 return request; 209 } 210 211 final JWT jwt; 212 213 if (request.getRequestURI() != null) { 214 215 // Check if request_uri is supported 216 if (jwtRetriever == null || jwtProcessor == null) { 217 throw new ResolveException(OIDCError.REQUEST_URI_NOT_SUPPORTED, request); 218 } 219 220 // Download request object 221 try { 222 jwt = JWTParser.parse(jwtRetriever.retrieveResource(request.getRequestURI().toURL()).getContent()); 223 } catch (MalformedURLException e) { 224 throw new ResolveException(OIDCError.INVALID_REQUEST_URI.setDescription("Malformed URL"), request); 225 } catch (IOException e) { 226 // Most likely client problem, possible causes: bad URL, timeout, network down 227 throw new ResolveException("Couldn't retrieve request_uri: " + e.getMessage(), 228 "Network error, check the request_uri", // error_description for client, hide details 229 request, e); 230 } catch (java.text.ParseException e) { 231 throw new ResolveException(OIDCError.INVALID_REQUEST_URI.setDescription("Invalid JWT"), request); 232 } 233 234 } else { 235 // Check if request by value is supported 236 if (jwtProcessor == null) { 237 throw new ResolveException(OIDCError.REQUEST_NOT_SUPPORTED, request); 238 } 239 240 // Request object inlined 241 jwt = request.getRequestObject(); 242 } 243 244 final JWTClaimsSet jwtClaims; 245 246 try { 247 jwtClaims = jwtProcessor.process(jwt, securityContext); 248 } catch (BadJOSEException e) { 249 throw new ResolveException("Invalid request object: " + e.getMessage(), 250 "Bad JWT / signature / HMAC / encryption", // error_description for client, hide details 251 request, e); 252 } 253 254 Map<String,String> finalParams = new HashMap<>(); 255 finalParams.putAll(request.toParameters()); 256 finalParams.putAll(reformatClaims(jwtClaims)); // Merge params from request object 257 finalParams.remove("request"); // make sure request object is deleted 258 finalParams.remove("request_uri"); // make sure request_uri is deleted 259 260 // Create new updated OpenID auth request 261 try { 262 return AuthenticationRequest.parse(request.getEndpointURI(), finalParams); 263 } catch (ParseException e) { 264 // E.g. missing OIDC required redirect_uri 265 throw new ResolveException("Couldn't create final OpenID authentication request: " + e.getMessage(), 266 "Invalid request object parameter(s): " + e.getMessage(), // error_description for client 267 request, e); 268 } 269 } 270}