001package com.nimbusds.openid.connect.sdk.op;
002
003
004import java.io.IOException;
005import java.net.MalformedURLException;
006import java.net.URL;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.Map;
010
011import net.jcip.annotations.ThreadSafe;
012
013import net.minidev.json.JSONObject;
014
015import com.nimbusds.jose.JOSEException;
016import com.nimbusds.jwt.JWT;
017import com.nimbusds.jwt.JWTClaimsSet;
018import com.nimbusds.jwt.JWTParser;
019
020import com.nimbusds.oauth2.sdk.ErrorObject;
021import com.nimbusds.oauth2.sdk.ParseException;
022import com.nimbusds.oauth2.sdk.SerializeException;
023
024import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
025import com.nimbusds.openid.connect.sdk.OIDCError;
026import com.nimbusds.openid.connect.sdk.util.JWTDecoder;
027import com.nimbusds.openid.connect.sdk.util.Resource;
028import com.nimbusds.openid.connect.sdk.util.ResourceRetriever;
029
030
031/**
032 * Resolves the final OpenID Connect authentication request by superseding its
033 * parameters with those found in the optional OpenID Connect request object.
034 * The request object is encoded as a JSON Web Token (JWT) and can be specified 
035 * directly (inline) using the {@code request} parameter, or by URL using the 
036 * {@code request_uri} parameter.
037 *
038 * <p>To process signed (JWS) and optionally encrypted (JWE) request object 
039 * JWTs a {@link com.nimbusds.openid.connect.sdk.util.JWTDecoder JWT decoder}
040 * for the expected JWS / JWE algorithms must be provided at construction time.
041 *
042 * <p>To fetch OpenID Connect request objects specified by URL a
043 * {@link com.nimbusds.openid.connect.sdk.util.ResourceRetriever JWT retriever}
044 * must be provided, otherwise only inlined request objects can be processed.
045 *
046 * <p>Related specifications:
047 *
048 * <ul>
049 *     <li>OpenID Connect Core 1.0, section 6.
050 * </ul>
051 */
052@ThreadSafe
053public class AuthenticationRequestResolver {
054
055
056        /**
057         * The JWT decoder.
058         */
059        private final JWTDecoder jwtDecoder;
060
061
062        /**
063         * Optional retriever for JWTs passed by URL.
064         */
065        private final ResourceRetriever jwtRetriever;
066
067
068        /**
069         * Creates a new minimal OpenID Connect authentication request
070         * resolver. It will not process OpenID Connect request objects and
071         * will throw a {@link ResolveException} if the authentication request
072         * includes a {@code request} or {@code request_uri} parameter.
073         */
074        public AuthenticationRequestResolver() {
075
076                jwtDecoder = null;
077                jwtRetriever = null;
078        }
079        
080        
081        /**
082         * Creates a new OpenID Connect authentication request resolver that
083         * supports OpenID Connect request objects passed by value (using the
084         * authentication {@code request} parameter). It will throw a
085         * {@link ResolveException} if the authentication request includes a
086         * {@code request_uri} parameter.
087         *
088         * @param jwtDecoder A configured JWT decoder providing JWS validation 
089         *                   and optional JWE decryption of the request
090         *                   objects. Must not be {@code null}.
091         */
092        public AuthenticationRequestResolver(final JWTDecoder jwtDecoder) {
093
094                if (jwtDecoder == null)
095                        throw new IllegalArgumentException("The JWT decoder must not be null");
096
097                this.jwtDecoder = jwtDecoder;
098
099                jwtRetriever = null;
100        }
101        
102        
103        /**
104         * Creates a new OpenID Connect request object resolver that supports
105         * OpenID Connect request objects passed by value (using the
106         * authentication {@code request} parameter) or by reference (using the
107         * authentication {@code request_uri} parameter).
108         * 
109         * @param jwtDecoder   A configured JWT decoder providing JWS 
110         *                     validation and optional JWE decryption of the
111         *                     request objects. Must not be {@code null}.
112         * @param jwtRetriever A configured JWT retriever for OpenID Connect
113         *                     request objects passed by URI. Must not be
114         *                     {@code null}.
115         */
116        public AuthenticationRequestResolver(final JWTDecoder jwtDecoder,
117                                             final ResourceRetriever jwtRetriever) {
118
119                if (jwtDecoder == null)
120                        throw new IllegalArgumentException("The JWT decoder must not be null");
121
122                this.jwtDecoder = jwtDecoder;
123
124
125                if (jwtRetriever == null)
126                        throw new IllegalArgumentException("The JWT retriever must not be null");
127
128                this.jwtRetriever = jwtRetriever;
129        }
130        
131        
132        /**
133         * Gets the JWT decoder.
134         *
135         * @return The JWT decoder, {@code null} if not specified.
136         */
137        public JWTDecoder getJWTDecoder() {
138        
139                return jwtDecoder;
140        }
141
142
143        /**
144         * Gets the JWT retriever.
145         *
146         * @return The JWT retriever, {@code null} if not specified.
147         */
148        public ResourceRetriever getJWTRetriever() {
149        
150                return jwtRetriever;
151        }
152        
153        
154        /**
155         * Retrieves a JWT from the specified URL. The content type of the URL 
156         * resource is not checked.
157         *
158         * @param url The URL of the JWT. Must not be {@code null}.
159         *
160         * @return The retrieved JWT.
161         *
162         * @throws ResolveException If no JWT retriever is configured, if the
163         *                          resource couldn't be retrieved, or parsed
164         *                          to a JWT.
165         */
166        private JWT retrieveRequestObject(final URL url)
167                throws ResolveException {
168        
169                if (jwtRetriever == null) {
170
171                        throw new ResolveException("OpenID Connect request object cannot be resolved: No JWT retriever is configured");
172                }
173
174                Resource resource;
175
176                try {
177                        resource = jwtRetriever.retrieveResource(url);
178                        
179                } catch (IOException e) {
180
181                        throw new ResolveException("Couldn't retrieve OpenID Connect request object: " + e.getMessage(), e);
182                }
183
184                try {
185                        return JWTParser.parse(resource.getContent());
186                
187                } catch (java.text.ParseException e) {
188
189                        throw new ResolveException("Couldn't parse OpenID Connect request object: " +  e.getMessage(), e);
190                }
191        }
192        
193        
194        /**
195         * Decodes the specified OpenID Connect request object, and if it's
196         * secured performs additional JWS signature validation and JWE
197         * decryption.
198         *
199         * @param requestObject The OpenID Connect request object to decode. 
200         *                      Must not be {@code null}.
201         *
202         * @return The extracted JWT claims of the OpenID Connect request 
203         *         object.
204         *
205         * @throws ResolveException If no JWT decoder is configured, if JWT 
206         *                          decoding, JWS validation or JWE decryption 
207         *                          failed.
208         */
209        private JWTClaimsSet decodeRequestObject(final JWT requestObject)
210                throws ResolveException {
211                
212                if (jwtDecoder == null) {
213
214                        throw new ResolveException("OpenID Connect request object cannot be decoded: No JWT decoder is configured");
215                }
216
217                try {
218                        return jwtDecoder.decodeJWT(requestObject);
219                                
220                } catch (JOSEException e) {
221                
222                        throw new ResolveException("Couldn't decode OpenID Connect request object JWT: " + e.getMessage(), e);
223                        
224                } catch (java.text.ParseException e) {
225
226                        throw new ResolveException("Couldn't parse OpenID Connect request object JWT claims: " + e.getMessage(), e);
227                }
228        }
229
230
231        /**
232         * Reformats the specified JWT claims set to a 
233         * {@literal java.util.Map&<String,String>} instance.
234         *
235         * @param claimsSet The JWT claims set to reformat. Must not be
236         *                  {@code null}.
237         *
238         * @return The JWT claims set as an unmodifiable map of string keys / 
239         *         string values.
240         *
241         * @throws ResolveException If reformatting of the JWT claims set 
242         *                          failed.
243         */
244        public static Map<String,String> reformatClaims(final JWTClaimsSet claimsSet)
245                throws ResolveException {
246
247                Map<String,Object> claims = claimsSet.getClaims();
248
249                // Reformat all claim values as strings
250                Map<String,String> reformattedClaims = new HashMap<>();
251
252                for (Map.Entry<String,Object> entry: claims.entrySet()) {
253
254                        Object value = entry.getValue();
255
256                        if (value instanceof String) {
257
258                                reformattedClaims.put(entry.getKey(), (String)value);
259
260                        } else if (value instanceof Boolean) {
261
262                                Boolean bool = (Boolean)value;
263                                reformattedClaims.put(entry.getKey(), bool.toString());
264
265                        } else if (value instanceof Number) {
266
267                                Number number = (Number)value;
268                                reformattedClaims.put(entry.getKey(), number.toString());
269
270                        } else if (value instanceof JSONObject) {
271
272                                JSONObject jsonObject = (JSONObject)value;
273                                reformattedClaims.put(entry.getKey(), jsonObject.toString());
274
275                        } else {
276
277                                throw new ResolveException("Couldn't process JWT claim \"" + entry.getKey() + "\": Unsupported type");
278                        }
279                }
280
281                return Collections.unmodifiableMap(reformattedClaims);
282        }
283
284
285        /**
286         * Resolves the specified OpenID Connect authentication request by
287         * superseding its parameters with those found in the optional OpenID 
288         * Connect request object (if any).
289         * 
290         * @param request The OpenID Connect authentication request. Must not be
291         *                {@code null}.
292         * 
293         * @return The resolved authentication request, or the original
294         *         unmodified request if no OpenID Connect request object was
295         *         specified.
296         * 
297         * @throws ResolveException If the request couldn't be resolved.
298         */
299        public AuthenticationRequest resolve(final AuthenticationRequest request)
300                throws ResolveException {
301
302                if (! request.specifiesRequestObject()) {
303                        // Return the same request
304                        return request;
305                }
306
307                try {
308                        JWT jwt;
309
310                        if (request.getRequestURI() != null) {
311
312                                // Download request object
313                                URL requestURL;
314
315                                try {
316                                        requestURL = request.getRequestURI().toURL();
317
318                                } catch (MalformedURLException e) {
319
320                                        throw new ResolveException(e.getMessage(), e);
321                                }
322
323                                jwt = retrieveRequestObject(requestURL);
324                        } else {
325                                // Request object inlined
326                                jwt = request.getRequestObject();
327                        }
328
329                        JWTClaimsSet jwtClaims = decodeRequestObject(jwt);
330
331                        Map<String, String> requestObjectParams = reformatClaims(jwtClaims);
332
333                        Map<String, String> finalParams = new HashMap<>();
334                        
335                        try {
336                                finalParams.putAll(request.toParameters());
337
338                        } catch (SerializeException e) {
339
340                                throw new ResolveException("Couldn't resolve final OpenID Connect authentication request: " + e.getMessage(), e);
341                        }
342
343                        // Merge params from request object
344                        finalParams.putAll(requestObjectParams);
345
346
347                        // Parse again
348                        AuthenticationRequest finalAuthRequest;
349
350                        try {
351                                finalAuthRequest = AuthenticationRequest.parse(request.getEndpointURI(), finalParams);
352
353                        } catch (ParseException e) {
354
355                                throw new ResolveException("Couldn't create final OpenID Connect authentication request: " + e.getMessage(), e);
356                        }
357                        
358                        return new AuthenticationRequest(
359                                finalAuthRequest.getEndpointURI(),
360                                finalAuthRequest.getResponseType(),
361                                finalAuthRequest.getResponseMode(),
362                                finalAuthRequest.getScope(),
363                                finalAuthRequest.getClientID(),
364                                finalAuthRequest.getRedirectionURI(),
365                                finalAuthRequest.getState(),
366                                finalAuthRequest.getNonce(),
367                                finalAuthRequest.getDisplay(),
368                                finalAuthRequest.getPrompt(),
369                                finalAuthRequest.getMaxAge(),
370                                finalAuthRequest.getUILocales(),
371                                finalAuthRequest.getClaimsLocales(),
372                                finalAuthRequest.getIDTokenHint(),
373                                finalAuthRequest.getLoginHint(),
374                                finalAuthRequest.getACRValues(),
375                                finalAuthRequest.getClaims(),
376                                null, // request object
377                                null); // request URI
378                        
379                } catch (ResolveException e) {
380                        
381                        // Repackage exception with redirection URI, state, error object
382                        
383                        ErrorObject err;
384                        
385                        if (request.getRequestURI() != null)
386                                err = OIDCError.INVALID_REQUEST_URI;
387                        else
388                                err = OIDCError.INVALID_REQUEST_OBJECT;
389                        
390                        throw new ResolveException(
391                                e.getMessage(),
392                                err,
393                                request.getClientID(),
394                                request.getRedirectionURI(),
395                                request.impliedResponseMode(),
396                                request.getState(),
397                                e.getCause());
398                }
399        }
400}