001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2013-2016 ForgeRock AS.
015 */
016
017package org.forgerock.json.jose.jwt;
018
019import static java.util.Collections.*;
020import static org.forgerock.json.jose.jwt.JwtClaimsSetKey.*;
021
022import java.net.URI;
023import java.util.ArrayList;
024import java.util.Date;
025import java.util.List;
026import java.util.Map;
027
028import org.forgerock.json.JsonValue;
029import org.forgerock.json.jose.utils.IntDate;
030import org.forgerock.json.jose.utils.StringOrURI;
031
032/**
033 * An implementation that holds a JWT's Claims Set.
034 * <p>
035 * Provides methods to set claims for all the reserved claim names as well as custom claims.
036 *
037 * @since 2.0.0
038 */
039public class JwtClaimsSet extends JWObject implements Payload {
040
041    /**
042     * Constructs a new, empty JwtClaimsSet.
043     */
044    public JwtClaimsSet() {
045    }
046
047    /**
048     * Constructs a new JwtClaimsSet, with its claims set to the contents of the given Map.
049     *
050     * @param claims A Map containing the claims to be set in the Claims Set.
051     */
052    public JwtClaimsSet(Map<String, Object> claims) {
053        setClaims(claims);
054    }
055
056    /**
057     * Gets the type of the contents of the Claims Set.
058     * <p>
059     * The values used for this claim SHOULD come from the same value space as the JWT header parameter "typ",
060     * with the same rules applying.
061     *
062     * @param type The Claims Set content type.
063     */
064    public void setType(String type) {
065        put(TYP.value(), type);
066    }
067
068    /**
069     * Gets the type of the contents of the Claims Set.
070     * <p>
071     * The values used for this claim SHOULD come from the same value space as the JWT header parameter "typ",
072     * with the same rules applying.
073     *
074     * @return The Claims Set content type.
075     */
076    public String getType() {
077        return get(TYP.value()).asString();
078    }
079
080    /**
081     * Sets the unique ID of the JWT.
082     *
083     * @param jwtId The JWT's ID.
084     */
085    public void setJwtId(String jwtId) {
086        put(JTI.value(), jwtId);
087    }
088
089    /**
090     * Gets the unique ID of the JWT.
091     *
092     * @return The JWT's ID or {@code null} if claim not present.
093     */
094    public String getJwtId() {
095        return get(JTI.value()).asString();
096    }
097
098    /**
099     * Sets the issuer this JWT was issued by.
100     * <p>
101     * The given issuer can be any arbitrary string without any ":" characters, if the string does contain a ":"
102     * character then it must be a valid URI.
103     *
104     * @param issuer The JWT's issuer.
105     */
106    public void setIssuer(String issuer) {
107        StringOrURI.validateStringOrURI(issuer);
108        put(ISS.value(), issuer);
109    }
110
111    /**
112     * Sets the issuer this JWT was issued by.
113     *
114     * @param issuer The JWT's issuer.
115     */
116    public void setIssuer(URI issuer) {
117        put(ISS.value(), issuer.toString());
118    }
119
120    /**
121     *
122     * Gets the issuer this JWT was issued by.
123     *
124     * @return The JWT's issuer or {@code null} if claim not present.
125     */
126    public String getIssuer() {
127        return get(ISS.value()).asString();
128    }
129
130    /**
131     * Sets the subject this JWT is issued to.
132     * <p>
133     * The given subject can be any arbitrary string without any ":" characters, if the string does contain a ":"
134     * character then it must be a valid URI.
135     *
136     * @param subject The JWT's principal.
137     * @see #setSubject(java.net.URI)
138     */
139    public void setSubject(String subject) {
140        StringOrURI.validateStringOrURI(subject);
141        put(SUB.value(), subject);
142    }
143
144    /**
145     * Sets the subject this JWT is issued to.
146     *
147     * @param subject The JWT's principal.
148     * @see #setSubject(String)
149     */
150    public void setSubject(URI subject) {
151        put(SUB.value(), subject.toString());
152    }
153
154    /**
155     * Gets the subject this JWT is issued to.
156     *
157     * @return The JWT's principal or {@code null} if claim not present.
158     */
159    public String getSubject() {
160        return get(SUB.value()).asString();
161    }
162
163    /**
164     * Adds an entry to the JWT's intended audience list, in the Claims Set.
165     * <p>
166     * The given audience can be any arbitrary string without any ":" characters, if the string does contain a ":"
167     * character then it must be a valid URI.
168     *
169     * @param audience The JWT's intended audience.
170     * @see #addAudience(java.net.URI)
171     */
172    public void addAudience(String audience) {
173        StringOrURI.validateStringOrURI(audience);
174        addAudienceWithTypeCheck(audience);
175    }
176
177    /**
178     * Adds an entry to the JWT's intended audience list, in the Claims Set.
179     *
180     * @param audience The JWT's intended audience.
181     * @see #addAudience(String)
182     */
183    public void addAudience(URI audience) {
184        addAudienceWithTypeCheck(audience.toString());
185    }
186
187    private void addAudienceWithTypeCheck(String audience) {
188        JsonValue audienceClaim = get(AUD.value());
189
190        if (audienceClaim.isNull()) {
191            put(AUD.value(), audience);
192        } else if (audienceClaim.isList()) {
193            audienceClaim.asList().add(audience);
194        } else {
195            List<String> audienceList = new ArrayList<>();
196            audienceList.add(audienceClaim.asString());
197            audienceList.add(audience);
198            put(AUD.value(), audienceList);
199        }
200    }
201
202    /**
203     * Gets the intended audience for the JWT from the Claims Set.
204     *
205     * @return The JWT's intended audience or {@code null} if claim not present.
206     */
207    public List<String> getAudience() {
208        JsonValue audience = get(AUD.value());
209        if (audience.isNull()) {
210            return null;
211        } else if (audience.isList()) {
212            return audience.asList(String.class);
213        } else {
214            return singletonList(audience.asString());
215        }
216    }
217
218    /**
219     * Sets the time the JWT was issued at, in the Claims Set.
220     * <p>
221     * The given date will be converted into an {@link IntDate} to be stored in the JWT Claims Set.
222     *
223     * @param issuedAtTime The JWT's issued at time.
224     */
225    public void setIssuedAtTime(Date issuedAtTime) {
226        put(IAT.value(), IntDate.toIntDate(issuedAtTime));
227    }
228
229    /**
230     * Sets the time the JWT was issued at, in the Claims Set.
231     * <p>
232     * This method takes a long representation of the number of <strong>seconds</strong> have passed since epoch.
233     *
234     * @param issuedAtTime The JWT's issued at time as a long in seconds.
235     * @see #setIssuedAtTime(java.util.Date)
236     */
237    private void setIssuedAtTime(long issuedAtTime) {
238        put(IAT.value(), issuedAtTime);
239    }
240
241    /**
242     * Gets the time the JWT was issued at, from the Claims Set.
243     *
244     * @return The JWT's issued at time or {@code null} if claim not present.
245     */
246    public Date getIssuedAtTime() {
247        return getDate(IAT.value());
248    }
249
250    /**
251     * Sets the time the JWT is not allowed to be processed before, in the Claims Set.
252     * <p>
253     * The given date will be converted into an {@link IntDate} to be stored in the JWT Claims Set.
254     *
255     * @param notBeforeTime The JWT's not before time.
256     */
257    public void setNotBeforeTime(Date notBeforeTime) {
258        put(NBF.value(), IntDate.toIntDate(notBeforeTime));
259    }
260
261    /**
262     * Sets the time the JWT is not allowed to be processed before, in the Claims Set.
263     * <p>
264     * The method takes a long representation of the number of <strong>seconds</strong> have passed since epoch.
265     *
266     * @param notBeforeTime The JWT's not before time.
267     * @see #setNotBeforeTime(java.util.Date)
268     */
269    private void setNotBeforeTime(long notBeforeTime) {
270        put(NBF.value(), notBeforeTime);
271    }
272
273    /**
274     * Gets the time the JWT is not allowed to be processed before, from the Claims Set.
275     *
276     * @return The JWT's not before time or {@code null} if claim not present.
277     */
278    public Date getNotBeforeTime() {
279        return getDate(NBF.value());
280    }
281
282    /**
283     * Sets the expiration time of the JWT in the Claims Set.
284     * <p>
285     * The given date will be converted into an {@link IntDate} to be stored in the JWT Claims Set.
286     *
287     * @param expirationTime The JWT's expiration time.
288     */
289    public void setExpirationTime(Date expirationTime) {
290        put(EXP.value(), IntDate.toIntDate(expirationTime));
291    }
292
293    /**
294     * Sets the expiration time of the JWT in the Claims Set.
295     * <p>
296     * This method takes a long representation of the number of <strong>seconds</strong> have passed since epoch.
297     *
298     * @param expirationTime The JWT's expiration time as a long in seconds.
299     * @see #setExpirationTime(java.util.Date)
300     */
301    private void setExpirationTime(long expirationTime) {
302        put(EXP.value(), expirationTime);
303    }
304
305    /**
306     * Gets the expiration time of the JWT from the Claims Set.
307     *
308     * @return The JWT's expiration time or {@code null} if claim not present.
309     */
310    public Date getExpirationTime() {
311        return getDate(EXP.value());
312    }
313
314    /**
315     * Sets a claim with the specified name and value.
316     * <p>
317     * If the key matches one of the reserved claim names, then the relevant <tt>set</tt> method is called to set that
318     * claim with the specified name and value.
319     *
320     * @param key The claim name.
321     * @param value The claim value.
322     */
323    public void setClaim(String key, Object value) {
324
325        JwtClaimsSetKey claimsSetKey = getClaimSetKey(key.toUpperCase());
326
327        switch (claimsSetKey) {
328        case TYP: {
329            checkValueIsOfType(value, String.class);
330            setType((String) value);
331            break;
332        }
333        case JTI: {
334            checkValueIsOfType(value, String.class);
335            setJwtId((String) value);
336            break;
337        }
338        case ISS: {
339            if (isValueOfType(value, URI.class)) {
340                setIssuer((URI) value);
341            } else {
342                checkValueIsOfType(value, String.class);
343                setIssuer((String) value);
344            }
345            break;
346        }
347        case SUB: {
348            if (isValueOfType(value, URI.class)) {
349                setSubject((URI) value);
350            } else {
351                checkValueIsOfType(value, String.class);
352                setSubject((String) value);
353            }
354            break;
355        }
356        case AUD: {
357            if (isValueOfType(value, List.class)) {
358                List<?> audienceList = (List<?>) value;
359                for (Object audience : audienceList) {
360                    if (isValueOfType(audience, URI.class)) {
361                        addAudience((URI) audience);
362                    } else {
363                        checkValueIsOfType(audience, String.class);
364                        addAudience((String) audience);
365                    }
366                }
367            } else {
368                if (isValueOfType(value, URI.class)) {
369                    addAudience((URI) value);
370                } else {
371                    checkValueIsOfType(value, String.class);
372                    addAudience((String) value);
373                }
374            }
375            break;
376        }
377        case IAT: {
378            if (isValueOfType(value, Number.class)) {
379                setIssuedAtTime(((Number) value).longValue());
380            } else {
381                checkValueIsOfType(value, Date.class);
382                setIssuedAtTime((Date) value);
383            }
384            break;
385        }
386        case NBF: {
387            if (isValueOfType(value, Number.class)) {
388                setNotBeforeTime(((Number) value).longValue());
389            } else {
390                checkValueIsOfType(value, Date.class);
391                setNotBeforeTime((Date) value);
392            }
393            break;
394        }
395        case EXP: {
396            if (isValueOfType(value, Number.class)) {
397                setExpirationTime(((Number) value).longValue());
398            } else {
399                checkValueIsOfType(value, Date.class);
400                setExpirationTime((Date) value);
401            }
402            break;
403        }
404        default: {
405            put(key, value);
406        }
407        }
408    }
409
410    /**
411     * Sets claims using the values contained in the specified map.
412     *
413     * @param claims The Map to use to set the claims.
414     */
415    public void setClaims(Map<String, Object> claims) {
416        for (String key : claims.keySet()) {
417            setClaim(key, claims.get(key));
418        }
419    }
420
421    /**
422     * Gets a claim value for the specified key.
423     * <p>
424     * If the key matches one of the reserved claim names, then the relevant <tt>get</tt> method is called to get that
425     * claim value.
426     *
427     * @param key The claim name.
428     * @return The value stored against the claim name.
429     */
430    public Object getClaim(String key) {
431
432        JwtClaimsSetKey claimsSetKey = getClaimSetKey(key.toUpperCase());
433
434        Object value;
435
436        switch (claimsSetKey) {
437        case TYP: {
438            value = getType();
439            break;
440        }
441        case JTI: {
442            value = getJwtId();
443            break;
444        }
445        case ISS: {
446            value = getIssuer();
447            break;
448        }
449        case SUB: {
450            value = getSubject();
451            break;
452        }
453        case AUD: {
454            value = getAudience();
455            break;
456        }
457        case IAT: {
458            value = getIssuedAtTime();
459            break;
460        }
461        case NBF: {
462            value = getNotBeforeTime();
463            break;
464        }
465        case EXP: {
466            value = getExpirationTime();
467            break;
468        }
469        default: {
470            value = get(key).getObject();
471        }
472        }
473
474        return value;
475    }
476
477    /**
478     * Gets a claim value for the specified claim name and then casts it to the specified type.
479     *
480     * @param key The claim name.
481     * @param clazz The class of the required type.
482     * @param <T> The required type for the claim value.
483     * @return The value stored against the claim name.
484     * @see #getClaim(String)
485     */
486    public <T> T getClaim(String key, Class<T> clazz) {
487        return clazz.cast(getClaim(key));
488    }
489
490    /**
491     * Builds the JWT's Claims Set into a <code>String</code> representation of a JSON object.
492     *
493     * @return A JSON string.
494     */
495    public String build() {
496        return toString();
497    }
498
499    /**
500     * Returns the specified item value as a {@link Date}. If no such member value exists, then a JSON value containing
501     * {@code null} is returned.
502     *
503     * @param key the {@code Map} key identifying the item to return.
504     * @return a {@link Date} representing the value or {@code null}.
505     */
506    private Date getDate(final String key) {
507        final JsonValue value = get(key);
508        return value.isNull() ? null : IntDate.fromIntDate(value.asLong());
509    }
510}