Amazon Cognito と 各種アカウントを連携させる

概要

  • AWS が提供する認証・認可基盤である Amazon Cognito と各種ソーシャルアカウントの連携を試してみました。
    • 今回は、 Google アカウント、LINE、Slack を試してみました。

目次

なお、本記事で作成した OpenID Provider は GitLabのリポジトリ にあげております。

Amazon Cognito とは

f:id:linkode-okazaki:20200109183627p:plain f:id:linkode-okazaki:20200109183734p:plain

  • ウェブアプリケーションやモバイルアプリケーションの認証・認可・ユーザー管理をサポートしてくれるサービスです。
    • 認証:その利用者が本人であるかを確認することを言います。ID とパスワードによって行うのが一般的です。
    • 認可:認証されたユーザがあるシステム資源に対するアクセスを判断することを言います。認証されたユーザの権限に応じて、読み取りのみ/読み書きOK/管理者権限などを割り当てます。
  • Cognito には大きく2つの機能があります。
    • ユーザープール
      • サインアップとサインインのサービスを提供します。
      • FacebookGoogle などからのソーシャルサインインも可能にします。
      • パスワードのポリシーや多要素認証(MFA)などのセキュリティ機能もあります。 f:id:linkode-okazaki:20200109183614p:plain
    • ID プール
      • ID プールを使用すると、権限が制限された一時的な AWS 認証情報を取得して、他の AWS サービスにアクセスすることができます。
      • 匿名ゲストユーザーと、ID プールのユーザーを認証するのに使用できる ID プロバイダーとして、 AWS Cognito のユーザープールだけでなく、FacebookGoogle でのソーシャルサインイン、OpenID Connect (OIDC) プロバイダ、開発者が認証した ID なども使えます。
  • ユーザープールと ID プールは、別々にすることも一緒にすることも可能です。

OAuth と OpenID Connect とは

  • OAuth は、認可の規格です。
    • 三者のアプリケーションにユーザーの ID & パスワードを渡さずに、サービス上にある自分のデータへのアクセスを許可するためのフレームワーク」と説明されています。
  • OpenID Connectは、OAuthのフローをベースにした認証の規格です。
    • 各個別のサービスは、認証のフローを OpenID Connect に準拠した他のサーバー(=OpenID Provider)にお任せして、その認証結果のみを ID トークンとしてサービスが受け取って認証する方式です。
  • 認証・認可の仕組みの詳細については、下記の参考資料をご覧ください。

今回やったことは?

  • AWS Cognito で管理するユーザとして、ソーシャル ID である Google アカウントと LINE アカウント、Slack のアカウントを利用できるようにしました。
    • Google アカウントは、Cognito のデフォルトで連携の仕組みが用意されています。
    • LINE はデフォルトで用意されているわけではありませんが、OpenID Connect に準拠しているため、連携可能です。
    • Slack アカウントは、OpenID Connect 準拠ではない(OAuth2.0 による API の利用は可能)ので、 Cognito と Slack を中継するサーバ(OpenID Provider)を自作して設置して連携させました。

作業手順

大まかな流れは以下の通りです。

  1. Cognito でユーザープールを作成
  2. アプリクライアントの作成と設定
  3. AWS Cognito で試しにサインアップしてみる
  4. 外部ソーシャル ID と連携させる

1. Cognito でユーザープールの作成

  • AWS マネジメントコンソールから「Cognito」を選択し、下の画面から「ユーザープールの作成」を選択します。 f:id:linkode-okazaki:20200109183716p:plain
  • 右上の「ユーザープールを作成する」より、新規にユーザープールを作成します。
  • 適当な名前を設定し、「デフォルトを確認する」をクリックして次に進みます。 f:id:linkode-okazaki:20200109183804p:plain
  • 左サイドバーより「属性」を選び、ユーザー情報として必要な属性を設定します。「どの標準属性が必要ですか?」の下から、ユーザ登録時に必須の属性を指定します。指定し終えたら「次のステップ」を押して進みます。
    • ここでの注意点は、必須の標準属性はユーザープール作成後、後から変更できないことです。最初に、アカウントのポリシーを決めておく必要があります。
    • また、ここで選択した必須属性は、連携する外部のソーシャル ID からユーザ ID を連携させる際にも、取得できることが前提条件となります。
      • 例えば、必須属性として「email」を選んだ場合、Google などの外部ソーシャル ID から メールアドレスが取得できなければなりません。外部の ID 側の設定を確認せねばなりません。
      • ソーシャル ID だけしか扱わないのであれば、email は必須ではありません。しかしながら、AWS Cognito ユーザープールの機能を使って、自身でもユーザ ID やパスワードを発行するのであれば、本人確認やパスワード忘れ対策のため、email があったほうが便利です。
    • 今回は、ユーザ ID としてメールアドレスを指定し、必須属性を指定しないというポリシーで進めます。 f:id:linkode-okazaki:20200109183738p:plain
  • 「ポリシー」の画面でパスワードポリシーを設定します。
    • 今回は、緩い設定ですが、特殊文字・大文字を必須としない設定にしました。 f:id:linkode-okazaki:20200109183809p:plain
  • ここまでで一旦設定を終え、左サイドバーより「確認」を選択します。下の「プールの作成」を押し、ユーザープールを作成します。 f:id:linkode-okazaki:20200109183640p:plain

2. アプリクライアントの作成と設定

  • 続いて、作成したユーザプールを利用するアプリケーションを登録します。
  • 左サイドバーより「アプリクライアント」を選択し、適当なアプリ名をつけて作成します。
    • 今回は、「クライアントシークレットを生成」のチェックを外します。 f:id:linkode-okazaki:20200109183708p:plain
  • 「クライアントアプリの作成」を押すと、次画面でアプリクライアント ID が発行されます。
  • 次に、左サイドバーより「アプリクライアントの設定」を選択します。ここで、先程作成したクライアントアプリと実際の Web アプリケーションとの紐付けを行います。
  • 設定内容は以下のようにしておきます。終わったら、「変更の保存」を押して、内容を保存します。
    • 有効な ID プロバイダ:「Cognito User Pool」にチェック
    • コールバック URL:(暫定的に)https://localhost:3001
    • サインアウト URL:(暫定的に)https://localhost:3001
    • 許可されている OAuth フロー:「Authorization code grant」にチェック
    • 許可されている OAuth スコープ:「email」と「openid」にチェック f:id:linkode-okazaki:20200109183747p:plain
  • 次に、AWS Cognito ドメイン名を決定します。
    • これは、外部ソーシャル ID のサーバが Cognito とやり取りするために使用されます。また、あらかじめ用意してくれるサインアップ/サインインの画面を呼び出すための URL でもあります。
    • 他のCognito利用者と重複できないので、誰も使っていそうもない値を設定します。 f:id:linkode-okazaki:20200109185238p:plain

3. AWS Cognito で試しにサインアップしてみる

  • 実際に立ち上げた Web アプリにサインアップしてみます。
  • 今回は、ngrok を利用して、ローカルで立ち上げたアプリを外部公開します。
  • 立ち上げるアプリは、 Node.js を用いて、クエリ文字列を表示するだけの簡単なものとします。
    • 下記のようなコマンドで環境を整えた後、index.js を作成します。
      • コマンド:
npm init
npm install express -–save
npm install cors -–save
mkdir public

index.js の内容(クリックで展開):

var express = require('express');
var app = express();

var cors = require('cors');

app.use(cors());
app.use(express.static('public'));
app.get('/', (req, res) =>{
        res.send('Welcome to Express!<br>' + 'QueryString:' + JSON.stringify(req.query) );
});
app.listen(3000, () =>{
        console.log('HTTP Server(3000) is running.');
});

  • アプリを立ち上げ、ngrok で公開します。
node index.js
ngrok http 3000
  • https://XXXXXX.ngrok.io というアドレスでアプリケーションが外部公開されてますので、これを利用します。 f:id:linkode-okazaki:20200109183720p:plain
  • Cognito のユーザープール管理画面に戻り、「アプリクライアントの設定」でとりあえず埋めていた「コールバック URL」と「サインアウト URL」を上で発行された [](https://XXXXXX.ngrok.io) に書き換えます。
  • 「アプリクライアントの設定」の画面にある「ホストされた UI の起動」リンクを押すか、ブラウザに以下の URL を入力して、Cognito が用意してくれるデフォルトのサインアップ画面を開きます。
    • ドメイン名:AWS Cognitoのドメイン名のページで指定したドメイン
    • アプリクライアントID:アプリクライアントのページで生成されたアプリクライアントID
    • リダイレクト先URL:アプリクライアントの設定指定したコールバックURL
  https://<ドメイン名>.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=<アプリクライアントID>&redirect_uri=<リダイレクト先URL>

f:id:linkode-okazaki:20200109183610p:plain

  • まだユーザ登録していないため、「Need an account? Sign up」からサインアップ画面に行きメールアドレスとパスワードを入力して「Sign up」を押します。すると、入力したメールアドレスに認証コードを通知するメールが届くので、次画面でそれを入力します。

    f:id:linkode-okazaki:20200109183725p:plain

  • 認証が無事終わり、次画面に遷移します。Cognito の管理画面より「ユーザーとグループ」より、ユーザが作成されていることも確認できます。

    f:id:linkode-okazaki:20200109183659p:plain f:id:linkode-okazaki:20200109183704p:plain

4. 外部のソーシャル ID と連携させる

  • ここまで来てようやく外部のアカウントと連携できるようになります。

(1) Google アカウントの連携を有効にする

  • Google アカウントとの連携の設定のためには、開発者アカウントが必要ですので、ない場合は作成します。
  • Google Cloud Platform を開き、新しいプロジェクトが無い場合は作成します。
  • プロジェクトを作成したら、左サイドバーの「API とサービス」を選択します。
  • 「OAuth 同意画面」より、User Type を選択します。
    • 今回は、組織外の人でも使える「外部」としました。 f:id:linkode-okazaki:20200109184148p:plain
    • また、「承認済みドメイン」に amazoncognito.com を追加しておきます。
  • 次に、「認証情報」より、認証情報の作成を行います。作成対象として OAuth クライアント ID を選択し、次の画面で次のように入力します。
    • アプリケーションの種類:ウェブアプリケーション
    • 名前:任意(たとえば、Cognito Test)
    • 認証済みのリダイレクトURIhttps://<ドメイン名>.auth.<リージョン>.amazoncognito.com/oauth2/idpresponse
  • すると、「クライアント ID」と「クライアントシークレット」が生成されるため、メモっておきます。
  • このクライアント ID とクライアントシークレットを Cognito に設定します。
    • ユーザープールの設定画面より「ID プロバイダー」を選択し、先程のクライアント ID とクライアントシークレットを入力する。
    • 承認スコープは「profile openid email」とする。 f:id:linkode-okazaki:20200207192636p:plain
  • Google から取得したメールアドレスをユーザプールに取り込むよう設定します。「属性マッピング」から Google の email にチェックを入れ、「ユーザプール属性」を Email とします。 f:id:linkode-okazaki:20200109183558p:plain
  • 左サイドバーより「アプリクライアントの設定」で「有効な ID プロバイダー」に Google が追加されているので、チェックする。
  • ここまで設定した後、再度サインイン画面 https://<ドメイン名>.auth.<リージョン>.amazoncognito.com/login?response_type=code&client_id=<アプリクライアントID>&redirect_uri=<リダイレクト先URL> を開くと、Google でログインするためのボタンが追加されています。 f:id:linkode-okazaki:20200109183830p:plain f:id:linkode-okazaki:20200109183645p:plain
  • Google での認証が済むと、次画面に遷移されます。また、Google アカウントとの連携の場合は、認証コードの入力なしに、ユーザ(「Google_…」で始まるユーザ)が作成されていることも確認できます。 f:id:linkode-okazaki:20200109183848p:plain

(2) LINE のアカウント連携を有効にする

  • 続いて、LINE アカウントとの連携です。こちらも、LINE のデベロッパーアカウントが必要なので、持っていない人は作成する必要があります。
  • デベロッパーアカウントでログインをしたら、プロバイダーを作成し、その中に連携で用いるチャンネルを作成します。 f:id:linkode-okazaki:20200109183818p:plain f:id:linkode-okazaki:20200109183800p:plain
    • チャンネル作成時に設定するポイントは以下です。
      • Channel Type:「LINE Login」にする
      • App Types:「Web App」にチェックを入れる
      • チャンネル作成後、Basic Settings のタブで、OpenID Connect を有効にします。
    • チャンネル作成後、「Channel ID」と「Channel Seacret」が発行されますので、これを控えておきます。
  • LINE Login の設定画面で「Callback URL」を https://<ドメイン名>.auth.<リージョン>.amazoncognito.com/oauth2/idpresponse に設定します。 f:id:linkode-okazaki:20200109183649p:plain
  • 再び、Cognito のユーザープールの設定画面に戻り、「ID プロバイダー」から LINE の設定を追加します。
    • OpenID Connect」を選択し、以下のように設定を入力します。
      • プロバイダ名:任意(「LINE」とでもしておきます)
      • クライアント ID:LINE のチャンネル設定のところで確認した「Channel ID」の値
      • クライアントのシークレット:LINE のチャンネル設定のところで確認した「Channel Secret」の値
      • 属性のリクエストメソッド:「GET」を選択
      • 承認スコープ:「profile email openid」を入力
      • 発行者:「 https://access.line.me 」を入力
    • この後、「検出の実行」ボタンを押します。失敗した旨のメッセージが出て入力欄が表示されるので、更に以下を入力します。
  • あとは、Google の設定のときと同様に OIDC 属性の設定で Email を追加し、アプリクライアントの設定から LINE を追加します。 f:id:linkode-okazaki:20200109183729p:plain
    f:id:linkode-okazaki:20200109183605p:plain
  • デフォルトのサインイン画面 https://<ドメイン名>.auth.<リージョン>.amazoncognito.com/login?response_type=code&client_id=<アプリクライアントID>&redirect_uri=<リダイレクト先URL> を開くと、LINE でのサインインボタンが追加されています。
    • ボタンを押すと、LINE でのログイン画面とアクセス許可を求められる画面に遷移します。OK して無事リダイレクト先画面に行った後、ユーザープールに「LINE_」で始まるユーザが追加されていることがわかります。 f:id:linkode-okazaki:20200109183637p:plain f:id:linkode-okazaki:20200109183826p:plain

(3) Slack のアカウントと連携させる

  • Slack のワークスペースのユーザのアカウントと連携させるには、一工夫必要です。最初にも述べたとおり、Slack のアカウントは OAuth2.0 の API の利用は可能であるものの、OpenID Connect に準拠していないためです。
  • そこで、Slack と Cognito を中継するサーバ(OpenID Provider)を立ち上げて、認証処理のやり取りを行います。

OpenID Provider の実装

  • 今回実装した OpenID Provider の処理の流れは以下のようなイメージです。 f:id:linkode-okazaki:20200109183622p:plain
  • 前回利用した、aws-serverless-express を利用して、AWS Lambda + API Gateway で実装しました。
    • ライブラリの追加
npm init
npm install express -–save
npm install request -–save
npm install body-parser -–save
npm install --save aws-serverless-express

OpenID Provider の index.js の実装(クリックで展開)

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

const isProd = () => {
    return !!process.env.AWS_REGION
}

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

if (isProd()) {
    console.log('Environment: Production');
    const awsServerlessExpressMiddleware = require("aws-serverless-express/middleware");
    app.use(awsServerlessExpressMiddleware.eventContext());
} 

// slack が認可コードを送ってくる先の URL を設定
// 実際には、一旦デプロイした後、 
// API Gateway のエンドポイント(https://xxxxxx.us-east-2.amazonaws.com/prod/)
// を確認してから記載
const slack_redirect_url = 'https://xxxxxx.us-east-2.amazonaws.com/prod/slackendpoint'

// Slackの認可コード受け取りのためのエンドポイント
app.get('/slackendpoint', (req, res) => {
    // 認可コードの取得
    const code = req.query["code"];
    const url = `https://linkode-oidc-sample.auth.us-east-2.amazoncognito.com/oauth2/idpresponse?state=${req.query.state}&code=${code}&scope=openid`
    res.redirect(url);
});

// Cognito からの認証要求を受け付けるエンドポイント
app.get('/authorization', (req, res) => {
    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 からのアクセストークンの要求を受け付けるエンドポイント
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 からのユーザー情報の要求を受け付けるエンドポイント
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)
      })
  })
})

if (isProd()) {
    module.exports = app;
} else {
    app.listen(4000, () => console.log(`Listening on: 4000`));
}

  • その他、必要なスクリプトや設定ファイルをコピーし、内容を適宜変更する点は、下記の記事を参照:

blog.linkode.co.jp

②Slack アプリの設定

  • Slack API: Applications | Slack から、新たに Slack アプリを立ち上げます。
  • Basic Information に記載されている「Client ID」と「Client Secret」は後ほど利用するため、控えておきます。 f:id:linkode-okazaki:20200109183654p:plain
  • Slack 側からコールバックを受ける OpenID Provider のURL を指定します。①で立ち上げたサーバの ngrok で公開された URL を記載します。
    • 記載する箇所は、左サイドバーより「OAuth & Permissions」を選び、「redirect URLs」のところです。 f:id:linkode-okazaki:20200109183838p:plain
  • 次に、スコープを設定します。同じ「OAuth & Permissions」の画面の下の「User Token Scopes」から、次の 2 つを許可します。
    • users:read
    • users:read.email

f:id:linkode-okazaki:20200319124056p:plain

③Cognito の設定

  • LINE のときと同様に、ID プロバイダーの追加を行います。Cognito のユーザープール管理画面から「ID プロバイダー」の「OpenID Connect」を以下の値を入力して追加します。

  • 属性マッピング、クライアントアプリの設定も忘れずにやります。 f:id:linkode-okazaki:20200109183729p:plainf:id:linkode-okazaki:20200109183814p:plain

  • 再度、サインイン画面を開くと、「Slack」が追加されています。 f:id:linkode-okazaki:20200109183619p:plain
  • ここから、Slack の認証の画面に飛び、同意すると Web アプリのページに飛びます。そして、ユーザープールに「Slack_」がついたユーザが追加されていることもわかります。 f:id:linkode-okazaki:20200109183744p:plain f:id:linkode-okazaki:20200109183822p:plain

まとめ

  • Amazon Cognito を用いて、ユーザー管理基盤であるユーザーープールを作成しました。
  • 独自のサインアップ・サインインのみならず、外部ソーシャル ID との連携もできました。
    • Google や LINE など、OpenID Connect に準拠したものなら、設定だけで連携させることができます。
    • Slack のように、OpenID Connect に準拠していなくても、OAuth2.0 の API が提供されているものであれば、独自に OpenID Provider となるサーバを立てて、無理やり連携させることも可能です。

参考資料