Ktor では JWT を利用した認証をすることが出来ますが、公式ドキュメントだと情報が少なくて苦労したので記事に残しておきます。
なお本記事では JWS に含まれる範囲も含めて JWT と表現しています。
HS256 か RS256 か
Ktor の公式ドキュメントには HS256 と RS256 の 2 つの署名方式について書いてあります。
HS256 は秘密鍵を共有して署名と検証を行います。
RS256 は公開鍵と秘密鍵のペアを使用して、署名と検証を行います。
RS256 は秘密鍵の所有者のみがトークンに署名でき、秘密鍵が漏れた際も公開鍵を更新するだけで署名鍵をローテーション出来ます。
公開鍵の更新は jwks という方式を使うため、ソフトウェアを更新する必要がありません。
特に理由がなければ RS256 を選ぶべきでしょう。
謎の UUID
ここにある UUID 6f8856ed-9189-488f-9011-0ff4b6c08edc
、最初は意味がわかりませんでした。これは jwk で使われる kid の値です。つまり鍵の ID です。
後で出てくる jwk のところで使用します。
どのクレームが要るのか
まず JWT 自体に必須のクレームはありません。
とりあえず kid があればどの鍵を使用すればいいかがわかるため kid があるといいです。iss や aud は検証側の実装によりますが、ライブラリによっては必須かもしれません。(特に iss は)
Ktor の場合いかのようになります。
JWT.create()
.withAudience("トークンの利用者 aud")
.withIssuer("トークンの発行者 iss")
.withKeyId("kid") //jwkで使用したkidの値を入力
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) //30分後に期限切れ
.sign(Algorithm.RSA256(publicKey, privateKey)) //RS256を使用して署名
jwk って何
これは Ktor のドキュメントでは完全にスルーされています。何故
jwk は JWT で使用する鍵を配布するための取り決めだと思ってください。多分詳しく読んだら違うのだと思いますがとりあえず動いたので良しとします。
あまり良くわかっていないのですがとりあえず https://example.com/.well-known/jwks.json に jwk の json を置いておけばいいです。
jwk の作り方
jwks.json の形式
形式によりますがこうなります。
{
"keys": [
{
"e": 鍵のpublicExponentをゴニョゴニョしてBase64URLエンコードしたもの,
"n": 鍵のmodulusをゴニョゴニョしてBase64URLエンコードしたもの,
"use": "sig",
"kid": 鍵のID (謎のUUID),
"kty": "RSA"
}
]
}
Java の RSAPublicKey を JWK の形式にする方法
上でゴニョゴニョしている部分を実装する必要があります。
長さが 8 の倍数かつ 1 バイト目が 0 で、かつ長さが 1bit 以上のとき 1 バイト目を削除した上で、Base64URL エンコードする必要があります。
val bigInteger = publicKey.publicExponent
var bytes = bigInteger.toByteArray()
if (bigInteger.bitLength() % 8 == 0 && (bytes[0] == 0.toByte()) && bytes.size > 1) {
bytes = Arrays.copyOfRange(bytes, 1, bytes.size)
}
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) //← これ
なんでこんなことやっているのか全くわかりません。
kid は結局何?
鍵の ID なので被らなければ何でもいいです。
署名するときの kid と jwks.json で公開する kid は同じにする必要があります。つまり署名するたびに kid を変えたりしてはいけないということです。
JWT の期限
JWT はステートレスなので一々 DB にアクセスせずともトークンに情報を持たせることが出来ます。逆に DB にアクセスせずに色々済ませるということはトークンが漏れて、それに気づいてもトークンを使用不可能にする方法が秘密鍵の変更しかありません。だって使用不可能なトークンかの判定を行えないんですもの。^1 そこで JWT の期限を短くしてリフレッシュトークンを使用します。
リフレッシュトークン
JWT の期限を 30 分だとしましょう。30 分間は利便性と不正利用のリスクとのトレードオフです。リフレッシュトークンは 2 週間に設定します。こっちも利便性と不正利用のリスクのトレードオフですが、JWT よりは長くしておきます。
API の利用者は、JWT の期限切れに気づいたときにリフレッシュトークンを使用して JWT を発行してもらいます。リフレッシュトークンの期限も切れていたときは再度パスワードなどを使用して認証を行った後、JWT を発行してもらいます。
リフレッシュトークンは DB などで保存し、いつでも破棄できるようにします。
リフレッシュトークンの実装については本題ではないので書きません。Ktor の他の認証プロバイダーを使って制限されたエンドポイントを作成すればいいと思います。
結局何をすれば使えるようになるのか
- /.well-known/jwks.json を作成する。
- 公式ドキュメント通り JWT 認証を追加する。