Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-3149] Add Instant support #537

Merged
merged 6 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions lib/src/main/java/com/auth0/jwt/JWTCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.fasterxml.jackson.databind.module.SimpleModule;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.Map.Entry;

Expand Down Expand Up @@ -152,7 +153,19 @@ public Builder withExpiresAt(Date expiresAt) {
}

/**
* Add a specific Not Before ("nbf") claim to the Payload. The claim will be written as seconds since the epoch.
* Add a specific Expires At ("exp") claim to the payload. The claim will be written as seconds since the epoch;
* Milliseconds will be truncated by rounding down to the nearest second.
*
* @param expiresAt the Expires At value.
* @return this same Builder instance.
*/
public Builder withExpiresAt(Instant expiresAt) {
addClaim(PublicClaims.EXPIRES_AT, expiresAt);
return this;
}

/**
* Add a specific Not Before ("nbf") claim to the Payload. The claim will be written as seconds since the epoch;
* Milliseconds will be truncated by rounding down to the nearest second.
*
* @param notBefore the Not Before value.
Expand All @@ -164,7 +177,19 @@ public Builder withNotBefore(Date notBefore) {
}

/**
* Add a specific Issued At ("iat") claim to the Payload. The claim will be written as seconds since the epoch.
* Add a specific Not Before ("nbf") claim to the Payload. The claim will be written as seconds since the epoch;
* Milliseconds will be truncated by rounding down to the nearest second.
*
* @param notBefore the Not Before value.
* @return this same Builder instance.
*/
public Builder withNotBefore(Instant notBefore) {
addClaim(PublicClaims.NOT_BEFORE, notBefore);
return this;
}

/**
* Add a specific Issued At ("iat") claim to the Payload. The claim will be written as seconds since the epoch;
* Milliseconds will be truncated by rounding down to the nearest second.
*
* @param issuedAt the Issued At value.
Expand All @@ -175,6 +200,18 @@ public Builder withIssuedAt(Date issuedAt) {
return this;
}

/**
* Add a specific Issued At ("iat") claim to the Payload. The claim will be written as seconds since the epoch;
* Milliseconds will be truncated by rounding down to the nearest second.
*
* @param issuedAt the Issued At value.
* @return this same Builder instance.
*/
public Builder withIssuedAt(Instant issuedAt) {
addClaim(PublicClaims.ISSUED_AT, issuedAt);
return this;
}

/**
* Add a specific JWT Id ("jti") claim to the Payload.
*
Expand Down Expand Up @@ -271,6 +308,21 @@ public Builder withClaim(String name, Date value) throws IllegalArgumentExceptio
return this;
}

/**
* Add a custom Claim value. The claim will be written as seconds since the epoch.
* Milliseconds will be truncated by rounding down to the nearest second.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Builder instance.
* @throws IllegalArgumentException if the name is null.
*/
public Builder withClaim(String name, Instant value) throws IllegalArgumentException {
assertNonNull(name);
addClaim(name, value);
return this;
}

/**
* Add a custom Array Claim with the given items.
*
Expand Down Expand Up @@ -453,7 +505,7 @@ private static boolean isBasicType(Object value) {
if (c.isArray()) {
return c == Integer[].class || c == Long[].class || c == String[].class;
}
return c == String.class || c == Integer.class || c == Long.class || c == Double.class || c == Date.class || c == Boolean.class;
return c == String.class || c == Integer.class || c == Long.class || c == Double.class || c == Date.class || c == Instant.class || c == Boolean.class;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions lib/src/main/java/com/auth0/jwt/JWTDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.List;
Expand Down Expand Up @@ -93,16 +94,31 @@ public Date getExpiresAt() {
return payload.getExpiresAt();
}

@Override
public Instant getExpiresAtAsInstant() {
return payload.getExpiresAtAsInstant();
}

@Override
public Date getNotBefore() {
return payload.getNotBefore();
}

@Override
public Instant getNotBeforeAsInstant() {
return payload.getNotBeforeAsInstant();
}

@Override
public Date getIssuedAt() {
return payload.getIssuedAt();
}

@Override
public Instant getIssuedAtAsInstant() {
return payload.getIssuedAtAsInstant();
}

@Override
public String getId() {
return payload.getId();
Expand Down
34 changes: 17 additions & 17 deletions lib/src/main/java/com/auth0/jwt/JWTVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Verification;

import java.util.*;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;

/**
* The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also its signature matches.
Expand Down Expand Up @@ -327,13 +330,13 @@ private void verifyClaimValues(DecodedJWT jwt, Map.Entry<String, Object> expecte
assertValidAudienceClaim(jwt.getAudience(), (List<String>) expectedClaim.getValue(), false);
break;
case PublicClaims.EXPIRES_AT:
assertValidDateClaim(jwt.getExpiresAt(), (Long) expectedClaim.getValue(), true);
assertValidInstantClaim(jwt.getExpiresAtAsInstant(), (Long) expectedClaim.getValue(), true);
break;
case PublicClaims.ISSUED_AT:
assertValidDateClaim(jwt.getIssuedAt(), (Long) expectedClaim.getValue(), false);
assertValidInstantClaim(jwt.getIssuedAtAsInstant(), (Long) expectedClaim.getValue(), false);
break;
case PublicClaims.NOT_BEFORE:
assertValidDateClaim(jwt.getNotBefore(), (Long) expectedClaim.getValue(), false);
assertValidInstantClaim(jwt.getNotBeforeAsInstant(), (Long) expectedClaim.getValue(), false);
break;
case PublicClaims.ISSUER:
assertValidIssuerClaim(jwt.getIssuer(), (List<String>) expectedClaim.getValue());
Expand Down Expand Up @@ -403,27 +406,24 @@ private void assertValidStringClaim(String claimName, String value, String expec
}
}

private void assertValidDateClaim(Date date, long leeway, boolean shouldBeFuture) {
Date today = new Date(clock.millis());
today.setTime(today.getTime() / 1000 * 1000); // truncate millis
private void assertValidInstantClaim(Instant claimVal, long leeway, boolean shouldBeFuture) {
Instant now = clock.instant().truncatedTo(ChronoUnit.SECONDS);
if (shouldBeFuture) {
assertDateIsFuture(date, leeway, today);
assertInstantIsFuture(claimVal, leeway, now);
} else {
assertDateIsPast(date, leeway, today);
assertInstantIsPast(claimVal, leeway, now);
}
}

private void assertDateIsFuture(Date date, long leeway, Date today) {
today.setTime(today.getTime() - leeway * 1000);
if (date != null && today.after(date)) {
throw new TokenExpiredException(String.format("The Token has expired on %s.", date));
private void assertInstantIsFuture(Instant claimVal, long leeway, Instant now) {
if (claimVal != null && now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal)) {
throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal));
}
}

private void assertDateIsPast(Date date, long leeway, Date today) {
today.setTime(today.getTime() + leeway * 1000);
if (date != null && today.before(date)) {
throw new InvalidClaimException(String.format("The Token can't be used before %s.", date));
private void assertInstantIsPast(Instant claimVal, long leeway, Instant now) {
if (claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)) {
throw new InvalidClaimException(String.format("The Token can't be used before %s.", claimVal));
}
}

Expand Down
11 changes: 10 additions & 1 deletion lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;

import java.io.IOException;
import java.lang.reflect.Array;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
Expand Down Expand Up @@ -63,6 +63,15 @@ public Date asDate() {
return new Date(seconds * 1000);
}

@Override
public Instant asInstant() {
if (!data.canConvertToLong()) {
return null;
}
long seconds = data.asLong();
return Instant.ofEpochSecond(seconds);
}

@Override
@SuppressWarnings("unchecked")
public <T> T[] asArray(Class<T> tClazz) throws JWTDecodeException {
Expand Down
6 changes: 6 additions & 0 deletions lib/src/main/java/com/auth0/jwt/impl/NullClaim.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;

import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -46,6 +47,11 @@ public Date asDate() {
return null;
}

@Override
public Instant asInstant() {
return null;
}

@Override
public <T> T[] asArray(Class<T> tClazz) throws JWTDecodeException {
return null;
Expand Down
12 changes: 6 additions & 6 deletions lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

import java.io.IOException;
import java.time.Instant;
import java.util.*;

/**
Expand Down Expand Up @@ -45,9 +46,9 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE
String issuer = getString(tree, PublicClaims.ISSUER);
String subject = getString(tree, PublicClaims.SUBJECT);
List<String> audience = getStringOrArray(tree, PublicClaims.AUDIENCE);
Date expiresAt = getDateFromSeconds(tree, PublicClaims.EXPIRES_AT);
Date notBefore = getDateFromSeconds(tree, PublicClaims.NOT_BEFORE);
Date issuedAt = getDateFromSeconds(tree, PublicClaims.ISSUED_AT);
Instant expiresAt = getInstantFromSeconds(tree, PublicClaims.EXPIRES_AT);
Instant notBefore = getInstantFromSeconds(tree, PublicClaims.NOT_BEFORE);
Instant issuedAt = getInstantFromSeconds(tree, PublicClaims.ISSUED_AT);
String jwtId = getString(tree, PublicClaims.JWT_ID);

return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, objectReader);
Expand All @@ -73,16 +74,15 @@ List<String> getStringOrArray(Map<String, JsonNode> tree, String claimName) thro
return list;
}

Date getDateFromSeconds(Map<String, JsonNode> tree, String claimName) {
Instant getInstantFromSeconds(Map<String, JsonNode> tree, String claimName) {
JsonNode node = tree.get(claimName);
if (node == null || node.isNull()) {
return null;
}
if (!node.canConvertToLong()) {
throw new JWTDecodeException(String.format("The claim '%s' contained a non-numeric date value.", claimName));
}
final long ms = node.asLong() * 1000;
return new Date(ms);
return Instant.ofEpochSecond(node.asLong());
}

String getString(Map<String, JsonNode> tree, String claimName) {
Expand Down
31 changes: 24 additions & 7 deletions lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.ObjectReader;

import java.io.Serializable;
import java.time.Instant;
import java.util.*;

import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim;
Expand All @@ -24,14 +25,14 @@ class PayloadImpl implements Payload, Serializable {
private final String issuer;
private final String subject;
private final List<String> audience;
private final Date expiresAt;
private final Date notBefore;
private final Date issuedAt;
private final Instant expiresAt;
private final Instant notBefore;
private final Instant issuedAt;
private final String jwtId;
private final Map<String, JsonNode> tree;
private final ObjectReader objectReader;

PayloadImpl(String issuer, String subject, List<String> audience, Date expiresAt, Date notBefore, Date issuedAt, String jwtId, Map<String, JsonNode> tree, ObjectReader objectReader) {
PayloadImpl(String issuer, String subject, List<String> audience, Instant expiresAt, Instant notBefore, Instant issuedAt, String jwtId, Map<String, JsonNode> tree, ObjectReader objectReader) {
this.issuer = issuer;
this.subject = subject;
this.audience = audience != null ? Collections.unmodifiableList(audience) : null;
Expand Down Expand Up @@ -64,19 +65,35 @@ public List<String> getAudience() {

@Override
public Date getExpiresAt() {
return expiresAt;
return (expiresAt != null) ? Date.from(expiresAt) : null;
}


@Override
public Date getNotBefore() {
return notBefore;
public Instant getExpiresAtAsInstant() {
return expiresAt;
}

@Override
public Date getIssuedAt() {
return (issuedAt != null) ? Date.from(issuedAt) : null;
}

@Override
public Instant getIssuedAtAsInstant() {
return issuedAt;
}

@Override
public Date getNotBefore() {
return (notBefore != null) ? Date.from(notBefore) : null;
}

@Override
public Instant getNotBeforeAsInstant() {
return notBefore;
}

@Override
public String getId() {
return jwtId;
Expand Down
11 changes: 9 additions & 2 deletions lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.time.Instant;
import java.util.*;

/**
Expand Down Expand Up @@ -71,13 +72,15 @@ private void writeAudience(JsonGenerator gen, Map.Entry<String, Object> e) throw
}

/**
* Serializes {@linkplain Date} to epoch second values, traversing maps and lists as needed.
* Serializes {@linkplain Instant} to epoch second values, traversing maps and lists as needed.
* @param value the object to serialize
* @param gen the JsonGenerator to use for JSON serialization
*/
private void handleSerialization(Object value, JsonGenerator gen) throws IOException {
if (value instanceof Date) { // EXPIRES_AT, ISSUED_AT, NOT_BEFORE, custom date claims
if (value instanceof Date) {
gen.writeNumber(dateToSeconds((Date) value));
} else if (value instanceof Instant) { // EXPIRES_AT, ISSUED_AT, NOT_BEFORE, custom Instant claims
gen.writeNumber(instantToSeconds((Instant) value));
} else if (value instanceof Map) {
serializeMap((Map<?, ?>) value, gen);
} else if (value instanceof List) {
Expand Down Expand Up @@ -105,6 +108,10 @@ private void serializeList(List<?> list, JsonGenerator gen) throws IOException {
gen.writeEndArray();
}

private long instantToSeconds(Instant instant) {
return instant.getEpochSecond();
}

private long dateToSeconds(Date date) {
return date.getTime() / 1000;
}
Expand Down
Loading