Krypton+Rekognition+Cognitoでお手軽三要素認証！

とりあえずこれを見てください。

![3factor-login-anime.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/ca44e01b-b2b4-eb53-a28a-7ccc11379cf3.gif)

今回のテーマはKrypton(SIM認証) + Rekognition(顔認証) + Cognito(パスワード認証)の三要素認証です。

# はじめに

以前書いて好評を博したブログ「[AWSコンソールのログインが面倒なのでSORACOM Kryptonを使う](https://qiita.com/1stship/items/77da0ab1c037d83d5d24)」のオチは、

> これって一要素認証だな。。

でした。SIMカードさえ持ってれば、パスワード入れなくてもログイン出来る、という画期的に便利なログインでしたが、便利すぎるのも考え物です。これ誰かに使われたら、アカウントID、ユーザー名、パスワードなど何も知らなくてもログイン出来てしまいます。

これは改善せねばなるまい、ということで今回は三要素認証に挑戦してみました。

# 三要素認証とは

認証とは本人であることを何らかの手段で確かめることであり、
一般的に多要素認証とは以下の３つのうち２つ以上を組み合わせたものを言います。

| 種類 | 内容 |
|:--|:--|
| 生体情報(What you are) | 本人の特長。指紋、虹彩、静脈、声紋、顔など |
| 所持情報(What you have) | 持っているもの。ハードウェアトークン、電話番号、SIMカードなど|
| 知識情報(What you know) | 知っていること。パスワード、秘密の質問、暗証番号など |

（認証の三要素に関する文献を探したのですが、原典となったものを見つけることができませんでした。とりあえず[Wikipediaの記事](https://ja.wikipedia.org/wiki/%E8%AA%8D%E8%A8%BC)を挙げておきます）

生体認証であれば、この指紋であれば○○さんであることはほぼ間違いない、とか、所持情報であればこれは△△の電話番号だからこれに応答できるのは△△とか、知識情報であればこの合い言葉を知ってるのは□□だけのはずだから□□さんである、とか、そういった手段で今やりとりしている相手が本来意図した相手本人であることを確かめます。

通常、インターネットサービスではユーザーID(メールアドレス) + パスワードを入力させて確かめますが、これは知識認証です。知識認証は手軽な反面、その情報が知られてしまえば誰でも認証が通ってしまうため、情報の流出や総当たり攻撃により破られる可能性が比較的高いです。本人も覚えていなければならないため、極端に覚えにくいパスワードにできないのも難点ですね。

そこで銀行などではハードウェアトークンを用意したり、スマートフォンでは指紋認証などができたりしますし、AWSもハードウェアによるMFAデバイス、ソフトウェアの仮想MFAデバイスを用意していますが、なかなか個人レベルでそういったものを用意するのは大変です。

そこで、SORACOMとAWSのサービスを組み合わせ、1時間以内で実装できる三要素認証をやってみました。(SORCOM IoT SIM持っている前提ですが)

所持情報としてSORACOM IoT SIM、生体情報としてAmazon Rekognition、知識認証としてAmazon Cognito User Poolsを用います。ではいってみましょう！

# 構成

構成はこんな感じです。

![3factor-login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/97e51881-803c-d284-d86e-7d36ef7e05b7.png)

ポイントはいくつかあります。

- 所持情報として[SORACOM Krypton](https://soracom.jp/services/krypton/)を使う
- [Amazon Cognito Identity Pools](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-identity.html)を２つ使う
- 2要素目、3要素目の認証は[AWS Lambda](https://aws.amazon.com/jp/lambda/)を使う
- 生体認証として[Amazon Rekognition](https://aws.amazon.com/jp/rekognition/)の[顔比較](https://docs.aws.amazon.com/rekognition/latest/dg/API_CompareFaces.html)を使う
- ユーザーやパスワードの管理は[Amazon Cognito User Pools](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html)を使う

それぞれの理由について説明します。

## SORACOM Kryptonを使う

SORACOM IoT SIMは個人でも比較的簡単に手に入れることができ(個人アカウント開設して、注文すれば翌日届く、1枚5ドル)、維持費は最低月0.4ドルです。SIMは中の情報を改ざんおよび窃取しにくいセキュリティチップですし、AWSの認証情報を取得するSORACOM Kryptonの設定も簡単です。いざというときには、通信をするためのSIMとしても用いることができます。このあたりは[前回の記事](https://qiita.com/1stship/items/77da0ab1c037d83d5d24)で述べたとおりですね。

## Amazon Cognito Identity Poolsを２つ使う

SORACOM Kryptonの認証は簡単なのですが難点がありまして、この段階で完全なAWSの認証情報が取れてしまうんですね。2、3段階にできない。また認証処理がSORACOM任せで、対応づけられるIAM Roleは１つです。これは同じ用途で大量のデバイスを扱うIoTには便利なのですが、それぞれ違う人間がログインする際にはやや不便です。(人ごとにSIMグループとKrypton設定すればよいので分離できないわけではないです)

そこで認証の手順を２つに分け、１段階目ではSORACOM Kryptonを使って、２段階目の認証手順に必要な最小限の権限のみをもつユーザーを取得します。かなり制限された権限なので、悪用されても実害はないと考えて良いでしょう。そして実際に必要な権限のユーザーは、１段階目の権限を用いて2つめのIdentity Poolにて取得することとします。

## 2要素目、3要素目の認証はAWS Lambdaを使う

2、3要素目の認証は、クライアントサイドでやるのではなく、Lambdaにやらせます。これは必要な認証手順をLabmdaの関数内に隠蔽するためです。具体的には顔比較の結果とパスワード認証をやらせるのですが、これがクライアントサイドにあると1要素目(SIM)、2要素目(顔)の認証をしなくても、3要素目(パスワード)の認証ができてしまいます。これをLambdaの中に入れることにより、1、２要素目の認証を通らないと、3要素目の認証ができない、という構成にすることができます。

これは他のシチュエーションでも使えるテクニックで、おおざっぱな権限しかないサービス(例えば何でもできる管理者権限しかない)に対して細かい権限分離をしたい場合、やらせたい処理ごとにLambda関数を作り、そのおおざっぱな権限をLambda関数に付与した上で、どのLabmda関数を起動できるかをユーザーの権限に与えることで実現できます。図にするとこんな感じです。

![LambdaAuth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/49d97fd3-a065-4cdb-632a-fb3e90cf7cfb.png)

## Amazon Rekognitionの顔比較を使う

生体認証にはAmazon Rekognitionを使います。生体認証の方法は指紋や虹彩、声紋など色々あるのですが、基本的にはなんらかの専用デバイスが必要になります。そこでAmazon Rekognitionは写真が撮れるカメラがあれば使用することができますので、ハードルがかなり低いです。写真さえあれば本人がいなくても認証できてしまうという面はありますが。

使用するAPIは顔比較でよいです。1要素目の認証で誰を認証すれば良いかは確定しますので、1:1の比較で済みます。単に顔写真だけ送って、登録されている顔写真の中から一番近いものを探す、という1:Nの処理は必要ありません。

顔比較試してみましたが、使い方はかなり簡単で、S3に顔写真を2つ入れて、それを指定してCompareFacesを呼び出せば良いだけです。参照用の顔写真は事前にS3に入れておいて、認証の際にその場で写真を撮ってS3に送るとよいでしょう。

自分の写真で試してみると、Similarity(一致度)は99.9%(小数点2桁以下四捨五入)と出ました。基本的にはPCの前に座った状態でPCのインカメラで撮る、という状況が固定された写真になるので、一致度のしきい値はかなり高くても良さそうです。試しに兄の写真と比較してみたところ19.2%、弟の写真と比較してみたところ22.9%でした。ほとんど他人やんけ。兄と弟を比較すると66.2%。こっちはまあまあ近い。僕だけ仲間はずれやんけ。

ちなみにMacの場合、コマンドラインから写真を撮るには[imagesnap](https://github.com/rharder/imagesnap)が使えます。`brew install imagesnap`でインストールでき、`imagesnap -q -w 1 face.jpg`といった簡単なコマンドで使えるので便利です。開発者のRobert Harder氏に感謝を捧げます。

そんな感じで、顔写真撮ってS3にアップロード、Lambdaで顔比較して一致度が十分高ければOK、低ければNGとすれば、顔認証機能が簡単に実現できますね。

![Rekognition.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/359e401e-da4d-cca7-e0e6-a3184e9e919f.png)
(Rekognitionのデモページ。左右に写真を入れると右側に一致結果が出ます。これだけでも面白い)

## ユーザーやパスワードの管理はAmazon Cognito User Poolsを使う

最終的なユーザー権限をどこから手に入れるかを考えます。

単純に考えるとIAMユーザーを使うことになりますが、IAMユーザーのユーザー名、パスワードが分かれば、Lambdaを使わなくても認証情報を取得することができてしまいます。(3要素目の認証のみの一要素認証になる)

また[AWS STS](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_temp.html)(一時的な認証情報を生成するサービス)をそのまま使って、Lambdaにて認証情報を作る手もありますが、この場合3要素目の認証をどうするのか、という問題が残ります。(3要素目の認証をしないのであれば、例えば1要素目の認証と結びついたロールをどこか(1要素目のロールと結びついたS3など)から取得して、引き当てる、という構成も取れます。パスワード入れるの面倒だから2要素でいいや、という場合はこの構成になります。SIMと顔写真が盗られたらログインされてしまいますが)

ということでAmazon Cognito User Poolsを使います。これはAWSが公式で用意している、ユーザーをパスワードなどで認証してログインすることができる仕組みです。User Poolsでは大量のユーザーIDやパスワードを安全に保存・管理し、小規模から大規模までの認証(相手が正当な相手かどうかを確認し、誰であるかを識別する)をサポートしています。一方Identity Poolsは認可(認証された相手に対し権限を与える)を司るサービスであり、User Poolで認証されたユーザーに対し、Identity Poolが権限を与える(AWSではIAM Roleを引き当てる)という動作になっています。User PoolsとIdentity Poolsとの連携では、ユーザーを所属させたグループにより引き当てるロールを変える、などもできるため、Identity Poolsがサポートしている認証プロバイダとしてもっとも相性が良いです。そして認証するアプリ(この場合はLabmda)を限定することができ、特定のクライアントIDを指定しなければ認証できません。ユーザーID、パスワードが分かっていたとしてもその組み合わせで認証処理を実施できるのは認証Lambda関数だけなので、3要素目の認証にはもってこいですね。

ということで構成はできましたので、認証の準備をしていきましょう。

# 認証の準備

必要な準備は以下です。

1.1要素目の認証準備
- 2要素目に使う顔写真のS3への保存
- 3要素目に使うUser Pools設定
- 3要素目に使うIdentity Pools設定
- ログイン後の権限を持つIAM Roleとグループの作成
- 2、3要素目に使うLambda関数
- 3要素認証のスクリプト

ちょっと多いですが、一つ一つは大したことないです。前回の記事でスクショを撮りまくって疲れたので、今回は逆に全てCLIで書きます。

## 1. 1要素目の認証準備

こちらの過去記事をご覧ください。
[AWSコンソールのログインが面倒なのでSORACOM Kryptonを使う](https://qiita.com/1stship/items/77da0ab1c037d83d5d24)

この際、Identity Poolsに割り当てるIAMロールのポリシーは以下のようにします。
(AWSアカウントIDとS3バケット名は自分の環境に合わせて書き換えてください)

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "UploadFaceToS3",
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<顔写真を置くS3バケット名>/${cognito-identity.amazonaws.com:sub}/face.jpg"
        },
        {
            "Sid": "InvokeLoginLambda",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:ap-northeast-1:<AWSアカウントID>:function:3FactorLoginFunction"
        }
    ]
}
```

顔写真を置くS3バケットやLambdaは後で作成します。
${cognito-identity.amazonaws.com:sub}はIAM ポリシー内で使える変数で、Cognitoによって認証されたユーザーのIDです。krypton-cliコマンドで認証情報を取得した時の"identityId"属性と同じ値になります。

```bash
krypton-cli -operation generateAmazonCognitoSessionCredentials
{"identityId":"ap-northeast-1:UUID","credentials":{"accessKeyId":"XXX","secretKey":"YYY","sessionToken":"ZZZ","expiration":1584159246000}}
```

これをポリシー内に入れることで、このユーザーには特定のパス以下のファイルしか操作できない、といった権限にすることができます。S3やDynamoDBのポリシーに使うと便利ですね。

## 2. 2要素目に使う顔写真のS3への保存

S3のバケットを作成し、その中に顔写真を入れておきましょう。
S3のバケット名はグローバルでユニークになる必要があるため、このコードのバケット名は使えません。書き換えてご使用ください。
顔写真が入ることになるので、バケットのパブリックアクセスは防いでおきます。
以下のコードで準備できます。(権限を持つawsコマンドの設定はできているものとします)

```bash
BUCKET_NAME='3factor-login-bucket' #顔認証用のバケット(要書き換え)

# バケットの作成
aws s3 mb s3://$BUCKET_NAME

# パブリックアクセスの禁止
aws s3api put-public-access-block --bucket 3factor-login-bucket --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# 写真のアップロード
KRYPTON_TOKEN=$(krypton-cli -operation generateAmazonCognitoSessionCredentials)
IDENTITY_ID=$(echo $KRYPTON_TOKEN | jq -r .identityId)
imagesnap -q -w 1 face.jpg
aws s3 cp face.jpg s3://$BUCKET_NAME/$IDENTITY_ID/ref.jpg
rm face.jpg
```

写真撮るところ(imagesnap)で、ちゃんと写真撮れているか確認した方がいいかもしれませんね。

## 3. 3要素目に使うUser Pools設定

ユーザープールを作成し、認証するクライアントを登録します。
ユーザープールはほぼデフォルト設定で作成しますが、ユーザーが自分自身でアカウントを作れないようにしておきます。

クライアントはほぼデフォルト設定で作成しますが、事前にユーザーを承認済み状態にするため、ADMIN_NO_SRP_AUTHが使えるようにしておきます。

どういうことかと言うと、ユーザーを作成すると、初回の認証で「FORCE_CHANGE_PASSWORD」という状態になり、この状態ではログイン出来ません。いったんログインをして、パスワードを変更する手順を踏む必要があるのですが、今回はユーザー自身の操作でそれをさせる想定ではないため、ここでやってしまいます。パスワード変更手順後には、「CONFIRMED」という状態になり、ログインすることができるようになります。

ここまで以下のスクリプトで実行できます。

```bash
USER_POOL_NAME='3FactorLoginUserPool' # パスワード認証用のユーザープール名
USER_POOL_CLIENT_NAME='3FactorLoginClient' # パスワード認証用のクライアント名
USER_NAME='1stship' # 設定したいユーザー名
PASSWORD='Passw0rd!' # 設定したいパスワード

# ユーザープール作成
USER_POOL=$(aws cognito-idp create-user-pool --pool-name $USER_POOL_NAME --admin-create-user-config AllowAdminCreateUserOnly=true)
USER_POOL_ID=$(echo $USER_POOL | jq -r .UserPool.Id)

# ユーザープールのクライアント作成
USER_POOL_CLIENT=$(aws cognito-idp create-user-pool-client --user-pool-id $USER_POOL_ID --client-name $USER_POOL_CLIENT_NAME --explicit-auth-flows ADMIN_NO_SRP_AUTH USER_PASSWORD_AUTH)
USER_POOL_CLIENT_ID=$(echo $USER_POOL_CLIENT | jq -r .UserPoolClient.ClientId)

# ユーザー作成
USER=$(aws cognito-idp admin-create-user --user-pool-id $USER_POOL_ID --username $USER_NAME --temporary-password "$PASSWORD")

# ユーザーの確認
SESSION=$(aws cognito-idp admin-initiate-auth --user-pool-id $USER_POOL_ID --client-id $USER_POOL_CLIENT_ID --auth-flow ADMIN_NO_SRP_AUTH --auth-parameters USERNAME=$USER_NAME,PASSWORD="$PASSWORD")
AUTH_PASS_SESSION=$(echo $SESSION | jq -r .Session)
aws cognito-idp admin-respond-to-auth-challenge --user-pool-id $USER_POOL_ID --client-id $USER_POOL_CLIENT_ID --challenge-name NEW_PASSWORD_REQUIRED --challenge-responses NEW_PASSWORD="$PASSWORD",USERNAME=$USER_NAME --session $AUTH_PASS_SESSION
```

この部分を書くにあたり、クラスメソッド社の以下の記事がとても参考になりました。ありがとうございます！
https://dev.classmethod.jp/cloud/aws/change-cognito-user-force_change_passwore-to-confirmed/

## 4. 3要素目に使うIdentity Pools設定

3. にて作成したUser Poolと結びつくIdentity Poolを作成します。ここでは認証された時に与えるロールを「トークンからロールを解決する」動作とします。これを使うとUser Poolsのグループで指定されたIAM Roleを引き当てることができるので、ユーザーが増えた場合でも権限を人ごとに分けるのが簡単です。

```bash
IDENTITY_POOL_NAME='3FactorLoginIdentity' # IDプール名

# IDプールの作成
IDENTITY_POOL=$(aws cognito-identity create-identity-pool --identity-pool-name $IDENTITY_POOL_NAME --no-allow-unauthenticated-identities --cognito-identity-providers "ProviderName=cognito-idp.ap-northeast-1.amazonaws.com/$USER_POOL_ID,ClientId=$USER_POOL_CLIENT_ID,ServerSideTokenCheck=true")
IDENTITY_POOL_ID=$(echo $IDENTITY_POOL | jq -r .IdentityPoolId)

# IDプールのロール解決設定
aws cognito-identity set-identity-pool-roles --identity-pool-id $IDENTITY_POOL_ID --roles "{}" --role-mappings "{\"cognito-idp.ap-northeast-1.amazonaws.com/${USER_POOL_ID}:${USER_POOL_CLIENT_ID}\":{\"Type\":\"Token\",\"AmbiguousRoleResolution\":\"Deny\"}}"
```

## 5. ログイン後の権限を持つIAM Roleとグループの作成

Identity Poolが作成できたので、実際にログイン後に使える権限のIAM Roleを作り、それをUser Poolのグループに割り当てて、ユーザーをグループに入れます。グループ内のユーザーは作成したIAM Roleの権限でコンソールやCLIを使うことができます。

ロールを作る際には、ロールにアタッチするポリシー、ロールが信頼するエンティティのポリシーがそれぞれ必要です。

アタッチするポリシーはとりあえずEC2関連の操作が全てできるポリシーにして、3factor-policy.jsonという名前で保存しておきます。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "3FactorLoginGroupRole",
            "Effect": "Allow",
            "Action": "ec2:*",
            "Resource": "*"
        }
    ]
}
```

信頼するエンティティのポリシーは作成したIdentity Poolを信頼するポリシーで、以下のような形になります。3factor-trust.jsonという名前で保存しておきます。<作成したIDプールのID>をIDENTITY_POOL_ID変数の値に書き換えてください。

```json
{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Effect":"Allow",
      "Action":"sts:AssumeRoleWithWebIdentity",
      "Principal":{
        "Federated":"cognito-identity.amazonaws.com"
      },
      "Condition":{
        "StringEquals":{
          "cognito-identity.amazonaws.com:aud":"<作成したIDプールのID>"
        }
      }
    }
  ]
}
```

ポリシーの準備ができましたら、以下実行します。

```bash
GROUP_NAME='3factor-login-group' # グループ名
GROUP_ROLE_POLICY_NAME='3factor-login-policy' # グループに割り当てるポリシー名
GROUP_ROLE_NAME='3factor-login-role' # グループに割り当てるロール名

# ポリシー作成
GROUP_POLICY=$(aws iam create-policy --policy-name $GROUP_ROLE_POLICY_NAME --policy-document file://3factor-policy.json)
GROUP_POLICY_ARN=$(echo $GROUP_POLICY | jq -r .Policy.Arn)

# ロール作成
GROUP_ROLE=$(aws iam create-role --role-name $GROUP_ROLE_NAME --assume-role-policy-document file://3factor-trust.json)
GROUP_ROLE_ARN=$(echo $GROUP_ROLE | jq -r .Role.Arn)

# ポリシーの割り当て
aws iam attach-role-policy --role-name $GROUP_ROLE_NAME --policy-arn "$GROUP_POLICY_ARN"

# ユーザープールのグループ作成
aws cognito-idp create-group --group-name $GROUP_NAME --user-pool-id $USER_POOL_ID --role-arn $GROUP_ROLE_ARN

# ユーザープールのグループにユーザー割り当て
aws cognito-idp admin-add-user-to-group --user-pool-id $USER_POOL_ID --username $USER_NAME --group-name $GROUP_NAME
```

これでログインしたらこのポリシーの権限の認証情報が取得できる状態になりました。

6. 2、3要素目に使うLambda関数

Labmdaを作成するには、まずIAMロールを作り、Labmda関数を作り、ソースコードをアップロードします。
コードはRubyで書きます。サーバーでのちょっとした処理を書くのにRubyは最高なのです。（僕調べ）

まずIAMロールのポリシーはこんな感じです。3factor-lambda-policy.jsonとして保存します。
(<AWSアカウントID>と<顔写真を置くS3バケット名>は書き換えてご利用ください)

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CreateLogGroup",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:<AWSアカウントID>:*"
        },
        {
            "Sid": "CreateLog",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:ap-northeast-1:<AWSアカウントID>:log-group:/aws/lambda/3FactorLoginFunction:*"
        },
        {
            "Sid": "CompareFaces",
            "Effect": "Allow",
            "Action": "rekognition:CompareFaces",
            "Resource": "*"
        },
        {
            "Sid": "AccessS3",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::<顔写真を置くS3バケット名>/*"
        }
    ]
}
```

信頼するエンティティのポリシーはLambdaを信頼するようにします。3factor-lambda-trust.jsonとして保存します。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

認証するLambdaのソースコードは以下のようになります。lambda_function.rbという名前で保存します。

```ruby
require 'aws-sdk-cognitoidentity'
require 'aws-sdk-cognitoidentityprovider'
require 'aws-sdk-rekognition'
require 'aws-sdk-s3'
require 'uri'
require 'json'
require 'net/http'

SIGNIN_URL="https://signin.aws.amazon.com/federation"
ISSUER_URL="https://mysignin.internal.mycompany.com/"
CONSOLE_URL="https://console.aws.amazon.com/"
ISSUER_URL_ENCODED=URI.encode_www_form_component(ISSUER_URL)
CONSOLE_URL_ENCODED=URI.encode_www_form_component(CONSOLE_URL)

def lambda_handler(event:, context:)
  # 顔認証
  face_compare_result = compare_faces(context.identity["cognitoIdentityId"])
  return { result: "NG" } if !face_compare_result

  # パスワード認証
  credentials = login_by_cognito(event["user"], event["password"])
  return { result: "NG" } if credentials.nil?
  
  # URL取得
  login_url = create_login_url(credentials)
  return { result: "NG" } if login_url.nil?
  
  { result: "OK", url: login_url }
end

def compare_faces(identity_id)
  rekognition = Aws::Rekognition::Client.new
  result = rekognition.compare_faces(
    similarity_threshold: 98,
    source_image: {
      s3_object: {
        bucket: ENV["BUCKET_NAME"],
        name: [identity_id, "ref.jpg"].join("/")
      }
    },
    target_image: {
      s3_object: {
        bucket: ENV["BUCKET_NAME"],
        name: [identity_id, "face.jpg"].join("/")
      }
    }
  )
  s3 = Aws::S3::Client.new
  s3.delete_object({
    bucket: ENV["BUCKET_NAME"],
    key: [identity_id, "face.jpg"].join("/")
  })
  !result.face_matches.empty?
rescue
  false
end

def login_by_cognito(user_id, password)
  cognito_idp = Aws::CognitoIdentityProvider::Client.new
  initiate_auth_result = cognito_idp.initiate_auth(
    auth_flow: 'USER_PASSWORD_AUTH',
    auth_parameters: {
      USERNAME: user_id,
      PASSWORD: password
    },
    client_id: ENV["CLIENT_ID"]
  )

  provider_id = "cognito-idp.ap-northeast-1.amazonaws.com/#{ENV["USER_POOL_ID"]}"
  cognito_identity = Aws::CognitoIdentity::Client.new
  identity = cognito_identity.get_id(
    identity_pool_id: ENV["IDENTITY_POOL_ID"],
    logins: {
      provider_id => initiate_auth_result.authentication_result.id_token
    }
  )

  credentials = cognito_identity.get_credentials_for_identity(
    identity_id: identity.identity_id,
    logins: {
      provider_id => initiate_auth_result.authentication_result.id_token
    }
  )

  credentials.credentials
rescue
  nil
end

def create_login_url(credentials)
  session_obj = {
    sessionId: credentials.access_key_id,
    sessionKey: credentials.secret_key,
    sessionToken: credentials.session_token
  }
  session_json = URI.encode_www_form_component(JSON.generate(session_obj))
  get_signin_token_url = "#{SIGNIN_URL}?Action=getSigninToken&SessionType=json&Session=#{session_json}"
  signin_token_response = Net::HTTP.get_response(URI.parse(get_signin_token_url))
  return nil if signin_token_response.code != "200"
  
  signin_token = JSON.parse(signin_token_response.body)["SigninToken"]
  login_url="#{SIGNIN_URL}?Action=login&Issuer=#{ISSUER_URL_ENCODED}&Destination=#{CONSOLE_URL_ENCODED}&SigninToken=#{URI.encode_www_form_component(signin_token)}"
  login_url
rescue
  nil
end
```

実行すると、認証成功すればOKとログインURLが、失敗するとNGが返ってきます。例外処理が雑なのはご容赦ください。

しかし顔認証、パスワード認証といった面倒な処理がこの100行程度のコードでできちゃうのは素晴らしいですね。SaaS様々です。

ちょっと補足をしておくと、Identity Poolsで取得した認証情報でLambdaにアクセスすると、context.identityにその情報が入ってます。ポリシーに入れた変数はこのID以下のファイルしかアクセス出来ないようにするものです。従ってこのIDを指定してS3にアクセスする必要があります。

また、顔比較の後ファイルを削除しています。これはLambdaの起動と顔写真のアップロードを一度に実行することはできず、削除しないとS3にファイルが残っていることになって次回顔写真をアップロードしなくても認証できてしまうためです。実質一要素減ってしまいますので、毎回ファイルは削除します。

以下を実行してソースファイルをzipファイルにしておきます。

```bash
zip lambda_function.zip lambda_function.rb
```

以下のスクリプトを実行するとLambda関数を作成することができます。ここまで作成してきたUser PoolsなどのIDは環境変数に設定することで、環境に依らないコードとすることができます。

```bash
LOGIN_FUNCTION_NAME='3FactorLoginFunction' # ログイン用Lambda関数名
LOGIN_ROLE_POLICY_NAME='3factor-login-lambda-policy' # ログイン用Lambdaのポリシー名
LOGIN_ROLE_NAME='3factor-login-lambda-role' # ログイン用Lambdaのロール名
LOGIN_POLICY=$(aws iam create-policy --policy-name $LOGIN_ROLE_POLICY_NAME --policy-document file://3factor-lambda-policy.json)
LOGIN_POLICY_ARN=$(echo $LOGIN_POLICY | jq -r .Policy.Arn)
LOGIN_ROLE=$(aws iam create-role --role-name $LOGIN_ROLE_NAME --assume-role-policy-document file://3factor-lambda-trust.json)
LOGIN_ROLE_ARN=$(echo $LOGIN_ROLE | jq -r .Role.Arn)
aws iam attach-role-policy --role-name $LOGIN_ROLE_NAME --policy-arn "$LOGIN_POLICY_ARN"

aws lambda create-function \
--function-name $LOGIN_FUNCTION_NAME \
--runtime ruby2.7 \
--role $LOGIN_ROLE_ARN \
--handler "lambda_function.lambda_handler" \
--timeout 60 \
--publish \
--environment "{\"Variables\":{\"BUCKET_NAME\":\"$BUCKET_NAME\",\"IDENTITY_POOL_ID\":\"$IDENTITY_POOL_ID\",\"USER_POOL_ID\":\"$USER_POOL_ID\",\"CLIENT_ID\":\"$USER_POOL_CLIENT_ID\"}}" \
--zip-file fileb://lambda_function.zip
```

7. 3要素認証スクリプト

では最後にこれらを利用して三要素認証するシェルスクリプトを書きましょう。
難しい処理は全てSORACOM KryptonとAWS Lambdaにあるので、スクリプトはそれを呼び出すだけです。
撮った写真や返ってきた結果は悪用されないよう削除します。
あと知らなかった小技としてreadに-sをつけると入力表示しないモードになる見たいですね。これは便利。
またmacOSのopenは-aでアプリケーションを指定できるみたいなので、Chromeで開くように指定しました。(SafariとAWS コンソールたまに相性悪いことがあるので)
以下のスクリプトを3factor-login.shとして保存します。

```bash
#!/bin/bash
set -e

# 定数宣言
BUCKET_NAME='3factor-login-bucket' # 要書き換え
FUNCTION_NAME='3FactorLoginFunction'

# ユーザー名、パスワードの入力
read -p "User: " user
read -sp "Password: " pass
echo # 入力行に続けて表示しないよう改行

# 写真を撮る
imagesnap -q -w 1 face.jpg

# KryptonによるCognito認証情報の取得
KRYPTON_TOKEN=$(krypton-cli -operation generateAmazonCognitoSessionCredentials)
IDENTITY_ID=$(echo ${KRYPTON_TOKEN} | jq -r .identityId)
AWS_ACCESS_KEY_ID=$(echo ${KRYPTON_TOKEN} | jq -r .credentials.accessKeyId)
AWS_SECRET_ACCESS_KEY=$(echo ${KRYPTON_TOKEN} | jq -r .credentials.secretKey)
AWS_SESSION_TOKEN=$(echo ${KRYPTON_TOKEN} | jq -r .credentials.sessionToken)
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_SESSION_TOKEN

# 顔認証、パスワード認証
aws s3 cp face.jpg s3://$BUCKET_NAME/${IDENTITY_ID}/face.jpg > /dev/null
rm -f face.jpg
aws lambda invoke --function-name $FUNCTION_NAME --payload "{\"user\":\"$user\",\"password\":\"$pass\"}" result.json > /dev/null
if [ $(cat result.json | jq -r .result) = "NG" ] ; then
  rm -f result.json
  echo "Login failed"
  exit
fi

# ログイン
echo "Login success"
LOGIN_URL=$(cat result.json | jq -r .url)
rm -f result.json
open -a '/Applications/Google Chrome.app' $LOGIN_URL
```

これで全ての準備が終わりました。お疲れ様でした！

# 実行

やってみましょう。

起動して
![start.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/ff1bdddf-a9f4-fcdf-563b-de4848ac52ec.jpeg)

ユーザー名、パスワード入力して
![pass.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/00698f61-7705-90d5-9152-d7f3a02e83cc.jpeg)

顔写真撮られて(インカメラのLEDが緑に光っている)
![face.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/64cc0af7-5aaf-5966-1328-0d37677e935a.jpeg)

SIMカード情報読み取って(わかりにくいですがカードリーダーのLEDが緑に点滅している)
![card.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/df9acd1c-cd0a-9a70-ec55-1d7cd987ffa8.jpeg)

しばらく待つと認証OKになり
![success.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/3f4358c7-3d9d-75cb-9562-b0ffd016d415.jpeg)

セッショントークンを使ったログインページにアクセスして
![loginpage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/8262361d-4322-0e0c-abbd-83c8dfda757d.jpeg)

ログイン出来ました
![login.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/1ba272ea-dc1c-b393-1309-b624d4b2c792.jpeg)

やったね！

# おわりに

個人でもそこそこ頑張れば三要素認証が実装できるの、良い時代になったものですね。