001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.openid.connect.sdk;
019
020
021import java.util.*;
022
023import com.nimbusds.langtag.LangTag;
024import com.nimbusds.langtag.LangTagException;
025import com.nimbusds.oauth2.sdk.ParseException;
026import com.nimbusds.oauth2.sdk.ResponseType;
027import com.nimbusds.oauth2.sdk.Scope;
028import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
029import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
030import net.jcip.annotations.Immutable;
031import net.minidev.json.JSONObject;
032
033
034/**
035 * Specifies the individual claims to return from the UserInfo endpoint and / 
036 * or in the ID Token.
037 *
038 * <p>Related specifications: 
039 * 
040 * <ul>
041 *     <li>OpenID Connect Core 1.0, section 5.5.
042 * </ul>
043 */
044public class ClaimsRequest {
045
046
047        /**
048         * Individual claim request.
049         *
050         * <p>Related specifications: 
051         * 
052         * <ul>
053         *     <li>OpenID Connect Core 1.0, section 5.5.1.
054         * </ul>
055         */
056        @Immutable
057        public static class Entry {
058
059
060                /**
061                 * The claim name.
062                 */
063                private final String claimName;
064
065
066                /**
067                 * The claim requirement.
068                 */
069                private final ClaimRequirement requirement;
070
071
072                /**
073                 * Optional language tag.
074                 */
075                private final LangTag langTag;
076
077
078                /**
079                 * Optional claim value.
080                 */
081                private final String value;
082
083
084                /**
085                 * Optional claim values.
086                 */
087                private final List<String> values;
088
089
090                /**
091                 * Creates a new individual claim request. The claim 
092                 * requirement is set to voluntary (the default) and no 
093                 * expected value(s) are specified.
094                 *
095                 * @param claimName   The claim name. Must not be {@code null}.
096                 * @param langTag     Optional language tag for the claim.
097                 */
098                public Entry(final String claimName, final LangTag langTag) {
099
100                        this(claimName, ClaimRequirement.VOLUNTARY, langTag, null, null);
101                }
102                
103                
104                /**
105                 * Creates a new individual claim request.
106                 *
107                 * @param claimName   The claim name. Must not be {@code null}.
108                 * @param requirement The claim requirement. Must not be 
109                 *                    {@code null}.
110                 */
111                public Entry(final String claimName, final ClaimRequirement requirement) {
112
113                        this(claimName, requirement, null, null, null);
114                }
115
116
117                /**
118                 * Creates a new individual claim request.
119                 *
120                 * @param claimName   The claim name. Must not be {@code null}.
121                 * @param requirement The claim requirement. Must not be 
122                 *                    {@code null}.
123                 * @param langTag     Optional language tag for the claim.
124                 * @param value       Optional expected value for the claim.
125                 */
126                public Entry(final String claimName, final ClaimRequirement requirement, 
127                             final LangTag langTag, final String value) {
128
129                        this(claimName, requirement, langTag, value, null);
130                }
131
132
133                /**
134                 * Creates a new individual claim request.
135                 *
136                 * @param claimName   The claim name. Must not be {@code null}.
137                 * @param requirement The claim requirement. Must not be 
138                 *                    {@code null}.
139                 * @param langTag     Optional language tag for the claim.
140                 * @param values      Optional expected values for the claim.
141                 */
142                public Entry(final String claimName, final ClaimRequirement requirement, 
143                             final LangTag langTag, final List<String> values) {
144
145                        this(claimName, requirement, langTag, null, values);
146                }
147
148
149                /**
150                 * Creates a new individual claim request. This constructor is
151                 * to be used privately. Ensures that {@code value} and 
152                 * {@code values} are not simultaneously specified.
153                 *
154                 * @param claimName   The claim name. Must not be {@code null}.
155                 * @param requirement The claim requirement. Must not be 
156                 *                    {@code null}.
157                 * @param langTag     Optional language tag for the claim.
158                 * @param value       Optional expected value for the claim. If
159                 *                    set, then the {@code values} parameter
160                 *                    must not be set.
161                 * @param values      Optional expected values for the claim. 
162                 *                    If set, then the {@code value} parameter
163                 *                    must not be set.
164                 */
165                private Entry(final String claimName, final ClaimRequirement requirement, final LangTag langTag, 
166                              final String value, final List<String> values) {
167
168                        if (claimName == null)
169                                throw new IllegalArgumentException("The claim name must not be null");
170
171                        this.claimName = claimName;
172
173
174                        if (requirement == null)
175                                throw new IllegalArgumentException("The claim requirement must not be null");
176
177                        this.requirement = requirement;
178
179
180                        this.langTag = langTag;
181
182
183                        if (value != null && values == null) {
184
185                                this.value = value;
186                                this.values = null;
187
188                        } else if (value == null && values != null) {
189
190                                this.value = null;
191                                this.values = values;
192
193                        } else if (value == null && values == null) {
194
195                                this.value = null;
196                                this.values = null;
197
198                        } else {
199
200                                throw new IllegalArgumentException("Either value or values must be specified, but not both");
201                        }
202                }
203
204
205                /**
206                 * Gets the claim name.
207                 *
208                 * @return The claim name.
209                 */
210                public String getClaimName() {
211
212                        return claimName;
213                }
214                
215                
216                /**
217                 * Gets the claim name, optionally with the language tag
218                 * appended.
219                 * 
220                 * <p>Example with language tag:
221                 * 
222                 * <pre>
223                 * name#de-DE
224                 * </pre>
225                 * 
226                 * @param withLangTag If {@code true} the language tag will be
227                 *                    appended to the name (if any), else not.
228                 * 
229                 * @return The claim name, with optionally appended language
230                 *         tag.
231                 */
232                public String getClaimName(final boolean withLangTag) {
233                        
234                        if (withLangTag && langTag != null)
235                                return claimName + "#" + langTag.toString();
236                        else
237                                return claimName;
238                }
239
240
241                /**
242                 * Gets the claim requirement.
243                 *
244                 * @return The claim requirement.
245                 */
246                public ClaimRequirement getClaimRequirement() {
247
248                        return requirement;
249                }
250
251
252                /**
253                 * Gets the optional language tag for the claim.
254                 *
255                 * @return The language tag, {@code null} if not specified.
256                 */
257                public LangTag getLangTag() {
258
259                        return langTag;
260                }
261
262
263                /**
264                 * Gets the optional value for the claim.
265                 *
266                 * @return The value, {@code null} if not specified.
267                 */
268                public String getValue() {
269
270                        return value;
271                }
272
273
274                /**
275                 * Gets the optional values for the claim.
276                 *
277                 * @return The values, {@code null} if not specified.
278                 */
279                public List<String> getValues() {
280
281                        return values;
282                }
283
284
285                /**
286                 * Returns the JSON object representation of the specified 
287                 * collection of individual claim requests.
288                 *
289                 * <p>Example:
290                 *
291                 * <pre>
292                 * {
293                 *   "given_name": {"essential": true},
294                 *   "nickname": null,
295                 *   "email": {"essential": true},
296                 *   "email_verified": {"essential": true},
297                 *   "picture": null,
298                 *   "http://example.info/claims/groups": null
299                 * }  
300                 * </pre>
301                 *
302                 * @param entries The entries to serialise. Must not be 
303                 *                {@code null}.
304                 *
305                 * @return The corresponding JSON object, empty if no claims 
306                 *         were found.
307                 */
308                public static JSONObject toJSONObject(final Collection<Entry> entries) {
309
310                        JSONObject o = new JSONObject();
311
312                        for (Entry entry: entries) {
313
314                                // Compose the optional value
315                                JSONObject entrySpec = null;
316
317                                if (entry.getValue() != null) {
318
319                                        entrySpec = new JSONObject();
320                                        entrySpec.put("value", entry.getValue());
321                                }
322
323                                if (entry.getValues() != null) {
324
325                                        // Either "value" or "values", or none
326                                        // may be defined
327                                        entrySpec = new JSONObject();
328                                        entrySpec.put("values", entry.getValues());
329                                }
330
331                                if (entry.getClaimRequirement().equals(ClaimRequirement.ESSENTIAL)) {
332
333                                        if (entrySpec == null)
334                                                entrySpec = new JSONObject();
335
336                                        entrySpec.put("essential", true);
337                                }
338
339                                o.put(entry.getClaimName(true), entrySpec);
340                        }
341
342                        return o;
343                }
344
345
346                /**
347                 * Parses a collection of individual claim requests from the
348                 * specified JSON object. Request entries that are not 
349                 * understood are silently ignored.
350                 *
351                 * @param jsonObject The JSON object to parse. Must not be
352                 *                   {@code null}.
353                 *
354                 * @return The collection of claim requests.
355                 */
356                public static Collection<Entry> parseEntries(final JSONObject jsonObject) {
357
358                        Collection<Entry> entries = new LinkedList<>();
359
360                        if (jsonObject.isEmpty())
361                                return entries;
362
363                        for (Map.Entry<String,Object> member: jsonObject.entrySet()) {
364
365                                // Process the key
366                                String claimNameWithOptLangTag = member.getKey();
367
368                                String claimName;
369                                LangTag langTag = null;
370
371                                if (claimNameWithOptLangTag.contains("#")) {
372
373                                        String[] parts = claimNameWithOptLangTag.split("#", 2);
374
375                                        claimName = parts[0];
376
377                                        try {
378                                                langTag = LangTag.parse(parts[1]);
379
380                                        } catch (LangTagException e) {
381
382                                                // Ignore and continue
383                                                continue;
384                                        }
385
386                                } else {
387                                        claimName = claimNameWithOptLangTag;
388                                }
389
390                                // Parse the optional value
391                                if (member.getValue() == null) {
392
393                                        // Voluntary claim with no value(s)
394                                        entries.add(new Entry(claimName, langTag));
395                                        continue;
396                                }
397
398                                try {
399                                        JSONObject entrySpec = (JSONObject)member.getValue();
400
401                                        ClaimRequirement requirement = ClaimRequirement.VOLUNTARY;
402
403                                        if (entrySpec.containsKey("essential")) {
404
405                                                boolean isEssential = (Boolean)entrySpec.get("essential");
406
407                                                if (isEssential)
408                                                        requirement = ClaimRequirement.ESSENTIAL;
409                                        }
410
411                                        if (entrySpec.containsKey("value")) {
412
413                                                String expectedValue = (String)entrySpec.get("value");
414
415                                                entries.add(new Entry(claimName, requirement, langTag, expectedValue));
416
417                                        } else if (entrySpec.containsKey("values")) {
418
419                                                List<String> expectedValues = new LinkedList<>();
420
421                                                for (Object v: (List)entrySpec.get("values")) {
422
423                                                        expectedValues.add((String)v);
424                                                }
425
426                                                entries.add(new Entry(claimName, requirement, langTag, expectedValues));
427
428                                        } else {
429                                                entries.add(new Entry(claimName, requirement, langTag, (String)null));
430                                        }
431
432                                } catch (Exception e) {
433                                        // Ignore and continue
434                                }
435                        }
436
437                        return entries;
438                }
439        }
440
441
442        /**
443         * The requested ID token claims, keyed by claim name and language tag.
444         */
445        private final Map<Map.Entry<String,LangTag>,Entry> idTokenClaims =
446                new HashMap<>();
447
448
449        /**
450         * The requested UserInfo claims, keyed by claim name and language tag.
451         */
452        private final Map<Map.Entry<String,LangTag>,Entry> userInfoClaims =
453                new HashMap<>();
454        
455
456        /**
457         * Creates a new empty claims request.
458         */
459        public ClaimsRequest() {
460
461                // Nothing to initialise
462        }
463        
464        
465        /**
466         * Adds the entries from the specified other claims request.
467         * 
468         * @param other The other claims request. If {@code null} no claims
469         *              request entries will be added to this claims request.
470         */
471        public void add(final ClaimsRequest other) {
472                
473                if (other == null)
474                        return;
475                
476                idTokenClaims.putAll(other.idTokenClaims);
477                userInfoClaims.putAll(other.userInfoClaims);
478        }
479
480
481        /**
482         * Adds the specified ID token claim to the request. It is marked as
483         * voluntary and no language tag and value(s) are associated with it.
484         *
485         * @param claimName The claim name. Must not be {@code null}.
486         */
487        public void addIDTokenClaim(final String claimName) {
488
489                addIDTokenClaim(claimName, ClaimRequirement.VOLUNTARY);
490        }
491
492
493        /**
494         * Adds the specified ID token claim to the request. No language tag 
495         * and value(s) are associated with it.
496         *
497         * @param claimName   The claim name. Must not be {@code null}.
498         * @param requirement The claim requirement. Must not be {@code null}.
499         */
500        public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement) {
501
502                addIDTokenClaim(claimName, requirement, null);
503        }
504
505
506        /**
507         * Adds the specified ID token claim to the request. No value(s) are 
508         * associated with it.
509         *
510         * @param claimName   The claim name. Must not be {@code null}.
511         * @param requirement The claim requirement. Must not be {@code null}.
512         * @param langTag     The associated language tag, {@code null} if not
513         *                    specified.
514         */
515        public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 
516                                    final LangTag langTag) {
517
518
519                addIDTokenClaim(claimName, requirement, langTag, (String)null);
520        }
521
522
523        /**
524         * Adds the specified ID token claim to the request.
525         *
526         * @param claimName   The claim name. Must not be {@code null}.
527         * @param requirement The claim requirement. Must not be {@code null}.
528         * @param langTag     The associated language tag, {@code null} if not
529         *                    specified.
530         * @param value       The expected claim value, {@code null} if not
531         *                    specified.
532         */
533        public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 
534                                    final LangTag langTag, final String value) {
535
536                addIDTokenClaim(new Entry(claimName, requirement, langTag, value));
537        }
538
539
540        /**
541         * Adds the specified ID token claim to the request.
542         *
543         * @param claimName   The claim name. Must not be {@code null}.
544         * @param requirement The claim requirement. Must not be {@code null}.
545         * @param langTag     The associated language tag, {@code null} if not
546         *                    specified.
547         * @param values      The expected claim values, {@code null} if not
548         *                    specified.
549         */
550        public void addIDTokenClaim(final String claimName, final ClaimRequirement requirement, 
551                                    final LangTag langTag, final List<String> values) {
552
553                addIDTokenClaim(new Entry(claimName, requirement, langTag, values));
554        }
555
556
557        /**
558         * Adds the specified ID token claim to the request.
559         *
560         * @param entry The individual ID token claim request. Must not be
561         *              {@code null}.
562         */
563        public void addIDTokenClaim(final Entry entry) {
564
565                Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>(
566                        entry.getClaimName(),
567                        entry.getLangTag());
568
569                idTokenClaims.put(key, entry);
570        }
571
572
573        /**
574         * Gets the requested ID token claims.
575         *
576         * @return The ID token claims, as an unmodifiable collection, empty 
577         *         set if none.
578         */
579        public Collection<Entry> getIDTokenClaims() {
580
581                return Collections.unmodifiableCollection(idTokenClaims.values());
582        }
583        
584        
585        /**
586         * Gets the names of the requested ID token claim names.
587         * 
588         * @param withLangTag If {@code true} the language tags, if any, will 
589         *                    be appended to the names, else not.
590         * 
591         * 
592         * @return The ID token claim names, as an unmodifiable set, empty set
593         *         if none.
594         */
595        public Set<String> getIDTokenClaimNames(final boolean withLangTag) {
596                
597                Set<String> names = new HashSet<>();
598                
599                for (Entry en: idTokenClaims.values())
600                        names.add(en.getClaimName(withLangTag));
601                
602                return Collections.unmodifiableSet(names);
603        }
604
605
606        /**
607         * Removes the specified ID token claim from the request.
608         *
609         * @param claimName The claim name. Must not be {@code null}.
610         * @param langTag   The associated language tag, {@code null} if none.
611         *
612         * @return The removed ID token claim, {@code null} if not found.
613         */
614        public Entry removeIDTokenClaim(final String claimName, final LangTag langTag) {
615
616                Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>(
617                        claimName,
618                        langTag);
619
620                return idTokenClaims.remove(key);
621        }
622
623
624        /**
625         * Removes the specified ID token claims from the request, in all
626         * existing language tag variations.
627         *
628         * @param claimName The claim name. Must not be {@code null}.
629         *
630         * @return The removed ID token claims, as an unmodifiable collection,
631         *         empty set if none were found.
632         */
633        public Collection<Entry> removeIDTokenClaims(final String claimName) {
634
635                Collection<Entry> removedClaims = new LinkedList<>();
636
637                Iterator<Map.Entry<Map.Entry<String,LangTag>,Entry>> it = idTokenClaims.entrySet().iterator();
638
639                while (it.hasNext()) {
640
641                        Map.Entry<Map.Entry<String,LangTag>,Entry> reqEntry = it.next();
642
643                        if (reqEntry.getKey().getKey().equals(claimName)) {
644
645                                removedClaims.add(reqEntry.getValue());
646
647                                it.remove();
648                        }
649                }
650
651                return Collections.unmodifiableCollection(removedClaims);
652        }
653
654
655        /**
656         * Adds the specified UserInfo claim to the request. It is marked as
657         * voluntary and no language tag and value(s) are associated with it.
658         *
659         * @param claimName The claim name. Must not be {@code null}.
660         */
661        public void addUserInfoClaim(final String claimName) {
662
663                addUserInfoClaim(claimName, ClaimRequirement.VOLUNTARY);
664        }
665
666
667        /**
668         * Adds the specified UserInfo claim to the request. No language tag 
669         * and value(s) are associated with it.
670         *
671         * @param claimName   The claim name. Must not be {@code null}.
672         * @param requirement The claim requirement. Must not be {@code null}.
673         */
674        public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement) {
675
676                addUserInfoClaim(claimName, requirement, null);
677        }
678
679
680        /**
681         * Adds the specified UserInfo claim to the request. No value(s) are 
682         * associated with it.
683         *
684         * @param claimName   The claim name. Must not be {@code null}.
685         * @param requirement The claim requirement. Must not be {@code null}.
686         * @param langTag     The associated language tag, {@code null} if not
687         *                    specified.
688         */
689        public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 
690                                     final LangTag langTag) {
691
692
693                addUserInfoClaim(claimName, requirement, langTag, (String)null);
694        }
695
696
697        /**
698         * Adds the specified UserInfo claim to the request.
699         *
700         * @param claimName   The claim name. Must not be {@code null}.
701         * @param requirement The claim requirement. Must not be {@code null}.
702         * @param langTag     The associated language tag, {@code null} if not
703         *                    specified.
704         * @param value       The expected claim value, {@code null} if not
705         *                    specified.
706         */
707        public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 
708                                     final LangTag langTag, final String value) {
709
710                addUserInfoClaim(new Entry(claimName, requirement, langTag, value));
711        }
712
713
714        /**
715         * Adds the specified UserInfo claim to the request.
716         *
717         * @param claimName   The claim name. Must not be {@code null}.
718         * @param requirement The claim requirement. Must not be {@code null}.
719         * @param langTag     The associated language tag, {@code null} if not
720         *                    specified.
721         * @param values      The expected claim values, {@code null} if not
722         *                    specified.
723         */
724        public void addUserInfoClaim(final String claimName, final ClaimRequirement requirement, 
725                                     final LangTag langTag, final List<String> values) {
726
727                addUserInfoClaim(new Entry(claimName, requirement, langTag, values));
728        }
729
730
731        /**
732         * Adds the specified UserInfo claim to the request.
733         *
734         * @param entry The individual UserInfo claim request. Must not be
735         *              {@code null}.
736         */
737        public void addUserInfoClaim(final Entry entry) {
738
739                Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>(
740                        entry.getClaimName(),
741                        entry.getLangTag());
742
743                userInfoClaims.put(key, entry);
744        }
745
746
747        /**
748         * Gets the requested UserInfo claims.
749         *
750         * @return The UserInfo claims, as an unmodifiable collection, empty 
751         *         set if none.
752         */
753        public Collection<Entry> getUserInfoClaims() {
754
755                return Collections.unmodifiableCollection(userInfoClaims.values());
756        }
757        
758        
759        /**
760         * Gets the names of the requested UserInfo claim names.
761         * 
762         * @param withLangTag If {@code true} the language tags, if any, will 
763         *                    be appended to the names, else not.
764         * 
765         * 
766         * @return The UserInfo claim names, as an unmodifiable set, empty set
767         *         if none.
768         */
769        public Set<String> getUserInfoClaimNames(final boolean withLangTag) {
770                
771                Set<String> names = new HashSet<>();
772                
773                for (Entry en: userInfoClaims.values())
774                        names.add(en.getClaimName(withLangTag));
775                
776                return Collections.unmodifiableSet(names);
777        }
778
779
780        /**
781         * Removes the specified UserInfo claim from the request.
782         *
783         * @param claimName The claim name. Must not be {@code null}.
784         * @param langTag   The associated language tag, {@code null} if none.
785         *
786         * @return The removed UserInfo claim, {@code null} if not found.
787         */
788        public Entry removeUserInfoClaim(final String claimName, final LangTag langTag) {
789
790                Map.Entry<String,LangTag> key = new AbstractMap.SimpleImmutableEntry<>(
791                        claimName,
792                        langTag);
793
794                return userInfoClaims.remove(key);
795        }
796
797
798        /**
799         * Removes the specified UserInfo claims from the request, in all
800         * existing language tag variations.
801         *
802         * @param claimName The claim name. Must not be {@code null}.
803         *
804         * @return The removed UserInfo claims, as an unmodifiable collection,
805         *         empty set if none were found.
806         */
807        public Collection<Entry> removeUserInfoClaims(final String claimName) {
808
809                Collection<Entry> removedClaims = new LinkedList<>();
810
811                Iterator<Map.Entry<Map.Entry<String,LangTag>,Entry>> it = userInfoClaims.entrySet().iterator();
812
813                while (it.hasNext()) {
814
815                        Map.Entry<Map.Entry<String,LangTag>,Entry> reqEntry = it.next();
816
817                        if (reqEntry.getKey().getKey().equals(claimName)) {
818
819                                removedClaims.add(reqEntry.getValue());
820
821                                it.remove();
822                        }
823                }
824
825                return Collections.unmodifiableCollection(removedClaims);
826        }
827
828
829        /**
830         * Returns the JSON object representation of this claims request.
831         *
832         * <p>Example:
833         *
834         * <pre>
835         * {
836         *   "userinfo":
837         *    {
838         *     "given_name": {"essential": true},
839         *     "nickname": null,
840         *     "email": {"essential": true},
841         *     "email_verified": {"essential": true},
842         *     "picture": null,
843         *     "http://example.info/claims/groups": null
844         *    },
845         *   "id_token":
846         *    {
847         *     "auth_time": {"essential": true},
848         *     "acr": {"values": ["urn:mace:incommon:iap:silver"] }
849         *    }
850         * }
851         * </pre>
852         *
853         * @return The corresponding JSON object, empty if no ID token and 
854         *         UserInfo claims are specified.
855         */
856        public JSONObject toJSONObject() {
857
858                JSONObject o = new JSONObject();
859
860                Collection<Entry> idTokenEntries = getIDTokenClaims();
861
862                if (! idTokenEntries.isEmpty()) {
863
864                        o.put("id_token", Entry.toJSONObject(idTokenEntries));
865                }
866
867                Collection<Entry> userInfoEntries = getUserInfoClaims();
868
869                if (! userInfoEntries.isEmpty()) {
870
871                        o.put("userinfo", Entry.toJSONObject(userInfoEntries));
872                }
873
874                return o;
875        }
876
877
878        @Override
879        public String toString() {
880
881                return toJSONObject().toString();
882        }
883        
884        
885        /**
886         * Resolves the claims request for the specified response type and
887         * scope. The scope values that are {@link OIDCScopeValue standard
888         * OpenID scope values} are resolved to their respective individual
889         * claims requests, any other scope values are ignored.
890         *
891         * @param responseType The response type. Must not be {@code null}.
892         * @param scope        The scope, {@code null} if not specified (for a
893         *                     plain OAuth 2.0 authorisation request with no
894         *                     scope explicitly specified).
895         * 
896         * @return The claims request.
897         */
898        public static ClaimsRequest resolve(final ResponseType responseType, final Scope scope) {
899        
900                return resolve(responseType, scope, Collections.<Scope.Value,Set<String>>emptyMap());
901        }
902        
903        
904        /**
905         * Resolves the claims request for the specified response type and
906         * scope. The scope values that are {@link OIDCScopeValue standard
907         * OpenID scope values} are resolved to their respective individual
908         * claims requests, any other scope values are checked in the specified
909         * custom claims map and resolved accordingly.
910         *
911         * @param responseType The response type. Must not be {@code null}.
912         * @param scope        The scope, {@code null} if not specified (for a
913         *                     plain OAuth 2.0 authorisation request with no
914         *                     scope explicitly specified).
915         * @param customClaims Custom scope to claim name map, {@code null} if
916         *                     not specified.
917         *
918         * @return The claims request.
919         */
920        public static ClaimsRequest resolve(final ResponseType responseType,
921                                            final Scope scope,
922                                            final Map<Scope.Value,Set<String>> customClaims) {
923
924                // Determine the claims target (ID token or UserInfo)
925                final boolean switchToIDToken =
926                        responseType.contains(OIDCResponseTypeValue.ID_TOKEN) &&
927                        ! responseType.contains(ResponseType.Value.CODE) &&
928                        ! responseType.contains(ResponseType.Value.TOKEN);
929
930                ClaimsRequest claimsRequest = new ClaimsRequest();
931                
932                if (scope == null) {
933                        // Plain OAuth 2.0 mode
934                        return claimsRequest;
935                }
936                
937                for (Scope.Value value: scope) {
938                        
939                        Set<ClaimsRequest.Entry> entries;
940                        
941                        if (value.equals(OIDCScopeValue.PROFILE)) {
942                                
943                                entries = OIDCScopeValue.PROFILE.toClaimsRequestEntries();
944                                
945                        } else if (value.equals(OIDCScopeValue.EMAIL)) {
946                                
947                                entries = OIDCScopeValue.EMAIL.toClaimsRequestEntries();
948                                
949                        } else if (value.equals(OIDCScopeValue.PHONE)) {
950                                
951                                entries = OIDCScopeValue.PHONE.toClaimsRequestEntries();
952                                
953                        } else if (value.equals(OIDCScopeValue.ADDRESS)) {
954                                
955                                entries = OIDCScopeValue.ADDRESS.toClaimsRequestEntries();
956                                
957                        } else if (customClaims != null && customClaims.containsKey(value)) {
958                                
959                                Set<String> claimNames = customClaims.get(value);
960                                
961                                if (claimNames == null || claimNames.isEmpty()) {
962                                        continue; // skip
963                                }
964                                
965                                entries = new HashSet<>();
966                                
967                                for (String claimName: claimNames) {
968                                        entries.add(new ClaimsRequest.Entry(claimName, ClaimRequirement.VOLUNTARY));
969                                }
970                                
971                        } else {
972                                
973                                continue; // skip
974                        }
975                        
976                        for (ClaimsRequest.Entry en: entries) {
977
978                                if (switchToIDToken)
979                                        claimsRequest.addIDTokenClaim(en);
980                                else
981                                        claimsRequest.addUserInfoClaim(en);
982                        }
983                }
984                
985                return claimsRequest;
986        }
987
988
989        /**
990         * Resolves the merged claims request from the specified OpenID
991         * authentication request parameters. The scope values that are
992         * {@link OIDCScopeValue standard OpenID scope values} are resolved to
993         * their respective individual claims requests, any other scope values
994         * are ignored.
995         *
996         * @param responseType  The response type. Must not be {@code null}.
997         * @param scope         The scope, {@code null} if not specified (for a
998         *                      plain OAuth 2.0 authorisation request with no
999         *                      scope explicitly specified).
1000         * @param claimsRequest The claims request, corresponding to the
1001         *                      optional {@code claims} OpenID Connect
1002         *                      authorisation request parameter, {@code null}
1003         *                      if not specified.
1004         *
1005         * @return The merged claims request.
1006         */
1007        public static ClaimsRequest resolve(final ResponseType responseType,
1008                                            final Scope scope,
1009                                            final ClaimsRequest claimsRequest) {
1010                
1011                return resolve(responseType, scope, claimsRequest, Collections.<Scope.Value,Set<String>>emptyMap());
1012        }
1013
1014
1015        /**
1016         * Resolves the merged claims request from the specified OpenID
1017         * authentication request parameters. The scope values that are
1018         * {@link OIDCScopeValue standard OpenID scope values} are resolved to
1019         * their respective individual claims requests, any other scope values
1020         * are checked in the specified custom claims map and resolved
1021         * accordingly.
1022         *
1023         * @param responseType  The response type. Must not be {@code null}.
1024         * @param scope         The scope, {@code null} if not specified (for a
1025         *                      plain OAuth 2.0 authorisation request with no
1026         *                      scope explicitly specified).
1027         * @param claimsRequest The claims request, corresponding to the
1028         *                      optional {@code claims} OpenID Connect
1029         *                      authorisation request parameter, {@code null}
1030         *                      if not specified.
1031         * @param customClaims  Custom scope to claim name map, {@code null} if
1032         *                      not specified.
1033         *
1034         * @return The merged claims request.
1035         */
1036        public static ClaimsRequest resolve(final ResponseType responseType,
1037                                            final Scope scope,
1038                                            final ClaimsRequest claimsRequest,
1039                                            final Map<Scope.Value,Set<String>> customClaims) {
1040
1041                ClaimsRequest mergedClaimsRequest = resolve(responseType, scope, customClaims);
1042
1043                mergedClaimsRequest.add(claimsRequest);
1044
1045                return mergedClaimsRequest;
1046        }
1047
1048
1049        /**
1050         * Resolves the merged claims request for the specified OpenID
1051         * authentication request. The scope values that are
1052         * {@link OIDCScopeValue standard OpenID scope values} are resolved to
1053         * their respective individual claims requests, any other scope values
1054         * are ignored.
1055         *
1056         * @param authRequest The OpenID authentication request. Must not be
1057         *                    {@code null}.
1058         *
1059         * @return The merged claims request.
1060         */
1061        public static ClaimsRequest resolve(final AuthenticationRequest authRequest) {
1062
1063                return resolve(authRequest.getResponseType(), authRequest.getScope(), authRequest.getClaims());
1064        }
1065
1066
1067        /**
1068         * Parses a claims request from the specified JSON object 
1069         * representation. Unexpected members in the JSON object are silently
1070         * ignored.
1071         *
1072         * @param jsonObject The JSON object to parse. Must not be 
1073         *                   {@code null}.
1074         *
1075         * @return The claims request.
1076         */
1077        public static ClaimsRequest parse(final JSONObject jsonObject) {
1078
1079                ClaimsRequest claimsRequest = new ClaimsRequest();
1080
1081                try {
1082                        if (jsonObject.containsKey("id_token")) {
1083
1084                                JSONObject idTokenObject = (JSONObject)jsonObject.get("id_token");
1085
1086                                Collection<Entry> idTokenClaims = Entry.parseEntries(idTokenObject);
1087
1088                                for (Entry entry: idTokenClaims)
1089                                        claimsRequest.addIDTokenClaim(entry);
1090                        }
1091
1092
1093                        if (jsonObject.containsKey("userinfo")) {
1094
1095                                JSONObject userInfoObject = (JSONObject)jsonObject.get("userinfo");
1096
1097                                Collection<Entry> userInfoClaims = Entry.parseEntries(userInfoObject);
1098
1099                                for (Entry entry: userInfoClaims)
1100                                        claimsRequest.addUserInfoClaim(entry);
1101                        }
1102
1103                } catch (Exception e) {
1104
1105                        // Ignore
1106                }
1107
1108                return claimsRequest;
1109        }
1110
1111
1112        /**
1113         * Parses a claims request from the specified JSON object string
1114         * representation. Unexpected members in the JSON object are silently
1115         * ignored.
1116         *
1117         * @param json The JSON object string to parse. Must not be
1118         *             {@code null}.
1119         *
1120         * @return The claims request.
1121         *
1122         * @throws ParseException If the string couldn't be parsed to a valid
1123         *                        JSON object.
1124         */
1125        public static ClaimsRequest parse(final String json)
1126                throws ParseException {
1127
1128                return parse(JSONObjectUtils.parse(json));
1129        }
1130}