Amazon CognitoとSlackアカウントで無理矢理OpenID Connect(動作概要編)


[Amazon CognitoとSlackで無理矢理OpenID Connect - Qiita](https://qiita.com/dbgso/items/0f6fd08b6184eb4bed58)の記事でSlackのOAuth認証を利用して、Cognitoに対するOpenID Providerを作成しました。

前回の記事では(~~書くのが疲れてしまって~~)長くなってしまったので、動作の概要はこちらの記事で書きます。  
社内のデモレベルで利用予定なので大丈夫とは思いますが、IDトークンを抜かしていたり諸々不明点あるのでもう少し勉強したいですね

# 動作概要解説

Cognitoと連携するためのOpenID Providerとして動作させるには、以下を用意する必要があります。

- 認可エンドポイント
- トークンエンドポイント
- USERINFOエンドポイント

以下も必要なんですが、今回の場合は必須でない(用意できない？)ので飛ばしました

- jwks.json
- openid-configuration

## 認可エンドポイント

認可コードを取得するためのエンドポイントです。  
認可を移譲するサービスのURIへリダイレクトするのが主な役割です。

> [認可エンドポイント - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/authorization-endpoint.html)
> 
> **リクエスト例**
> 
> ```
> GET https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/authorize?
> response_type=code&
> client_id=ad398u21ijw3s9w3939&
> redirect_uri=https://YOUR_APP/redirect_uri&
> state=STATE&
> scope=openid+profile+aws.cognito.signin.user.admin
> ```
> 
> **レスポンス例**
>
> ```http
> HTTP/1.1 302 Found
> Location: https://YOUR_APP/redirect_uri?code=AUTHORIZATION_CODE&state=STATE
> ```

今回の場合、`/authorization`というエンドポイントで受けて、Slackの`https://slack.com/oauth/authorize`認可許可ページに飛ばしています。

```javascript
const slack_redirect_url = 'https://*****.ngrok.io/slackendpoint'
app.get('/authorization', (req, res) => {
    if (req.)
    // 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)
});
```

※ 本来は以下の通り`scope`属性を見て、返却するデータを制限する必要がありますが、今回は簡略化のために考慮していないコードとしています。

> scope
> クライアントに関連付けられた任意のシステム予約されたスコープまたはカスタムスコープを組み合わせることができます。スコープはスペースで区切る必要があります。システム予約されたスコープは openid、email、phone、profile、および aws.cognito.signin.user.admin です。使用するスコープはクライアントにあらかじめ関連付けられている必要があります。これを行わない場合、実行時に無視されます。



stateについては以下のように言われているので、リダイレクトのパラメータにも引き継いでおいて後の処理でも使うようにしています
> クライアントが初期リクエストに追加する OPAQUE 値。認可サーバはクライアントにリダイレクトして戻るときに、この値を含めます。
> この値は、CSRF 攻撃を防ぐために、クライアントで使用する必要があります。
> オプションですが、強くお勧めします。

response_typeは以下の通り`token`の場合もあるのですが、Slackでは`code`のみサポートなので一応弾いています。この辺りちょっと適当です。
> レスポンスのタイプ。code または token を指定する必要があります。クライアントがエンドユーザーの認可コードを求めるか (認可コード付与フロー)、エンドユーザーに直接トークンを発行するか (暗黙的フロー) を示します。

ここでリダイレクトすると ![alt](https://camo.qiitausercontent.com/10a8a3096e3fb4407cf74441c5710e18ccb3fbc5/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3333303531342f30366561616266652d303237382d313735632d303231632d3032386432316561383963372e706e67) のようなページが表示されるので、許可すると`redirect_uri`のページに認可コードを含んだリクエストがきます。

今回の場合は`https://*****.ngrok.io/slackendpoint`で受けています。  
ここで認可コードを受け取って、`https://****.auth.us-west-2.amazoncognito.com/oauth2/idpresponse`にリダイレクトすることでcognitoに認可コードを教えています。

```javascript
app.get('/slackendpoint', (req, res) => {
    // 認可コードの取得
    const code = req.query["code"];

    const url = `https://****.auth.us-west-2.amazoncognito.com/oauth2/idpresponse?state=${req.query.state}&code=${code}&scope=openid+&authuser=1`
    res.redirect(url);
    return
});
```


## トークンエンドポイント

アクセストークンを取得するためのエンドポイントです。  
認可コードを含んだリクエストが飛んでくるので、そのコードに紐づいたユーザのアクセストークンを取得します

> [トークンエンドポイント - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html)

> **リクエスト例**
> 
>     POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token&
>     Content-Type='application/x-www-form-urlencoded'&
>     Authorization=Basic aSdxd892iujendek328uedj
> 
>     grant_type=authorization_code&
>     client_id=djc98u3jiedmi283eu928&
>     code=AUTHORIZATION_CODE&
>     redirect_uri=com.myclientapp://myclient/redirect
> 
> **レスポンス例**
> 
> ```http
> HTTP/1.1 200 OK
> Content-Type: application/json
> 
> { 
>     "access_token":"eyJz9sdfsdfsdfsd", 
>     "refresh_token":"dn43ud8uj32nk2je", 
>     "id_token":"dmcxd329ujdmkemkd349r",
>     "token_type":"Bearer", 
>     "expires_in":3600
> }
> ```

やっていることは割とシンプルで、cognitoからのリクエストのbody部にSlackのクライアントIDが含まれているので、これを使ってSlackに聞いているだけです。 
そして、そこで返ってきたアクセストークンをcognitoへのレスポンスとしています。 
`refresh_token`, `id_token`はSlackがサポートしていないので返していません。
これで良いのだろうか？Cognitoに登録された後はCognitoから発行されるID Token、Access Tokenを見ることになるので問題はなさそうではある。

```javascript
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)
    })
})
```

## USERINFOエンドポイント

アクセストークンに紐づいたユーザ情報を返すエンドポイントです。
ヘッダーの`Authorization`にアクセストークンが入っているので、それを使ってSlackに問い合わせます。

> [USERINFO エンドポイント - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/userinfo-endpoint.html)
> 
> **リクエスト例**
> 
>     GET https://<your-user-pool-domain>/oauth2/userInfo
>     Authorization: Bearer <access_token>
> 
> **肯定応答の例**
> 
> ```http
> HTTP/1.1 200 OK
> Content-Type: application/json;charset=UTF-8
> 
> {
>     "sub": "248289761001",
>     "name": "Jane Doe",
>     "given_name": "Jane",
>     "family_name": "Doe",
>     "preferred_username": "j.doe",
>     "email": "janedoe@example.com"
> }
> ```


1. アクセストークンを使って`user_id`を取得
1. `user_id`を使ってユーザ情報取得

emailを取得するにはこの二段階が必要なので２回リクエストしています。  
Slackからのレスポンスには [users.info method | Slack](https://api.slack.com/methods/users.info) のデータが取れるので、必要なデータを選んでcognitoに返します。

OpenIDでは`sub`というパラメータにユーザIDを入れる仕様となっているようなので、ここに入れています。  
あと、今回はemailも欲しかったので同様に`email`パラメータい入れています。
`※ 本来はscopeパラメータに従って制限する必要があります`


```javascript
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;
            console.log(params)
            const response_data = {
                "sub": params.id,
                "email": params.profile.email
            }
            res.send(response_data)
        })
    })
})
```

このレスポンスのキー名とCognitoの`Attribute mapping`で指定した値が一致する必要があります。

![attribute_mapping](https://camo.qiitausercontent.com/984338babea21ddcf320d861f2ef89901f01edee/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3333303531342f33663939353338362d303736632d653066352d646330372d3035336435333533393630392e706e67)

## Jwks uri

これは、発行したトークンが正しいかどうかを検証するための仕組みとのことです。
ただ、今回はSlackが発行したトークンを使うので、秘密鍵やらなんやらそもそもわからないので検証のしようがない？

とりあえず、内部向けの利用なので保留とします。

## Issuer

[IDトークンが分かれば OpenID Connect が分かる - Qiita](https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06) によると `/.well-known/openid-configuration` にサーバ情報を返すJSONを返す役割のようです。  

ここでURLの定義を返せるので、実装しておけばよかったのですが、[Boto3でAmazon CognitoのOIDC Providerを作成する - Qiita](https://qiita.com/dbgso/items/a9c4d1c67d9ab92378ce) で設定を楽する方法を選んでしまったので、後回しにしてしまいました。
ここは後日やっておきたいです。

会社ごとに頑張り具合に差があるのがなんとも。やるときは仕様をちゃんと確認しないとですね

- [yahoo](https://auth.login.yahoo.co.jp/yconnect/v2/.well-known/openid-configuration)
- [line](https://access.line.me/.well-known/openid-configuration)
- [google](https://accounts.google.com/.well-known/openid-configuration)


# 参考

[IDトークンが分かれば OpenID Connect が分かる - Qiita](https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06)

[Amazon Cognito API リファレンス - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-reference.html)

[なんちゃってOAuth2/OpenID Connectサーバを自作する - Qiita](https://qiita.com/poruruba/items/042adba2ac9a34839546)

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