Rails 6 で omniauth-cognito-idp を使って Amazon Cognito 認証を実装するサンプル

## 要点

Amazon Cognitoを使えば、deviseに依存せずに、ユーザ登録・ログイン・ログアウト・パスワードリセット・ソーシャルログイン・n段階認証・SAMLなど、様々な機能がアプリケーションコードから分離するかたちで追加可能です。Amazon Cognitoの他にも、Auth0やFirebase Authentication等があり、IDaaS（Identity as a Service）と呼ばれます。

本記事では、Railsアプリケーションで動作検証をする場合の手順を記載しています。

1. Amazon Cognitoのユーザープールの作成とクライアントの設定の基本的な手順を解説（作業時間: 10分程度）
2. 動作検証するためのサンプルアプリの実装方法を解決（作業時間: 10分程度）

なお、記事中のスクリーンショットに記載されたクライアントIDやシークレットは、既に削除されているため利用することはできません。ご自身で設定して動作検証してみてください。

## 環境

```
% ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]

% bin/rails version
Rails 6.0.3.2
```

## 手順1: ユーザープールの作成とクライアントの設定

作業時間: 10分程度

### Amazon Cognito のコンソールから「ユーザープールを作成する」を押下

![Screenshot from 2020-08-30 12-33-26](https://user-images.githubusercontent.com/15371677/91650641-0877aa00-eabd-11ea-96b1-4c1f42cd3e7d.png)

### プール名を入力

サンプルなのでデフォルトを確認するを押下

![Screenshot from 2020-08-30 11-48-21](https://user-images.githubusercontent.com/15371677/91650137-cfd4d200-eab6-11ea-82e3-d4f180fc9ba9.png)

### プールの作成

![Screenshot from 2020-08-30 11-51-25](https://user-images.githubusercontent.com/15371677/91650177-57224580-eab7-11ea-987e-863ceac07801.png)

### 作成完了

`プール ID` をメモする:

![Screenshot from 2020-08-30 11-53-53](https://user-images.githubusercontent.com/15371677/91650190-85a02080-eab7-11ea-8ea0-cc823beba10a.png)

### ドメイン名を設定

ナビゲーションの `ドメイン名` をクリックし、認証フォームのURLを設定。設定した、 `https://{your prefix}.auth.ap-northeast-1.amazoncognito.com` をメモする:

![Screenshot from 2020-08-30 11-55-48](https://user-images.githubusercontent.com/15371677/91650219-ce57d980-eab7-11ea-90d3-12e00fdbca30.png)

### アプリクライアントの作成

ナビゲーションの `アプリクライアント` をクリックし、 `アプリクライアントの追加` をクリック:

![Screenshot from 2020-08-30 12-00-18](https://user-images.githubusercontent.com/15371677/91650279-6b1a7700-eab8-11ea-9475-ba39b7486f17.png)

![Screenshot from 2020-08-30 12-01-25](https://user-images.githubusercontent.com/15371677/91650297-94d39e00-eab8-11ea-98b7-5838fd44f853.png)

`アプリクライアントID` と `アプリクライアントのシークレット` をメモ:

![Screenshot from 2020-08-30 12-02-23](https://user-images.githubusercontent.com/15371677/91650309-c6e50000-eab8-11ea-8052-fe17bc9ce234.png)

### アプリクライアントの設定

ナビゲーションの `アプリクライアントの設定を押下`し、コールバックURLとサインアウトURLを設定（それ以外の設定はスクリーンショットを参照）:

![Screenshot from 2020-08-30 12-04-49](https://user-images.githubusercontent.com/15371677/91650326-0c093200-eab9-11ea-9a5c-51c9fb35110e.png)

これでcognito側の設定は完了です（なお、スクリーンショットの値は利用できません）。

## 手順2: アプリケーションの実装

作業時間: 10分程度

### rails new

サンプルなので割愛

### omniauth-cognito-idp を追加

```
gem 'omniauth-cognito-idp'
```

### クレデンシャルの追加

今回はサンプルなので `development` で、ユーザプール作成時に生成した諸々をセットする:

```
$ bin/rails credentials:edit -e development
```

```yaml
aws:
  access_key_id: 123
  secret_access_key: 345
  # 上記でメモした「ドメイン名」に含まれるリージョン
  region: ap-northeast-1
  # 上記でメモした「アプリクライアントID」
  cognito_client_id: *****YOUR CLIENT ID*****
  # 上記でメモした「アプリクライアントのシークレット」
  cognito_client_secret: *****YOUR SECRET*****
  # 上記でメモした「ドメイン名」
  cognito_user_pool_site: https://*****YOUR PREFIX*****.auth.ap-northeast-1.amazoncognito.com
  # 上記でメモした「プールID」
  cognito_user_pool_id: *****YOUR POOL ID*****
```

### イニシャライザの追加

```ruby
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider(
    :cognito_idp,
    Rails.application.credentials.aws[:cognito_client_id],
    Rails.application.credentials.aws[:cognito_client_secret],
    client_options: {
      site: Rails.application.credentials.aws[:cognito_user_pool_site]
    },
    scope: 'email openid',
    user_pool_id: Rails.application.credentials.aws[:cognito_user_pool_id],
    aws_region: Rails.application.credentials.aws[:region],
  )
end
```

### HomeControllerの実装

root で表示する画面:

```
$ bin/rails g controller home show --skip-assets --skip-helper
```

```erb
<h1>Home#show</h1>
<p>Find me in app/views/home/show.html.erb</p>

<h1>RoR Cognito Sample</h1>
<p>Step 1 - Login.</p>
<%= button_to 'Login', 'auth/cognito-idp', method: :post %>
```

```ruby
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
end
```

### CognitoIdpControllerの実装

OmniAuthで認証後の処理をするコールバック:

```
$ bin/rails g controller cognito_idp --skip-template-engine --skip-assets --skip-helper
```

```ruby
class CognitoIdpController < ApplicationController
  def callback
    # 実際は provider と uid を使って、Userレコードを作成/参照し、user_id をsessionにセットするが
    # サンプルなのでauth hashをセット
    session[:userinfo] = request.env['omniauth.auth']

    redirect_to '/dashboard'
  end
end
```

```ruby
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
  get 'auth/cognito-idp/callback' => 'cognito_idp#callback'
end
```

セッションストアでCookieを利用して `session[:userinfo]` をセットしようとすると最大容量を超過するので、セッションストアをメモリに変更:

```ruby
# config/initializers/session_store.rb
Rails.application.config.session_store :cache_store
```

`config/environments/development.rb` に `config.cache_store = :memory_store` を追加。

### ApplicationControllerに認証に関する振る舞いの追加:

実際はcurrent_userなども実装するがサンプルなのでuserinfoがsessionにセットされているかどうかを確認:

```ruby
class ApplicationController < ActionController::Base
  private

  def authenticate_user!
    return redirect_to(root_path) unless signed_in?
  end

  def signed_in?
    session[:userinfo].present?
  end

  helper_method :signed_in?
end
```

### DashboardControllerの実装

ログイン後の画面を実装する:

```
$ bin/rails g controller dashboard show --skip-assets --skip-helper
```

```ruby
class DashboardController < ApplicationController
  before_action :authenticate_user!

  def show
  end
end
```

```ruby
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
  get 'auth/cognito-idp/callback' => 'cognito_idp#callback'
  get 'dashboard' => 'dashboard#show'
end
```

```erb
<h1>Dashboard#show</h1>
<p>Find me in app/views/dashboard/show.html.erb</p>
<p><%= session[:userinfo].inspect %></p>

<p><%= link_to 'logout', logout_path, method: :delete %></p>
```

### SessionsControllerの実装:

ログアウト処理を実装（cognito側もログアウトする必要がある）:

```
$ bin/rails g controller sessions --skip-template-engine --skip-assets --skip-helper
```

```ruby
class SessionsController < ApplicationController
  before_action :authenticate_user!

  def destroy
    reset_session
    redirect_to cognito_logout_url
  end
end
```

direct ルーティングを使って、cognito側のログアウトを実装:

```ruby
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
  get 'auth/cognito-idp/callback' => 'cognito_idp#callback'
  get 'dashboard' => 'dashboard#show'
  delete 'logout' => 'sessions#destroy'
  
  direct :cognito_logout do
    query = {
      client_id: Rails.application.credentials.aws[:cognito_client_id],
      logout_uri: root_url,
    }.to_param
    "#{Rails.application.credentials.aws[:cognito_user_pool_site]}/logout?#{query}"
  end
end
```

## 動作検証

`rails server` を起動して `localhost:3000` にアクセス:

### ルートを表示

`Login` を押下:

![Screenshot from 2020-08-30 12-11-06](https://user-images.githubusercontent.com/15371677/91650381-eaf51100-eab9-11ea-8983-304f48b90870.png)

### cognitoのログインフォームが表示されるのでサインアップする

![Screenshot from 2020-08-30 12-13-05](https://user-images.githubusercontent.com/15371677/91650398-31e30680-eaba-11ea-86aa-9144a3d3992e.png)

![Screenshot from 2020-08-30 12-14-22](https://user-images.githubusercontent.com/15371677/91650421-5f2fb480-eaba-11ea-9826-cc20af029fee.png)

メールアドレスを確認して認証コードを入力するとログインが完了します。

![Screenshot from 2020-08-30 12-16-44](https://user-images.githubusercontent.com/15371677/91650449-b5045c80-eaba-11ea-8802-15ad2cb7d2a2.png)

## 参考

- [Amazon Cognito のドキュメント](https://docs.aws.amazon.com/cognito/index.html)
- [Sage/omniauth-cognito-idp: OmniAuth Strategy for AWS Cognito in Ruby](https://github.com/Sage/omniauth-cognito-idp)
- [Auth0 Ruby On Rails SDK Quickstarts: Login](https://auth0.com/docs/quickstart/webapp/rails/01-login)

以上