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.claims;
019
020
021import java.net.URI;
022import java.net.URL;
023import java.util.*;
024
025import net.minidev.json.JSONArray;
026import net.minidev.json.JSONAware;
027import net.minidev.json.JSONObject;
028
029import com.nimbusds.jwt.JWTClaimsSet;
030import com.nimbusds.jwt.util.DateUtils;
031import com.nimbusds.langtag.LangTag;
032import com.nimbusds.langtag.LangTagUtils;
033import com.nimbusds.oauth2.sdk.ParseException;
034import com.nimbusds.oauth2.sdk.id.Audience;
035import com.nimbusds.oauth2.sdk.id.Issuer;
036import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
037
038
039/**
040 * Claims set with basic getters and setters, serialisable to a JSON object.
041 */
042public class ClaimsSet implements JSONAware {
043        
044        
045        /**
046         * The issuer claim name.
047         */
048        public static final String ISS_CLAIM_NAME = "iss";
049        
050        
051        /**
052         * The audience claim name.
053         */
054        public static final String AUD_CLAIM_NAME = "aud";
055        
056        
057        /**
058         * The names of the standard top-level claims.
059         */
060        private static final Set<String> STD_CLAIM_NAMES = Collections.unmodifiableSet(
061                new HashSet<>(Arrays.asList(
062                        ISS_CLAIM_NAME,
063                        AUD_CLAIM_NAME
064                )));
065        
066        
067        /**
068         * Gets the names of the standard top-level claims.
069         *
070         * @return The names of the standard top-level claims (read-only set).
071         */
072        public static Set<String> getStandardClaimNames() {
073                
074                return STD_CLAIM_NAMES;
075        }
076
077
078        /**
079         * The JSON object representation of the claims set.
080         */
081        protected final JSONObject claims;
082
083
084        /**
085         * Creates a new empty claims set.
086         */
087        public ClaimsSet() {
088
089                claims = new JSONObject();
090        }
091
092
093        /**
094         * Creates a new claims set from the specified JSON object.
095         *
096         * @param jsonObject The JSON object. Must not be {@code null}.
097         */
098        public ClaimsSet(final JSONObject jsonObject) {
099
100                if (jsonObject == null)
101                        throw new IllegalArgumentException("The JSON object must not be null");
102
103                claims = jsonObject;
104        }
105
106
107        /**
108         * Puts all claims from the specified other claims set.
109         *
110         * @param other The other claims set. Must not be {@code null}.
111         */
112        public void putAll(final ClaimsSet other) {
113
114                putAll(other.claims);
115        }
116
117
118        /**
119         * Puts all claims from the specified map.
120         *
121         * @param claims The claims to put. Must not be {@code null}.
122         */
123        public void putAll(final Map<String,Object> claims) {
124
125                this.claims.putAll(claims);
126        }
127
128
129        /**
130         * Gets a claim.
131         *
132         * @param name The claim name. Must not be {@code null}.
133         *
134         * @return The claim value, {@code null} if not specified.
135         */
136        public Object getClaim(final String name) {
137
138                return claims.get(name);
139        }
140
141
142        /**
143         * Gets a claim that casts to the specified class.
144         *
145         * @param name  The claim name. Must not be {@code null}.
146         * @param clazz The Java class that the claim value should cast to.
147         *              Must not be {@code null}.
148         *
149         * @return The claim value, {@code null} if not specified or casting
150         *         failed.
151         */
152        public <T> T getClaim(final String name, final Class<T> clazz) {
153
154                try {
155                        return JSONObjectUtils.getGeneric(claims, name, clazz);
156                } catch (ParseException e) {
157                        return null;
158                }
159        }
160
161
162        /**
163         * Returns a map of all instances, including language-tagged, of a
164         * claim with the specified base name.
165         *
166         * <p>Example JSON serialised claims set:
167         *
168         * <pre>
169         * {
170         *   "month"    : "January",
171         *   "month#de" : "Januar"
172         *   "month#es" : "enero",
173         *   "month#it" : "gennaio"
174         * }
175         * </pre>
176         *
177         * <p>The "month" claim instances as java.util.Map:
178         *
179         * <pre>
180         * null = "January" (no language tag)
181         * "de" = "Januar"
182         * "es" = "enero"
183         * "it" = "gennaio"
184         * </pre>
185         *
186         * @param name  The claim name. Must not be {@code null}.
187         * @param clazz The Java class that the claim values should cast to.
188         *              Must not be {@code null}.
189         *
190         * @return The matching language-tagged claim values, empty map if
191         *         none. A {@code null} key indicates the value has no language
192         *         tag (corresponds to the base name).
193         */
194        public <T> Map<LangTag,T> getLangTaggedClaim(final String name, final Class<T> clazz) {
195
196                Map<LangTag,Object> matches = LangTagUtils.find(name, claims);
197                Map<LangTag,T> out = new HashMap<>();
198
199                for (Map.Entry<LangTag,Object> entry: matches.entrySet()) {
200
201                        LangTag langTag = entry.getKey();
202                        String compositeKey = name + (langTag != null ? "#" + langTag : "");
203
204                        try {
205                                out.put(langTag, JSONObjectUtils.getGeneric(claims, compositeKey, clazz));
206                        } catch (ParseException e) {
207                                // skip
208                        }
209                }
210
211                return out;
212        }
213
214
215        /**
216         * Sets a claim.
217         *
218         * @param name  The claim name, with an optional language tag. Must not
219         *              be {@code null}.
220         * @param value The claim value. Should serialise to a JSON entity. If
221         *              {@code null} any existing claim with the same name will
222         *              be removed.
223         */
224        public void setClaim(final String name, final Object value) {
225
226                if (value != null)
227                        claims.put(name, value);
228                else
229                        claims.remove(name);
230        }
231
232
233        /**
234         * Sets a claim with an optional language tag.
235         *
236         * @param name    The claim name. Must not be {@code null}.
237         * @param value   The claim value. Should serialise to a JSON entity.
238         *                If {@code null} any existing claim with the same name
239         *                and language tag (if any) will be removed.
240         * @param langTag The language tag of the claim value, {@code null} if
241         *                not tagged.
242         */
243        public void setClaim(final String name, final Object value, final LangTag langTag) {
244
245                String keyName = langTag != null ? name + "#" + langTag : name;
246                setClaim(keyName, value);
247        }
248
249
250        /**
251         * Gets a string-based claim.
252         *
253         * @param name The claim name. Must not be {@code null}.
254         *
255         * @return The claim value, {@code null} if not specified or casting
256         *         failed.
257         */
258        public String getStringClaim(final String name) {
259
260                try {
261                        return JSONObjectUtils.getString(claims, name, null);
262                } catch (ParseException e) {
263                        return null;
264                }
265        }
266
267
268        /**
269         * Gets a string-based claim with an optional language tag.
270         *
271         * @param name    The claim name. Must not be {@code null}.
272         * @param langTag The language tag of the claim value, {@code null} to
273         *                get the non-tagged value.
274         *
275         * @return The claim value, {@code null} if not specified or casting
276         *         failed.
277         */
278        public String getStringClaim(final String name, final LangTag langTag) {
279
280                return langTag == null ? getStringClaim(name) : getStringClaim(name + '#' + langTag);
281        }
282
283
284        /**
285         * Gets a boolean-based claim.
286         *
287         * @param name The claim name. Must not be {@code null}.
288         *
289         * @return The claim value, {@code null} if not specified or casting
290         *         failed.
291         */
292        public Boolean getBooleanClaim(final String name) {
293
294                try {
295                        return JSONObjectUtils.getBoolean(claims, name);
296                } catch (ParseException e) {
297                        return null;
298                }
299        }
300
301
302        /**
303         * Gets a number-based claim.
304         *
305         * @param name The claim name. Must not be {@code null}.
306         *
307         * @return The claim value, {@code null} if not specified or casting
308         *         failed.
309         */
310        public Number getNumberClaim(final String name) {
311
312                try {
313                        return JSONObjectUtils.getNumber(claims, name);
314                } catch (ParseException e) {
315                        return null;
316                }
317        }
318
319
320        /**
321         * Gets an URL string based claim.
322         *
323         * @param name The claim name. Must not be {@code null}.
324         *
325         * @return The claim value, {@code null} if not specified or parsing
326         *         failed.
327         */
328        public URL getURLClaim(final String name) {
329
330                try {
331                        return JSONObjectUtils.getURL(claims, name);
332                } catch (ParseException e) {
333                        return null;
334                }
335        }
336
337
338        /**
339         * Sets an URL string based claim.
340         *
341         * @param name  The claim name. Must not be {@code null}.
342         * @param value The claim value. If {@code null} any existing claim
343         *              with the same name will be removed.
344         */
345        public void setURLClaim(final String name, final URL value) {
346
347                if (value != null)
348                        setClaim(name, value.toString());
349                else
350                        claims.remove(name);
351        }
352
353
354        /**
355         * Gets an URI string based claim.
356         *
357         * @param name The claim name. Must not be {@code null}.
358         *
359         * @return The claim value, {@code null} if not specified or parsing
360         *         failed.
361         */
362        public URI getURIClaim(final String name) {
363
364                try {
365                        return JSONObjectUtils.getURI(claims, name, null);
366                } catch (ParseException e) {
367                        return null;
368                }
369        }
370
371
372        /**
373         * Sets an URI string based claim.
374         *
375         * @param name  The claim name. Must not be {@code null}.
376         * @param value The claim value. If {@code null} any existing claim
377         *              with the same name will be removed.
378         */
379        public void setURIClaim(final String name, final URI value) {
380
381                if (value != null)
382                        setClaim(name, value.toString());
383                else
384                        claims.remove(name);
385        }
386
387
388        /**
389         * Gets a date / time based claim, represented as the number of seconds
390         * from 1970-01-01T0:0:0Z as measured in UTC until the date / time.
391         *
392         * @param name The claim name. Must not be {@code null}.
393         *
394         * @return The claim value, {@code null} if not specified or parsing
395         *         failed.
396         */
397        public Date getDateClaim(final String name) {
398
399                try {
400                        return DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getNumber(claims, name).longValue());
401                } catch (Exception e) {
402                        return null;
403                }
404        }
405
406
407        /**
408         * Sets a date / time based claim, represented as the number of seconds
409         * from 1970-01-01T0:0:0Z as measured in UTC until the date / time.
410         *
411         * @param name  The claim name. Must not be {@code null}.
412         * @param value The claim value. If {@code null} any existing claim
413         *              with the same name will be removed.
414         */
415        public void setDateClaim(final String name, final Date value) {
416
417                if (value != null)
418                        setClaim(name, DateUtils.toSecondsSinceEpoch(value));
419                else
420                        claims.remove(name);
421        }
422
423
424        /**
425         * Gets a string list based claim.
426         *
427         * @param name The claim name. Must not be {@code null}.
428         *
429         * @return The claim value, {@code null} if not specified or parsing
430         *         failed.
431         */
432        public List<String> getStringListClaim(final String name) {
433
434                try {
435                        return JSONObjectUtils.getStringList(claims, name);
436                } catch (ParseException e) {
437                        return null;
438                }
439        }
440        
441        
442        /**
443         * Gets a JSON object based claim.
444         *
445         * @param name The claim name. Must not be {@code null}.
446         *
447         * @return The claim value, {@code null} if not specified or parsing
448         *         failed.
449         */
450        public JSONObject getJSONObjectClaim(final String name) {
451                
452                try {
453                        return JSONObjectUtils.getJSONObject(claims, name);
454                } catch (ParseException e) {
455                        return null;
456                }
457        }
458        
459        
460        /**
461         * Gets a JSON array based claim.
462         *
463         * @param name The claim name. Must not be {@code null}.
464         *
465         * @return The claim value, {@code null} if not specified or parsing
466         *         failed.
467         */
468        public JSONArray getJSONArrayClaim(final String name) {
469                
470                try {
471                        return JSONObjectUtils.getJSONArray(claims, name);
472                } catch (ParseException e) {
473                        return null;
474                }
475        }
476        
477        
478        /**
479         * Gets the issuer. Corresponds to the {@code iss} claim.
480         *
481         * @return The issuer, {@code null} if not specified.
482         */
483        public Issuer getIssuer() {
484                
485                String iss = getStringClaim(ISS_CLAIM_NAME);
486                
487                return iss != null ? new Issuer(iss) : null;
488        }
489        
490        
491        /**
492         * Sets the issuer. Corresponds to the {@code iss} claim.
493         *
494         * @param iss The issuer, {@code null} if not specified.
495         */
496        public void setIssuer(final Issuer iss) {
497                
498                if (iss != null)
499                        setClaim(ISS_CLAIM_NAME, iss.getValue());
500                else
501                        setClaim(ISS_CLAIM_NAME, null);
502        }
503        
504        
505        /**
506         * Gets the audience. Corresponds to the {@code aud} claim.
507         *
508         * @return The audience, {@code null} if not specified.
509         */
510        public List<Audience> getAudience() {
511                
512                if (getClaim(AUD_CLAIM_NAME) instanceof String) {
513                        // Special case - aud is a string
514                        return new Audience(getStringClaim(AUD_CLAIM_NAME)).toSingleAudienceList();
515                }
516                
517                // General case - JSON string array
518                List<String> rawList = getStringListClaim(AUD_CLAIM_NAME);
519                
520                if (rawList == null) {
521                        return null;
522                }
523                
524                List<Audience> audList = new ArrayList<>(rawList.size());
525                
526                for (String s: rawList)
527                        audList.add(new Audience(s));
528                
529                return audList;
530        }
531        
532        
533        /**
534         * Sets the audience. Corresponds to the {@code aud} claim.
535         *
536         * @param aud The audience, {@code null} if not specified.
537         */
538        public void setAudience(final Audience aud) {
539                
540                if (aud != null)
541                        setAudience(aud.toSingleAudienceList());
542                else
543                        setClaim(AUD_CLAIM_NAME, null);
544        }
545        
546        
547        /**
548         * Sets the audience list. Corresponds to the {@code aud} claim.
549         *
550         * @param audList The audience list, {@code null} if not specified.
551         */
552        public void setAudience(final List<Audience> audList) {
553                
554                if (audList != null)
555                        setClaim(AUD_CLAIM_NAME, Audience.toStringList(audList));
556                else
557                        setClaim(AUD_CLAIM_NAME, null);
558        }
559
560
561        /**
562         * Gets the JSON object representation of this claims set.
563         *
564         * <p>Example:
565         *
566         * <pre>
567         * {
568         *   "country"       : "USA",
569         *   "country#en"    : "USA",
570         *   "country#de_DE" : "Vereinigte Staaten",
571         *   "country#fr_FR" : "Etats Unis"
572         * }
573         * </pre>
574         *
575         * @return The JSON object representation.
576         */
577        public JSONObject toJSONObject() {
578                
579                JSONObject out = new JSONObject();
580                out.putAll(claims);
581                return out;
582        }
583        
584        
585        @Override
586        public String toJSONString() {
587                return toJSONObject().toJSONString();
588        }
589
590
591        /**
592         * Gets the JSON Web Token (JWT) claims set for this claim set.
593         *
594         * @return The JWT claims set.
595         *
596         * @throws ParseException If the conversion to a JWT claims set fails.
597         */
598        public JWTClaimsSet toJWTClaimsSet()
599                throws ParseException {
600
601                try {
602                        // Parse from JSON string to handle nested JSONArray & JSONObject properly
603                        // Work around https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/347/revise-nested-jsonarray-and-jsonobject
604                        return JWTClaimsSet.parse(claims.toJSONString());
605
606                } catch (java.text.ParseException e) {
607
608                        throw new ParseException(e.getMessage(), e);
609                }
610        }
611        
612        
613        @Override
614        public boolean equals(Object o) {
615                if (this == o) return true;
616                if (!(o instanceof ClaimsSet)) return false;
617                ClaimsSet claimsSet = (ClaimsSet) o;
618                return claims.equals(claimsSet.claims);
619        }
620        
621        
622        @Override
623        public int hashCode() {
624                return Objects.hash(claims);
625        }
626        
627        
628        @Override
629        public String toString() {
630                return toJSONString();
631        }
632}