Railsアプリケーションに2要素認証を導入する

# はじめに

[Zeals Advent Calendar 2020](https://qiita.com/advent-calendar/2020/zeals)の16日目の記事です。

Zealsでバックエンドエンジニアをやってる高久田です。本日はRailsアプリケーションに対して2要素認証を導入するあたって、どのような方法があるのか調査したことについて記事にしようと思います。
主にAWS cognito ユーザープールの内容になるので [Amazon Cognito ユーザープール](## Amazon Cognito ユーザープール) からがメインになります。

# 背景

みなさんが普段使っている、もしくは開発しているサービスには2要素認証が導入されていますか？
email, passwordのみでログインできるものや、最近だとGoogleやFacebookを利用したSNSログインで完了できるものがほとんどではないでしょうか
最近だと不正ログインにより、大きな問題になったサービスもいくつか耳にしたことがあると思います。
その中でよく2段階認証という言葉を聞くと思うのですが、今回タイトルに書いてある2要素認証とは何が違うのでしょうか？

まずは「認証要素」について説明します。
「認証要素」とは認証を行うために必要な認証方法の種類になります。

- 知識要素

知っていることや記憶していること（パスワードや秘密の質問）

- 所持要素

自分が持っているデバイスに送信されてくる情報
Google Authenticatorを使う方法やSMSなどで送信されてくるメッセージを利用する

- 生体要素

指紋認証や顔認証

この3つの認証要素のうち2つ以上の認証要素を利用したものが2要素認証、多要素認証と呼ばれます。
2段階認証はemail, passwordを入力後、秘密の質問に答えて認証を完了するようなものになり、複数の段階を踏んで認証を完了させるものになります。

海外では2段階認証という言葉あまり使われないらしく、また安全性を考えるうえで必要なのは段階の数ではなく、認証要素の数になります。

# 2FA

それでは実際にRailsアプリケーションに対して2FAを実装していきます。

## 環境

- ruby 2.6.6
- Rails 5.2.4.4

## devise

Railsアプリケーションでログイン機能を作成しているのであればほとんどの人が[devise](https://github.com/heartcombo/devise)を利用しているのではないでしょうか？
deviseを利用しているのならば、deviseを拡張するgemを導入することで2FAを実装することができます。

[devise-two-factor](https://github.com/tinfoil/devise-two-factor) 簡単に2要素認証を導入することができる
[rqrcode](https://github.com/whomwah/rqrcode) QRコードの生成を行う

または
[google-authenticator](https://github.com/jaredonline/google-authenticator) Google Authenticatorと統合できる

など既にあるgemを利用する形で実装することができます。

## Amazon Cognito ユーザープール

[Amazon Cognito ユーザープール](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html) とはアプリケーションで必要な認証機能を提供してくれるサービスになります。

自前で認証機能を実装する必要がなく、クラウドで提供されているものを利用していきます。
[sdk](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider.html) も提供されているので、これを利用していきます。

### クライアント作成
```ruby
def initialize
 cognito_client = Aws::CognitoIdentityProvider::Client.new(
   region: ENV['COGNITO_REGION'],
   access_key_id: ENV['COGNITO_ACCESS_KEY_ID'],
   secret_access_key: ENV['COGNITO_SECRET_ACCESS_KEY']
  )
end
```

### ユーザープール作成

ユーザプールは Amazon Cognito のユーザディレクトリです。ユーザープールを使用すると、ユーザーは Amazon Cognito を通じてウェブまたはモバイルアプリにログインできます。また、ユーザーは Google、Facebook、Amazon、Apple などのソーシャル ID プロバイダー、および SAML ベースの ID プロバイダー経由でユーザープールにサインインすることもできます。ユーザーが直接またはサードパーティーを通じてサインインするかどうかにかかわらず、ユーザープールのすべてのメンバーには、Software Development Kit (SDK) を通じてアクセスできるディレクトリプロファイルがあります。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html

```ruby
def create_user_pool(user_pool_name)
  res = client.create_user_pool(
    pool_name: user_pool_name,
    policies: {
      password_policy: {
        minimum_length: 8,
        require_uppercase: false,
        require_lowercase: false,
        require_numbers: false,
        require_symbols: false,
        temporary_password_validity_days: 1
      }
    }
  )
  res.user_pool.id
end
```

### アプリクライアントの作成
パスワードの登録、サインインなどのAPI操作を呼び出すには、アプリケーションクライアントIDとクライアントシークレットが必要になります。承認されたクライアントアプリのみがこれらの未認証操作を呼び出すことができるようにします。

```ruby
def create_cognito_app_client(user_pool_id)
  res = client.create_user_pool_client(
    user_pool_id: 'ap-northeast-1_XXXXXXXXX',
    client_name: 'cognito-app-client',
    explicit_auth_flows: ['ADMIN_NO_SRP_AUTH'],
    prevent_user_existence_errors: 'ENABLED'
  )
  res.user_pool_client.client_id
end
```

### sign_up

```ruby
def sign_up(email, password, app_client_id)
  res = client.sign_up(
    client_id: app_client_id,
    username: email,
    password: password
  )
  res.user_sub
end
```

### sign_in

```ruby
def sign_in(email, password, app_client_id)
  res = client.admin_initiate_auth(
    user_pool_id: 'ap-northeast-1_XXXXXXXXX',
    client_id: app_client_id,
    auth_flow: 'ADMIN_NO_SRP_AUTH',
    auth_parameters: {
      USERNAME: email,
      PASSWORD: password
    }
  )
  res.authentication_result.access_token
end
```

だいたいの基本的な操作をするコードをこのようなものになります。

多要素認証を有効化する場合はユーザープールの設定を変更することで有効化することができます。

![Screen Shot 2020-12-16 at 18.59.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179900/80780141-9ae0-1957-bbb3-731af1ad6779.png)

省略可能にチェックするとユーザー毎にMFAを適応するかどうかを設定することができます。
第2の要素としてSMSテキストメッセージかワンタイムパスワード（Google Authenticator）を設定することができます。
これで認証要素のうちの所持要素を確認することができます。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-mfa.html

2要素認証を設定した後は、送信されてくる/Google Authenticatorで登録した確認コードを使って認証を行うことができます。

```ruby
resp = client.confirm_sign_up({
  client_id: app_client_id,
  username:  "user_name",
  confirmation_code: "*******",
  force_alias_creation: false,
})
```

cognitoでMFAを容易に設定できるだけではなく、ログインページ自体が提供されていたり、既存の認証基盤からcognitoへ移行する仕組みなども提供されています。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-lambda-migrate-user.html

## まとめ
普段Railsを触っているとどうしてもRails内でどのように問題を解決するかに意識が向いてしまっていたのですが、Cognitoなどを利用することによって認証部分などを外部サービスを利用して切り出していくことができると思います。
サービスが大きくなっていったり、複数のサービスを提供するようになった際に、それぞれのサービスで認証基盤を作成するのは工数がかかります。
その際に楽できるところは楽していきたいですね！