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