Expiration of JWT not working when using expiration date in UTC

Ujjwal Jung Thapa picture Ujjwal Jung Thapa · May 17, 2019 · Viewed 11k times · Source

I am using jjwt for jwt token creation. Everything works fine when setting expiration date with local system time, i.e.

Date expDate = new Date(new Date().getTime() + 180000); //java.util.Date

But I tried using UTC format date time and signed the jwt token with same 3 min expiry date. And now it is throwing ExpiredJwtException though even i am validating as soon as creating the token. I am using SimpleDateFormat for setting timezone to utc. This is my code for creating token using jjwt in java:

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date expDate, issDate;
    try {
        expDate = (Date) simpleDateFormat.parse(sdf.format(new Date().getTime() + 180000));
        issDate = (Date) simpleDateFormat.parse(sdf.format(new Date().getTime()));
        JwtBuilder builder = Jwts.builder()
                .setExpiration(expDate)
                .setIssuedAt(issDate)
                .setId(id)
                .signWith(signingKey, signatureAlgorithm);
        jwtToken = builder.compact();
    } catch (ParseException ex) {
    }

The token gets successfully created. I can verify the contents online as well. expDate is 3 min ahead of issDate. I am also calling method for verifying the token as soon as after it was created by passing that created token. My verification method has:

    try {
        Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token);
        log.info("jwt verification success");
    } catch (ExpiredJwtException exJwt) {
        log.info("expired jwt : \n{}", exJwt.getMessage());
    } catch (JwtException e) {
        log.info("tampered jwt");
    }

But I am getting ExpiredJwtException. The error is

expired jwt : JWT expired at 2019-05-17T01:24:48Z. Current time: 2019-05-17T07:06:48Z, a difference of 20520836 milliseconds. Allowed clock skew: 0 milliseconds.

From my log, the issued date and expiration date in my token at this time is:

issued date is: 2019-05-17T07:06:48.000+0545
expiry date is: 2019-05-17T07:09:48.000+0545

How is this happening? And thank you for you help.

Answer

cassiomolin picture cassiomolin · May 17, 2019

There's no need for SimpleDateFormat here, as Date represents the number of milliseconds since the Unix Epoch, that is, midnight on January 1st 1970 (UTC).

What may cause confusion, however, is the toString() method, as it applies the JVM's default time zone when generating a string representing that value.

As you are concerned about UTC, let me just bring your attention to what the Coordinated Universal Time (UTC) actually is: It is a time standard (and not a timezone) and it's determined by highly precise atomic clocks combined with the Earth's rotation.

The UTC time standard was adjusted several times until 1972, when leap seconds were introduced to keep UTC in line with the Earth's rotation, which is not entirely even, and less exact than atomic clocks. As the Earth’s rotation is slowing down, every now and then we have to insert leap seconds here and there:

A graph from xkcd documenting the war between timekeepers and time

While the internal value of Date is intended to reflect UTC, it may not do so exactly due to those leap seconds.

Java 8 and the new API for date and time

Even though Date suits your needs when it comes to UTC, you should avoid it. It's a legacy class now.

Java 8 introduced a new API for dates, times, instants and durations based on the ISO calendar system. The closest equivalent to Date is Instant which represents a timestamp, a moment on the timeline in UTC.

To capture the current moment in UTC, you can use the following:

Instant.now();              // Capture the current moment in UTC

And you can use the following to get a string representing such value:

Instant.now().toString();   // 2019-05-17T12:50:40.474Z

This string is formatted according the ISO 8601, where Z indicates that the given time is in UTC.

Interoperability with JJWT

For interoperability with JJWT, which doesn't support the java.time types yet, you can create an instance of Date from Instant:

Date.from(Instant.now());   // Convert from modern class to legacy class

And here's a test that demonstrates how you can issue and validate a token:

@Test
public void shouldMatchIssuedAtAndExpiration() {

    Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    Instant issuedAt = Instant.now().truncatedTo(ChronoUnit.SECONDS);
    Instant expiration = issuedAt.plus(3, ChronoUnit.MINUTES);

    log.info("Issued at: {}", issuedAt);
    log.info("Expires at: {}", expiration);

    String jws = Jwts.builder()
            .setIssuedAt(Date.from(issuedAt))
            .setExpiration(Date.from(expiration))
            .signWith(key)
            .compact();

    Claims claims = Jwts.parser()
            .setSigningKey(key)
            .parseClaimsJws(jws)
            .getBody();

    assertThat(claims.getIssuedAt().toInstant(), is(issuedAt));
    assertThat(claims.getExpiration().toInstant(), is(expiration));
}

For the above example, I've used JJWT 0.10.5 with the dependencies listed in the documentation. In case you need, the above code was written with the following import statements:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.security.Key;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Date;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;