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;
026import java.util.Objects;
027
028import net.jcip.annotations.ThreadSafe;
029
030import com.nimbusds.jose.JOSEException;
031import com.nimbusds.jose.proc.BadJOSEException;
032import com.nimbusds.jose.proc.SecurityContext;
033import com.nimbusds.jose.util.ResourceRetriever;
034import com.nimbusds.jwt.JWT;
035import com.nimbusds.jwt.JWTClaimsSet;
036import com.nimbusds.jwt.JWTParser;
037import com.nimbusds.jwt.proc.JWTProcessor;
038import com.nimbusds.oauth2.sdk.OAuth2Error;
039import com.nimbusds.oauth2.sdk.ParseException;
040import com.nimbusds.oauth2.sdk.util.JWTClaimsSetUtils;
041import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
042
043
044/**
045 * Resolves the final OpenID Connect authentication request by superseding its
046 * parameters with those found in the optional OpenID Connect request object.
047 * The request object is encoded as a JSON Web Token (JWT) and can be specified 
048 * directly (inline) using the {@code request} parameter, or by URL using the 
049 * {@code request_uri} parameter.
050 *
051 * <p>To process signed and optionally encrypted request objects a
052 * {@link JWTProcessor JWT processor} for the expected JWS / JWE algorithms
053 * must be provided at construction time.
054 *
055 * <p>To fetch OpenID Connect request objects specified by URL a
056 * {@link ResourceRetriever JWT retriever} must be provided, otherwise only
057 * inlined request objects can be processed.
058 *
059 * <p>Related specifications:
060 *
061 * <ul>
062 *     <li>OpenID Connect Core 1.0
063 * </ul>
064 */
065@ThreadSafe
066public class AuthenticationRequestResolver<C extends SecurityContext> {
067
068
069        /**
070         * The JWT processor.
071         */
072        private final JWTProcessor<C> jwtProcessor;
073
074
075        /**
076         * Optional retriever for request objects passed by URL.
077         */
078        private final ResourceRetriever jwtRetriever;
079
080
081        /**
082         * Creates a new minimal OpenID Connect authentication request
083         * resolver. It will not process OpenID Connect request objects and
084         * will throw a {@link ResolveException} if the authentication request
085         * includes a {@code request} or {@code request_uri} parameter.
086         */
087        public AuthenticationRequestResolver() {
088                jwtProcessor = null;
089                jwtRetriever = null;
090        }
091        
092        
093        /**
094         * Creates a new OpenID Connect authentication request resolver that
095         * supports OpenID Connect request objects passed by value (using the
096         * authentication {@code request} parameter). It will throw a
097         * {@link ResolveException} if the authentication request includes a
098         * {@code request_uri} parameter.
099         *
100         * @param jwtProcessor A configured JWT processor providing JWS
101         *                     validation and optional JWE decryption of the
102         *                     request objects. Must not be {@code null}.
103         */
104        public AuthenticationRequestResolver(final JWTProcessor<C> jwtProcessor) {
105                this.jwtProcessor = Objects.requireNonNull(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                this.jwtProcessor = Objects.requireNonNull(jwtProcessor);
126                this.jwtRetriever = Objects.requireNonNull(jwtRetriever);
127        }
128        
129        
130        /**
131         * Returns the JWT processor.
132         *
133         * @return The JWT processor, {@code null} if not specified.
134         */
135        public JWTProcessor<C> getJWTProcessor() {
136        
137                return jwtProcessor;
138        }
139
140
141        /**
142         * Returns the JWT retriever.
143         *
144         * @return The JWT retriever, {@code null} if not specified.
145         */
146        public ResourceRetriever getJWTRetriever() {
147        
148                return jwtRetriever;
149        }
150
151
152        /**
153         * Reformats the specified JWT claims set to a
154         * {@literal java.util.Map} instance.
155         *
156         * @param claimsSet The JWT claims set to reformat. Must not be
157         *                  {@code null}.
158         *
159         * @return The JWT claims set as an unmodifiable map of string keys / 
160         *         string values.
161         *
162         * @deprecated Use {@link JWTClaimsSetUtils#toJWTClaimsSet}.
163         */
164        @Deprecated
165        public static Map<String,List<String>> reformatClaims(final JWTClaimsSet claimsSet) {
166
167                return JWTClaimsSetUtils.toMultiValuedParameters(claimsSet);
168        }
169
170
171        /**
172         * Resolves the specified OpenID Connect authentication request by
173         * superseding its parameters with those found in the optional OpenID
174         * Connect request object (if any).
175         *
176         * @param request         The OpenID Connect authentication request.
177         *                        Must not be {@code null}.
178         * @param securityContext Optional security context to pass to the JWT
179         *                        processor, {@code null} if not specified.
180         *
181         * @return The resolved authentication request, or the original
182         *         unmodified request if no OpenID Connect request object was
183         *         specified.
184         *
185         * @throws ResolveException If the request couldn't be resolved.
186         * @throws JOSEException    If an invalid request JWT is found.
187         */
188        public AuthenticationRequest resolve(final AuthenticationRequest request,
189                                             final C securityContext)
190                throws ResolveException, JOSEException {
191
192                if (! request.specifiesRequestObject()) {
193                        // Return unmodified
194                        return request;
195                }
196
197                final JWT jwt;
198
199                if (request.getRequestURI() != null) {
200
201                        // Check if request_uri is supported
202                        if (jwtRetriever == null || jwtProcessor == null) {
203                                throw new ResolveException(OAuth2Error.REQUEST_URI_NOT_SUPPORTED, request);
204                        }
205
206                        // Download request object
207                        try {
208                                jwt = JWTParser.parse(jwtRetriever.retrieveResource(request.getRequestURI().toURL()).getContent());
209                        } catch (MalformedURLException e) {
210                                throw new ResolveException(OAuth2Error.INVALID_REQUEST_URI.setDescription("Malformed URL"), request);
211                        } catch (IOException e) {
212                                // Most likely client problem, possible causes: bad URL, timeout, network down
213                                throw new ResolveException("Couldn't retrieve request_uri: " + e.getMessage(),
214                                        "Network error, check the request_uri", // error_description for client, hide details
215                                        request, e);
216                        } catch (java.text.ParseException e) {
217                                throw new ResolveException(OAuth2Error.INVALID_REQUEST_URI.setDescription("Invalid JWT"), request);
218                        }
219
220                } else {
221                        // Check if request by value is supported
222                        if (jwtProcessor == null) {
223                                throw new ResolveException(OAuth2Error.REQUEST_NOT_SUPPORTED, request);
224                        }
225
226                        // Request object inlined
227                        jwt = request.getRequestObject();
228                }
229
230                final JWTClaimsSet jwtClaims;
231
232                try {
233                        jwtClaims = jwtProcessor.process(jwt, securityContext);
234                } catch (BadJOSEException e) {
235                        throw new ResolveException("Invalid request object: " + e.getMessage(),
236                                "Bad JWT / signature / HMAC / encryption", // error_description for client, hide details
237                                request, e);
238                }
239
240                Map<String,List<String>> finalParams = new HashMap<>();
241                finalParams.putAll(request.toParameters());
242                finalParams.putAll(JWTClaimsSetUtils.toMultiValuedParameters(jwtClaims)); // Merge params from request object
243                finalParams.remove("request"); // make sure request object is deleted
244                finalParams.remove("request_uri"); // make sure request_uri is deleted
245
246                // Create new updated OpenID auth request
247                try {
248                        return AuthenticationRequest.parse(request.getEndpointURI(), finalParams);
249                } catch (ParseException e) {
250                        // E.g. missing OIDC required redirect_uri
251                        throw new ResolveException("Couldn't create final OpenID authentication request: " + e.getMessage(),
252                                "Invalid request object parameter(s): " + e.getMessage(), // error_description for client
253                                request, e);
254                }
255        }
256}