Amazon CognitoとSlackで無理矢理OpenID Connect

Cognitoの認証にSlackのユーザ情報を使おうと思ったら、SlackではOpenID Connectをサポートしていないからできないとのことだったので、無理矢理OpenID Connectに対応させる方法を探してみました。

[前回の記事](https://qiita.com/dbgso/items/a95a3364d9a8c67f3387)で試したSlack Oauth2.0を利用して実装しました。
SlackのOAuth2.0のみを使いたいという方はこちらを参照ください。


# CognitoとSlack連携の上での問題点

Amazon CognitoにはOpenID Connectによる連携がサポートされています([Open ID Connect プロバイダー (ID プール) - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/open-id.html))。  
これを使って、LINEやYahooを連携させている方もいます。([AWS CognitoにGoogleとYahooとLINEアカウントを連携させる - Qiita](https://qiita.com/poruruba/items/189945dc64edfe1f2464))
この方法であれば、キーを取得してCognitoに設定する程度で連携することができます。

しかし、SlackはOAuth2.0によるAPIの利用はできるのですが、OpenID Connectには対応していないので、Cognitoと直接連携することはできないです。非常に残念

そこで、自身でCognitoとSlackを中継するようなサーバを用意して**無理矢理**連携させてみました。  

# イメージ

必要な処理を端折っている部分はありますが、以下のようなフローで進むイメージです。

OpenID Connect対応のサーバ(OpenID Provider)を開発して、実際の認証周りはSlackのOAuth2.0APIに丸投げしている感じです。

![seq_flow](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/850c9208-3305-8657-4f19-e1138293f434.png)



# 環境

* Node.js v10.15.3
* express 4.17
* request 2.88

[前回の記事](https://qiita.com/dbgso/items/a95a3364d9a8c67f3387)で作成したプログラムを利用してOpenID Providerを作っていきます。
また、OpenID ProviderはCognitoから見えるようパブリックに公開させる必要があります。  
最終的にはLambdaでやりたいのですが、手っ取り早く動作確認したかったので [ngrok](https://ngrok.com/) を使いました。

初めて使ったのですが、 [ngrokが便利すぎる - Qiita](https://qiita.com/mininobu/items/b45dbc70faedf30f484e) で紹介されているように本当に便利すぎます。  
ただ、セキュリティリスクの話はあるので今回は自宅でお試ししてみました。
ですので、ところどころ前回とアイコン周りが違ったりします。

# 手順概要

1. ngrokでポートを公開
1. OpenID Providerサーバを起動
1. Slackアプリに登録する
1. Slackアプリの権限設定などを行う
1. CognitoにOpenID Providerの設定を行う

## ngrokでポートを公開

今回はlocalhostに立てたサーバを`ngrok`を使って公開してみました。初めて使いましたが非常に便利です。  
Githubアカウントあればすぐに使えるのも非常に楽でした。

公式ページ見ただけでも特に詰まることもなかったので細かい手順などは書きませんが、起動すると以下のようにローカルサーバを公開してくれます。  
[ngrokが便利すぎる - Qiita](https://qiita.com/mininobu/items/b45dbc70faedf30f484e) を参考にしていただければ十分わかると思います。

ngrokの準備が整ったら以下のコマンドで3000ポートを公開します。同じのようなログが流れればOKです。

```console
$ ngrok http 3000
ngrok by @inconshreveable
Session Status                online
Account                       dbgso (Plan: Free)
Version                       2.3.29
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://XXXX.ngrok.io -> http://localhost:3000
Forwarding                    https://XXXX.ngrok.io -> http://localhost:3000
Connections                   ttl     opn     rt1     rt5     p50     p90
184     0       0.00    0.00    5.50    120.02
```

`https://XXXX.ngrok.io`の`XXXX`の部分は起動のたびに変わるので、起動したままにしておいてください。(お金を払えば固定できるみたいです。 [ngrokで常に同じアドレスを使う方法 - Qiita](https://qiita.com/makopo/items/338791261f86dfec1adf))
Cognitoにはhttps必須なので、httpsのほうを使います。

本当は最後に起動が良いのですが、SlackにURLを登録しないといけないので手順上は最初にURLを押さえておきます。

## Slackアプリの登録・設定

SlackのOAuth2.0を使う必要があるため、[前回の記事](https://qiita.com/dbgso/items/a95a3364d9a8c67f3387#slack%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E7%99%BB%E9%8C%B2)の手順の内、Slackアプリの作業を実施しておいてください。

設定は同じで大丈夫です。Redirect URLsのみ、今回のngrokで取得した`https://XXXX.ngrok.io`にしておいてください。


## OpenID Providerサーバを起動

ファイル一つで完結するので、ソース貼っちゃいます。  
一記事でやるとややこしそうだったので、動作の詳細については別記事にしようと思います。

`※ 今回はSlack向けに必要最低限のフローしかサポートしていないため、OpenIDの仕様をフルでは網羅していません。`

依存ライブラリは以下のコマンドで入れておいてください

```bash
yarn add express request body-parser # or npm install express request body-parser
```

以下の、Cognitoとngrokのドメインを実際のものと合わせてください。リージョンとかも合っているか注意

この辺り動的に行けると思うんですが、色々疲れたのでひとまずこれで

```diff
- const url = `https://XXXX.auth.us-west-2.amazoncognito.com/oauth2/idpresponse?state=${req.query.state}&code=${code}&scope=openid`
+ const url = `https://<実際のドメイン>.auth.us-west-2.amazoncognito.com/oauth2/idpresponse?state=${req.query.state}&code=${code}&scope=openid`

- const slack_redirect_url = 'https://XXXX##.ngrok.io/slackendpoint'
+ const slack_redirect_url = 'https://<実際のドメイン>.ngrok.io/slackendpoint'
```

```app.js
//@ts-check

const request = require('request')
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.urlencoded({
    extended: true
}));

// // SSL関連でエラーが出るので、応急処置 SlackのAPIにリクエストするときに死んでいる模様。おそらく社内ネットワークのせい
// process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";

const slack_redirect_url = 'https://XXXX##.ngrok.io/slackendpoint'

// Slackの認可コード受け取りのための
app.get('/slackendpoint', (req, res) => {
    // 認可コードの取得
    const code = req.query["code"];

    const url = `https://XXXX.auth.us-west-2.amazoncognito.com/oauth2/idpresponse?state=${req.query.state}&code=${code}&scope=openid`
    res.redirect(url);
    return
});
// Cognito Authorization endpoint
app.get('/authorization', (req, res) => {
    // Cognitoからの
    console.log("auth", req)
    const redirect_uri = req.query.redirect_uri
    const state = req.query.state;
    const client_id = req.query.client_id;
    const url = `https://slack.com/oauth/authorize?client_id=${client_id}&scope=identify&redirect_uri=${slack_redirect_url}&state=${state}&target_uri=${redirect_uri}`;
    res.redirect(url)
});
// Cognito Token endpoint
app.post('/token', (req, res, next) => {
    console.log("token", req);
    const client_id = req.body.client_id;
    const client_secret = req.body.client_secret;
    request({
        url: "https://slack.com/api/oauth.access",
        method: "POST",
        form: {
            client_id: client_id,
            client_secret: client_secret,
            code: req.body.code,
            redirect_uri: slack_redirect_url
        }
    }, (error, response, body) => {
        res.setHeader('Content-Type', 'application/json');
        const param = JSON.parse(body);
        const access_token = param['access_token'] // アクセストークン

        const j = {
            access_token: access_token,
            expires_in: 3600,
            "token_type": "Bearer",
        }
        res.send(j)
    })
})
// Cognito Userinfo endpoint
app.get('/userinfo', (req, res) => {
    res.setHeader('Content-Type', 'application/json');
    const access_token = req.headers.authorization.split(' ')[1];
    console.log("userinfo", req)
    // ユーザIDを取得するためのリクエスト
    request("https://slack.com/api/auth.test", {
        method: "POST",
        form: {
            token: access_token
        }
    }, (error, response, body) => {
        const user = JSON.parse(body);
        console.log(user);
        // アクセストークンを使ってユーザ情報をリクエスト
        request("https://slack.com/api/users.info ", {
            method: 'POST',
            form: {
                token: access_token,
                user: user.user_id
            }
        }, (error, response, body) => {
            const params = JSON.parse(body).user;
            params.sub = params.id
            params.email = params.profile.email
            console.log(params)
            res.send(params)
        })
    })
})

app.listen(3000, () => {
    console.log('HTTP Server(3000) is running.');
});
```

以下のコマンドで3000ポートで起動します

```javascript
node app.js
```

## CognitoにOpenID Providerの設定を行う

デフォルト設定で作成した後に変更するのが簡単だったので、それでいきます。`Review defaults`で`Create pool`で大丈夫です。

### App client

1. 適当に名前をつけて、`Generate client secret`のチェックは外す。
1. 保存
1. `App client id`を控えておく

![app_clients.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/fcf95594-b794-b859-82f5-fd59d858d331.png)


### Domain name

1. 被らないように適当にドメインをつける
    * ソースコードに書いたドメイン名と合わせること
    * 動作確認時はこのURLを使います
1. 保存

![domain.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/74a39f0d-d8f3-15c4-ad05-3dddbd5e5438.png)


### Identity providers

1. OpenID Connectを洗濯
1. 以下を設定
1. Create provider

設定項目は以下の通りです。  

|設定項目名 | 設定値 | 備考|
|---|---|---|
|Provider name| Slack| Cognitoのログイン画面に表示される名前|
| Client ID | Slackのclient id| 登録したSlackアプリから確認 |
| Client secret | Slackのclient secret| 登録したSlackアプリから確認 |
| Attributes request method | GET| |
| Authorize scope | openid email | opeind必須|
| Issuer| `https://slack.com`| 今回は手作業で各エンドポイントを設定するため、ダミーでも良い |
| Authorization endpoint | `https://XXXX.ngrok.io/authorization`| パスは任意。実装と一致すれば良い |
| Token endpoint | `https://XXXX.ngrok.io/token`| 同上 |
| Userinfo endpoint | `https://XXXX.ngrok.io/userinfo` | 同上 |
| Jwks uri | `https://XXXX.ngrok.io/jwks` | 未使用。今回の例の場合はいらない？|

先ほど起動したサーバが`Authorization endpoint`、`Token endpoint`、`Userinfo endpoint`へのリクエストを受け取るOpenID Providerとして動作します

![oidc_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/988f2826-34d3-6409-1050-4af33aa616ba.png)


### Attribute mapping

OpenID Providerから返した情報をCognitoに反映するために`Attribute mapping`を設定します。

1. OIDC attribute -> Email を追加します
    * sub -> Username は必須のようなのでそのままです
1. Save changes

![mapping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/3f995386-076c-e0f5-dc07-053d53539609.png)


### App client settings

1. Enabled Identify Providers の`Slack`にチェック(Identity providersに追加すると出てきます)
1. Callback、SignoutのURLを今回は`http://localhost:8000`にする(実際のアプリに合わせる)
    * ここで入れるURLはOpenID Providerではなく、サービス側のアプリです。
    * 今回はIDを受け取るだけなので、Pythonの簡易サーバにします
1. Authorization code grantを指定(Slackはこれのみ対応のため)
1. Allows OAuth Scopesに`openid`を指定(必須のため)
1. Save changes

<img width="1369" alt="app_clients_settings.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/a8580dd8-f4c8-3374-b5b9-df84746e1376.png">


## 動作確認

確認用のアプリとして、pythonの`http.server`を立てておきます

```bash
$ python3 -m http.server -p 8000
```


以下のURLにアクセス。アドレスなど間違えないように

```
https://<cognitoのドメイン>/login?response_type=code&client_id=<cognitoのclient id>&redirect_uri=http://localhost:8000
```

こんな画面がでるはずです

![cognito_login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/c7673c0d-8aab-6e7d-2947-8da274fe318e.png)


`Slack`ボタンを押すと、Slackの認可ページに飛びます

![](https://camo.qiitausercontent.com/10a8a3096e3fb4407cf74441c5710e18ccb3fbc5/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3333303531342f30366561616266652d303237382d313735632d303231632d3032386432316561383963372e706e67)

`Authorize`を押すと`OpenID Provider`、`Slack`、`Cognito`の間でやりとりが行われて、アプリ(http.server)へcodeと共にリダイレクトされます。(すみません、キャプチャでは8082ポートになっていますね)

![resut.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/39e641ae-7638-8d3f-1d4e-f2292bd11ec1.png)


CognitoのUser Poolを確認すると`Slack`で始まるユーザが追加されています。

![user_added.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330514/058bf164-fcd1-5eb9-05a1-3ee652304b5b.png)

これで、SlackとCognitoを連携させることができました

# おわりに

Cognito+Slackユーザとしてログインさせる処理をさせたかったので調べてみました。
OpenID Connectの知識ほぼ0の状態でいきなり特殊なことをしたので、疲れました。。。
ただ、OpenIDの通信の流れはよく理解できた感じがして良かったです。  

ただし、自分で使いたい処理の部分のみ実装した形なので、仕様をフルで網羅できてはいないです。  
この辺りのお話はまた後日

# 参考

[Amazon Cognito ユーザープール の Auth API リファレンス - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html)

[ngrokが便利すぎる - Qiita](https://qiita.com/mininobu/items/b45dbc70faedf30f484e)

[AWS CognitoにGoogleとYahooとLINEアカウントを連携させる - Qiita](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

[AWS Cognitoのエンドポイントを使いこなす - Qiita](https://qiita.com/poruruba/items/a284b9f522d8d6e1e3f9)

[一番分かりやすい OAuth の説明 - Qiita](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)

[OAuth 2.0 全フローの図解と動画 - Qiita](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f)