いしぐめも

プログラミングとかしたことを書きます。

IDトークンのパースにはjjwtが便利

Java で行う JWT (Json Web Token) のパース/検証には jjwt が便利です。

github.com

現在、私も jjwt を使用してOpenID Connectの IDトークンをパースする実装を行っています。

jjwt の基本的な使い方

jjwt は jwt をパースすると同時に与えられた鍵を使用して署名検証までしてくれます。

Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(idToken);

こんな感じで io.jsonwebtoken.JwtParserparseClaimsJws を呼ぶだけで、idToken に含まれる諸々をパースしてくれます。検証結果については、失敗した場合に例外が投げられるようになっているので、 try-catch でハンドリングしていけばよいです。

鍵の指定

JwtParser 作成の際には setSigningKey を使用して、検証に使用する鍵を指定することができます。

あらかじめ、使用される鍵がわかっている場合はいいんですが、IDトークンの場合、鍵情報が JWT のヘッダー部に格納されていて、それを参照する必要があります。

それについては、io.jsonwebtoken.SigningKeyResolverAdapter を継承したクラスを作成して、resolveSigningKey 関数で使用する鍵を検索する処理を記述します。

public class MySigningKeyResolverJwksResolver extends SigningKeyResolverAdapter {
    
    @Override
    public Key resolveSigningKey(JwsHeader header, Claims claims) {
        // ここにIDトークンのヘッダー部にある情報を使ってKeyを探す処理を書く
    }
}

上記アダプタは下記のように渡すことで使用可能です。

Jws<Claims> jws = Jwts.parserBuilder().setSigningKeyResolver(resolver).build().parseClaimsJws(idToken);

検索対象となる鍵のリストは、あらかじめどこかから入手する必要があるんですが、大方の OP は openid-configuration のURLがドキュメントに載ってたりするので、適宜とってくるのがよいと思います。

developer.yahoo.co.jp

注意点

jjwtを使用するときの注意点として、IDトークンのPayloadに aud クレームを配列で返してくるものには一工夫必要なことがあげられます。

実際にYahoo! ID連携がそうで、aud クレームを配列で返してきます。

developer.yahoo.co.jp

ID Tokenの発行対象のClient IDの配列(1つのClient IDは最大255Byteの可変長文字列)

これについては、IDトークン自体が aud に単一の文字列または配列としており、それに jjwt が対応できていない、というのが現状のようです。これに対し、プルリクエストやIssueも上がっているのですが、 インタフェースの変更を伴うのもあり、本流には取り込まれていないようです。

Audience String array by ricveal · Pull Request #238 · jwtk/jjwt · GitHub

Claims array support (Audience, etc) · Issue #336 · jwtk/jjwt · GitHub

Suport Array of String for Audience · Issue #77 · jwtk/jjwt · GitHub

jjwt の現状

実際に、jjwt で aud を取得してみます。

String audience = claims.getAudience()

とすることで、aud は取得可能ですが、Payload の aud が配列だった場合、

[client_id]

といった形の String が返ってきます。要素がダブルクオーテーションで囲まれていないなど、Json の配列とはまた異なった書かれ方をしています。これはたぶん、ArrayList を無理やり String にキャストしているのかなと思います。(下のような例)

ArrayList<String> hoge = new ArrayList<String>();
hoge.add("blahblah");
hoge.add("foofoo");
System.out.println(hoge);
[blahblah, foofoo]

対処

これに対しては、Payload に含まれる他の Claim の取得と同じように、 get を使用します。

ArrayList<String> audList = claims.get("aud", ArrayList.class);

claims.get の第二引数には requiredType を指定可能で、仮に第一引数で指定された key が示す claim がこれとは異なったクラスでパースされたときは RequiredTypeException が吐かれます。

よって、

try {
  // まずはArrayListでパース
  ArrayList<String> audList = claims.get("aud", ArrayList.class);
} catch(RequiredTypeExcpetion e) {
  // ArrayList ではないので String で処理
  String aud = claims.getAudience();
}

みたいにしてお茶を濁すことができます。

終わりに

最近触っている JWT パースのライブラリ紹介でした。ではでは!