JSON Web Token + DataStore で期限付き、無効化可能なトークンを実装する方法


JSON Web Token + DataStore で期限付き、無効化可能なトークンを実装する方法

ritou です。

結構前に JSON Web Signature の署名生成/検証あたりの考え方をまとめた投稿をしました。
JSON Web Signature導入における鍵周りの基本的な考え方

これはいろんなところで JWT(JWS) を使ってる中で署名の鍵をちゃんと整理しないといかんというところから出てきたお話でした。
今回はその "いろんな用途" の中の一つで、"有効期限付き" で "無効化可能" なトークンの実装例を文章化しておきます。

用途

  • 何かの処理を始めるときに作成
  • 処理をしている中でデータを積んだりする
  • 処理が終わったら無効化

みたいな処理を "できるだけ DataStore の利用容量を抑えつつ" 実装するイメージです。

この "処理" の部分が1つであれば "ワンタイム" と言えますが、複数の処理を連動させたりも考慮して "無効化可能" として紹介します。
一般的なWebアプリケーションであればセッション機構を用いて値を積んだり消したりすれば済む程度のことを API ベースでやろうと思った時などに使えると思います。

DataStore

RDB とか NoSQL 、あとはキャッシュのみの利用などの細かいことを意識しないで済むように、ここでは一連の処理の識別子について

  • get
  • put
  • delete

ぐらいしか使わないような実装を意識しています。

関連する仕様

なるべく RFC7519 JSON Web Token (JWT) 日本語訳 で定義されている標準的な Header / Payload を使おうぐらいです。

実装例 : CSRF Tokenみたいなやつ

特別なコンテンツを含まず、自分で生成、検証、無効化する例です。

1. JWT生成

下記のようなHeader / Payloadを作成して JWS を作成します。
鍵周りは上述の記事を参考にしてもらえばと思います。

  • Header
    • typ : (必須)このJWTの種別
    • alg : (任意)
    • kid : (任意)
  • Payload
    • usage : (任意)JWTの用途を入れたりする。Header の typ を使っても良さそう。
    • jti : (必須)識別子。UUID だったり bigint とか。
    • exp : (必須)有効期限を設定することで、以前発行したトークンの検証をスキップできる。
    • iss : (任意)発行元
    • aud : (任意)発行先

似たような処理でJWTを使いまくりたくなると思うので(任意)とある部分もなるべく利用するのが良さそうです。

ここで DataStore をどう使うかというあたりで、2種類ぐらいの使い方があるかなと思います。

  • 有効な識別子を保存している場合 : jti の値を put
  • 無効化された識別子を保存している場合 : 何もしない

識別子に bigint 的なのを使って生成時に衝突の可能性もある場合、前者のように jti の値を保存しておきます。
UUID やちょっと前に話題になってた ULID みたいな、識別子がぶつからない仕組みになっている場合は、後者でやると DataStore の容量を節約できそうです。
最後に書いてある "DataStore に残るゴミデータ" についても見ておいてください。

2. JWT検証

  1. JWTの種別、署名の検証 : 用途の異なる JWT や不正な JWT を除外できる
  2. iss, aud, exp の検証 : 古い JWT などを除外できる
  3. jti の検証 : 無効化された JWT を除外できる

3 の検証時は、DataStore で以下の処理が行われます。

  • 有効な識別子を保存している場合 : jti の値で get して値があることを確認
  • 無効化された識別子を保存している場合 : jti の値で get して値がないことを確認

3. JWT無効化

処理が終わったらこの JWT を無効化する必要があります。

  • 有効な識別子を保存している場合 : jti の値のデータを delete する
  • 無効化された識別子を保存している場合 : jti の値を put する

これで再度 "2. JWT検証" で jti の検証が走っても除外することができます。

ちょっと複雑な処理への応用

このやり方は、例えばアカウント新規登録時のようにいくつかのデータを1つずつ受け取って検証、最後にガーッと登録処理を行うような場合にも DataStore 側を拡張せずに利用できます。
センシティブなデータを扱うだったら JWE 使ってもいいと思いますが、お金とかになったらそもそもこんなやり方できないと思うのでその辺はよしなに判断してください。

例えば

  1. SMS送信による電話番号確認
  2. ニックネーム設定
  3. パスワード設定

といった処理を複数のAPIリクエストを用いて行う場合、

  • 確認対象(完了前後の)電話番号
  • ニックネーム
  • パスワード

あたりを JWT の Payload に含んで署名を再生成して引回すことで、最後の本登録の処理の部分までデータを引回すことができます。
ちょっと JWT 関連の処理は増えるものの、DataStoreの方に仮登録状態のようなデータを持たずに識別子の管理だけで実装可能なため、登録処理に必要な情報が増えても割と簡単に対応可能です。たぶん。

DataStore に残るゴミデータ

このような実装は、当然 DataStore にゴミデータみたいなのが残ります。

  • 有効な識別子を保存している場合 : JWTの有効期限が切れても無効化されなかった jti の値
  • 無効化された識別子を保存している場合 : 無効化された jti の値

put する際に JWT の exp に指定した値をうまく使って定期的に削除を試みるなど、ゴミデータが残りにくい仕組みにしておく必要があるでしょう。

まとめ

  • JWTを使った有効期限つき、無効化可能なトークンの作り方を紹介した
  • DataStoreの種類にもあんまり影響しない書き方したつもりだけど実際はよしなにやってくれ
  • CSRFトークンみたいな使い方や、登録処理のような複数のデータを持ちたい場合にも使えそう

以上です。
ではまた。