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