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                // Required in OIDC
829                URL redirectURI = ar.getRedirectionURI();
830
831                if (redirectURI == null)
832                        throw new ParseException("Missing \"redirect_uri\" parameter", 
833                                                 OAuth2Error.INVALID_REQUEST);
834
835                State state = ar.getState();
836
837                ResponseType rt = ar.getResponseType();
838                
839                try {
840                        OIDCResponseTypeValidator.validate(rt);
841                        
842                } catch (IllegalArgumentException e) {
843                        
844                        throw new ParseException("Unsupported \"response_type\" parameter: " + e.getMessage(), 
845                                                 OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, 
846                                                 redirectURI, state);
847                }
848                
849                // Required in OIDC, must include "openid" parameter
850                Scope scope = ar.getScope();
851
852                if (scope == null)
853                        throw new ParseException("Missing \"scope\" parameter", 
854                                                 OAuth2Error.INVALID_REQUEST, 
855                                                 redirectURI, state);
856
857                if (! scope.contains(OIDCScopeValue.OPENID))
858                        throw new ParseException("The scope must include an \"openid\" token",
859                                                 OAuth2Error.INVALID_REQUEST, 
860                                                 redirectURI, state);
861
862                ClientID clientID = ar.getClientID();
863
864
865                // Parse the remaining OIDC parameters
866                Nonce nonce = Nonce.parse(params.get("nonce"));
867                
868                // Nonce required in implicit flow
869                if (rt.impliesImplicitFlow() && nonce == null)
870                        throw new ParseException("Missing \"nonce\" parameter: Required in implicit flow",
871                                                 OAuth2Error.INVALID_REQUEST,
872                                                 redirectURI, state);
873                
874                Display display = null;
875                
876                try {
877                        display = Display.parse(params.get("display"));
878
879                } catch (ParseException e) {
880
881                        throw new ParseException("Invalid \"display\" parameter: " + e.getMessage(), 
882                                                 OAuth2Error.INVALID_REQUEST,
883                                                 redirectURI, state, e);
884                }
885                
886                
887                Prompt prompt = null;
888                
889                try {
890                        prompt = Prompt.parse(params.get("prompt"));
891                                
892                } catch (ParseException e) {
893                        
894                        throw new ParseException("Invalid \"prompt\" parameter: " + e.getMessage(), 
895                                                 OAuth2Error.INVALID_REQUEST,
896                                                 redirectURI, state, e);
897                }
898
899
900                String v = params.get("max_age");
901
902                int maxAge = 0;
903
904                if (StringUtils.isNotBlank(v)) {
905
906                        try {
907                                maxAge = Integer.parseInt(v);
908
909                        } catch (NumberFormatException e) {
910
911                                throw new ParseException("Invalid \"max_age\" parameter: " + e.getMessage(),
912                                                         OAuth2Error.INVALID_REQUEST,
913                                                         redirectURI, state, e);
914                        }
915                }
916
917
918                v = params.get("ui_locales");
919
920                List<LangTag> uiLocales = null;
921
922                if (StringUtils.isNotBlank(v)) {
923
924                        uiLocales = new LinkedList<LangTag>();
925
926                        StringTokenizer st = new StringTokenizer(v, " ");
927
928                        while (st.hasMoreTokens()) {
929
930                                try {
931                                        uiLocales.add(LangTag.parse(st.nextToken()));
932
933                                } catch (LangTagException e) {
934
935                                        throw new ParseException("Invalid \"ui_locales\" parameter: " + e.getMessage(),
936                                                                 OAuth2Error.INVALID_REQUEST,
937                                                                 redirectURI, state, e);
938                                }
939                        }
940                }
941
942
943                v = params.get("claims_locales");
944
945                List<LangTag> claimsLocales = null;
946
947                if (StringUtils.isNotBlank(v)) {
948
949                        claimsLocales = new LinkedList<LangTag>();
950
951                        StringTokenizer st = new StringTokenizer(v, " ");
952
953                        while (st.hasMoreTokens()) {
954
955                                try {
956                                        claimsLocales.add(LangTag.parse(st.nextToken()));
957
958                                } catch (LangTagException e) {
959
960                                        throw new ParseException("Invalid \"claims_locales\" parameter: " + e.getMessage(),
961                                                                 OAuth2Error.INVALID_REQUEST,
962                                                                 redirectURI, state, e);
963                                }
964                        }
965                }
966
967
968                v = params.get("id_token_hint");
969                
970                JWT idTokenHint = null;
971                
972                if (StringUtils.isNotBlank(v)) {
973                
974                        try {
975                                idTokenHint = JWTParser.parse(v);
976                                
977                        } catch (java.text.ParseException e) {
978                
979                                throw new ParseException("Invalid \"id_token_hint\" parameter: " + e.getMessage(), 
980                                                         OAuth2Error.INVALID_REQUEST,
981                                                         redirectURI, state, e);
982                        }
983                }
984
985                String loginHint = params.get("login_hint");
986
987
988                v = params.get("acr_values");
989
990                List<ACR> acrValues = null;
991
992                if (StringUtils.isNotBlank(v)) {
993
994                        acrValues = new LinkedList<ACR>();
995
996                        StringTokenizer st = new StringTokenizer(v, " ");
997
998                        while (st.hasMoreTokens()) {
999
1000                                acrValues.add(new ACR(st.nextToken()));
1001                        }
1002                }
1003
1004
1005                v = params.get("claims");
1006
1007                ClaimsRequest claims = null;
1008
1009                if (StringUtils.isNotBlank(v)) {
1010
1011                        JSONObject jsonObject = null;
1012
1013                        try {
1014                                jsonObject = JSONObjectUtils.parseJSONObject(v);
1015
1016                        } catch (ParseException e) {
1017
1018                                throw new ParseException("Invalid \"claims\" parameter: " + e.getMessage(),
1019                                                         OAuth2Error.INVALID_REQUEST,
1020                                                         redirectURI, state, e);
1021                        }
1022
1023                        // Parse exceptions silently ignored
1024                        claims = ClaimsRequest.parse(jsonObject);
1025                }
1026                
1027                
1028                v = params.get("request_uri");
1029                
1030                URL requestURI = null;
1031                
1032                if (StringUtils.isNotBlank(v)) {
1033
1034                        try {
1035                                requestURI = new URL(v);
1036                
1037                        } catch (MalformedURLException e) {
1038                        
1039                                throw new ParseException("Invalid \"redirect_uri\" parameter: " + e.getMessage(), 
1040                                                         OAuth2Error.INVALID_REQUEST,
1041                                                         redirectURI, state, e);
1042                        }
1043                }
1044
1045                v = params.get("request");
1046
1047                JWT requestObject = null;
1048
1049                if (StringUtils.isNotBlank(v)) {
1050
1051                        // request_object and request_uri must not be defined at the same time
1052                        if (requestURI != null) {
1053
1054                                throw new ParseException("Invalid request: Found mutually exclusive \"request\" and \"request_uri\" parameters",
1055                                                         OAuth2Error.INVALID_REQUEST,
1056                                                         redirectURI, state, null);
1057                        }
1058
1059                        try {
1060                                requestObject = JWTParser.parse(v);
1061                                
1062                        } catch (java.text.ParseException e) {
1063                
1064                                throw new ParseException("Invalid \"request_object\" parameter: " + e.getMessage(), 
1065                                                         OAuth2Error.INVALID_REQUEST,
1066                                                         redirectURI, state, e);
1067                        }
1068                }
1069                
1070                
1071                // Select the appropriate constructor
1072                
1073                // Inline request object
1074                if (requestObject != null)
1075                        return new OIDCAuthorizationRequest(uri, rt, scope, clientID, redirectURI, state, nonce,
1076                                                            display, prompt, maxAge, uiLocales, claimsLocales, 
1077                                                            idTokenHint, loginHint, acrValues, claims, requestObject);
1078        
1079                // Request object by URL reference
1080                if (requestURI != null)
1081                        return new OIDCAuthorizationRequest(uri, rt, scope, clientID, redirectURI, state, nonce,
1082                                                            display, prompt, maxAge, uiLocales, claimsLocales, 
1083                                                            idTokenHint, loginHint, acrValues, claims, requestURI);
1084                
1085                // No request object or URI
1086                return new OIDCAuthorizationRequest(uri, rt, scope, clientID, redirectURI, state, nonce,
1087                                                    display, prompt, maxAge, uiLocales, claimsLocales, 
1088                                                    idTokenHint, loginHint, acrValues, claims);
1089        }
1090        
1091        
1092        /**
1093         * Parses an OpenID Connect authorisation request from the specified 
1094         * URL query string.
1095         *
1096         * <p>Example URL query string:
1097         *
1098         * <pre>
1099         * response_type=token%20id_token
1100         * &amp;client_id=s6BhdRkqt3
1101         * &amp;redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
1102         * &amp;scope=openid%20profile
1103         * &amp;state=af0ifjsldkj
1104         * &amp;nonce=n-0S6_WzA2Mj
1105         * </pre>
1106         *
1107         * @param uri   The URI of the authorisation endpoint. May be 
1108         *              {@code null} if the {@link #toHTTPRequest()} method 
1109         *              will not be used.
1110         * @param query The URL query string. Must not be {@code null}.
1111         *
1112         * @return The OpenID Connect authorisation request.
1113         *
1114         * @throws ParseException If the query string couldn't be parsed to an 
1115         *                        OpenID Connect authorisation request.
1116         */
1117        public static OIDCAuthorizationRequest parse(final URL uri, final String query)
1118                throws ParseException {
1119        
1120                return parse(uri, URLUtils.parseParameters(query));
1121        }
1122        
1123        
1124        /**
1125         * Parses an authorisation request from the specified HTTP GET or HTTP
1126         * POST request.
1127         *
1128         * <p>Example HTTP request (GET):
1129         *
1130         * <pre>
1131         * https://server.example.com/op/authorize?
1132         * response_type=code%20id_token
1133         * &amp;client_id=s6BhdRkqt3
1134         * &amp;redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
1135         * &amp;scope=openid
1136         * &amp;nonce=n-0S6_WzA2Mj
1137         * &amp;state=af0ifjsldkj
1138         * </pre>
1139         *
1140         * @param httpRequest The HTTP request. Must not be {@code null}.
1141         *
1142         * @return The OpenID Connect authorisation request.
1143         *
1144         * @throws ParseException If the HTTP request couldn't be parsed to an 
1145         *                        OpenID Connect authorisation request.
1146         */
1147        public static AuthorizationRequest parse(final HTTPRequest httpRequest) 
1148                throws ParseException {
1149                
1150                String query = httpRequest.getQuery();
1151                
1152                if (query == null)
1153                        throw new ParseException("Missing URL query string");
1154                
1155                return parse(httpRequest.getURL(), query);
1156        }
1157}