この記事について
本記事は、2022年11月に実施した社内勉強会資料の内容をもとに、社外向けに再編集したものです。
記載の情報は執筆当時のものであり、最新の仕様やベストプラクティスとは異なる可能性があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認ください。
免責事項
本記事には学習のためのサンプルコードや簡略化した説明が含まれます。
正確性を保証するものではありません。
概要
私たちが Sign in with Google や Sign in with Apple などにより ID プロバイダ(IdP)を利用してサインインするとき、OpenID Connect(OIDC)という規格に基づき IDトークン(JWT 形式)がやり取りされます。
まずは実際に自分の ID トークンを触ってみて、イメージをつかんでおきましょう。
IDトークンを取得する
Googleの発行するIDトークンをRailsアプリケーションで取得するためには、少し準備が必要です。
- OmniAuth(google_oauth2ストラテジー)のインストールと設定
- Google Cloud Consoleでのプロジェクト作成・認証情報作成
- 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.com
は your-client-id.apps.googleusercontent.com
というアプリと連携してよいかどうかを(認証画面でユーザーに表示して)、(ブラウザでの操作といったインタラクションなどに基づいて認証した)user-id-1234567890
さんに確認し、このトークンを発行した
このトークンは約 3600 秒(1時間)で失効するようですね。
署名
Googleの場合、検証に使う公開鍵は以下から取得できます(定期的にローテーションされています)。
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におけるデファクトスタンダードですが、ネイティブアプリのバックエンドとして機能するかわかりませんでした。