🔐

簡易的なパスワード認証ページをJavaScript(SPA)で実装する方法

2022/12/30に公開

Web サイト上で特定のユーザーのみに閲覧権を与えたいという要件があった場合、サーバー側の認証機能を利用したり、認証機構を自作したりといった選択肢があるかと思います。

通常フロントだけでは中々完結しないですが、扱う情報に機密性がなく、パスワードでの簡易的な閲覧認証だけ形式的に行いたいケースでは今回紹介する方法が活用できるかもしれません。

今回の記事ではヘッドレス CMS のプレビュー画面にパスワード認証機能を実装するケースで紹介したいと思います。

前提条件

ヘッドレス CMS のプレビュー画面を CSR で実装している場合、プレビュー用のページを作成するかと思います。ここでは以下記事の内容に認証機能を実装する流れをベースに紹介します。

ざっくり説明すると webpack、TypeScript、Svelte を使った環境になります。

https://zenn.dev/kazuki_tam/articles/82055d0928cc8b

やりたいこと

以下の内容を実現できることを目指します。

  • プレビュー用ページを閲覧した場合、閲覧パスワードの入力が求められる
  • キャンセルや誤入力を 4 回以上行なった場合、404 エラーページへリダイレクトされる
  • 管理者側で設定する正しいパスワードは環境変数で管理できる
  • パスワードの正誤チェックはハッシュ化された値で行う
  • パスワードが一致するまではコンテンツ情報の API リクエストが行われない
  • セッション内ではパスワードの認証結果が引き継がれる
    • 一度正解パスワードを入力すればリロードしても都度パスワードは聞かれないが、別タブでの展開やページを閉じた際に初期化される

実装方法

まずは閲覧時に利用するパスワードを考えます。普段利用するパスワードを流用することは避けてください。

https://www.luft.co.jp/cgi/randam.php

環境変数の設定

用意したパスワードをプロジェクトの環境変数に設定します。ローカルでは .env ファイルに環境変数 PREVIEW_PASSWORD を追記します。

.env
PREVIEW_PASSWORD=<YOUR-PREVIEW-PASSWORD>

webpack の設定

今回の構成ではクライアントサイドで結局パスワードの整合性チェックを行うのですが、平文のままフロントエンドコードへ渡してしまうと正しいパスワードが Web 上に露出してしまいます。

そこで webpack でのバンドル時に PREVIEW_PASSWORD で設定したパスワードをハッシュ関数でハッシュ化させてからフロントエンドコードへ環境変数として渡します。

JavaScript でハッシュ関数を利用する場合、crypto-js という便利なライブラリがあります。

https://www.npmjs.com/package/crypto-js

.config/webpack.config.js
const path = require('path');
const assetsPath = path.resolve(__dirname, '../dist/assets/js');
const Dotenv = require('dotenv-webpack');
const SHA256 = require("crypto-js/sha256");
const webpack = require('webpack');
require('dotenv').config();

const MODE = process.env.NODE_ENV;
const PREVIEW_PASSWORD = process.env.PREVIEW_PASSWORD;

const webpackConfig = {
  mode: MODE,
  // Entry point
  entry: {
    main: './src/assets/ts/main.ts',
    preview: './src/assets/ts/preview.ts',
  },
  // Output files
  output: {
    path: assetsPath,
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
      },
      {
        test: /\.(html|svelte)$/,
        use: [
          {
            loader: 'svelte-loader'
          }
        ]
      }
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.json'],
  },
  target: ['web', 'es5'],
  plugins: [
    new Dotenv({systemvars: true}),
    new webpack.EnvironmentPlugin({
      'HASHED_PREVIEW_PASSWORD': SHA256(PREVIEW_PASSWORD).toString()
    })
  ],
};

module.exports = webpackConfig;

PREVIEW_PASSWORD を以下の記述では SHA256 でハッシュ化させて新たに HASHED_PREVIEW_PASSWORD を環境変数に登録しています。

new webpack.EnvironmentPlugin({
  HASHED_PREVIEW_PASSWORD: SHA256(PREVIEW_PASSWORD).toString(),
});

閲覧パスワード検証関数を作成

まずはユーザーにパスワードを求める関数を作成します。

askPreviewPassword.ts
import { validatePreviewPassword } from "./validatePreviewPassword";
import { validateSessionStorage } from "./validateSessionStorage";

/**
 * askPreviewPassword
 * @returns { boolean }
 */
function askPreviewPassword(): boolean {
  let showScreenFlag = false;
  let askCount = 0;
  const hasPreviewSession = validateSessionStorage();
  // セッションチェック
  if (hasPreviewSession) {
    showScreenFlag = true;
  }
  // 回答確認
  while (!showScreenFlag) {
    const inputPassword = prompt("Input preview password:", "");
    if (askCount >= 3 || inputPassword === null) {
      location.replace("/404.html");
      break;
    }
    showScreenFlag = validatePreviewPassword(inputPassword);
    if (!showScreenFlag) {
      askCount += 1;
    }
  }
  return showScreenFlag;
}

export { askPreviewPassword };

上記処理の大まかな流れは以下になります。

  1. validateSessionStorage 関数でセッション内にパスワード入力で認証通過しているか確認
  2. 認証にこれまで通過していない場合は正解するまで prompt でパスワード入力を求める
  3. validatePreviewPassword 関数で入力されたパスワードの整合性チェックを行う
  4. キャンセルや誤入力が 4 回以上発生した場合、404 ページへリダイレクト
  5. 正しいパスワードが入力された場合、showScreenFlagtrue にして返す。

validatePreviewPassword 関数

validatePreviewPassword 関数では prompt で入力された値を SHA256 でハッシュ化し、あらかじめ正しいパスワードのハッシュ値 HASHED_PREVIEW_PASSWORD と整合性チェックを行なっています。

validatePreviewPassword.ts
import sha256 from 'crypto-js/sha256';

/**
 * validateReviewPassword
 * @param { String } password
 * @returns { boolean }
 */
 function validatePreviewPassword(password: string | null): boolean {
  const HASHED_PREVIEW_PASSWORD = process.env.HASHED_PREVIEW_PASSWORD;
  if (!HASHED_PREVIEW_PASSWORD || !password) return false;

  const hashDigest = sha256(password).toString();

  const isValid = HASHED_PREVIEW_PASSWORD === hashDigest;

  if (isValid) {
    sessionStorage.setItem("preview", hashDigest);
  }

  return isValid;
}

export { validatePreviewPassword };

上記処理の大まかな流れは以下になります。

  1. prompt の整合性チェックを行い真偽値を返す
  2. 正しいパスワードが入力された場合はセッションストレージにパスワード通過フラグを保存する

validateSessionStorage 関数

セッションストレージでのパスワード通過チェックを行なっています。

validateSessionStorage.ts
/**
 * validateSessionStorage
 * @returns { boolean }
 */
function validateSessionStorage (): boolean {
  const HASHED_PREVIEW_PASSWORD = process.env.HASHED_PREVIEW_PASSWORD;
  const password = sessionStorage.getItem("preview");
  return HASHED_PREVIEW_PASSWORD === password;
}

export { validateSessionStorage };

askPreviewPassword の利用

最後に閲覧パスワードの検証を担う askPreviewPassword を対象ベージで実行します。
以下サンプルでは Svelte コンポーネント内で API リクエスト実行判断に活用しています。

Doc.svelte
<script lang="ts">
  import queryString from "query-string";
  import { askPreviewPassword } from "./helper/askPassword";
  const MICROCMS_DOMAIN = process.env.MICROCMS_DOMAIN;
  const MICROCMS_API_KEY = process.env.MICROCMS_API_KEY;
  const { draftKey } = queryString.parse(location.search);
  // パスワード検証
  const previewFlag = askPreviewPassword();
  const fetchData = async() => {
    const fetchOptions = {
      method: "GET",
      headers: {
        "X-MICROCMS-API-KEY": MICROCMS_API_KEY
      }
    }
    // パスワード認証を通過した場合に下書き記事情報を取得
    const res = previewFlag ? await fetch(`${MICROCMS_DOMAIN}/api/v1/doc?draftKey=${draftKey}`, fetchOptions) : false;
    if (res) {
      return await res.json();
    } else {
      throw new Error("Error...");
    }
  };
</script>

まとめ

今回紹介した内容はあくまで簡易的なパスワード認証になります。フロントエンドのみで実装する場合はあくまで形式上の実装となることをご留意ください。

Discussion