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