💎

OmniAuthでAmazon Cognitoを使う

2023/08/10に公開

Rails 7で、Amazon CognitoのUser Poolで認証をするのに、OmniAuthを使う方法について簡単にまとめる。

前提条件

Ruby on RailsやAmazon Cognitoの初期セットアップはすでに済んでいるものとする。

ソフトウェアとバージョン

  • Ruby 3.2.x
  • Rails 7.0.x
  • OmniAuth 2.1.x
  • omniauth_openid_connect 0.7.x

omniauth-cognito-idpというのもあるが、若干設定が分かりづらいというのもあるため、今回は汎用性が高いomniauth_openid_connectを使う。

導入

OmniAuthを使うために、セキュリティ対策のため、omniauth-rails_csrf_protectionも必要になる。下記のようにbundle addを実行してインストールする。

bundle add omniauth_openid_connect omniauth-rails_csrf_protection

具体的な設定内容

config/initialize/omniauth.rbに、下記のような設定を追加する。

config/initialize/omniauth.rb
 Rails.application.config.middleware.use OmniAuth::Builder do
  provider :openid_connect, {
    issuer: "https://cognito-idp.#{aws_region}.amazonaws.com/#{user_pool_id}",
    discovery: true,
    scope: [:openid, :email, :profile],
    response_type: :code,
    client_options: {
      identifier: "#{client_id}",
      secret: "#{client_secret}",
      redirect_uri: "#{callback_url}"
    }
  }
 end

issuer

issuerは、https://cognito-idp.#{aws_region}.amazonaws.com/#{user_pool_id}という形式になる。

user_pool_idは、AWS CLIを使って取得できる(ユーザープールが1つしかない場合)。

aws cognito-idp list-user-pools --max-results 1 | jq ".UserPools[0].Id"

ドキュメントには明確に記載がないように思えるのだが、AWSのドキュメントOAuth 2.0、OpenID Connect、および SAML 2.0 フェデレーションエンドポイントリファレンス - Amazon Cognitoに、「ユーザープールの OIDC アーキテクチャのディレクトリ」として、いわゆるOIDC Discoveryエンドポイントが、https://cognito-idp.リージョン.amazonaws.com/ユーザープール ID/.well-known/openid-configurationであることがわかる。ここにアクセスすれば、Issuerも含む構成に必要な値が取得できる[1]

discovery

omniauth_openid_connectのデフォルトでは、認可エンドポイントのFQDNは、暗黙的にIssuerのものを使うようになっている。おそらく、一般的なOpenID Provider(OP)では、ドメインが同一であることがほとんであるためだろう。しかし、Cognitoの場合はIssuerのドメインと、認可エンドポイントで使うドメインが異なるため、必須となっている必要最低限の設定だけでは動かない。

discoverytrueを設定すると、OIDC Discoveryエンドポイントの情報を読み取って、各種エンドポイントを自動で設定をしてくれるので、エンドポイントのドメインが異なっていても問題なく動作するようになる。Cognitoに限らず、利用可能なら積極的に使うと良いと思う。

scope

Cognitoで予約されているscopeは、openid, email, phone, profile, aws.cognito.signin.user.adminの5つ[2]

aws.cognito.signin.user.adminは、CognitoのユーザープールのAPIを利用するためのもの。アプリケーションからCognitoの操作をする必要がある場合に指定する。

client_options

identifierにクライアントID、secretに、クライアントシークレットを指定する。いずれもAWS CLIで取得できる。

aws cognito-idp list-user-pool-clients --user-pool-id ${USER_POOL_ID} | jq -r ".UserPoolClients[0].ClientId"
aws cognito-idp describe-user-pool-client --user-pool-id ${USER_POOL_ID} --client-id ${CLIENT_ID} | jq -r ".UserPoolClient.ClientSecret"

redirect_uriは、認証後にアプリケーションがトークンを受け取るURLを指定する。OmniAuthを使っている場合は、デフォルトででは、auth/:provider/callbackとなる。omniauth_openid_connectの場合だと、auth/openid_connect/callbackとなる。callbackの処理については後述する。

以上の設定で、Cognitoを使ったOIDCによる認証をアプリケーションに組み込むことができる。

CognitoでホストされたUIを使った認証関連の処理

ここでは、Cognitoに用意されている「ホストされたUI[3]」を使うのを前提として、サインアップ、サインイン、サインアウトの方法について簡単に説明する。

セルフサービスサインアップ(アカウント登録)

もしセルフサービスサインアップ(利用者による自己登録)を使う場合は、サインアップエンドポイントを使う。

セルフサービスサインアップを有効にすると、サインインページにサインアップページへのリンクがあるのでそれを使っても良い。あるいは、直接サインアップエンドポイントにリクエストすることもできる。その場合はパラメータをいくつか渡す必要がある[4]

最低限必要なのは、client_id, redirect_uri, response_typeで、これは設定に使ったのと同じ値となる。

サインイン(認証)

サインインする場合は、/auth/openid_connectにPOSTリクエストをすると、OmniAuthがCognitoの認可エンドポイント(/authorize)にリダイレクトしてくれる。

<%= button_to 'サインイン','/auth/openid_connect', method: :post, data: { turbo: false } %>

Rails 7では、デフォルトでTurbo Driveが有効になっているため、ここでは無効にする設定を追加している。

ユーザーがサインインした後は、redirect_uriに指定したエンドポイントにリダイレクトされる。

例えば、Sessionコントローラーのcallbackメソッドで処理をする場合は、config/routes.rbで、下記のような設定を追加する。

config/routes.rb
get 'auth/:provider/callback' => 'session#callback'

request.env['omniauth.auth']に、認証した結果が含まれるオブジェクトが入ってくるので、そこから情報を読み取って使う[5]

app/controller/session_controller.rb
class SessionController < ApplicationController
  # ...
  def callback
    auth_hash = request.env['omniauth.auth']

    # Cognitoのsubjectであるuidが取得できる
    uid = auth_hash[:uid]

    # OmniAuthのプロバイダー名が取得できる
    # 複数のproviderを使う場合に区別することができる
    provider = auth_hash[:provider]
    
    # 以下、セッションを有効化する処理を実装する
    # ...
  end
  # ...
end

ちなみに、アプリケーションでRDBMSにアカウントのデータを保存する場合は、uidをキーとすれば良い。ただし、他のプロバイダーが存在する場合は、uidproviderの複合キーで扱う必要がある。

サインアウト

アプリケーションからサインアウトするに、Cognitoからもサインアウトする場合は、ログアウトエンドポイントを使う。ログアウトエンドポイントは、client_idと、logout_uriを渡す。logout_uriは、Cognitoからサインアウトした後にリダイレクトされるアプリケーションのURLを渡す。

まず、sessionを削除するなどして、アプリケーションからのサインアウト処理を実施する。その後、サインアウトエンドポイントにリダイレクトする。Cognitoからサインアウトされたら、logout_uriに指定したURLにリダイレクトされてくる。

例えば、サインアウトする際には、/auth/signoutを使うとする。まず、config/routes.rbに下記のような設定をする。

config/routes.rb
delete '/auth/signout', to: 'session#destroy'

session#destroyを使うので、methodをdeleteにする。

サインアウトするためのボタンは下記のようになる。

<%= button_to 'サインアウト', '/auth/signout', method: :delete, data: { turbo: false } %>

Sessionコントローラーで、下記のような実装をする。

app/controllers/session_controller.rb
class SessionController < ApplicationController
  # ...
  def destroy
    # アプリケーションのセッションを無効にする処理を実装する
    # ...
    
    # Cognitoのlogoutエンドポイントにリダイレクトする
    cognito_logout_url = "https://#{cognito_domain}/logout?client_id=#{client_id}&logout_uri=#{logout_uri}"
    redirect_to cognito_logout_url, allow_other_host: true
  end
  # ...
end

cognito_domainは、カスタムドメインを使っていない場合は、#{user_pool_name}.auth.#{aws_region}.amazoncognito.comとなる。user_pool_nameは、AWS CLIを使って取得できる(ユーザープールが1つしかない場合)。

aws cognito-idp list-user-pools --max-results 1 | jq ".UserPools[0].Name"

logout_urlは、root_urlなどで良いだろう。

脚注
  1. ちなみに、OIDC DiscoveryのURLは、Issuer + /.well-known/openid-configurationと決められている。Final: OpenID Connect Discovery 1.0 incorporating errata set 1を参照のこと。 ↩︎

  2. ログインエンドポイント - Amazon Cognito ↩︎

  3. サインアップおよびサインインでの Amazon Cognito でホストされる UI の使用 - Amazon Cognito ↩︎

  4. 必要なパラメータは、ログインエンドポイントと同じものを使う必要がある。ログインエンドポイント - Amazon Cognito ↩︎

  5. Auth Hashの定義についてはドキュメントを参照のこと。Auth Hash Schema · omniauth/omniauth Wiki ↩︎

Discussion