JWSを作ってみる

OAuth2を使っててJWSとかでてきて、こいつは一体何者なんだって思うときあるじゃないですか。なので調べてみて使ってみたいなと。

JWSとはなにか

関連するRFCは以下の通り。

日本語訳されたものもありました。

また、以前OAuth徹底入門という本を買ったんですが、こちらに詳しく書いて合ったので参考に。

JWS (JSON Web Signature)はOAuth2においてトークンの一種類になるのかなと。OAuth2では認可サーバーがクライアントに渡すトークンの詳細については言及していなくて、開発者がそれぞれの状況に応じて適切な実装を行うことになりますが、そこで使われるものの一つというわけです。名前からも分かる通り、JSONフォーマットをトークンとしクライアントに権限付与されたスコープなどがトークンの中に含まれていたりします。

JWTは署名を付与することでJWS (JSON Web Signature)となり、*1 JWTまたはJWSは署名を持つことでリソースサーバーは受け取ったトークンの改ざんを検知することができます。この仕組によってリソースサーバーがトークンを検証するときに認可サーバーと直接やり取りする必要がなくなり、各サーバーの独立性を高めることができるというわけですね。ちなみに署名なしJWT、JWSというのもあるみたいですが改ざん検知ができないので実環境で使用されることはないでしょう。

JWSは以下の構造になります。

  • header
  • payload
  • signature ←これが署名

JWSはインターネットを介してやり取りされることを目的としているのでそれぞれの要素はBase64エンコードされ、ドット(.)で連結されます。その値自体をトークン値として認可サーバーは発行し、リソースサーバーは検証するという流れになります。

JWSの構造のイメージはこんな感じかなと思います。

f:id:bau1537:20210110152644p:plain

署名を作成する方法はRFC7518のセクション3にまとめてあります。ネットをざっと調べてみた感じ、共有鍵を用いたHS256と、公開鍵を用いたRS256が一般的に使われている印象でした。

こうして作成されるJWSを用いて、OAuth2の各登場人物たちがどのように振る舞うかというイメージはこんな感じかなと思います。ここで大切なのはクライアントはトークンがUUIDだろうがJWSだろうが、知る必要はないということですかね。なので、JWSを使用するにあたってクライアントは特別何かをしなくちゃいけないわけではありません。UUIDだろうが、JWSだろうが、トークン値を認可サーバーから取得してクライアントに示すという流れは変わらないということですね。そう考えてみるとOAuth2というのが実装に対しては柔軟にできているのかなと感じます。もしかしたら今後、JWSに変わるなにか別のトークンの形式が発明されたとしてもクライアントはその中身を知る必要はないのでしょう。

f:id:bau1537:20210110155029p:plain

JavaでJWSを作ってみる

とりあえず、作って検証してみようかなと。

ライブラリはこれを使います。

https://connect2id.com/products/nimbus-jose-jwt

Mavenでプロジェクトを作成。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>nimbus</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>14</maven.compiler.source>
        <maven.compiler.target>14</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>9.4.1</version>
        </dependency>
    </dependencies>

</project>

ライブラリのホームページにサンプルが合ったのでそれに合わせてコードを書いてみました。このライブラリ、サンプルがとても充実しているように見えるのでとても助かります。

以下はHS256で署名をする例です。HS256は共有鍵暗号なので、JWTを署名する側、検証する側が同じ鍵を使用することになるかと。コードでも同じ鍵を用いて署名、検証しているのがわかります。

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;

import java.security.SecureRandom;
import java.text.ParseException;

public class JWS_HS256 {
    public static void main(String[] args) throws JOSEException, ParseException {
        // JWTを作成
        // header と payload の値をセットする
        JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), new Payload("hello, world!"));

        // 共有鍵の作成
        // 32byte = 256bit
        byte[] sharedKey = new byte[32];
        new SecureRandom().nextBytes(sharedKey);

        // 共有鍵で署名
        jwsObject.sign(new MACSigner(sharedKey));

        // JWSを出力
        System.out.println(jwsObject.serialize());

        // JWSを共有鍵を用いて検証
        String s = jwsObject.serialize();
        JWSObject parsedJWSObject = JWSObject.parse(s);
        MACVerifier verifier = new MACVerifier(sharedKey);
        System.out.println(parsedJWSObject.verify(verifier));

        System.out.println(parsedJWSObject.getHeader().toString());
        System.out.println(parsedJWSObject.getPayload().toString());
        System.out.println(parsedJWSObject.getSignature());
    }
}

作成されたJWSはこんな感じになりました。ドットで3つに区切られているのがわかりますね。左から順にヘッダー、ペイロード、署名になっているはずです。

eyJhbGciOiJIUzI1NiJ9.aGVsbG8sIHdvcmxkIQ.t1tDM7CZzXWZH07HL2Ey7M9mp1sFIlyCAgWdFN9pPzI

今度はRS256のアルゴリズムで署名を作成してみます。RS256はRSAを用いて公開鍵暗号で署名を作るので、署名を作成する側は秘密鍵を、署名を検証する側は公開鍵を使います。

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;

import java.text.ParseException;

public class JWS_RS256 {
    public static void main(String[] args) throws JOSEException, ParseException {
        // RSAの鍵ペアをJWKの形式で生成
        // 鍵の長さは2048以上が推奨されている
        RSAKey rsaJWK = new RSAKeyGenerator(2048)
                .keyID("123")
                .generate();
        System.out.println("RSA private key (JWK) : " + rsaJWK.toJSONString());
        RSAKey rsaPublicJWK = rsaJWK.toPublicJWK();
        System.out.println("RSA public key (JWK) : " + rsaPublicJWK.toJSONString());

        // JWT を作成
        JWSObject jwsObject = new JWSObject(
                new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(),
                new Payload("In RSQ we trust!")
        );

        // RSAのプライベート鍵で署名
        JWSSigner signer = new RSASSASigner(rsaJWK);
        jwsObject.sign(signer);
        String s = jwsObject.serialize();

        System.out.println("Generated JWS signed RS256 : " + s);

        // RSAの公開鍵で検証
        JWSObject parsedJWSObject = JWSObject.parse(s);
        RSAKey parsedRsaPublicJWK = RSAKey.parse(rsaPublicJWK.toJSONString());
        JWSVerifier verifier = new RSASSAVerifier(parsedRsaPublicJWK);

        System.out.println(parsedJWSObject.verify(verifier));
        System.out.println(parsedJWSObject.getPayload().toString());
    }
}

RS256で作成されたJWSも先程と同様のドットで区切られた値が得られます。公開鍵暗号で作成された署名はちょっと長いのでここでは割愛します。

*1:修正:JWTは署名を付与することでJWSになるのではない。ペイロードJSONを持つものをJWTというのであり、署名の有無はJWTもJWSも関係ない。https://stackoverflow.com/questions/27640930/what-is-the-difference-between-json-web-signature-jws-and-json-web-token-jwt