メインコンテンツまでスキップ

JWT(JSON Web Token)

参考URL
JWTハンドブック(今度読む)
JWS,JWE,JWKなどの仕様について詳しい
SPAで使うことの懸念

JWTとは

JWTとは、JSON形式で表現されたクレーム (claim) の集合を、JWS もしくは JWE に埋め込んだもの。
JWTではシンプルにトークンの形式のみが規定されておりpayload部分のフォーマットやトークン自身の使われ方についてはほとんど言及していない。
言及しているのがOpenID Connect。

JWTはJSONベースのデータを暗号化して作られる文字列で認証や認可のための仕組みとして利用される。
属性情報(Claim: クレーム)をJSONデータ構造で表現したトークンの仕様。 JSONを使ったコンパクトでurl-safeなクレームの表現方法であり、OAuth2やOpenID Connectなんかで使われます。

通常のトークン認証との違い

通常のトークン形式の認証では、トークンの正当性を確認するためにサーバへの問い合わせが必要。
→これはサーバの負荷を増やし、通信の遅延を引き起こす可能性があります。

一方、JWT(JSON Web Token)では、公開鍵を利用してクライアント側でトークンの正当性を確認できる。
JWTは署名されており、その署名を検証することでトークンの改ざんや不正使用を検知できる。
公開鍵は事前にサーバからクライアントに配信され、クライアントはそれを使ってトークンを検証します。これにより、サーバへの問い合わせが不要になり、認証処理が効率的に行えるという利点があります。

また、JWTはトークン自体にユーザの情報や許可権限などのデータを含めることができるため、サーバへの再度の問い合わせを必要とせず、トークン自体に必要な情報が含まれているため、スケーラビリティとパフォーマンスの向上にも寄与します。

総合的に言えば、JWTは通常のトークン認証よりもセキュアで効率的な認証手法と言えます。

通常のトークン形式の認証ではトークンの正当性を確認するためにサーバへの問い合わせが必要。 JWTでは公開鍵を利用してクライアント側でトークンの正当性を確認できるという特徴がある。 トークンはオフラインで正当性が保証されるため、逆に一度発行したトークンが困難であるというデメリットも存在します。

また通常のトークンがそれ自体ではまったく意味を持たないケースがほとんどであるのに対し、JWTはそれ自体が情報を持つトークン。 このためJWTの内部に個人情報などを含めることは推奨されない。

JWT発行側は秘密鍵を使う

JWTの発行側は秘密鍵(シークレットキー)を使用してトークンを署名する。
秘密鍵はサーバ側で厳重に管理されるべきであり、第三者に漏洩しないようにする必要がある。
JWTの署名は、発行側が秘密鍵を使用してトークンに署名し、その署名はトークン自体に含まれます。
この署名を検証するために、トークンの受け取り側は公開鍵(または共有の鍵)を使用します。公開鍵は信頼できるサーバから提供され、署名の検証に使用されます。

秘密鍵を保持することで、トークンの署名を検証することが可能。
もしトークンが改ざんされた場合、署名の検証が失敗し、トークンは無効と見なされます。 この秘密鍵による署名と検証の仕組みにより、JWTは信頼性の高い認証手段となります。ただし、秘密鍵の保管や管理には注意が必要であり、不正なアクセスから保護するために適切なセキュリティ対策が必要です。

OpenID ConnectでのJWT

OpenID ConnectとJWT の関わり - Oauth2.0 との違いなど

OpenID Connectで規程されるID Tokenは、JWTのpayload部に一定のフォーマットを提供するもの。 また、ID Tokenを利用した認証フローの流れなどトークンの利用方法についても細かい規定を追加していまする

特徴

署名・暗号化でき、URL-safeであることが挙げられる

JWT歴史

JWTでは

  • 署名付きデータの場合はJWS
  • 暗号化する場合はJWE の仕様に基づき、JWTが利用されますが、現状使われている多くのJWTが署名付きのものである

JWT認証メリット

JWT認証のメリットはその実装のシンプルさと、ステートレスなことにある。
現実的にはDB参照とか必要になったりする(一時トークンを保存するテーブル)が、JWT認証の場合改ざん検証だけで済むのは魅力的。
しかしトレードオフでリアルタイムでユーザ無効化ができないくらい。

アクセストークンとしてJWTを利用することの利点

わかりやすい

JWTは単なるJSONのため、アクセストークンとして用いることにおりさまざまな情報を含めることができる。 それにより、認証サーバとアプリケーションサーバを分割しているようなアーキテクチャであれば、認証サーバの負荷を軽減することに寄与します。

JWT構造

一般的にはJWS形式のJWTが使われる。
JWS形式のJWTは以下のフォーマット
<ヘッダー>.<ペイロード>.<署名>

署名の対象は ヘッダー.ペイロード に対して。
JWTは元のデータを base64url でエンコードしたもの。

ヘッダー

ヘッダーを除いた残りのセクションに関する情報を伝えるのに使われる。 文字列の形式としては、キー名と値のペアで表現されたJSONをBase64urlエンコードした文字列となるl

Base64エンコードの場合は"+", "/", "="が含まれるが、JWTはURIのクエリパラメーターなどに使用されることを想定しているため URL-safeに表現するためにBase64urlエンコードがされている。

// JWTをデコードする
// このヘッダーから署名アルゴリズムでJWTの検証を行う
{
"typ": "JWT",
"alg": "HS256"
}

ペイロード

payloadに含める値をクレームと言う。 ペイロードはやり取りに必要な属性情報(Claim: クレーム) ペイロードの内容はアプリケーションによっては異なるため、必須とされるものは存在しませんが、相互運用性のある属性情報については予約済みパラメーターとして提供されています。 ヘッダーと同様に、JSONをBase64urlエンコードした文字列なので、デコードが容易にできます。

{
"admin": true,
"name": "John Doe",
"sub": "1234567890"
}

Claim(クレーム)

参考URL

情報のやりとりにおいて要求する項目のこと(日本で一般的に使われるクレームではない)
一般的なデータ構造に加えて、JWTは異なるアプリケーション間で使えるようにするためにクレームをと呼ばれるフィールドを用意している。
JWTで表現されるJSONオブジェクトはClaims Set(Claimの集合)と呼ばれます。
ClaimとはJSONの key, value の一対を意味する。
JWTに含めるべきクレームとして必須なものは実はない。
JWTがどのようなクレーム群を含むべきかは、JWTを利用する個々のアプリケーションの範疇としている。
JWTのクレームで必須を定義しているのはOpenID Connect

aud(Audience)クレーム

アクセストークンのaudクレームとIDトークンのaudクレーム

  • アクセストークンのaudクレーム

    aud(audience)クレームはJWTを利用することが想定された主体の識別子一覧である。JWT を処理するために意図されたそれぞれの対象は, オーディエンスクレームの値に自身の識別子が含まれていることを確認し, aud に自身の識別子が含まれない場合はその JWT の処理を拒否しなければならない

JWTの仕様ではaudクレームは「JWTを利用する主体を表現するため」のクレームらしい。 [OAuth 2.0]の認可を受けたクライアントはアクセストークンを検証せずにリソースサーバに送る。(アクセストークンとしてJWTが使われていれば)リソースサーバがJWTの検証をするので、そのときにリソースサーバ自身のURIがaudに含まれていることを確認して、もし含まれていなければ検証失敗にする必要がある。 つまりアクセストークンのaudはアクセスされるリソースサーバのURIを入れるのが正しい?

  • IDトークンのaudクレーム

    REQUIRED. ID Token の想定されるオーディエンス (Audience). この値は Relying Party の OAuth 2.0 client_id を含まなければならない (MUST). 他のオーディエンスの識別子を含んでもよい (MAY). 一般的には aud は大文字小文字を区別した文字列の配列であるが, オーディエンスが単体の場合は aud 値を大文字小文字を区別した単一文字列としてもよい (MAY).j

OpenIDの仕様としてはJWT (ID Token) の検証をする主体がIDPのクライアントになるので、クライアントが自身のclient_idがaudに含まれていることを確認して、もし含まれていなければ検証失敗にする必要がある。

exp

  • 期限を無期限にする JWT内に存在する場合にのみ、exp フィールドを検証します。JWTにこのフィールドが含まれていない場合、トークンは無期限に有効とみなされます。

署名

署名パートは、エンコード済みヘッダー、ピリオド(".")、エンコード済みペイロードを連結したものを入力値として"alg"の署名アルゴリズムで署名し、Base64urlエンコードすることにより作成されます。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

↑の文字列をヘッダーで指定されたアルゴリズムで署名検証することによりID Tokenの正当性を評価できる。

JWT認証フロー

JWTを認証用トークンに使う時調べたこと

Cookieとの認証フロー比較

認証フロー自体はほぼ同じ。
Cookieは Cookie ヘッダーを使う。
Tokenは Authorization ヘッダーを使う。

Token認証のメリット

  • Cookieに依存しないため、CSRF脆弱性がなくなる
  • ネイティブプラットフォームで扱いやすい
  • 特定の暗号スキームに依存しない(=JWT以外も扱える)
  • 短命であればステートレスも許容できる(=サーバ側にセッションストアが不要)
  • 長命であればrevoke機能は必須なのでセッションストアは必要。

Tokenの送信に使うヘッダー

Authorization ヘッダーに対し、Bearerスキームを設定するのが一般的。
Authorization: Bearer <token>


Token発行フロー

Image from Gyazo

以下みたいに分けてシステムを構築するのがベスト。

  • Authサーバ認証&認可を行う JWTを発行(署名)
  • リソースサーバ認可されていればリソース(機能)を提供する(=通常のAPIサーバ)JWTの署名検証を行う

モノリスなシステム構成であればAuthサーバとリソースサーバを同一にするケースもよくある。


JWTの保存先

Tokenの保存先は主に インメモリ or Cookie or localStorage がある。
JWTの保存方法は、アプリケーションのセキュリティ要件と利便性のバランスを考慮して選択する必要があり、一般的にはセキュリティを最優先する場合はCookiesのHttpOnly属性を使用する方法が推奨される JWTの保存先には、ローカルストレージがよく挙げられるがJavaScriptで簡単にアクセスできてしまう(XSS) 認証に使用するJWTはログインメールアドレスとパスワードと同じ意味を持つため、外部のJavaScriptからアクセスできない場所に保管することが望ましい。
そのためJWTをCookien保存する。方法もある

aws-amplify を用いて実装しようとしていたときに、色々調べていると、Amplify は Cognito が発行する JWT トークンをデフォルトで localStorage に保存するという仕様だと分かりました。 localStorage は Javascript からアクセスすることができるので、localStorage にトークンがある状態では、XSS 攻撃のターゲットになった際に、トークンが抜き取られてしまう可能性があります。なので、トークンを Javascript で取得できるところに保管しないようにする必要がありました。 調べていると、HttpOnly 属性が ON になっている Cookie に保存するとよいことが判明しました。

インメモリのケース

  • メリット

    • サードパーティライブラリによるサプライチェーン攻撃の影響がほぼない
    • CookieではないのでCSRFを防げる
  • デメリット

    • JavaScriptでアクセスできるのでXSSの影響を受ける
    • ブラウザを閉じると揮発するのでJWTの再発行が必要

Cookieのケース

  • メリット
    • secure属性(httpsの通信の時のみCookieを送信)、httpOnly属性(JavaScriptからアクセスできなくなる)をつけることでJavaScriptを通じたアクセスを防ぎ、XSS攻撃のリスクを減少させることが可能。

利点:

  • HttpOnly属性により、JavaScriptを介したトークンの盗難を防げる。

  • Secure属性を設定してHTTPS経由でのみアクセスを許可できる。

  • SameSite属性でCSRF攻撃を防ぐことが可能。

  • デメリット

    • Cookieヘッダーでサーバへ送る場合はCSRF脆弱性が残る(ただしSameSite属性を使うことで防御できる)
    • 重要な処理に「パスワード再入力」といった対策を挟むのも1つ
    • 単なる保存先として利用(httpOnly属性を使わない)し、Authorizationヘッダーで送る場合はCSRFを防げるが、上記のhttpOnly属性が使えない
    • またJavaScriptでアクセスできるようになるためXSSの影響を受けるようになる

localStorageのケース

  • メリット

    • CookieではないのでCSRFを防げる
  • デメリット

    • Cookieと異なり、secure属性やhttpOnly属性がないため、XSS脆弱性があった場合セッションハイジャックされやすい(なのでフレームワークなどを利用してXSS脆弱性を排除する対策を行う必要がある)
    • JavaScriptでアクセスできるのでサードパーティライブラリのサプライチェーン攻撃を受ける可能性がある
    • きちんと対応していないブラウザがちょこちょこある
    • IE系で使えないバージョンがある
    • iOSのプライベートブラウジングで使えない

※localStorageはcookieと異なりプロトコル(http, https)まで一致するかを見て同一生成元ポリシーを適用するので、デフォルトでsecure属性があるものと言える。

Image from Gyazo

サーバーサイドストレージのケース

いくつかのアプリケーションでは、サーバーサイドでセッションIDのみをクライアントに渡し、実際のJWTはサーバー上に保持する方法を採用しています。この方式では、クライアントがサーバーにリクエストするたびにセッションIDを通じてJWTが参照されます。

  • 利点
    • JWTがクライアントサイドに存在しないため、XSSやCSRFのリスクを大幅に減少させることができる。
  • 欠点
    • 実装が複雑で、サーバーの負荷が増大する可能性がある。

現在

現在はSameSite属性が主要ブラウザでほぼサポートされたので、Cookieの方がセキュアに保存できると言えます。 なのでWebブラウザではCookieを用いて管理することが推奨されます。


サーバ側のJWT取得方法

  1. リクエストのヘッダーから取得する方法
  2. クッキーに保存したトークンを取得するパターン

1について (リクエストヘッダー)

主に一時的に発行したトークンを取得する際に使用する セキュリティを考慮してトークンの有効期限を短めに設定する必要がある

一時的に発行するトークンを使用する場面

  • 会員登録時のメールアドレス認証時
  • パスワードリセット時
  • メールアドレス変更時

フロントからリクエストヘッダーに埋め込む

export default ({ $axios }) => {
$axios.onRequest((config) => {
config.headers.common.Authorization = `Bearer ${<accessToken>}`
})
}

2について(Cookie)

JWT(JSON Web Token)

JWTを使った今どきのSPAの認証について

JWTを用いることでパスワードなどの認証情報をDBに保存する必用がないというメリットがある

2つのパーティー間で情報を安全に送信するための方法→『二者間で情報のやりとりを目的とした、JSONベースの形式について規定した標準仕様』

実態は、JSONオブジェクトをエンコードした文字列でこの文字列をトークンと呼ぶ。

3つの文字列がピリオドで連結されている JWTが発行したトークンは、ドットによって3つにわかれている それぞれBase64でエンコードされた文字列となっている. <ヘッダ>.<ペイロード>.<署名>

  • ヘッダー(署名の検証に必用な情報) ここにはトークンのタイプや、使用されている署名アルゴリズムの情報を持っています。
// エンコード => デコード
eyJhbGciOiJIUzI1NiJ9.
=> { "alg": "HS256", "typ": "JWT" }
  • ペイロード(やり取りに必用な情報。ユーザー情報など) 2番目の文字列をペイロードと呼び、任意の情報を指定できる。 基本的には、このペイロードをカスタマイズしてユーザー認証に必要な情報を埋め込む
// エンコード => デコード
eyJleHAiOjE2MDA1OTY0MzEsInN1YiI6MSwibmFtZSI6InVzZXIwIn0.
=> {"exp"=>1600596431, "sub"=>1, "name"=>"user0"}

expやsubなどのそれぞれの値をクレームと呼ぶ デフォルトで指定されている値を予約クレーム、使用者が任意に指定した値をパブリッククレームと呼ぶ

  • 署名(検証する内容): 署名には秘密鍵を使うため、これをも用いて検証を行うことができる。 3番目の文字列には署名情報がある。 この署名は、トークンが変更されていないか確認するために使用される。
// エンコード => デコード
lXcwASyLX5GEsMvPYDVhe0ovJj631fUiC0q2ojK-yK0
=> HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)

sub(Subject)クレーム

JWTの主語となる主体の識別子で予約クレームのひとつ。 直訳すると「件名・主題」で、一般的にはオブジェクトを識別する一意性の値を指定します。 ユーザーテーブルで言うと、ユーザーIDのことです。 このクレームは任意で { user_id: 1 } と言ったペイロードでも問題ありません。 ただし、他アプリケーションとの衝突を避けるために予約クレームを使用することが推奨されています。

  • 鍵の指定 トークンの発行には署名時に使用する鍵が必要。 この鍵が漏れると、誰でもトークンの発行と検証ができてしまうため非公開であるRailsのシークレットキーを使用する(Railsの場合)

発行手順

irb(main):001:0> payload = { sub: 1}
irb(main):002:0> secret_key = Rails.application.credentials.secret_key_base
irb(main):003:0> token = JWT.encode(payload, secret_key) # 暗号化し発行
irb(main):004:0> token
=> "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjF9.0HBLImIKESioXrusu-yz4g53qpxrAahNyXRfgTQ5eJ0"
irb(main):005:0> JWT.decode(token, secret_key) # JWTをデコードする
=> [{"sub"=>1}, {"alg"=>"HS256"}]

JWTのいいところ

  1. JWTの最大のメリットは情報が改ざんできない
  2. ユーザテーブルにトークンを保有するカラムを作成しなくてよくなる この仕組みを認証に使えば、ユーザーテーブルにトークンを一時的に保有するカラムを作成しなくて良くなる。
  3. トークン発行時に電子署名を付与できるため、署名をした鍵を持つものしかトークンを検証できない

署名時に付けた鍵と同じ鍵を使って検証する署名アルゴリズムを「HS256」、 秘密鍵と公開鍵のペアで検証する署名アルゴリズムを「RS256」

JWTの注意点

トークンはエンコード(Base64)されているだけで暗号化はされていない。 トークンの中身は以下で見られてしまう。

jwt.io

漏れたらまずい個人情報(メールアドレスなど)はトークンへ埋め込まないようにする。

JWTはCookieを使った認証の代わりにはならない

JWTの有効期間が切れた後に、再度ログイン画面を表示する必要がある。JWTが切れるたびにログイン情報を入力させるのは、Cookieを使用したステートフルな認証に比べてUXが劣る。

JWTは有効期間を長くすると危険。今回の用途だとJWTはc、HttpOnly cookiesのようにJSからアクセスできない領域に保存できないため、常に漏洩のリスクと戦うことになる。npmの最近のCVEを見る限り、Third partyのJSライブラリをすべて監査することは不可能。そのためあまり有効期限を長くできない。これはrefresh tokenの仕組みを使っても同じ。

認証トークンの歴史

参考URL

JWTでトークンを管理する

OpenID ConnectはIDトークンをやり取りする、これはJWTというトークンの形式になっている。 JWTはユーザー認証において大切な概念なので、簡単に説明しておきますね。

JWTは『二者間で情報のやりとりを目的とした、JSONベースの形式について規定した標準仕様』 サーバー側でトークンが正しいかどうかをその場で検証できます。JWTを用いることで、パスワードなどの認証情報をデータベースに保存する必要がない、というメリットがあります。

JWTによるアクセストークンをどう引き回すか

この認証結果として、アクセストークンが返されることになります。 以降は、このアクセストークンを用いて認証することになる(JWTで)

方法は2つある。

1. Authorizationヘッダー

このやり方は、認証を行うために定義されているAuthorizationヘッダーにアクセストークン(JWT)を入れるやり方たとえば次のような形式

Authorization: Bearer <アクセストークン> このAuthorizationヘッダーは、Basic認証やDigest認証で使われていた。 その後RFC6750でBearerというスキームが策定されました。これは単一の文字列を認証情報として送信するのに適しています。

1の特徴をまとめる

  • ステートレス。サーバー側にセッションストアがいらない
  • ネイティブプラットフォームで扱いやすい
  • Cookieが使用できない環境でも問題ない Authorizationヘッダーを用いるやり方は、OpenID Connect、パスワードのどちらの認証方法にも適しています

2. Cookieヘッダー

このやり方は、CookieヘッダーにセッションIDを入れつつ、サーバ上でも保存しておくやり方 ユーザー認証を一度行ったら、以降はCookieヘッダーに含まれるセッションIDとサーバー側のセッションIDを照合してユーザーを識別します。

# サーバ側からクライアントにCookieのセットをリクエスト
Set-Cookie: SID=<セッションID>

# クライアントからサーバにリクエストを行うときにセット
Cookie: SID=<セッションID>

2の特徴をまとめる

  • ステートフル。データベースなど外部のストレージからセッションを取得する必要がある
  • サーバーにリクエストするたびに自動でCookieが送信される
  • CSRF脆弱性への対策をする必要がある
  • 異なるドメインに対して制約がつく(=CORS)

APIは一般的にステートレスで行うため、Cookieヘッダーによる引き回しは実用的ではないといえます。 このやり方はパスワード認証かつ、ネイティブアプリやSPAでない従来のWebアプリケーションに適していると思います。

アクセストークンをどう保持するか

パスワードやOpenID Connectでユーザー認証を行うと、アクセストークンが発行されます。
これをヘッダーに乗せて認証します。つまり、クライアント側でアクセストークンを保持しておかなければなりません。

アクセストークンはどう保存すればいいのでしょうか。大きく次の4つがありますが、結論からいうと可能な限り『OS標準のストレージ』か『メモリ』に保持します:

OS標準のストレージ iOSのKeyChain、AndroidのKeyStore

iOSのKeyChainやAndroidのKeyStoreなど、OSが標準で提供しているストレージを利用するやり方です。Auth0によるアクセストークンの保持についての記事でも、次のメモリとあわせて推奨されているやり方です。

メモリ(JavaScriptの変数など)

JWTを使った今どきのSPA認証について

JavaScriptの変数などに格納し、CookieやlocalStorageには保存しないやり方です。スコープに気をつける必要はありますが、永続化しないため安全といえます。 ただ、ページから離脱するとアクセストークンが消えてしまうので、ソフトウェアが要件を満たせる場合のみ採用できるやり方になります。

Cookie(WebブラウザのCookie)

これはWebブラウザのCookieを使うやり方ですね。Cookieを使うやり方にはいくつか問題があります。たとえばXSS脆弱性やCSRF脆弱性などです。 CookieにSecure属性やHttpOnly属性をつければ安全性はいくらか高まります。ただ、Authorizationヘッダーを用いる場合JavaScriptを用いることになるのでHttpOnly属性をつけられません。つまりXSS脆弱性が残ってしまいます。

localStorage(Web Storage APIのlocalStorage)

Web Storage APIのlocalStorageを使うやり方です。これもCookieと同じくJavaScriptから操作可能なので、XSS脆弱性が残ります。また、localStorageには『HTML5のLocal Storageを使ってはいけない』で書かれているようないくつかの問題点もあります。

以上、いずれの場合もクライアントとサーバーのやり取りにHTTPSで通信するのは必須ですね。HTTPだと通信が見えてしまうので、アクセストークンが盗まれる可能性があります。

また、CookieやlocalStorageを使う場合は有効期限を短くしてリスクを下げるなどの対策が必要だと思います。

アクセストークンとリフレッシュトークン

JWS(JSON Web Signature)

JSONに電子署名をして URL-safe な文字列として表現したもの。
RSAで電子署名するのが一般的 JWTによるJSONに対する電子署名と、そのユースケース

JWE(JSON Web Encryption)

JSONを暗号化して URL-safe な文字列として表現したもの。

SPAでのJWT

参考URL

SPAでJWTを利用する場合、JWTを保存する場所によって発生するセキュリティ上の脆弱性について理解しておく必要があります。 XSS や CSRF の脆弱性に対する根拠のある対策ができていない場合、サーバー側でRedisなどを利用したセッション管理を検討したほうが良いかと思います。