Java で JWT の利用

JSON Web Tokens - jwt.io
Java で利用する。
利用環境、pom.xml  Maven

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

JWTの生成

// 有効期間  3分
int minutes = 3;
// 秘密鍵
String secret = "123abc";
// 期間設定の為の計算
LocalDateTime nowtime = LocalDateTime.now();
long nowmilisec = nowtime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
java.util.Date before = Date.from(ZonedDateTime.of(nowtime, ZoneId.systemDefault()).toInstant());
java.util.Date expiretime = new Date(nowmilisec + (60 * minutes * 1000));

// ヘッダに任意のデータセットは Mapで
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("ha", "HA");
headerClaims.put("point", 12);

// アルゴリズムを秘密鍵で生成
Algorithm algorithm = Algorithm.HMAC256(secret);

String token = JWT.create()
   .withJWTId("222")
   .withKeyId("aaaa")
   .withAudience("A", "B", "C")
   .withIssuer("issuer")
   .withSubject("subjects")
   .withIssuedAt(Date.from(ZonedDateTime.of(nowtime, ZoneId.systemDefault()).toInstant()))
   .withNotBefore(before)
   .withExpiresAt(expiretime)
   .withClaim("X-EMAIL", "yip@xxx.xxx.com")
   .withClaim("X-SIZE", 1024)
   .withHeader(headerClaims)
   .sign(algorithm);

ヘッダのJSONが、以下のとおりにできて、

{"kid":"aaaa","ha":"HA","typ":"JWT","alg":"HS256","point":12}

ペイロードJSON が、以下のとおりにできて、

{"aud":["A","B","C"],"sub":"subjects","nbf":1602924613,"iss":"issuer","X-EMAIL":"yip@xxx.xxx.com","exp":1602924793,"iat":1602924613,"X-SIZE":1024,"jti":"222"}

署名が作られて、Base64 エンコードした文字列、String token が作成できます。
これは、それぞれ

String headerDecoded = new String(Base64.getUrlDecoder().decode(JWT.decode(token).getHeader().getBytes()), StandardCharsets.UTF_8);
String payloadDecoded = new String(Base64.getUrlDecoder().decode(JWT.decode(token).getPayload().getBytes()), StandardCharsets.UTF_8);

JSON文字列化することです。

JWT発行時間検査の為の時刻を生成するのに、com.auth0.jwt は、java.util.Date
withIssuedAt , withNotBefore , withExpiresAt で生成しなくてはならず、
java.time.LocalDateTime を使用するのスタンダードな今の時代ではちょっと使いにくいです。
次のJWTトークンの読込み&検証でも使用する以下のユーティリティを用意しておくと便利です。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Locale;
import com.auth0.jwt.exceptions.TokenExpiredException;

public final class JwtUtil {
   private JwtUtil() {}
   public static LocalDateTime getExpiredDate(TokenExpiredException x){
      try{
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
               .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return Instant.ofEpochMilli(dt.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
   public static long getExpireOverTime(TokenExpiredException x) {
      try{
         long epocmili = Instant.now().toEpochMilli();
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
                     .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return epocmili - dt.getTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
   public static java.util.Date toDate(LocalDateTime at){
      return java.util.Date.from(ZonedDateTime.of(at, ZoneId.systemDefault()).toInstant());
   }
   public static LocalDateTime toLocalDateTime(java.util.Date date) {
      return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
   }
}

JWTトークンの読込み&検証

受信したトークン(Base64エンコード済)は、
 JWT.require(アルゴリズム).build()
で生成した JWTVerifier で、verify(token)実行することで、署名検証&デコードまでするのですが、
以下のように、ラムダ実行できるものを用意すると便利なはずです。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Locale;
import java.util.function.Consumer;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;

public final class JWTVerifyAction{
   private Algorithm algorithm;

   private JWTVerifyAction(Algorithm algorithm){
      this.algorithm = algorithm;
   }
   public static JWTVerifyAction of(Algorithm algorithm){
      return new JWTVerifyAction(algorithm);
   }

   public boolean verifyRead(String token, Consumer<DecodedJWT> decoded, Consumer<Throwable> error){
      JWTVerifier verifier = JWT.require(algorithm).build();
      try{
         DecodedJWT jwt = verifier.verify(token);
         decoded.accept(jwt);
         return true;
      }catch(Throwable e){
         error.accept(e);
      }
      return false;
   }

   public LocalDateTime getExpiredDate(TokenExpiredException x){
      try{
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
               .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return Instant.ofEpochMilli(dt.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
   public long getExpireOverTime(TokenExpiredException x) {
      try{
         long epocmili = Instant.now().toEpochMilli();
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
                     .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return epocmili - dt.getTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
}

検証で発生する例外Exception は、

SignatureVerificationException secretKeyや署名にエラーがある場合
AlgorithmMismatchException 使用 Algorithm が不一致
JWTDecodeException token 改竄エラー
TokenExpiredException 有効期限切れエラー
InvalidClaimException トークン使用する前にトークン生成時刻になっているエラー


になります。

使用例

Algorithm algorithm = Algorithm.HMAC256("123abc");

JWTVerifyAction action = JWTVerifyAction.of(algorithm);
action.verifyRead(token,
   d->{
      System.out.println("aud        = "+ d.getAudience()  );
      System.out.println("iss        = "+ d.getIssuer()  );
      System.out.println("sub        = "+ d.getSubject()  );
      System.out.println("expired    = "+ d.getExpiresAt() );
      System.out.println("expired    = "+ JwtUtil.toLocalDateTime(d.getExpiresAt()) );
      System.out.println("nbf        = "+ d.getNotBefore() );
      System.out.println("nbf        = "+ JwtUtil.toLocalDateTime(d.getNotBefore()) );
      System.out.println("iat        = "+ d.getIssuedAt() );
      System.out.println("iat        = "+ JwtUtil.toLocalDateTime(d.getIssuedAt()) );
      System.out.println("X-EMAI     = "+ d.getClaim("X-EMAIL").asString()  );
      System.out.println("X-SIZE     = "+ d.getClaim("X-SIZE").asInt()  );
      System.out.println("jti        = "+ d.getId()  );
      System.out.println("Algorithm  = "+ d.getAlgorithm()  );
      System.out.println("header typ = "+ d.getType()  );
      System.out.println("header kid = "+ d.getKeyId() );
      System.out.println("headerClaim ha    = "+ d.getHeaderClaim("ha").asString()  );
      System.out.println("headerClaim point = "+ d.getHeaderClaim("point").asInt()  );

      System.out.println("signature  = "+ d.getSignature() );
      System.out.println("payload    = "+ d.getPayload() );
      System.out.println("header     = "+ d.getHeader() );

      String payloadJson = new String(Base64.getUrlDecoder().decode(d.getPayload().getBytes()), StandardCharsets.UTF_8);
      System.out.println(payloadJson);
      String headerJson = new String(Base64.getUrlDecoder().decode(d.getHeader().getBytes()), StandardCharsets.UTF_8);
      System.out.println(headerJson);
   },
   e->{
      if (e instanceof TokenExpiredException){
         LocalDateTime expAt = JwtUtil.getExpiredDate((TokenExpiredException) e);
         System.out.print("TokenExpiredException !! expAt = "+expAt);
         long overtime = JwtUtil.getExpireOverTime((TokenExpiredException) e);
         System.out.println("  over : " + overtime + " msec");

      }else if(e instanceof SignatureVerificationException){
         System.out.print("SignatureVerificationException !! secretKey or 署名にエラー ");

      }else if(e instanceof AlgorithmMismatchException){
         System.out.print("AlgorithmMismatchException !! アルゴリズムが一致しない ");

      }else if(e instanceof JWTDecodeException){
         System.out.print("JWTDecodeException !! トークンが改竄 ");

      }else if(e instanceof InvalidClaimException){
         System.out.print("InvalidClaimException !! nbf > iat のトークン使用する前にトークン生成時刻になっているエラー");

      }else{
         System.out.println("その他のエラー:" + e.getMessage());
      }
      // TODO

   }
);

com.auth0.jwt は、JSON を処理するのに、Jackson を使用しています。
使用する依存ライブラリは気をつけたほうが良いでしょう。