Help us understand the problem. What is going on with this article?

Amazon CognitoとSlackで無理矢理OpenID Connect

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

前回の記事で試したSlack Oauth2.0を利用して実装しました。
SlackのOAuth2.0のみを使いたいという方はこちらを参照ください。

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

Amazon CognitoにはOpenID Connectによる連携がサポートされています(Open ID Connect プロバイダー (ID プール) - Amazon Cognito)。

これを使って、LINEやYahooを連携させている方もいます。(AWS CognitoにGoogleとYahooとLINEアカウントを連携させる - Qiita)
この方法であれば、キーを取得してCognitoに設定する程度で連携することができます。

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

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

イメージ

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

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

seq_flow

環境

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

前回の記事で作成したプログラムを利用してOpenID Providerを作っていきます。
また、OpenID ProviderはCognitoから見えるようパブリックに公開させる必要があります。

最終的にはLambdaでやりたいのですが、手っ取り早く動作確認したかったので ngrok を使いました。

初めて使ったのですが、 ngrokが便利すぎる - Qiita で紹介されているように本当に便利すぎます。

ただ、セキュリティリスクの話はあるので今回は自宅でお試ししてみました。
ですので、ところどころ前回とアイコン周りが違ったりします。

手順概要

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

ngrokでポートを公開

今回はlocalhostに立てたサーバをngrokを使って公開してみました。初めて使いましたが非常に便利です。

Githubアカウントあればすぐに使えるのも非常に楽でした。

公式ページ見ただけでも特に詰まることもなかったので細かい手順などは書きませんが、起動すると以下のようにローカルサーバを公開してくれます。

ngrokが便利すぎる - Qiita を参考にしていただければ十分わかると思います。

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

$ 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.ioXXXXの部分は起動のたびに変わるので、起動したままにしておいてください。(お金を払えば固定できるみたいです。 ngrokで常に同じアドレスを使う方法 - Qiita)
Cognitoにはhttps必須なので、httpsのほうを使います。

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

Slackアプリの登録・設定

SlackのOAuth2.0を使う必要があるため、前回の記事の手順の内、Slackアプリの作業を実施しておいてください。

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

OpenID Providerサーバを起動

ファイル一つで完結するので、ソース貼っちゃいます。

一記事でやるとややこしそうだったので、動作の詳細については別記事にしようと思います。

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

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

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

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

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

- 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ポートで起動します

node app.js

CognitoにOpenID Providerの設定を行う

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

App client

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

app_clients.png

Domain name

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

domain.png

Identity providers

  1. OpenID Connectを洗濯
  2. 以下を設定
  3. 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 endpointToken endpointUserinfo endpointへのリクエストを受け取るOpenID Providerとして動作します

oidc_setting.png

Attribute mapping

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

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

mapping.png

App client settings

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

app_clients_settings.png

動作確認

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

$ 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

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

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

resut.png

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

user_added.png

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

おわりに

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

ただし、自分で使いたい処理の部分のみ実装した形なので、仕様をフルで網羅できてはいないです。

この辺りのお話はまた後日

参考

Amazon Cognito ユーザープール の Auth API リファレンス - Amazon Cognito

ngrokが便利すぎる - Qiita

AWS CognitoにGoogleとYahooとLINEアカウントを連携させる - Qiita

AWS Cognitoのエンドポイントを使いこなす - Qiita

一番分かりやすい OAuth の説明 - Qiita

OAuth 2.0 全フローの図解と動画 - Qiita

dbgso
cloudpack
Amazon Web Services (AWS) の導入設計、環境構築、運用・保守をサポートするマネジドホスティングサービス
https://cloudpack.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした