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

#60 Add expireAfter functionality to JWT Builder #883

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
21 changes: 21 additions & 0 deletions api/src/main/java/io/jsonwebtoken/JwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.security.interfaces.RSAKey;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* A builder for constructing Unprotected JWTs, Signed JWTs (aka 'JWS's) and Encrypted JWTs (aka 'JWE's).
Expand Down Expand Up @@ -585,6 +586,26 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
// for better/targeted JavaDoc
JwtBuilder id(String jti);

/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
* <code>exp</code></a> (expiration) claim. It will set the expiration Date to the issuedAt time plus the duration
* specified if it has been set, otherwise it will use the current system time plus the duration specified
*
* <p>A JWT obtained after this timestamp should not be used.</p>
*
* <p>This is a convenience wrapper for:</p>
* <blockquote><pre>
* {@link #claims()}.{@link ClaimsMutator#expiration(Date) expiration(exp)}.{@link BuilderClaims#and() and()}</pre></blockquote>
*
* @param duration The duration after the issue time that the JWT should expire. It is added to the issue time to
* calculate the expiration time.
* @param timeUnit The time unit of the duration parameter. This specifies the unit of measurement for the
* duration (e.g., seconds, minutes, hours, etc.), determining how the duration value should
* be interpreted when calculating the expiration time.
* @return the builder instance for method chaining.
*/
JwtBuilder expireAfter(long duration, TimeUnit timeUnit);

/**
* Signs the constructed JWT with the specified key using the key's <em>recommended signature algorithm</em>
* as defined below, producing a JWS. If the recommended signature algorithm isn't sufficient for your needs,
Expand Down
24 changes: 24 additions & 0 deletions impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.impl.lang.Functions;
import io.jsonwebtoken.impl.lang.JwtDateConverter;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.impl.security.DefaultAeadRequest;
Expand Down Expand Up @@ -76,7 +77,9 @@
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class DefaultJwtBuilder implements JwtBuilder {

Expand Down Expand Up @@ -477,6 +480,27 @@ public JwtBuilder id(String jti) {
return claims().id(jti).and();
}

@Override
public JwtBuilder expireAfter(final long duration, final TimeUnit timeUnit) { // TODO: use java.time and optionals from jdk 8 for version 1.0
Assert.gt(duration, 0L, "duration must be > 0.");
Assert.notNull(timeUnit, "timeUnit cannot be null.");

Date issuedAtDate = this.claimsBuilder.get(DefaultClaims.ISSUED_AT);
long expiryEpochMillis;
if (null != issuedAtDate) {
expiryEpochMillis = issuedAtDate.getTime() + timeUnit.toMillis(duration);
} else {
expiryEpochMillis = (System.currentTimeMillis() + timeUnit.toMillis(duration));
}
Date expiryDate = JwtDateConverter.INSTANCE.applyFrom(expiryEpochMillis / 1000L);

/*Instant expiryInstant = Optional.ofNullable(this.claimsBuilder.get(DefaultClaims.ISSUED_AT)) // this should return an instant I guess
.orElseGet(() -> Instant.now())
.plus(duration, timeUnit);*/

return claims().expiration(expiryDate).and();
}

private void assertPayloadEncoding(String type) {
if (!this.encodePayload) {
String msg = "Payload encoding may not be disabled for " + type + "s, only JWSs.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import org.junit.Test

import javax.crypto.Mac
import javax.crypto.SecretKey
import java.util.concurrent.TimeUnit

import static org.junit.Assert.*

Expand Down Expand Up @@ -277,6 +278,75 @@ class DefaultJwtParserTest {
}
}

@Test
void testExpiredAfterDurationValidationMessage() {
def duration = -1L
def timeUnit = TimeUnit.MINUTES
try {
Jwts.builder().expireAfter(duration, timeUnit).compact()
} catch (IllegalArgumentException expected) {
String msg = "duration must be > 0."
assertEquals msg, expected.message
}
}

@Test
void testExpiredAfterTimeUnitValidationMessage() {
def duration = 15L
def timeUnit = null
try {
Jwts.builder().expireAfter(duration, timeUnit).compact()
} catch (IllegalArgumentException expected) {
String msg = "timeUnit cannot be null."
assertEquals msg, expected.message
}
}


@Test
void testExpiredAfterExceptionMessage() {
long differenceMillis = 781 // arbitrary, anything > 0 is fine
def duration = 15L
def timeUnit = TimeUnit.MINUTES
def expectedExpiry = JwtDateConverter.INSTANCE.applyFrom((System.currentTimeMillis() + timeUnit.toMillis(duration)) / 1000L)
def later = new Date(expectedExpiry.getTime() + differenceMillis)
def s = Jwts.builder().expireAfter(duration, timeUnit).compact()

try {
Jwts.parser().unsecured().clock(new FixedClock(later)).build().parse(s)
} catch (ExpiredJwtException expected) {
def exp8601 = DateFormats.formatIso8601(expectedExpiry, true)
def later8601 = DateFormats.formatIso8601(later, true)
String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " +
"Current time: ${later8601}. Allowed clock skew: 0 milliseconds."
assertEquals msg, expected.message
}
}

@Test
void testExpiredAfterWithIssuedAtExceptionMessage() {
long differenceMillis = 781 // arbitrary, anything > 0 is fine
def duration = 15L
def timeUnit = TimeUnit.MINUTES
def issuedAt = JwtDateConverter.INSTANCE.applyFrom((System.currentTimeMillis() + timeUnit.toMillis(-1L)) / 1000L) //set it to one minute earlier
def expectedExpiry = JwtDateConverter.INSTANCE.applyFrom((System.currentTimeMillis() + timeUnit.toMillis(duration - 1L)) / 1000L) // we expect it to expire a minute earlier
def later = new Date(expectedExpiry.getTime() + differenceMillis)
def s = Jwts.builder()
.issuedAt(issuedAt)
.expireAfter(duration, timeUnit)
.compact()

try {
Jwts.parser().unsecured().clock(new FixedClock(later)).build().parse(s)
} catch (ExpiredJwtException expected) {
def exp8601 = DateFormats.formatIso8601(expectedExpiry, true)
def later8601 = DateFormats.formatIso8601(later, true)
String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " +
"Current time: ${later8601}. Allowed clock skew: 0 milliseconds."
assertEquals msg, expected.message
}
}

@Test
void testNotBeforeExceptionMessage() {

Expand All @@ -291,7 +361,7 @@ class DefaultJwtParserTest {
def nbf8601 = DateFormats.formatIso8601(nbf, true)
def earlier8601 = DateFormats.formatIso8601(earlier, true)
String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " +
"Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds.";
"Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds."
assertEquals msg, expected.message
}
}
Expand Down