001package com.nimbusds.openid.connect.sdk;
002
003
004import java.net.MalformedURLException;
005import java.net.URL;
006import java.util.Collections;
007import java.util.LinkedList;
008import java.util.List;
009import java.util.Map;
010import java.util.StringTokenizer;
011
012import net.jcip.annotations.Immutable;
013
014import org.apache.commons.lang3.StringUtils;
015
016import net.minidev.json.JSONObject;
017
018import com.nimbusds.langtag.LangTag;
019import com.nimbusds.langtag.LangTagException;
020
021import com.nimbusds.jwt.JWT;
022import com.nimbusds.jwt.JWTParser;
023
024import com.nimbusds.oauth2.sdk.AuthorizationRequest;
025import com.nimbusds.oauth2.sdk.OAuth2Error;
026import com.nimbusds.oauth2.sdk.ParseException;
027import com.nimbusds.oauth2.sdk.ResponseType;
028import com.nimbusds.oauth2.sdk.Scope;
029import com.nimbusds.oauth2.sdk.SerializeException;
030import com.nimbusds.oauth2.sdk.id.ClientID;
031import com.nimbusds.oauth2.sdk.id.State;
032import com.nimbusds.oauth2.sdk.http.HTTPRequest;
033import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
034import com.nimbusds.oauth2.sdk.util.URLUtils;
035
036import com.nimbusds.openid.connect.sdk.claims.ACR;
037
038
039/**
040 * OpenID Connect authorisation request. Used to authenticate (if required) an 
041 * end-user and request the end-user's authorisation to release information to 
042 * the client. This class is immutable.
043 *
044 * <p>Example HTTP request (code flow):
045 *
046 * <pre>
047 * https://server.example.com/op/authorize?
048 * response_type=code%20id_token
049 * &amp;client_id=s6BhdRkqt3
050 * &amp;redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
051 * &amp;scope=openid
052 * &amp;nonce=n-0S6_WzA2Mj
053 * &amp;state=af0ifjsldkj
054 * </pre>
055 *
056 * <p>Related specifications:
057 *
058 * <ul>
059 *     <li>OpenID Connect Messages 1.0, section 2.1.1.
060 *     <li>OpenID Connect Standard 1.0, section 2.3.1.
061 * </ul>
062 *
063 * @author Vladimir Dzhuvinov
064 */
065@Immutable
066public final class OIDCAuthorizationRequest extends AuthorizationRequest {
067        
068        
069        /**
070         * The nonce (required for implicit flow, optional for code flow).
071         */
072        private final Nonce nonce;
073        
074        
075        /**
076         * The requested display type (optional).
077         */
078        private final Display display;
079        
080        
081        /**
082         * The requested prompt (optional).
083         */
084        private final Prompt prompt;
085
086
087        /**
088         * The required maximum authentication age, in seconds, 0 if not 
089         * specified (optional).
090         */
091        private final int maxAge;
092
093
094        /**
095         * The end-user's preferred languages and scripts for the user 
096         * interface (optional).
097         */
098        private final List<LangTag> uiLocales;
099
100
101        /**
102         * The end-user's preferred languages and scripts for claims being 
103         * returned (optional).
104         */
105        private final List<LangTag> claimsLocales;
106
107
108        /**
109         * Previously issued ID Token passed to the authorisation server as a 
110         * hint about the end-user's current or past authenticated session with
111         * the client (optional). Should be present when {@code prompt=none} is 
112         * used.
113         */
114        private final JWT idTokenHint;
115
116
117        /**
118         * Hint to the authorisation server about the login identifier the 
119         * end-user may use to log in (optional).
120         */
121        private final String loginHint;
122
123
124        /**
125         * Requested Authentication Context Class Reference values (optional).
126         */
127        private final List<ACR> acrValues;
128
129
130        /**
131         * Individual claims to be returned (optional).
132         */
133        private final ClaimsRequest claims;
134        
135        
136        /**
137         * Request object (optional).
138         */
139        private final JWT requestObject;
140        
141        
142        /**
143         * Request object URL (optional).
144         */
145        private final URL requestURI;
146        
147        
148        /**
149         * Creates a new minimal OpenID Connect authorisation request.
150         *
151         * @param uri         The URI of the authorisation endpoint. May be 
152         *                    {@code null} if the {@link #toHTTPRequest()}
153         *                    method will not be used.
154         * @param rt          The response type. Corresponds to the 
155         *                    {@code response_type} parameter. Must specify a
156         *                    valid OpenID Connect response type. Must not be
157         *                    {@code null}.
158         * @param scope       The request scope. Corresponds to the
159         *                    {@code scope} parameter. Must contain an
160         *                    {@link OIDCScopeValue#OPENID openid value}. Must
161         *                    not be {@code null}.
162         * @param clientID    The client identifier. Corresponds to the
163         *                    {@code client_id} parameter. Must not be 
164         *                    {@code null}.
165         * @param redirectURI The redirection URI. Corresponds to the
166         *                    {@code redirect_uri} parameter. Must not be 
167         *                    {@code null}.
168         * @param state       The state. Corresponds to the {@code state}
169         *                    parameter. May be {@code null}.
170         * @param nonce       The nonce. Corresponds to the {@code nonce} 
171         *                    parameter. May be {@code null} for code flow.
172         */
173        public OIDCAuthorizationRequest(final URL uri,
174                                        final ResponseType rt,
175                                        final Scope scope,
176                                        final ClientID clientID,
177                                        final URL redirectURI,
178                                        final State state,
179                                        final Nonce nonce) {
180
181                // Not specified: display, prompt, maxAge, uiLocales, claimsLocales, 
182                // idTokenHint, loginHint, acrValues, claims
183                this(uri, rt, scope, clientID, redirectURI, state, nonce, 
184                     null, null, 0, null, null, 
185                     null, null, null, null);
186        }
187        
188        
189        /**
190         * Creates a new OpenID Connect authorisation request without a request
191         * object.
192         *
193         * @param uri           The URI of the authorisation endpoint. May be 
194         *                      {@code null} if the {@link #toHTTPRequest()}
195         *                      method will not be used.
196         * @param rt            The response type. Corresponds to the 
197         *                      {@code response_type} parameter. Must specify a
198         *                      valid OpenID Connect response type. Must not be
199         *                      {@code null}.
200         * @param scope         The request scope. Corresponds to the
201         *                      {@code scope} parameter. Must contain an
202         *                      {@link OIDCScopeValue#OPENID openid value}. 
203         *                      Must not be {@code null}.
204         * @param clientID      The client identifier. Corresponds to the
205         *                      {@code client_id} parameter. Must not be 
206         *                      {@code null}.
207         * @param redirectURI   The redirection URI. Corresponds to the
208         *                      {@code redirect_uri} parameter. Must not be 
209         *                      {@code null}.
210         * @param state         The state. Corresponds to the recommended 
211         *                      {@code state} parameter. {@code null} if not 
212         *                      specified.
213         * @param nonce         The nonce. Corresponds to the {@code nonce} 
214         *                      parameter. May be {@code null} for code flow.
215         * @param display       The requested display type. Corresponds to the 
216         *                      optional {@code display} parameter. 
217         *                      {@code null} if not specified.
218         * @param prompt        The requested prompt. Corresponds to the 
219         *                      optional {@code prompt} parameter. {@code null} 
220         *                      if not specified.
221         * @param maxAge        The required maximum authentication age, in
222         *                      seconds. Corresponds to the optional 
223         *                      {@code max_age} parameter. Zero if not 
224         *                      specified.
225         * @param uiLocales     The preferred languages and scripts for the 
226         *                      user interface. Corresponds to the optional 
227         *                      {@code ui_locales} parameter. {@code null} if 
228         *                      not specified.
229         * @param claimsLocales The preferred languages and scripts for claims
230         *                      being returned. Corresponds to the optional
231         *                      {@code claims_locales} parameter. {@code null}
232         *                      if not specified.
233         * @param idTokenHint   The ID Token hint. Corresponds to the optional 
234         *                      {@code id_token_hint} parameter. {@code null} 
235         *                      if not specified.
236         * @param loginHint     The login hint. Corresponds to the optional
237         *                      {@code login_hint} parameter. {@code null} if 
238         *                      not specified.
239         * @param acrValues     The requested Authentication Context Class
240         *                      Reference values. Corresponds to the optional
241         *                      {@code acr_values} parameter. {@code null} if
242         *                      not specified.
243         * @param claims        The individual claims to be returned. 
244         *                      Corresponds to the optional {@code claims} 
245         *                      parameter. {@code null} if not specified.
246         */
247        public OIDCAuthorizationRequest(final URL uri,
248                                        final ResponseType rt,
249                                        final Scope scope,
250                                        final ClientID clientID,
251                                        final URL redirectURI,
252                                        final State state,
253                                        final Nonce nonce,
254                                        final Display display,
255                                        final Prompt prompt,
256                                        final int maxAge,
257                                        final List<LangTag> uiLocales,
258                                        final List<LangTag> claimsLocales,
259                                        final JWT idTokenHint,
260                                        final String loginHint,
261                                        final List<ACR> acrValues,
262                                        final ClaimsRequest claims) {
263                                    
264                                    
265                this(uri, rt, scope, clientID, redirectURI, state, nonce, display, prompt,
266                     maxAge, uiLocales, claimsLocales, idTokenHint, loginHint, acrValues,
267                     claims, (JWT)null);
268        }
269        
270        
271        /**
272         * Creates a new OpenID Connect authorisation request with a request 
273         * object specified by value.
274         *
275         * @param uri           The URI of the authorisation endpoint. May be 
276         *                      {@code null} if the {@link #toHTTPRequest()}
277         *                      method will not be used.
278         * @param rt            The response type set. Corresponds to the 
279         *                      {@code response_type} parameter. Must specify a
280         *                      valid OpenID Connect response type. Must not be
281         *                      {@code null}.
282         * @param scope         The request scope. Corresponds to the
283         *                      {@code scope} parameter. Must contain an
284         *                      {@link OIDCScopeValue#OPENID openid value}. 
285         *                      Must not be {@code null}.
286         * @param clientID      The client identifier. Corresponds to the
287         *                      {@code client_id} parameter. Must not be 
288         *                      {@code null}.
289         * @param redirectURI   The redirection URI. Corresponds to the
290         *                      {@code redirect_uri} parameter. Must not be 
291         *                      {@code null}.
292         * @param state         The state. Corresponds to the recommended 
293         *                      {@code state} parameter. {@code null} if not 
294         *                      specified.
295         * @param nonce         The nonce. Corresponds to the {@code nonce} 
296         *                      parameter. May be {@code null} for code flow.
297         * @param display       The requested display type. Corresponds to the 
298         *                      optional {@code display} parameter. 
299         *                      {@code null} if not specified.
300         * @param prompt        The requested prompt. Corresponds to the 
301         *                      optional {@code prompt} parameter. {@code null} 
302         *                      if not specified.
303         * @param maxAge        The required maximum authentication age, in
304         *                      seconds. Corresponds to the optional 
305         *                      {@code max_age} parameter. Zero if not 
306         *                      specified.
307         * @param uiLocales     The preferred languages and scripts for the 
308         *                      user interface. Corresponds to the optional 
309         *                      {@code ui_locales} parameter. {@code null} if 
310         *                      not specified.
311         * @param claimsLocales The preferred languages and scripts for claims
312         *                      being returned. Corresponds to the optional
313         *                      {@code claims_locales} parameter. {@code null}
314         *                      if not specified.
315         * @param idTokenHint   The ID Token hint. Corresponds to the optional 
316         *                      {@code id_token_hint} parameter. {@code null} 
317         *                      if not specified.
318         * @param loginHint     The login hint. Corresponds to the optional
319         *                      {@code login_hint} parameter. {@code null} if 
320         *                      not specified.
321         * @param acrValues     The requested Authentication Context Class
322         *                      Reference values. Corresponds to the optional
323         *                      {@code acr_values} parameter. {@code null} if
324         *                      not specified.
325         * @param claims        The individual claims to be returned. 
326         *                      Corresponds to the optional {@code claims} 
327         *                      parameter. {@code null} if not specified.
328         * @param requestObject The request object. Corresponds to the optional
329         *                      {@code request} parameter. {@code null} if not
330         *                      specified.
331         */
332        public OIDCAuthorizationRequest(final URL uri,
333                                        final ResponseType rt,
334                                        final Scope scope,
335                                        final ClientID clientID,
336                                        final URL redirectURI,
337                                        final State state,
338                                        final Nonce nonce,
339                                        final Display display,
340                                        final Prompt prompt,
341                                        final int maxAge,
342                                        final List<LangTag> uiLocales,
343                                        final List<LangTag> claimsLocales,
344                                        final JWT idTokenHint,
345                                        final String loginHint,
346                                        final List<ACR> acrValues,
347                                        final ClaimsRequest claims,
348                                        final JWT requestObject) {
349                                    
350                super(uri, rt, clientID, redirectURI, scope, state);
351
352                if (redirectURI == null)
353                        throw new IllegalArgumentException("The redirect URI must not be null");
354                
355                OIDCResponseTypeValidator.validate(rt);
356
357                if (scope == null)
358                        throw new IllegalArgumentException("The scope must not be null");
359
360                if (! scope.contains(OIDCScopeValue.OPENID))
361                        throw new IllegalArgumentException("The scope must include an \"openid\" token");
362                
363                
364                // Nonce required for implicit protocol flow
365                if (rt.impliesImplicitFlow() && nonce == null)
366                        throw new IllegalArgumentException("Nonce is required in implicit protocol flow");
367                
368                this.nonce = nonce;
369                
370                // Optional parameters
371                this.display = display;
372                this.prompt = prompt;
373                this.maxAge = maxAge;
374
375                if (uiLocales != null)
376                        this.uiLocales = Collections.unmodifiableList(uiLocales);
377                else
378                        this.uiLocales = null;
379
380                if (claimsLocales != null)
381                        this.claimsLocales = Collections.unmodifiableList(claimsLocales);
382                else
383                        this.claimsLocales = null;
384
385                this.idTokenHint = idTokenHint;
386                this.loginHint = loginHint;
387
388                if (acrValues != null)
389                        this.acrValues = Collections.unmodifiableList(acrValues);
390                else
391                        this.acrValues = null;
392
393                this.claims = claims;
394                this.requestObject = requestObject;
395                this.requestURI = null;
396        }
397        
398        
399        /**
400         * Creates a new OpenID Connect authorisation request with a request 
401         * object specified by URL.
402         *
403         * @param uri           The URI of the authorisation endpoint. May be 
404         *                      {@code null} if the {@link #toHTTPRequest()}
405         *                      method will not be used.
406         * @param rt            The response type. Corresponds to the 
407         *                      {@code response_type} parameter. Must specify a
408         *                      a valid OpenID Connect response type. Must not 
409         *                      be {@code null}.
410         * @param scope         The request scope. Corresponds to the
411         *                      {@code scope} parameter. Must contain an
412         *                      {@link OIDCScopeValue#OPENID openid value}. 
413         *                      Must not be {@code null}.
414         * @param clientID      The client identifier. Corresponds to the
415         *                      {@code client_id} parameter. Must not be 
416         *                      {@code null}.
417         * @param redirectURI   The redirection URI. Corresponds to the
418         *                      {@code redirect_uri} parameter. Must not be 
419         *                      {@code null}.
420         * @param state         The state. Corresponds to the recommended 
421         *                      {@code state} parameter. {@code null} if not 
422         *                      specified.
423         * @param nonce         The nonce. Corresponds to the {@code nonce} 
424         *                      parameter. May be {@code null} for code flow.
425         * @param display       The requested display type. Corresponds to the 
426         *                      optional {@code display} parameter. 
427         *                      {@code null} if not specified.
428         * @param prompt        The requested prompt. Corresponds to the 
429         *                      optional {@code prompt} parameter. {@code null} 
430         *                      if not specified.
431         * @param maxAge        The required maximum authentication age, in
432         *                      seconds. Corresponds to the optional 
433         *                      {@code max_age} parameter. Zero if not 
434         *                      specified.
435         * @param uiLocales     The preferred languages and scripts for the 
436         *                      user interface. Corresponds to the optional 
437         *                      {@code ui_locales} parameter. {@code null} if 
438         *                      not specified.
439         * @param claimsLocales The preferred languages and scripts for claims
440         *                      being returned. Corresponds to the optional
441         *                      {@code claims_locales} parameter. {@code null}
442         *                      if not specified.
443         * @param idTokenHint   The ID Token hint. Corresponds to the optional 
444         *                      {@code id_token_hint} parameter. {@code null} 
445         *                      if not specified.
446         * @param loginHint     The login hint. Corresponds to the optional
447         *                      {@code login_hint} parameter. {@code null} if 
448         *                      not specified.
449         * @param acrValues     The requested Authentication Context Class
450         *                      Reference values. Corresponds to the optional
451         *                      {@code acr_values} parameter. {@code null} if
452         *                      not specified.
453         * @param claims        The individual claims to be returned. 
454         *                      Corresponds to the optional {@code claims} 
455         *                      parameter. {@code null} if not specified.
456         * @param requestURI    The request object URL. Corresponds to the 
457         *                      optional {@code request_uri} parameter. 
458         *                      {@code null} if not specified.
459         */
460        public OIDCAuthorizationRequest(final URL uri,
461                                        final ResponseType rt,
462                                        final Scope scope,
463                                        final ClientID clientID,
464                                        final URL redirectURI,
465                                        final State state,
466                                        final Nonce nonce,
467                                        final Display display,
468                                        final Prompt prompt,
469                                        final int maxAge,
470                                        final List<LangTag> uiLocales,
471                                        final List<LangTag> claimsLocales,
472                                        final JWT idTokenHint,
473                                        final String loginHint,
474                                        final List<ACR> acrValues,
475                                        final ClaimsRequest claims,
476                                        final URL requestURI) {
477                                    
478                super(uri, rt, clientID, redirectURI, scope, state);
479
480                if (redirectURI == null)
481                        throw new IllegalArgumentException("The redirect URI must not be null");
482
483                OIDCResponseTypeValidator.validate(rt);
484                
485                if (scope == null)
486                        throw new IllegalArgumentException("The scope must not be null");
487
488                if (! scope.contains(OIDCScopeValue.OPENID))
489                        throw new IllegalArgumentException("The scope must include an \"openid\" token");
490                
491                
492                // Nonce required for implicit protocol flow
493                if (rt.impliesImplicitFlow() && nonce == null)
494                        throw new IllegalArgumentException("Nonce is required in implicit protocol flow");
495                
496                this.nonce = nonce;
497                
498                // Optional parameters
499                // Optional parameters
500                this.display = display;
501                this.prompt = prompt;
502                this.maxAge = maxAge;
503
504                if (uiLocales != null)
505                        this.uiLocales = Collections.unmodifiableList(uiLocales);
506                else
507                        this.uiLocales = null;
508
509                if (claimsLocales != null)
510                        this.claimsLocales = Collections.unmodifiableList(claimsLocales);
511                else
512                        this.claimsLocales = null;
513
514                this.idTokenHint = idTokenHint;
515                this.loginHint = loginHint;
516
517                if (acrValues != null)
518                        this.acrValues = Collections.unmodifiableList(acrValues);
519                else
520                        this.acrValues = null;
521
522                this.claims = claims;
523                this.requestObject = null;
524                this.requestURI = requestURI;
525        }
526        
527        
528        /**
529         * Gets the nonce. Corresponds to the conditionally optional 
530         * {@code nonce} parameter.
531         *
532         * @return The nonce, {@code null} if not specified.
533         */
534        public Nonce getNonce() {
535        
536                return nonce;
537        }
538        
539        
540        /**
541         * Gets the requested display type. Corresponds to the optional
542         * {@code display} parameter.
543         *
544         * @return The requested display type, {@code null} if not specified.
545         */
546        public Display getDisplay() {
547        
548                return display;
549        }
550        
551        
552        /**
553         * Gets the requested prompt. Corresponds to the optional 
554         * {@code prompt} parameter.
555         *
556         * @return The requested prompt, {@code null} if not specified.
557         */
558        public Prompt getPrompt() {
559        
560                return prompt;
561        }
562
563
564        /**
565         * Gets the required maximum authentication age. Corresponds to the
566         * optional {@code max_age} parameter.
567         *
568         * @return The maximum authentication age, in seconds; 0 if not 
569         *         specified.
570         */
571        public int getMaxAge() {
572        
573                return maxAge;
574        }
575
576
577        /**
578         * Gets the end-user's preferred languages and scripts for the user
579         * interface, ordered by preference. Corresponds to the optional
580         * {@code ui_locales} parameter.
581         *
582         * @return The preferred UI locales, {@code null} if not specified.
583         */
584        public List<LangTag> getUILocales() {
585
586                return uiLocales;
587        }
588
589
590        /**
591         * Gets the end-user's preferred languages and scripts for the claims
592         * being returned, ordered by preference. Corresponds to the optional
593         * {@code claims_locales} parameter.
594         *
595         * @return The preferred claims locales, {@code null} if not specified.
596         */
597        public List<LangTag> getClaimsLocales() {
598
599                return claimsLocales;
600        }
601
602
603        /**
604         * Gets the ID Token hint. Corresponds to the conditionally optional 
605         * {@code id_token_hint} parameter.
606         *
607         * @return The ID Token hint, {@code null} if not specified.
608         */
609        public JWT getIDTokenHint() {
610        
611                return idTokenHint;
612        }
613
614
615        /**
616         * Gets the login hint. Corresponds to the optional {@code login_hint} 
617         * parameter.
618         *
619         * @return The login hint, {@code null} if not specified.
620         */
621        public String getLoginHint() {
622
623                return loginHint;
624        }
625
626
627        /**
628         * Gets the requested Authentication Context Class Reference values.
629         * Corresponds to the optional {@code acr_values} parameter.
630         *
631         * @return The requested ACR values, {@code null} if not specified.
632         */
633        public List<ACR> getACRValues() {
634
635                return acrValues;
636        }
637
638
639        /**
640         * Gets the individual claims to be returned. Corresponds to the 
641         * optional {@code claims} parameter.
642         *
643         * @return The individual claims to be returned, {@code null} if not
644         *         specified.
645         */
646        public ClaimsRequest getClaims() {
647
648                return claims;
649        }
650        
651        
652        /**
653         * Gets the request object. Corresponds to the optional {@code request} 
654         * parameter.
655         *
656         * @return The request object, {@code null} if not specified.
657         */
658        public JWT getRequestObject() {
659        
660                return requestObject;
661        }
662        
663        
664        /**
665         * Gets the request object URL. Corresponds to the optional 
666         * {@code request_uri} parameter.
667         *
668         * @return The request object URL, {@code null} if not specified.
669         */
670        public URL getRequestURI() {
671        
672                return requestURI;
673        }
674        
675        
676        /**
677         * Returns {@code true} if this authorisation request specifies an
678         * OpenID Connect request object (directly through the {@code request} 
679         * parameter or by reference through the {@code request_uri} parameter).
680         *
681         * @return {@code true} if a request object is specified, else 
682         *         {@code false}.
683         */
684        public boolean specifiesRequestObject() {
685        
686                if (requestObject != null || requestURI != null) {
687
688                        return true;
689
690                } else {
691                        
692                        return false;
693                }
694        }
695
696
697        @Override
698        public Map<String,String> toParameters()
699                throws SerializeException {
700
701                Map <String,String> params = super.toParameters();
702                
703                if (nonce != null)
704                        params.put("nonce", nonce.toString());
705                
706                if (display != null)
707                        params.put("display", display.toString());
708                
709                if (prompt != null)
710                        params.put("prompt", prompt.toString());
711
712                if (maxAge > 0)
713                        params.put("max_age", "" + maxAge);
714
715                if (uiLocales != null) {
716
717                        StringBuilder sb = new StringBuilder();
718
719                        for (LangTag locale: uiLocales) {
720
721                                if (sb.length() > 0)
722                                        sb.append(' ');
723
724                                sb.append(locale.toString());
725                        }
726
727                        params.put("ui_locales", sb.toString());
728                }
729
730                if (claimsLocales != null) {
731
732                        StringBuilder sb = new StringBuilder();
733
734                        for (LangTag locale: claimsLocales) {
735
736                                if (sb.length() > 0)
737                                        sb.append(' ');
738
739                                sb.append(locale.toString());
740                        }
741
742                        params.put("claims_locales", sb.toString());
743                }
744
745                if (idTokenHint != null) {
746                
747                        try {
748                                params.put("id_token_hint", idTokenHint.serialize());
749                                
750                        } catch (IllegalStateException e) {
751                        
752                                throw new SerializeException("Couldn't serialize ID token hint: " + e.getMessage(), e);
753                        }
754                }
755
756                if (loginHint != null)
757                        params.put("login_hint", loginHint);
758
759                if (acrValues != null) {
760
761                        StringBuilder sb = new StringBuilder();
762
763                        for (ACR acr: acrValues) {
764
765                                if (sb.length() > 0)
766                                        sb.append(' ');
767
768                                sb.append(acr.toString());
769                        }
770
771                        params.put("acr_values", sb.toString());
772                }
773                        
774
775                if (claims != null)
776                        params.put("claims", claims.toJSONObject().toString());
777                
778                if (requestObject != null) {
779                
780                        try {
781                                params.put("request", requestObject.serialize());
782                                
783                        } catch (IllegalStateException e) {
784                        
785                                throw new SerializeException("Couldn't serialize request object to JWT: " + e.getMessage(), e);
786                        }
787                }
788                
789                if (requestURI != null)
790                        params.put("request_uri", requestURI.toString());
791
792                return params;
793        }
794
795
796        /**
797         * Parses an OpenID Connect authorisation request from the specified 
798         * parameters.
799         *
800         * <p>Example parameters:
801         *
802         * <pre>
803         * response_type = token id_token
804         * client_id     = s6BhdRkqt3
805         * redirect_uri  = https://client.example.com/cb
806         * scope         = openid profile
807         * state         = af0ifjsldkj
808         * nonce         = -0S6_WzA2Mj
809         * </pre>
810         *
811         * @param uri    The URI of the authorisation endpoint. May be 
812         *               {@code null} if the {@link #toHTTPRequest()} method 
813         *               will not be used.
814         * @param params The parameters. Must not be {@code null}.
815         *
816         * @return The OpenID Connect authorisation request.
817         *
818         * @throws ParseException If the parameters couldn't be parsed to an
819         *                        OpenID Connect authorisation request.
820         */
821        public static OIDCAuthorizationRequest parse(final URL uri, final Map<String,String> params)
822                throws ParseException {
823
824                // Parse and validate the core OAuth 2.0 autz request params in 
825                // the context of OIDC
826                AuthorizationRequest ar = AuthorizationRequest.parse(uri, params);
827
828                ClientID clientID = ar.getClientID();
829                State state = ar.getState();
830
831                // Required in OIDC
832                URL redirectURI = ar.getRedirectionURI();
833
834                if (redirectURI == null)
835                        throw new ParseException("Missing \"redirect_uri\" parameter", 
836                                                 OAuth2Error.INVALID_REQUEST, clientID, null, state);
837
838
839                ResponseType rt = ar.getResponseType();
840                
841                try {
842                        OIDCResponseTypeValidator.validate(rt);
843                        
844                } catch (IllegalArgumentException e) {
845                        
846                        throw new ParseException("Unsupported \"response_type\" parameter: " + e.getMessage(), 
847                                                 OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, 
848                                                 clientID, redirectURI, state);
849                }
850                
851                // Required in OIDC, must include "openid" parameter
852                Scope scope = ar.getScope();
853
854                if (scope == null)
855                        throw new ParseException("Missing \"scope\" parameter", 
856                                                 OAuth2Error.INVALID_REQUEST, 
857                                                 clientID, redirectURI, state);
858
859                if (! scope.contains(OIDCScopeValue.OPENID))
860                        throw new ParseException("The scope must include an \"openid\" token",
861                                                 OAuth2Error.INVALID_REQUEST, 
862                                                 clientID, redirectURI, state);
863
864
865
866
867                // Parse the remaining OIDC parameters
868                Nonce nonce = Nonce.parse(params.get("nonce"));
869                
870                // Nonce required in implicit flow
871                if (rt.impliesImplicitFlow() && nonce == null)
872                        throw new ParseException("Missing \"nonce\" parameter: Required in implicit flow",
873                                                 OAuth2Error.INVALID_REQUEST,
874                                                 clientID, redirectURI, state);
875                
876                Display display;
877                
878                try {
879                        display = Display.parse(params.get("display"));
880
881                } catch (ParseException e) {
882
883                        throw new ParseException("Invalid \"display\" parameter: " + e.getMessage(), 
884                                                 OAuth2Error.INVALID_REQUEST,
885                                                 clientID, redirectURI, state, e);
886                }
887                
888                
889                Prompt prompt;
890                
891                try {
892                        prompt = Prompt.parse(params.get("prompt"));
893                                
894                } catch (ParseException e) {
895                        
896                        throw new ParseException("Invalid \"prompt\" parameter: " + e.getMessage(), 
897                                                 OAuth2Error.INVALID_REQUEST,
898                                                 clientID, redirectURI, state, e);
899                }
900
901
902                String v = params.get("max_age");
903
904                int maxAge = 0;
905
906                if (StringUtils.isNotBlank(v)) {
907
908                        try {
909                                maxAge = Integer.parseInt(v);
910
911                        } catch (NumberFormatException e) {
912
913                                throw new ParseException("Invalid \"max_age\" parameter: " + e.getMessage(),
914                                                         OAuth2Error.INVALID_REQUEST,
915                                                         clientID, redirectURI, state, e);
916                        }
917                }
918
919
920                v = params.get("ui_locales");
921
922                List<LangTag> uiLocales = null;
923
924                if (StringUtils.isNotBlank(v)) {
925
926                        uiLocales = new LinkedList<LangTag>();
927
928                        StringTokenizer st = new StringTokenizer(v, " ");
929
930                        while (st.hasMoreTokens()) {
931
932                                try {
933                                        uiLocales.add(LangTag.parse(st.nextToken()));
934
935                                } catch (LangTagException e) {
936
937                                        throw new ParseException("Invalid \"ui_locales\" parameter: " + e.getMessage(),
938                                                                 OAuth2Error.INVALID_REQUEST,
939                                                                 clientID, redirectURI, state, e);
940                                }
941                        }
942                }
943
944
945                v = params.get("claims_locales");
946
947                List<LangTag> claimsLocales = null;
948
949                if (StringUtils.isNotBlank(v)) {
950
951                        claimsLocales = new LinkedList<LangTag>();
952
953                        StringTokenizer st = new StringTokenizer(v, " ");
954
955                        while (st.hasMoreTokens()) {
956
957                                try {
958                                        claimsLocales.add(LangTag.parse(st.nextToken()));
959
960                                } catch (LangTagException e) {
961
962                                        throw new ParseException("Invalid \"claims_locales\" parameter: " + e.getMessage(),
963                                                                 OAuth2Error.INVALID_REQUEST,
964                                                                 clientID, redirectURI, state, e);
965                                }
966                        }
967                }
968
969
970                v = params.get("id_token_hint");
971                
972                JWT idTokenHint = null;
973                
974                if (StringUtils.isNotBlank(v)) {
975                
976                        try {
977                                idTokenHint = JWTParser.parse(v);
978                                
979                        } catch (java.text.ParseException e) {
980                
981                                throw new ParseException("Invalid \"id_token_hint\" parameter: " + e.getMessage(), 
982                                                         OAuth2Error.INVALID_REQUEST,
983                                                         clientID, redirectURI, state, e);
984                        }
985                }
986
987                String loginHint = params.get("login_hint");
988
989
990                v = params.get("acr_values");
991
992                List<ACR> acrValues = null;
993
994                if (StringUtils.isNotBlank(v)) {
995
996                        acrValues = new LinkedList<ACR>();
997
998                        StringTokenizer st = new StringTokenizer(v, " ");
999
1000                        while (st.hasMoreTokens()) {
1001
1002                                acrValues.add(new ACR(st.nextToken()));
1003                        }
1004                }
1005
1006
1007                v = params.get("claims");
1008
1009                ClaimsRequest claims = null;
1010
1011                if (StringUtils.isNotBlank(v)) {
1012
1013                        JSONObject jsonObject;
1014
1015                        try {
1016                                jsonObject = JSONObjectUtils.parseJSONObject(v);
1017
1018                        } catch (ParseException e) {
1019
1020                                throw new ParseException("Invalid \"claims\" parameter: " + e.getMessage(),
1021                                                         OAuth2Error.INVALID_REQUEST,
1022                                                         clientID, redirectURI, state, e);
1023                        }
1024
1025                        // Parse exceptions silently ignored
1026                        claims = ClaimsRequest.parse(jsonObject);
1027                }
1028                
1029                
1030                v = params.get("request_uri");
1031                
1032                URL requestURI = null;
1033                
1034                if (StringUtils.isNotBlank(v)) {
1035
1036                        try {
1037                                requestURI = new URL(v);
1038                
1039                        } catch (MalformedURLException e) {
1040                        
1041                                throw new ParseException("Invalid \"redirect_uri\" parameter: " + e.getMessage(), 
1042                                                         OAuth2Error.INVALID_REQUEST,
1043                                                         clientID, redirectURI, state, e);
1044                        }
1045                }
1046
1047                v = params.get("request");
1048
1049                JWT requestObject = null;
1050
1051                if (StringUtils.isNotBlank(v)) {
1052
1053                        // request_object and request_uri must not be defined at the same time
1054                        if (requestURI != null) {
1055
1056                                throw new ParseException("Invalid request: Found mutually exclusive \"request\" and \"request_uri\" parameters",
1057                                                         OAuth2Error.INVALID_REQUEST,
1058                                                         clientID, redirectURI, state, null);
1059                        }
1060
1061                        try {
1062                                requestObject = JWTParser.parse(v);
1063                                
1064                        } catch (java.text.ParseException e) {
1065                
1066                                throw new ParseException("Invalid \"request_object\" parameter: " + e.getMessage(), 
1067                                                         OAuth2Error.INVALID_REQUEST,
1068                                                         clientID, redirectURI, state, e);
1069                        }
1070                }
1071                
1072                
1073                // Select the appropriate constructor
1074                
1075                // Inline request object
1076                if (requestObject != null)
1077                        return new OIDCAuthorizationRequest(uri, rt, scope, clientID, redirectURI, state, nonce,
1078                                                            display, prompt, maxAge, uiLocales, claimsLocales, 
1079                                                            idTokenHint, loginHint, acrValues, claims, requestObject);
1080        
1081                // Request object by URL reference
1082                if (requestURI != null)
1083                        return new OIDCAuthorizationRequest(uri, rt, scope, clientID, redirectURI, state, nonce,
1084                                                            display, prompt, maxAge, uiLocales, claimsLocales, 
1085                                                            idTokenHint, loginHint, acrValues, claims, requestURI);
1086                
1087                // No request object or URI
1088                return new OIDCAuthorizationRequest(uri, rt, scope, clientID, redirectURI, state, nonce,
1089                                                    display, prompt, maxAge, uiLocales, claimsLocales, 
1090                                                    idTokenHint, loginHint, acrValues, claims);
1091        }
1092        
1093        
1094        /**
1095         * Parses an OpenID Connect authorisation request from the specified 
1096         * URL query string.
1097         *
1098         * <p>Example URL query string:
1099         *
1100         * <pre>
1101         * response_type=token%20id_token
1102         * &amp;client_id=s6BhdRkqt3
1103         * &amp;redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
1104         * &amp;scope=openid%20profile
1105         * &amp;state=af0ifjsldkj
1106         * &amp;nonce=n-0S6_WzA2Mj
1107         * </pre>
1108         *
1109         * @param uri   The URI of the authorisation endpoint. May be 
1110         *              {@code null} if the {@link #toHTTPRequest()} method 
1111         *              will not be used.
1112         * @param query The URL query string. Must not be {@code null}.
1113         *
1114         * @return The OpenID Connect authorisation request.
1115         *
1116         * @throws ParseException If the query string couldn't be parsed to an 
1117         *                        OpenID Connect authorisation request.
1118         */
1119        public static OIDCAuthorizationRequest parse(final URL uri, final String query)
1120                throws ParseException {
1121        
1122                return parse(uri, URLUtils.parseParameters(query));
1123        }
1124        
1125        
1126        /**
1127         * Parses an authorisation request from the specified HTTP GET or HTTP
1128         * POST request.
1129         *
1130         * <p>Example HTTP request (GET):
1131         *
1132         * <pre>
1133         * https://server.example.com/op/authorize?
1134         * response_type=code%20id_token
1135         * &amp;client_id=s6BhdRkqt3
1136         * &amp;redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
1137         * &amp;scope=openid
1138         * &amp;nonce=n-0S6_WzA2Mj
1139         * &amp;state=af0ifjsldkj
1140         * </pre>
1141         *
1142         * @param httpRequest The HTTP request. Must not be {@code null}.
1143         *
1144         * @return The OpenID Connect authorisation request.
1145         *
1146         * @throws ParseException If the HTTP request couldn't be parsed to an 
1147         *                        OpenID Connect authorisation request.
1148         */
1149        public static AuthorizationRequest parse(final HTTPRequest httpRequest) 
1150                throws ParseException {
1151                
1152                String query = httpRequest.getQuery();
1153                
1154                if (query == null)
1155                        throw new ParseException("Missing URL query string");
1156                
1157                return parse(httpRequest.getURL(), query);
1158        }
1159}