工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

ブラウザアプリでAmazon Cognitoを利用したログイン機能をつける

概要

ブラウザアプリでAmazon Cognitoを利用したログイン機能をつける方法を調べ、実施してみた。




背景と目的

ブラウザアプリにAmazon Cognitoを利用したログイン機能をつけようと思い、かつて実施した

blog.livedoor.jp

の方法を適用しようと思ったのだが、そのときからだいぶ時間もたっていてAWS SDK for Javascriptのメジャーバージョンがv3になっていた。 かつての方法でももちろん動くのだが、今から古い方法を使って実装するのもどうかと思うので、最新の方法でやってみる。



詳細

0. やりたいこと

以下ができる超簡単なブラウザアプリを作成する。

  • ユーザー名/パスワードでログイン
  • ログイン時に取得したトークンで、APIGatewayのREST APIを叩く
  • エラー処理系は最低限


1. PC開発環境の準備

  • Windows11
  • Node.js 20.10.0 インストール済み
    • 追加の必要ツールインストールも適用済み

@aws-sdk/client-cognito-identity-providerのインストール

docs.aws.amazon.com

npm install @aws-sdk/client-cognito-identity-provider

Cognito

Cognito上に、過去に作成済みのユーザープール、アプリクライアントがあるのでそれらを利用する。後述するユーザー名/パスワードでのログインをするため、アプリクライアントの認証フローで、ALLOW_USER_PASSWORD_AUTHを設定しておく。


APIGateway

使用するAPIは、JSONのパラメータを与えて、JSONのレスポンスが返ってくるものを使う。

使用するAPIのオーソライザーでユーザープールを追加。

使用するメソッドのメソッドリクエストの認可に、ユーザープールを設定。トークンを格納するヘッダ名は、Authorizationとする。

設定が終わったら、デプロイ。


ログインするJavascriptソースコード実装

ユーザー名/パスワードでログインするには、InitiateAuthCommandを用いる。

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/command/InitiateAuthCommand/

上記ドキュメントに従って実装。

  • パラメータ群inputのAuthFlowは、USER_PASSWORD_AUTHを指定
  • ClientIdは、ユーザープールに紐づくアプリクライアントID
  • AuthParametersのSECRET_HASHは、アプリクライアントの設定で指定している場合は必要。
// import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider";
const { CognitoIdentityProviderClient, InitiateAuthCommand } = require("@aws-sdk/client-cognito-identity-provider");
const client = new CognitoIdentityProviderClient({
    region: "ap-northeast-1"
});

async function login_user_password(ClientId, username, password) {
  const input = {
    "AuthFlow": "USER_PASSWORD_AUTH",
    "AuthParameters": {
      "USERNAME": username,
      "PASSWORD": password
    },
    "ClientId": ClientId
  };
  const command = new InitiateAuthCommand(input);
  try {
    const response = await client.send(command);
    console.log(JSON.stringify(response, null, "\t"));
  } catch (error) {
    console.log(error);
  }
}

成功した場合のレスポンスは以下。

{
    "$metadata": {
        "httpStatusCode": 200,
        "requestId": "eb3dde55-e498-4cbd-8392-1e727c4f5746",
        "attempts": 1,
        "totalRetryDelay": 0
    },
    "AuthenticationResult": {
        "AccessToken": "****",
        "RefreshToken": "****",
        "IdToken": "****",
        "TokenType": "Bearer",
        "ExpiresIn": 3600
    },
    "ChallengeParameters": {}
}

この中で、IdTokenというトークンを後で使用する。


APIGatewayを叩くJavascriptソースコード実装

トークンがもらえたので、それを付加してAPIGatewayを叩くが、この部分はAWS SDKとは関係ないのでどのように実装してもいいのだが、とりあえず以下のような感じ。

APIGateway側の設定で、Authorizationというヘッダにトークンを格納することにしたので、ログイン成功時にもらえたIdTokenをtokenという引数に設定。

// JSONのデータを投げて、JSONのレスポンスをもらう
async function postJSON(token, url, data) {
  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Authorization": token,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(data)
    });
    const result = await response.json();
    console.log("success:", JSON.stringify(result, null, "\t"));
  } catch (error) {
    console.log("error:", error);
  }
}

// 呼び出し
const url = "叩くAPIのURL";
data = {
    // APIにPOSTするJSONパラメータ
};
postJSON(response.AuthenticationResult.IdToken, url, data);

成功したら、そのREST APIの正常なレスポンスがもらえた。

{
    // JSONレスポンス
}


ブラウザ用SDKを作成

AWSのドキュメント

docs.aws.amazon.com

を見ると、 AWS SDK for Javascript V3は、かつてあったようなaws-sdk.min.jsみたいなファイルは用意されていない。そのため、ブラウザアプリに組み込みたければ自分で作れと書いてあった。 方法としては、webpackというツールを用いる。

webpackの準備

docs.aws.amazon.com

CLIもインストール。

npm install --save-dev webpack
npm install --save-dev webpack-cli
npm install --save-dev path-browserify

作成

src/index.jsに、以下を記述。上記で作成したlogin_user_password、postJSONを記述。success_callback、error_callbackは、本来ここに含めるべきでもないが、試しなのでどうでもいい。最後に、login_buttonは、HTML側で呼べるようにしておく。

import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider";

const REGION = "ap-northeast-1";
const CLIENT_ID = "****";

const client = new CognitoIdentityProviderClient({
    region: REGION
});

async function login_user_password(username, password, success_callback, error_callback) {
    const input = {
        "AuthFlow": "USER_PASSWORD_AUTH",
        "AuthParameters": {
            "USERNAME": username,
            "PASSWORD": password
        },
        "ClientId": CLIENT_ID
    };
    const command = new InitiateAuthCommand(input);
    try {
        const response = await client.send(command);
        success_callback(response);
    } catch (error) {
        error_callback(error);
    }
}

async function postJSON(token, url, data, success_callback, error_callback) {
    try {
        const response = await fetch(url, {
            method: "POST",
            headers: {
                "Authorization": token,
                "Content-Type": "application/json"
            },
            body: JSON.stringify(data)
        });
        const result = await response.json();
        success_callback(result);
    } catch (error) {
        error_callback(error);
    }
}

function login_success_callback(response) {
    const url = "****";
    const data = {
        // データ
    };
    postJSON(response.AuthenticationResult.IdToken, url, data, post_success_callback, post_error_callback);
}

function login_error_callback(error) {
    console.log("login_error_callback:", error);
    document.getElementById("result").innerText = "ログイン失敗";
}

function post_success_callback(result) {
    console.log("post_success_callback:", JSON.stringify(result, null, "\t"));
    document.getElementById("result").innerText = "POST成功";
}

function post_error_callback(error) {
    console.log("post_error_callback:", error);
    document.getElementById("result").innerText = "POST失敗";
}

function login_button() {
    const username = document.getElementById("userid").value;
    const password = document.getElementById("password").value;
    login_user_password(username, password, login_success_callback, login_error_callback);
}

// Expose the function to the browser
window.login_button = login_button;

webpackを使用して、ブラウザ用SDKを作成。

npx webpack --mode production --target web --devtool false

dist/main.jsが作成された。modeをproductionにすると中身が難読化されている。

(()=>{var e={446:(e,t,r)=>{"use strict"
:


簡単なブラウザアプリを実装

ユーザー名、パスワードの窓とボタン、結果出力用のdivだけ。先ほど作ったmain.jsを読み込む。

<!DOCTYPE html>
<html>
<head>
    <script src="./dist/main.js"></script>
</head>
<body>
    <input id="userid" type="text" placeholder="ユーザー名">
    <input id="password" type="password" placeholder="パスワード">
    <button onclick="login_button();">ログイン</button>
    <div id="result"></div>
</body>
</html>

ログインボタンを押したところ、正しく動いた。


Appendix: 仮パスワードの変更

ユーザープールの設定で、新しいユーザーには仮パスワードを与えて変更をさせるようにしてあったので、初めてログインする際に新しいパスワードを設定させる必要があった。なので、新しいパスワードを設定する方法をメモしておく。

  • 仮パスワードを変更するには、RespondToAuthChallengeCommandというAPIを使う。
  • ChallengeNameは、NEW_PASSWORD_REQUIREDとする。
  • Sessionは、InitiateAuthCommandのレスポンスに含まれるSessionを与える。
const { RespondToAuthChallengeCommand } = require("@aws-sdk/client-cognito-identity-provider");

async function change_password(ClientId, session, username, new_password){
  const input = {
    ClientId: ClientId,
    ChallengeName: "NEW_PASSWORD_REQUIRED",
    ChallengeResponses: {
      "USERNAME": username,
      "NEW_PASSWORD": new_password
    },
    Session: session
  };
  const command = new RespondToAuthChallengeCommand(input);
  const response = await client.send(command);
  console.log(JSON.stringify(response, null, "\t"));
}

docs.aws.amazon.com


参考



まとめと今後の課題

ブラウザアプリでAmazon Cognitoを利用したログイン機能を実現できた。とりあえず、今後利用できそうだ。