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}