Skip to content

Commit

Permalink
jwtk#60 Add expireAfter functionality to JWT Builder
Browse files Browse the repository at this point in the history
The `expireAfter` method, accepting duration and timeUnit parameters, has been added to the JwtBuilder interface. This method calculates the JWT expiration date as either the issue time plus the duration or the system current time plus the duration if an issuedAt time hasn't been set. Additional tests for this feature have been included in `DefaultJwtParserTest.groovy`.
  • Loading branch information
pveeckhout committed Dec 25, 2023
1 parent 917ffbb commit ff6e2df
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 0 deletions.
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 it if 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
17 changes: 17 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,20 @@ public JwtBuilder id(String jti) {
return claims().id(jti).and();
}

@Override
public JwtBuilder expireAfter(long duration, TimeUnit timeUnit) { // TODO: use java.time for version 1.0?
Assert.state(duration > 0, "duration must be a positive value.");
Assert.stateNotNull(timeUnit, "timeUnit is required.");

Date exp = Optional.ofNullable(this.claimsBuilder.get(DefaultClaims.ISSUED_AT))
.map(Date::getTime)
.map(time -> time + timeUnit.toMillis(duration))
.map(expMillis -> JwtDateConverter.INSTANCE.applyFrom(expMillis / 1000L))
.orElse(JwtDateConverter.INSTANCE.applyFrom((System.currentTimeMillis() + timeUnit.toMillis(duration)) / 1000L));

return claims().expiration(exp).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,50 @@ class DefaultJwtParserTest {
}
}

@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 Down

0 comments on commit ff6e2df

Please sign in to comment.