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}