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}