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