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