Skip to content

Commit 7cb4faa

Browse files
committed
allow to decode custom claims of a limited type
1 parent 7d303ff commit 7cb4faa

8 files changed

Lines changed: 557 additions & 54 deletions

File tree

lib/src/main/java/com/auth0/jwt/JWTCreator.java

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import com.auth0.jwt.algorithms.Algorithm;
44
import com.auth0.jwt.exceptions.JWTCreationException;
55
import com.auth0.jwt.exceptions.SignatureGenerationException;
6+
import com.auth0.jwt.impl.ClaimsHolder;
7+
import com.auth0.jwt.impl.PayloadSerializer;
68
import com.auth0.jwt.impl.PublicClaims;
79
import com.fasterxml.jackson.core.JsonProcessingException;
810
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.databind.module.SimpleModule;
912
import org.apache.commons.codec.binary.Base64;
1013

1114
import java.util.Date;
@@ -24,8 +27,12 @@ class JWTCreator {
2427
private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {
2528
this.algorithm = algorithm;
2629
try {
27-
headerJson = toSafeJson(headerClaims);
28-
payloadJson = toSafeJson(payloadClaims);
30+
ObjectMapper mapper = new ObjectMapper();
31+
SimpleModule module = new SimpleModule();
32+
module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
33+
mapper.registerModule(module);
34+
headerJson = mapper.writeValueAsString(headerClaims);
35+
payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
2936
} catch (JsonProcessingException e) {
3037
throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", e);
3138
}
@@ -89,12 +96,7 @@ public Builder withSubject(String subject) {
8996
* @return this same Builder instance.
9097
*/
9198
public Builder withAudience(String[] audience) {
92-
//FIXME: Use a custom Serializer
93-
if (audience.length == 1) {
94-
addClaim(PublicClaims.AUDIENCE, audience[0]);
95-
} else if (audience.length > 1) {
96-
addClaim(PublicClaims.AUDIENCE, audience);
97-
}
99+
addClaim(PublicClaims.AUDIENCE, audience);
98100
return this;
99101
}
100102

@@ -104,7 +106,7 @@ public Builder withAudience(String[] audience) {
104106
* @return this same Builder instance.
105107
*/
106108
public Builder withExpiresAt(Date expiresAt) {
107-
addClaim(PublicClaims.EXPIRES_AT, dateToSeconds(expiresAt));
109+
addClaim(PublicClaims.EXPIRES_AT, expiresAt);
108110
return this;
109111
}
110112

@@ -114,7 +116,7 @@ public Builder withExpiresAt(Date expiresAt) {
114116
* @return this same Builder instance.
115117
*/
116118
public Builder withNotBefore(Date notBefore) {
117-
addClaim(PublicClaims.NOT_BEFORE, dateToSeconds(notBefore));
119+
addClaim(PublicClaims.NOT_BEFORE, notBefore);
118120
return this;
119121
}
120122

@@ -124,7 +126,7 @@ public Builder withNotBefore(Date notBefore) {
124126
* @return this same Builder instance.
125127
*/
126128
public Builder withIssuedAt(Date issuedAt) {
127-
addClaim(PublicClaims.ISSUED_AT, dateToSeconds(issuedAt));
129+
addClaim(PublicClaims.ISSUED_AT, issuedAt);
128130
return this;
129131
}
130132

@@ -154,11 +156,6 @@ public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCrea
154156
return new JWTCreator(algorithm, headerClaims, payloadClaims).sign();
155157
}
156158

157-
private int dateToSeconds(Date date) {
158-
//FIXME: Use a custom Serializer
159-
return (int) (date.getTime() / 1000);
160-
}
161-
162159
private void addClaim(String name, Object value) {
163160
if (value == null) {
164161
payloadClaims.remove(name);
@@ -168,11 +165,6 @@ private void addClaim(String name, Object value) {
168165
}
169166
}
170167

171-
private String toSafeJson(Map<String, Object> claims) throws JsonProcessingException {
172-
ObjectMapper mapper = new ObjectMapper();
173-
return mapper.writeValueAsString(claims);
174-
}
175-
176168
private String sign() throws SignatureGenerationException {
177169
String header = Base64.encodeBase64URLSafeString((headerJson.getBytes()));
178170
String payload = Base64.encodeBase64URLSafeString((payloadJson.getBytes()));

lib/src/main/java/com/auth0/jwt/JWTVerifier.java

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.auth0.jwt.algorithms.Algorithm;
44
import com.auth0.jwt.exceptions.*;
55
import com.auth0.jwt.impl.PublicClaims;
6+
import com.auth0.jwt.interfaces.Claim;
67
import org.apache.commons.codec.binary.Base64;
78

89
import java.util.*;
@@ -53,6 +54,7 @@ static class Verification {
5354
/**
5455
* Require a specific Issuer ("iss") claim.
5556
*
57+
* @param issuer the required Issuer value
5658
* @return this same Verification instance.
5759
*/
5860
public Verification withIssuer(String issuer) {
@@ -63,6 +65,7 @@ public Verification withIssuer(String issuer) {
6365
/**
6466
* Require a specific Subject ("sub") claim.
6567
*
68+
* @param subject the required Subject value
6669
* @return this same Verification instance.
6770
*/
6871
public Verification withSubject(String subject) {
@@ -73,6 +76,7 @@ public Verification withSubject(String subject) {
7376
/**
7477
* Require a specific Audience ("aud") claim.
7578
*
79+
* @param audience the required Audience value
7680
* @return this same Verification instance.
7781
*/
7882
public Verification withAudience(String[] audience) {
@@ -147,13 +151,36 @@ public Verification acceptIssuedAt(long delta) throws IllegalArgumentException {
147151
/**
148152
* Require a specific JWT Id ("jti") claim.
149153
*
154+
* @param jwtId the required Id value
150155
* @return this same Verification instance.
151156
*/
152157
public Verification withJWTId(String jwtId) {
153158
requireClaim(PublicClaims.JWT_ID, jwtId);
154159
return this;
155160
}
156161

162+
/**
163+
* Require a specific Claim value.
164+
*
165+
* @param name the Claim's name
166+
* @param value the Claim's value. Must be an instance of Integer, Double, Boolean, Date or String class.
167+
* @return this same Verification instance.
168+
* @throws IllegalArgumentException if the name is null or the value class is not allowed.
169+
*/
170+
public Verification withClaim(String name, Object value) throws IllegalArgumentException {
171+
final boolean validValue = value instanceof Integer || value instanceof Double ||
172+
value instanceof Boolean || value instanceof Date || value instanceof String;
173+
if (name == null) {
174+
throw new IllegalArgumentException("The Custom Claim's name can't be null.");
175+
}
176+
if (!validValue) {
177+
throw new IllegalArgumentException("The Custom Claim's value class must be an instance of Integer, Double, Boolean, Date or String.");
178+
}
179+
180+
requireClaim(name, value);
181+
return this;
182+
}
183+
157184
/**
158185
* Creates a new and reusable instance of the JWTVerifier with the configuration already provided.
159186
*
@@ -227,35 +254,81 @@ private void verifyAlgorithm(JWT jwt, Algorithm expectedAlgorithm) throws Algori
227254

228255
private void verifyClaims(JWT jwt, Map<String, Object> claims) {
229256
for (Map.Entry<String, Object> entry : claims.entrySet()) {
230-
assertValidClaim(jwt, entry.getKey(), entry.getValue());
257+
switch (entry.getKey()) {
258+
case PublicClaims.AUDIENCE:
259+
assertValidAudienceClaim(jwt.getAudience(), (String[]) entry.getValue());
260+
break;
261+
case PublicClaims.EXPIRES_AT:
262+
assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true);
263+
break;
264+
case PublicClaims.ISSUED_AT:
265+
assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false);
266+
break;
267+
case PublicClaims.NOT_BEFORE:
268+
assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false);
269+
break;
270+
case PublicClaims.ISSUER:
271+
assertValidStringClaim(entry.getKey(), jwt.getIssuer(), (String) entry.getValue());
272+
break;
273+
case PublicClaims.JWT_ID:
274+
assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue());
275+
break;
276+
case PublicClaims.SUBJECT:
277+
assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue());
278+
break;
279+
default:
280+
assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue());
281+
break;
282+
}
231283
}
232284
}
233285

234-
private void assertValidClaim(JWT jwt, String claimName, Object expectedValue) throws InvalidClaimException {
235-
String errMessage = String.format("The Claim '%s' value doesn't match the required one.", claimName);
286+
private void assertValidClaim(Claim claim, String claimName, Object value) {
287+
boolean isValid = false;
288+
if (value instanceof String) {
289+
isValid = value.equals(claim.asString());
290+
} else if (value instanceof Integer) {
291+
isValid = value.equals(claim.asInt());
292+
} else if (value instanceof Boolean) {
293+
isValid = value.equals(claim.asBoolean());
294+
} else if (value instanceof Double) {
295+
isValid = value.equals(claim.asDouble());
296+
} else if (value instanceof Date) {
297+
isValid = value.equals(claim.asDate());
298+
}
299+
300+
if (!isValid) {
301+
throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName));
302+
}
303+
}
304+
305+
private void assertValidStringClaim(String claimName, String value, String expectedValue) {
306+
if (!expectedValue.equals(value)) {
307+
throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName));
308+
}
309+
}
310+
311+
private void assertValidDateClaim(Date date, long delta, boolean shouldBeFuture) {
312+
Date today = clock.getToday();
236313
boolean isValid;
237-
if (PublicClaims.AUDIENCE.equals(claimName)) {
238-
isValid = Arrays.equals(jwt.getAudience(), (String[]) expectedValue);
239-
} else if (PublicClaims.NOT_BEFORE.equals(claimName) || PublicClaims.EXPIRES_AT.equals(claimName) || PublicClaims.ISSUED_AT.equals(claimName)) {
240-
long deltaValue = (long) expectedValue;
241-
Date today = clock.getToday();
242-
Date date = jwt.getClaim(claimName).asDate();
243-
if (PublicClaims.EXPIRES_AT.equals(claimName)) {
244-
today.setTime(today.getTime() - deltaValue);
245-
isValid = date == null || !today.after(date);
246-
errMessage = String.format("The Token has expired on %s.", date);
247-
} else {
248-
today.setTime(today.getTime() + deltaValue);
249-
isValid = date == null || !today.before(date);
250-
errMessage = String.format("The Token can't be used before %s.", date);
251-
}
314+
String errMessage;
315+
if (shouldBeFuture) {
316+
today.setTime(today.getTime() - delta);
317+
isValid = date == null || !today.after(date);
318+
errMessage = String.format("The Token has expired on %s.", date);
252319
} else {
253-
String stringValue = (String) expectedValue;
254-
isValid = stringValue.equals(jwt.getClaim(claimName).asString());
320+
today.setTime(today.getTime() + delta);
321+
isValid = date == null || !today.before(date);
322+
errMessage = String.format("The Token can't be used before %s.", date);
255323
}
256-
257324
if (!isValid) {
258325
throw new InvalidClaimException(errMessage);
259326
}
260327
}
328+
329+
private void assertValidAudienceClaim(String[] audience, String[] value) {
330+
if (!Arrays.equals(audience, value)) {
331+
throw new InvalidClaimException("The Claim 'aud' value doesn't match the required one.");
332+
}
333+
}
261334
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.auth0.jwt.impl;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* The ClaimsHolder class is just a wrapper for the Map of Claims.
8+
*/
9+
public final class ClaimsHolder {
10+
private Map<String, Object> claims;
11+
12+
public ClaimsHolder(Map<String, Object> claims) {
13+
this.claims = claims == null ? new HashMap<String, Object>() : claims;
14+
}
15+
16+
Map<String, Object> getClaims() {
17+
return claims;
18+
}
19+
}

lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE
3535
String issuer = getString(tree, PublicClaims.ISSUER);
3636
String subject = getString(tree, PublicClaims.SUBJECT);
3737
String[] audience = getStringOrArray(tree, PublicClaims.AUDIENCE);
38-
Date expiresAt = getDate(tree, PublicClaims.EXPIRES_AT);
39-
Date notBefore = getDate(tree, PublicClaims.NOT_BEFORE);
40-
Date issuedAt = getDate(tree, PublicClaims.ISSUED_AT);
38+
Date expiresAt = getDateFromSeconds(tree, PublicClaims.EXPIRES_AT);
39+
Date notBefore = getDateFromSeconds(tree, PublicClaims.NOT_BEFORE);
40+
Date issuedAt = getDateFromSeconds(tree, PublicClaims.ISSUED_AT);
4141
String jwtId = getString(tree, PublicClaims.JWT_ID);
4242

4343
return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree);
4444
}
4545

4646
String[] getStringOrArray(Map<String, JsonNode> tree, String claimName) throws JWTDecodeException {
47-
JsonNode node = tree.get(claimName);
47+
JsonNode node = tree.remove(claimName);
4848
if (node == null || node.isNull() || !(node.isArray() || node.isTextual())) {
4949
return null;
5050
}
@@ -64,8 +64,8 @@ String[] getStringOrArray(Map<String, JsonNode> tree, String claimName) throws J
6464
return arr;
6565
}
6666

67-
Date getDate(Map<String, JsonNode> tree, String claimName) {
68-
JsonNode node = tree.get(claimName);
67+
Date getDateFromSeconds(Map<String, JsonNode> tree, String claimName) {
68+
JsonNode node = tree.remove(claimName);
6969
if (node == null || node.isNull() || !node.canConvertToLong()) {
7070
return null;
7171
}
@@ -74,7 +74,7 @@ Date getDate(Map<String, JsonNode> tree, String claimName) {
7474
}
7575

7676
String getString(Map<String, JsonNode> tree, String claimName) {
77-
JsonNode node = tree.get(claimName);
77+
JsonNode node = tree.remove(claimName);
7878
if (node == null || node.isNull()) {
7979
return null;
8080
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.auth0.jwt.impl;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.databind.SerializerProvider;
5+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
6+
7+
import java.io.IOException;
8+
import java.util.Date;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
12+
public class PayloadSerializer extends StdSerializer<ClaimsHolder> {
13+
14+
public PayloadSerializer() {
15+
this(null);
16+
}
17+
18+
private PayloadSerializer(Class<ClaimsHolder> t) {
19+
super(t);
20+
}
21+
22+
@Override
23+
public void serialize(ClaimsHolder holder, JsonGenerator gen, SerializerProvider provider) throws IOException {
24+
HashMap<Object, Object> safePayload = new HashMap<>();
25+
for (Map.Entry<String, Object> e : holder.getClaims().entrySet()) {
26+
switch (e.getKey()) {
27+
case PublicClaims.AUDIENCE:
28+
if (e.getValue() instanceof String) {
29+
safePayload.put(e.getKey(), e.getValue());
30+
break;
31+
}
32+
String[] audArray = (String[]) e.getValue();
33+
if (audArray.length == 1) {
34+
safePayload.put(e.getKey(), audArray[0]);
35+
} else if (audArray.length > 1) {
36+
safePayload.put(e.getKey(), audArray);
37+
}
38+
break;
39+
case PublicClaims.EXPIRES_AT:
40+
case PublicClaims.ISSUED_AT:
41+
case PublicClaims.NOT_BEFORE:
42+
Date date = (Date) e.getValue();
43+
int seconds = (int) (date.getTime() / 1000);
44+
safePayload.put(e.getKey(), seconds);
45+
break;
46+
default:
47+
safePayload.put(e.getKey(), e.getValue());
48+
break;
49+
}
50+
}
51+
52+
gen.writeObject(safePayload);
53+
}
54+
}

0 commit comments

Comments
 (0)