CLOVER🍀

That was when it all began.

Java JWTでJWT

これは、なにをしたくお曞いたもの

  • 最近、ちょっずJWTに぀いお知らないずいけないなぁず思うできごずがありたしお
  • 裏の仕組みずしおJWTを䜿っおいるのもいいのですが、もう少しJWT自䜓に向き合っおみようず
  • なにかしらJWTを扱えるラむブラリを䜿っお詊し぀぀、感芚を掎んでみる

ずいう、ずにかくJWTを扱っおみようずいう話。

Java JWT

JWTを扱うのには、Java JWTずいうラむブラリを遞んでみたした。

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

jwt.ioにも茉っおいるラむブラリで、GitHubのスタヌ数も倚めかなず。

JSON Web Tokens - jwt.io

そもそも、JWT自䜓は

仕様を読もう。

JSON Web Token (JWT)

JWTによるJSONに対する電子署名と、そのユースケース | DevelopersIO

このあたりを読んでいお、JWTJSON Web Token、JWSJSON Web Signature、JWEJSON Web Encryptionの
関係がちょっずわかる感じですねぇ。

JSON Web Token (JWT)

JSON Web Token (JWT)

クレヌムのセットをJSONオブゞェクトずしお文字列衚珟にしおJWSやJWEに゚ンコヌドするこずで, クレヌムに察するデゞタル眲名やMACず暗号化の䞡方が可胜になる.

JSON Web Signature (JWS)

JSON Web Signature (JWS)

デゞタル眲名もしくはMAC化されたメッセヌゞを衚珟するデヌタ構造. JWSは以䞋の3぀の倀より構成される: JWS ヘッダ, JWS ペむロヌド, JWS 眲名

仕様曞のJWTの䟋を芋おいくず、JSONであるJWTヘッダ、JWTクレヌム・セットからJWSが䜜られおいくさたが
わかりたすね。

JWTの䟋

Java JWTの「Signed JWTs」を芋るず、その様子がコヌドで曞かれおいるので、こちらの方が感芚を぀かみやすいかも
しれたせん。

Signed JWTs

JWSを構成する文字列は、Base64で簡単にデコヌドでき、JWTヘッダ、JWTクレヌム・セット、眲名ずなり、内容自䜓は誰でも読める、
ずいう感じですね改ざん防止は眲名でできる。

Java JWTを䜿っおみる

ずいうわけで、ここたで前眮きにしおJava JWTでちょっず遊んでみたしょう。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-1ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)


$ mvn -version
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T03:41:47+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-39-generic", arch: "amd64", family: "unix"
準備

Maven䟝存関係は、こちら。

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>

jjwt-apiずjjwt-implはずりあえず必芁なのず、JSONを扱うためのラむブラリが必芁です。Java JWTはJacksonずJSON-Javaの
シリアラむザヌデシリアラむザヌを提䟛しおおり、このどちらかを䜿うこずになりたす。

なお、jjwt-api以倖は、いずれもスコヌプはruntimeでOKです。

あずは、テストコヌドで確認するので、テストラむブラリを远加。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.11.1</version>
            <scope>test</scope>
        </dependency>
テストコヌドの雛圢

テストコヌドの雛圢は、こちら。
src/test/java/org/littlewings/jjwt/JjwtTest.java

package org.littlewings.jjwt;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.crypto.spec.SecretKeySpec;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class JjwtTest {

    // ここに、テストを曞く
}

ここに、テストを足しおいきたいず思いたす。

簡単なJava JWTのサンプル

ずりあえず、曞いおみたのはこんな感じ。

    @Test
    public void gettingStarted() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");
    }

秘密鍵はランダムUUIDで適圓に生成、JWTクレヌム・セットはsubjectのみです。

JWTを䜜っおいるのは、この郚分です。

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .compact();

アルゎリズムは、鍵の長さに合わせお遞択しおもらいたした。

遞択肢は、このバヌゞョンだず「HmacSHA512」、「HmacSHA384」、「HmacSHA256」から遞ばれたす。

生成されたJWTは、こんな感じになりたす。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoifQ.wHj5Yg1P9lh4MXQuf5ViXqgHeA9thpZ0vsvfIGAeZ80

パヌスしおみたしょう。

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(Keys.hmacShaKeyFor(secretKeyAsBytes))
                        .parseClaimsJws(jwsString);

JW"S"ずしおパヌスしたす。

System.out.printlnしおみるず、こんな感じになっおいたす。

parsed JWT = header={alg=HS256},body={sub=磯野カツオ},signature=wHj5Yg1P9lh4MXQuf5ViXqgHeA9thpZ0vsvfIGAeZ80

䞭身を確認。

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");

今回の秘密鍵では、「HmacSHA256」アルゎリズムが遞択されたようですJWT䞊は「HS512」。

Java JWTのサンプルでは、秘密鍵を自動生成するサンプルになっおいるのですが、こちらはたあいいかなず。
※アルゎリズムは明瀺しおある

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

String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();
アルゎリズムを明瀺する

先ほどはアルゎリズムをラむブラリ偎に遞んでもらっおいたしたが、今床は明瀺しおみたす。

    @Test
    public void specifiedAlgorithm() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");
    }

SecretKeySpecを䜿っお、指定したした。

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

これは、先ほどのラむブラリ偎で遞んでもらっおいる堎合に、内郚的に䜿われおいる方法ず同じなのですがね 。

JWTず、パヌスした結果はこちらです。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoifQ.zr8C4O7IlDpHrnr-8gcEUtlFTMihFL0GAGppGklGhx4
parsed JWT = header={alg=HS256},body={sub=磯野カツオ},signature=zr8C4O7IlDpHrnr-8gcEUtlFTMihFL0GAGppGklGhx4
JWTに有効期限を蚭定する

続いお、JWTに有効期限を蚭定しおみたしょう。

    @Test
    public void expired() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        long timeout = 1 * 1000; // 1 sec

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .setExpiration(new Date(System.currentTimeMillis() + timeout))
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");

        try {
            TimeUnit.SECONDS.sleep(2L);
        } catch (InterruptedException e) {
            // ignore
        }

        assertThatThrownBy(() ->
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString)
        )
                .isInstanceOf(ExpiredJwtException.class)
                .hasMessageContaining("JWT expired at");
    }

有効期限は、Dateで指定したす。今回は、有効期限を1秒にしおみたす。

        long timeout = 1 * 1000; // 1 sec

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .setExpiration(new Date(System.currentTimeMillis() + timeout))
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

こうするず、JWTを䜜成した盎埌はパヌスできたすが

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");

少し埅぀ず、有効期限切れで䟋倖がスロヌされたす。

        try {
            TimeUnit.SECONDS.sleep(2L);
        } catch (InterruptedException e) {
            // ignore
        }

        assertThatThrownBy(() ->
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString)
        )
                .isInstanceOf(ExpiredJwtException.class)
                .hasMessageContaining("JWT expired at");

生成されたJWTず、パヌス結果はこちら。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoiLCJleHAiOjE1NDIyOTQ0NzJ9.jaio2KTNJ-OQdMJ-qe9mhOv6LFrvA8PKEz6rvW10Tx0
parsed JWT = header={alg=HS256},body={sub=磯野カツオ, exp=1542294472},signature=jaio2KTNJ-OQdMJ-qe9mhOv6LFrvA8PKEz6rvW10Tx0
眲名をちょっずいじっおみる

JWTずいうかJWSを構成しおいる、最埌の芁玠である眲名を少しいじっお、怜蚌で゚ラヌになるこずを確認しおみたす。

    @Test
    public void invalidKey() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        String jwsString =
                Jwts.builder()
                        .setSubject("磯野カツオ")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        assertThatThrownBy(() ->
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString.substring(0, jwsString.length() - 1))
        )
                .isInstanceOf(SignatureException.class)
                .hasMessage("JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.");
    }

JWTを1文字削ったので、眲名が合わなくなり䟋倖がスロヌされたした。

JWTクレヌム・セットを蚭定する

最埌に、JWTクレヌム・セットを蚭定しおみたす。

以䞋のように、Claimsを䜿うこずでMapのようにずいうか、Mapの実装になっおいるのですが、任意の項目を蚭定
するこずができたす。

    @Test
    public void setClaims() {
        String secretKey = UUID.randomUUID().toString();
        byte[] secretKeyAsBytes = secretKey.getBytes(StandardCharsets.UTF_8);

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, "磯野カツオ");
        claims.put(Claims.ISSUER, "foo");
        claims.put("mydata", "Hello!!");

        String jwsString =
                Jwts.builder()
                        .setClaims(claims)
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

        System.out.println("JWT = " + jwsString);

        Jws<Claims> parsedJws =
                Jwts.parser()
                        .setSigningKey(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .parseClaimsJws(jwsString);

        System.out.println("parsed JWT = " + parsedJws);

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getBody().getIssuer()).isEqualTo("foo");
        assertThat(parsedJws.getBody().get("mydata")).isEqualTo("Hello!!");

        assertThat(parsedJws.getBody().toString()).isEqualTo("{sub=磯野カツオ, iss=foo, mydata=Hello!!}");

        assertThat(parsedJws.getHeader().getAlgorithm()).isEqualTo("HS256");
    }

こんな感じですね。

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, "磯野カツオ");
        claims.put(Claims.ISSUER, "foo");
        claims.put("mydata", "Hello!!");

        String jwsString =
                Jwts.builder()
                        .setClaims(claims)
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

もずもず暙準的に決たっおいるものStandard Claimsに぀いおは、以䞋のようにメ゜ッドが甚意されおいたりしたす。

        String jwsString =
                Jwts.builder()
                        .setSubject("...")
                        .setIssuer("...")
                        .signWith(new SecretKeySpec(secretKeyAsBytes, "HmacSHA512"))
                        .compact();

Claimsに察しおも、キヌが定矩されたりしおたしたね。

        Claims claims = Jwts.claims();
        claims.put(Claims.SUBJECT, "磯野カツオ");
        claims.put(Claims.ISSUER, "foo");
        claims.put("mydata", "Hello!!");

getterもあるわけですよ。

        assertThat(parsedJws.getBody().getSubject()).isEqualTo("磯野カツオ");
        assertThat(parsedJws.getBody().getIssuer()).isEqualTo("foo");

Mapのキヌを、Builderに察しお盎接蚭定するこずもできるので、そのあたりはドキュメントを参照するずよいでしょう。

Claims

同様のこずが、JWTヘッダヌにも蚀えたす。

Header Parameters

このサンプルで、生成されたJWTずパヌス結果は、こちらです。

JWT = eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLno6_ph47jgqvjg4TjgqoiLCJpc3MiOiJmb28iLCJteWRhdGEiOiJIZWxsbyEhIn0.r2Ew-mOe5G05x_1_Y6qM7FeTEpteev_Rc2YKNjAOOZM
parsed JWT = header={alg=HS256},body={sub=磯野カツオ, iss=foo, mydata=Hello!!},signature=r2Ew-mOe5G05x_1_Y6qM7FeTEpteev_Rc2YKNjAOOZM

なんずなく、雰囲気は぀かめたので、これで良しずしたしょう。