JWTを読んでみよう

 

この記事について

本記事は、2022年11月に実施した社内勉強会資料の内容をもとに、社外向けに再編集したものです。
記載の情報は執筆当時のものであり、最新の仕様やベストプラクティスとは異なる可能性があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認ください。


免責事項

本記事には学習のためのサンプルコードや簡略化した説明が含まれます。
正確性を保証するものではありません。

概要

私たちが Sign in with Google や Sign in with Apple などにより ID プロバイダ(IdP)を利用してサインインするとき、OpenID Connect(OIDC)という規格に基づき IDトークン(JWT 形式)がやり取りされます。

まずは実際に自分の ID トークンを触ってみて、イメージをつかんでおきましょう。

目次

IDトークンを取得する

Googleの発行するIDトークンをRailsアプリケーションで取得するためには、少し準備が必要です。

  1. OmniAuth(google_oauth2ストラテジー)のインストールと設定
  2. Google Cloud Consoleでのプロジェクト作成・認証情報作成
  3. Rails側のルーティング・初期化処理の追加

OmniAuthの設定方法については、公式GitHubリポジトリを参照してください。

gem 'omniauth'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-google-oauth2'
Rails.application.routes.draw do
  get 'auth/:provider/callback', to: 'sessions#create'
  get '/login', to: 'sessions#new'
end
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
end

OmniAuth.config.allowed_request_methods = %i[post]
<%= form_tag('/auth/google_oauth2', method: 'post', data: {turbo: false}) do %>
  <button type='submit'>Login with google_oauth2</button>
<% end %>

OmniAuthのコントローラを下記のように実装して、Googleの発行したIDトークンを表示できます。

class SessionsController < ApplicationController
  def new
    render :new
  end

  def create
    case request.env['omniauth.auth']['provider']
    when 'google_oauth2' then
      render json: { id_token: request.env['omniauth.auth']['extra']['id_token'] }
    else
      raise '未定義のプロバイダです'
    end
  end
end

 

表示したトークン。Base64エンコードされたJSONの雰囲気がしますね。

eyJhbGciO...

下記で内容を確認できます。

require 'jwt'
JWT.decode(token, nil, false) # 検証無しデコード

JWTの中身を確認すると、大きく3つの部分(ヘッダー・ペイロード(データ部分)・署名)に分かれていることがわかります。

ヘッダ

{
  "alg": "RS256",
  "kid": "sample-key-id-xxxxxxxxxxxx",
  "typ": "JWT"
}

データ部分

{
  "iss": "https://accounts.google.com",
  "azp": "your-client-id.apps.googleusercontent.com",
  "aud": "your-client-id.apps.googleusercontent.com",
  "sub": "user-id-1234567890",
  "email": "user@example.com",
  "email_verified": true,
  "at_hash": "sample-hash-value",
  "name": "Taro Yamada",
  "picture": "https://lh3.googleusercontent.com/a/sample-profile-image.png",
  "given_name": "Taro",
  "family_name": "Yamada",
  "locale": "ja",
  "iat": 1668588760,
  "exp": 1668592360
}

下記のような解釈になると思います。


https://accounts.google.comyour-client-id.apps.googleusercontent.com というアプリと連携してよいかどうかを(認証画面でユーザーに表示して)、(ブラウザでの操作といったインタラクションなどに基づいて認証した)user-id-1234567890 さんに確認し、このトークンを発行した

このトークンは約 3600 秒(1時間)で失効するようですね。

署名

Googleの場合、検証に使う公開鍵は以下から取得できます(定期的にローテーションされています)。

Google 公開鍵(JWKs)

RSASHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-google-public-key
)

IDトークンを受け取ったら

署名の検証を行います。

JWTライブラリをただ使えば安全、とは限らないので注意してください。

gem 'jwt'
require 'jwt'
require 'open-uri'

uri = URI('https://www.googleapis.com/oauth2/v3/certs')
jwks = uri.open { |io| JSON.parse(io.string) }
jwt = JWT.decode(token, nil, true,
                  jwks: jwks,
                  algorithm: 'RS256', # だめ→ header['alg']
                  iss: ['https://accounts.google.com', 'accounts.google.com'],
                  verify_iss: true,
                  aud: 'your-client-id.apps.googleusercontent.com',
                  verify_aud: true)

alg=noneによる署名検証回避

出典:JWTのalg=noneによる署名検証回避はどうして起こるのか
https://zenn.dev/ritou/articles/49518bac3565f9

  algorithm に外部入力を与えてはなりません。

 

JWTを発行してセッション機構を実装したい

事前にリスクアセスメントを行うことが必要です。

参考:co3k.org – どうしてリスクアセスメントせずに JWT をセッションに使っちゃうわけ?
https://co3k.org/blog/why-do-you-use-jwt-for-session

  • 「ログアウト」がセッションを無効化しない
  • 鍵のローテーションをしなければならない
    (先ほど Google の URL を見ましたね)
  • ライブラリの成熟度?
    先ほどの alg-none の問題など
  • その他

この話題は、Railsのデフォルトのセッションストレージである CookieStore の議論とも関連しますね。

認証SaaS、ミドルウェア

話題の Passkey は、今後のスタンダードとなるのでしょうか。

PayPalの場合は、私の Android 端末ではまだ対応していない様子でした。 ※2022年11月時点

(US限定など、地域別で対応している可能性もあります)

Passkeys.io – A Real-World Passkey Demo & Info Page
https://www.passkeys.io/

Hanko

Hanko.io は FIDO Alliance のメンバーです。

Hanko — Modern Authentication. On Your Terms.
https://www.hanko.io

Passkeyファースト、クラウドファーストの認証ミドルウェア。

まだBeta版です ※2022年11月現在

Auth0

定番の認証SaaSです。

Passkey対応はまだみたいです。2022年11月現在

 

例えばバックエンドで Auth0 の IDトークンを受け取る ことで、IDP(Google、Apple、Twitter、Facebook、…)個別対応 のリスクを低減できます。

Firebase Authentication

バックエンドでFirebaseのIDトークンを受け取る形で実装するものと考えられます。

OmniAuth

Rubyにおけるデファクトスタンダードですが、ネイティブアプリのバックエンドとして機能するかわかりませんでした。

役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!
目次