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.HashMap;
024import java.util.List;
025import java.util.Map;
026
027import net.jcip.annotations.ThreadSafe;
028
029import com.nimbusds.jose.JOSEException;
030import com.nimbusds.jose.proc.BadJOSEException;
031import com.nimbusds.jose.proc.SecurityContext;
032import com.nimbusds.jose.util.ResourceRetriever;
033import com.nimbusds.jwt.JWT;
034import com.nimbusds.jwt.JWTClaimsSet;
035import com.nimbusds.jwt.JWTParser;
036import com.nimbusds.jwt.proc.JWTProcessor;
037import com.nimbusds.oauth2.sdk.OAuth2Error;
038import com.nimbusds.oauth2.sdk.ParseException;
039import com.nimbusds.oauth2.sdk.util.JWTClaimsSetUtils;
040import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
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         * @deprecated Use {@link JWTClaimsSetUtils#toJWTClaimsSet}.
169         */
170        @Deprecated
171        public static Map<String,List<String>> reformatClaims(final JWTClaimsSet claimsSet) {
172
173                return JWTClaimsSetUtils.toMultiValuedParameters(claimsSet);
174        }
175
176
177        /**
178         * Resolves the specified OpenID Connect authentication request by
179         * superseding its parameters with those found in the optional OpenID
180         * Connect request object (if any).
181         *
182         * @param request         The OpenID Connect authentication request.
183         *                        Must not be {@code null}.
184         * @param securityContext Optional security context to pass to the JWT
185         *                        processor, {@code null} if not specified.
186         *
187         * @return The resolved authentication request, or the original
188         *         unmodified request if no OpenID Connect request object was
189         *         specified.
190         *
191         * @throws ResolveException If the request couldn't be resolved.
192         * @throws JOSEException    If an invalid request JWT is found.
193         */
194        public AuthenticationRequest resolve(final AuthenticationRequest request,
195                                             final C securityContext)
196                throws ResolveException, JOSEException {
197
198                if (! request.specifiesRequestObject()) {
199                        // Return unmodified
200                        return request;
201                }
202
203                final JWT jwt;
204
205                if (request.getRequestURI() != null) {
206
207                        // Check if request_uri is supported
208                        if (jwtRetriever == null || jwtProcessor == null) {
209                                throw new ResolveException(OAuth2Error.REQUEST_URI_NOT_SUPPORTED, request);
210                        }
211
212                        // Download request object
213                        try {
214                                jwt = JWTParser.parse(jwtRetriever.retrieveResource(request.getRequestURI().toURL()).getContent());
215                        } catch (MalformedURLException e) {
216                                throw new ResolveException(OAuth2Error.INVALID_REQUEST_URI.setDescription("Malformed URL"), request);
217                        } catch (IOException e) {
218                                // Most likely client problem, possible causes: bad URL, timeout, network down
219                                throw new ResolveException("Couldn't retrieve request_uri: " + e.getMessage(),
220                                        "Network error, check the request_uri", // error_description for client, hide details
221                                        request, e);
222                        } catch (java.text.ParseException e) {
223                                throw new ResolveException(OAuth2Error.INVALID_REQUEST_URI.setDescription("Invalid JWT"), request);
224                        }
225
226                } else {
227                        // Check if request by value is supported
228                        if (jwtProcessor == null) {
229                                throw new ResolveException(OAuth2Error.REQUEST_NOT_SUPPORTED, request);
230                        }
231
232                        // Request object inlined
233                        jwt = request.getRequestObject();
234                }
235
236                final JWTClaimsSet jwtClaims;
237
238                try {
239                        jwtClaims = jwtProcessor.process(jwt, securityContext);
240                } catch (BadJOSEException e) {
241                        throw new ResolveException("Invalid request object: " + e.getMessage(),
242                                "Bad JWT / signature / HMAC / encryption", // error_description for client, hide details
243                                request, e);
244                }
245
246                Map<String,List<String>> finalParams = new HashMap<>();
247                finalParams.putAll(request.toParameters());
248                finalParams.putAll(JWTClaimsSetUtils.toMultiValuedParameters(jwtClaims)); // Merge params from request object
249                finalParams.remove("request"); // make sure request object is deleted
250                finalParams.remove("request_uri"); // make sure request_uri is deleted
251
252                // Create new updated OpenID auth request
253                try {
254                        return AuthenticationRequest.parse(request.getEndpointURI(), finalParams);
255                } catch (ParseException e) {
256                        // E.g. missing OIDC required redirect_uri
257                        throw new ResolveException("Couldn't create final OpenID authentication request: " + e.getMessage(),
258                                "Invalid request object parameter(s): " + e.getMessage(), // error_description for client
259                                request, e);
260                }
261        }
262}