ITエンジニアによるITエンジニアのためのブログ

【Rails】 OpenID ConnectのリレーイングパーティとオープンIDプロバイダーを構築する

本記事では、Ruby on RailsでOpenID Connectのリレーイングパーティ(RP)とOpenIDプロバイダー(OP)を構築する方法を解説します。

前提として、前回の記事で構築したRails製のOAuth 2.0クライアントと認可サーバーをベースに実装を進めます。先にそちらをお読みいただくと、よりスムーズに理解できるかと思います。

前回記事

【Rails】OAuth 2.0の認可サーバー・リソースサーバー・クライアントを構築する

目次

  1. OpenID Connect1.0がOAuth2.0に追加するもの
  2. OpenID Connectの仕組み
  3. オープンIDプロバイダー(OP)の設定
  4. リレーイングパーティ(RP)の設定
  5. 全体の動作フロー
  6. まとめ
  7. 補足:IDトークン検証用の公開鍵(JWK)

1. OpenID Connect 1.0がOAuth 2.0に追加するもの

OpenID Connect (OIDC) 1.0は、OAuth 2.0が提供する認可の仕組みの上に、認証のレイヤーを追加したプロトコルです。

OAuth 2.0をベースに独自の認証機能を実装することも可能ですが、各社が独自の仕様を定めると相互運用性が失われ、クライアント開発者は接続先ごとに実装を変えなければなりません。この課題を解決するために策定された標準認証プロトコルが、OpenID Connectです。

具体的には、OIDCはOAuth 2.0にない以下の機能を提供します。

  • IDトークン: OIDCは、ユーザーの認証情報を含むIDトークンをJWT形式で提供します。このトークンはOPによって電子署名されており、RPは署名を検証することで、トークンの完全性と発行元を保証できます。
  • Userinfoエンドポイント: OIDCは、クライアントがユーザー情報を取得するための標準化されたAPIエンドポイント(Userinfoエンドポイント)を定義しています。
  • 標準化された認証フロー: OIDCは、認証に関する詳細なフローを厳密に定義・標準化しています。

2. OpenID Connectの仕組み

登場人物

OIDCには、主に以下の5つの登場人物がいます。一部はOIDC独自の呼称ですが、役割はOAuth 2.0とほぼ同じです。

  • リソースオーナー: エンドユーザー本人です。
  • ユーザーエージェント: エンドユーザーが操作するブラウザです。
  • リレーイング・パーティ(RP): OAuth 2.0のクライアントに相当します。
  • オープンIDプロバイダー(OP): OAuth 2.0の認可サーバーに相当します。アクセストークンに加えてIDトークンも発行します。
  • リソースサーバー: OAuth 2.0のリソースサーバーと同様です。ユーザー情報(Claim)を取得するためのUserinfoエンドポイントを提供します。

OpenID Connectのフロー

OIDCの認証フローは、OAuth 2.0の認可フローを拡張したものです。

  1. [認証リクエスト] ユーザーがRPのサイトで「外部サービスでログイン」などのボタンをクリックすると、RPはユーザーをOPへリダイレクトさせます。このリクエストは、OAuth 2.0の認可リクエストにOIDC固有のパラメータ(例: scopeopenidを追加)を加えたものです。
  2. [ユーザー認証・同意] OPは、ユーザーにIDとパスワードの入力を求めて認証します。その後、「(RP名)にあなたの情報を提供しますか?」といった同意画面を表示します。
  3. [認可コード発行] ユーザーが同意すると、OPは一時的な認可コードを発行し、指定されたコールバックURLへユーザーをリダイレクトさせます。
  4. [トークン発行] RPは受け取った認可コードを使い、バックグラウンド通信でOPにアクセストークンIDトークンを要求します。OPは認可コードを検証し、問題なければ2つのトークンを発行します。
  5. [IDトークン検証] RPはOPの公開鍵を使ってIDトークンの署名を検証し、ユーザー認証が正当であることを確認します。
  6. [ユーザー情報取得] RPは、取得したアクセストークンを使ってリソースサーバーのUserinfoエンドポイントにアクセスし、ユーザー情報を要求します。リソースサーバーはアクセストークンを検証し、ユーザー情報を返します。

OAuth 2.0とのフロー比較

フローOAuth 2.0OpenID Connect 1.0
1. 認証リクエスト認可エンドポイントにアクセスscopeopenidを追加
・リプレイ攻撃防止のnonceパラメータを追加
2. ユーザー認証・同意スコープに応じた同意画面openidスコープに基づき、認証に関する同意も求める
3. 認可コード発行認可コードを発行OAuth 2.0と同じ
4. トークン発行アクセストークンアクセストークンIDトークン
5. IDトークン検証なし必須
6. ユーザー情報取得任意のAPIエンドポイント標準化されたUserinfoエンドポイント

3. オープンIDプロバイダー(OP)の設定

ここからは、Railsでの実装を解説します。まず、既存のOAuth 2.0認可サーバー(Doorkeeper)にOIDC機能を追加します。

doorkeeper-openid_connectの導入

doorkeeper-openid_connect gemを追加します。

# Gemfile
gem 'doorkeeper-openid_connect'
bundle install

設定ファイルを生成し、ルーティングを追加します。

rails generate doorkeeper:openid_connect:install

マイグレーションファイルを実行し、OIDC用のテーブルを作成します。

rails generate doorkeeper:openid_connect:migration
rake db:migrate

Doorkeeper設定ファイルの変更

resource_owner_authenticator ブロックが、ユーザー未認証時にnilを返すように修正します。これにより、リダイレクトループを防ぎます。

# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  resource_owner_authenticator do
    User.find_by(id: session[:user_id]) || begin
      session['user_return_to'] = request.url
      redirect_to(api_v1_auth_path)
      nil # 追加箇所
    end
  end
end

IDトークン署名用の秘密鍵作成

IDトークンの署名(JWS)に使用するRSA秘密鍵を生成します。

# Bash
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

以下のような秘密鍵が生成されます。

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhgC84...
...
...
...
-----END PRIVATE KEY-----

生成されたPEM形式の鍵は複数行にわたるため、環境変数に格納しやすいよう1行に変換します。

# ヘッダーとフッターを削除し、改行をなくす
sed '1d;$d' private_key.pem | tr -d '\n'

.envファイルに追記します。

OIDC_PRIVATE_KEY=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhgC84...

doorkeeper_openid_connect設定ファイル

doorkeeper_openid_connectの設定ファイルを編集します。

# config/initializers/doorkeeper_openid_connect.rb
Doorkeeper::OpenidConnect.configure do
  # IDトークンの発行者(issuer)を識別するURL
  issuer do |resource_owner, application|
    'https://engineerjutsu.com:3000/'
  end

  # IDトークンの署名に使うRSA秘密鍵
  signing_key <<~KEY
    -----BEGIN RSA PRIVATE KEY-----
    #{ENV['OIDC_PRIVATE_KEY']}
    -----END RSA PRIVATE KEY-----
  KEY

  subject_types_supported [:public]

  # アクセストークンからユーザーを特定する
  resource_owner_from_access_token do |access_token|
    User.find_by(id: access_token.resource_owner_id)
  end

  # IDトークンのsubject(ユーザー識別子)としてユーザーIDを使用
  subject do |resource_owner, _application|
    resource_owner.id
  end
  
  # ... 省略 ...
end

ルーティング設定

use_doorkeeper_openid_connectconfig/routes.rbに追加することで、Userinfoエンドポイント (/oauth/userinfo) や公開鍵を提供するエンドポイント (/oauth/discovery/keys) が自動的に設定されます。

Rails.application.routes.draw do
  use_doorkeeper_openid_connect
  # ... 既存のルーティング ...
end

4. リレーイングパーティ(RP)の設定

次に、OAuth 2.0クライアントにOIDCのRP機能を追加します。

omniauth_openid_connectの導入

omniauth_openid_connect gemを追加します。

# Gemfile
gem 'omniauth_openid_connect'
bundle install

Devise設定ファイル

config/initializers/devise.rb にOmniAuthプロバイダーとしてOIDCの設定を追記します。

Devise.setup do |config|
  config.omniauth :openid_connect, {
    name: :openid_connect,
    scope: [:openid, :email, :profile],
    issuer: 'https://engineerjutsu.com:3000',
    response_type: :code,
    client_options: {
      authorization_endpoint: '/oauth/authorize',
      token_endpoint: '/oauth/token',
      userinfo_endpoint: '/oauth/userinfo',
      jwks_uri: 'https://engineerjutsu.com:3000/oauth/discovery/keys',
      port: 3000,
      scheme: "http",
      host: "localhost",
      identifier: ENV['ENJINEERJUTSU_CLIENT_ID'],
      secret: ENV['ENJINEERJUTSU_CLIENT_SECRET'],
      redirect_uri: 'https://engineerjutsu.com:3001/auth/openid_connect/callback'
    },
  }
end

各フィールドの意味は以下の通りです。

  • name: プロバイダー名。認証パス (/auth/:name) やコールバックメソッド (OmniauthCallbacksController#:name) がこの名前で定義されます。
  • scope: 要求する権限。openidは必須です。ここで指定したスコープは、事前にOP側でクライアントに許可されている必要があります。
  • issuer: OPの識別子。RPは、受け取ったIDトークンのissクレームがこの値と完全に一致することを確認します。
  • response_type: codeを指定し、認可コードフローを利用します。
  • client_options:
    • authorization_endpoint: OPの認可エンドポイント。
    • token_endpoint: OPのアクセストークン・IDトークン発行エンドポイント。
    • userinfo_endpoint: リソースサーバーのUserinfoエンドポイント。
    • jwks_uri: OPがIDトークン検証用の公開鍵(JWK Set)を提供するURI。
    • port: OPのポート番号。
    • scheme: OPのhttpスキーム。
    • host: OPのホスト名。
    • identifier: OPで発行されたクライアントID
    • secret: OPで発行されたクライアントシークレット
    • redirect_uri: OPでの認証・認可後にリダイレクトされるRPのコールバックURI。

以上で、RPの設定は完了です。

5. 全体の動作フロー

OPサーバーをポート3000、RPをポート3001で起動し、実際の動作を確認します。

# OP & リソースサーバー
rails s -p 3000
# RP (クライアント)
rails s -p 3001

ステップ1:RPからOPへのリダイレクト

ユーザーがRP(https://engineerjutsu.com:3001)の「ログイン」リンクをクリックすると、OmniAuthがリクエストを受け取り、ユーザーをOP(https://engineerjutsu.com:3000)の認可エンドポイントへリダイレクトさせます。

リダイレクト先URLの例:

https://engineerjutsu.com:3000/oauth/authorize?client_id=...&nonce=...&redirect_uri=...&response_type=code&scope=openid%20email&state=...

パラメータ説明
client_idRPを識別するクライアントID。
nonceリプレイ攻撃を防ぐためのランダムな文字列。IDトークン検証時に利用します。
redirect_uri認可後に戻ってくるRPのコールバックURL。
response_typecodeを指定し、認可コードフローを要求します。
scopeoopenidは必須。emailなどを追加で要求できます。
stateCSRF攻撃を防ぐためのランダムな文字列。
Export to Sheets

OIDCでは、OAuth 2.0のフローに加えてscopeの値の定義とnonceパラメータが重要になります。

ステップ2:OPでの認証と同意

ユーザーはOPのログイン画面で認証を行います。成功後、「(RP名)があなたのアカウント情報にアクセスすることを許可しますか?」という同意画面が表示されます。scopeopenidが含まれるため、認証に関する同意も求められます。

OAuth2.0のみの場合

OIDCの場合

ステップ3:認可コード発行とRPへのリダイレクト

このステップはOAuth 2.0と全く同じです。ユーザーが「Authorize」ボタンをクリックすると、OPは一時的な認可コードを発行し、ユーザーをRPのコールバックURLへリダイレクトさせます。

リダイレクト先URLの例:

https://engineerjutsu.com:3001/auth/openid_connect/callback?code=...&state=...

URLパラメーターの意味は以下の通りです。

  • code: アクセストークン発行に必要な認可コード
  • state: クライアントから認可サーバーへの元々のリダイレクトに含まれていた、CSRF防止の値と同一の値。


ステップ 4: OmniAuthによるバックグラウンド処理

RPのコールバックURLが呼び出されると、OmniAuthのopenid_connectストラテジーが裏側で以下の処理を自動的に行います。

  1. 受け取った認可コードを使い、OPのトークンエンドポイント(/oauth/token)にアクセスしてアクセストークンIDトークンを取得します。 POSTパラメーターは以下の通りです。
    • scope: 認可エンドポイントで認可をもらったスコープと同様です。例:openid email
    • code: 認可サーバーより受け取った認可コード。
    • grant_type: 固定値で”authorization_code”。
    • redirect_uri: 最初のRPからOPへのリダイレクトURLに含まれていたredirect_uriパラメーターと同一。この例ではhttps://engineerjutsu.com:3001/auth/openid_connect/callback
  2. OPのjwks_urihttps://engineerjutsu.com:3000/oauth/discovery/keys)から公開鍵を取得し、IDトークンの電子署名を検証します。この公開鍵については本記事の巻末にて解説します。
  3. 取得したアクセストークンを使い、リソースサーバーのUserinfoエンドポイントにアクセスしてユーザー情報を取得します。

ステップ 5: コールバック処理の実行

OmniAuthは、ステップ4で取得したすべての情報(UID, ユーザー情報, 各種トークンなど)をrequest.env['omniauth.auth']ハッシュに格納し、Users::OmniauthCallbacksControlleropenid_connectアクションを呼び出します。

開発者はこのomniauth.authハッシュを利用して、ユーザーのサインインや新規登録といった処理を実装します。

6. まとめ

本記事では、既存のOAuth 2.0実装にdoorkeeper-openid_connectomniauth_openid_connectを追加し、OpenID ConnectのOPとRPを構築しました。

実際に全体を構築することで、各コンポーネントの役割や、認可コード、アクセストークン、IDトークンがどのように連携して安全な認証を実現しているかの理解が深まったのではないでしょうか。

参考リンク

補足:IDトークン検証用の公開鍵(JWK)

IDトークンの署名検証には、OPが公開する公開鍵が必要です。RPは、OPのjwks_uri(本記事の例では https://engineerjutsu.com:3000/oauth/discovery/keys)にアクセスして、JSON Web Key (JWK) 形式で公開鍵を取得します。

この公開鍵は、OP側で設定した秘密鍵(.envOIDC_PRIVATE_KEY)からdoorkeeper-openid_connect gemによって自動的に生成・公開されます。

JWKのレスポンス例:

{
  "keys": [
    {
      "kty": "RSA",
      "n": "s5QKPGABtgO...",
      "e": "AQAB",
      "kid": "hMYCFYqrlyU...",
      "use": "sig",
      "alg": "RS256"
    }
  ]
}

上記JWKの属性の意味は以下の通りです。

  • keys: JWKセット(複数のJWK)の配列。
  • kty (Key Type): RSAなどの鍵タイプ。
  • n, e: RSA公開鍵の構成要素(modulus, exponent)。
  • kid (Key ID): 複数の鍵を区別するためのID。
  • use (Public Key Use): sig(署名検証用)などの用途。
  • alg (Algorithm): RS256(RSASSA-PKCS1-v1_5 using SHA-256)などの署名アルゴリズム。

RPはこの情報を基に、IDトークンの署名が正当なものであることを検証します。