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