Cloudflareのミドルウェアを使ってBasic認証をかける方法

Cloudflareのミドルウェアを使ってBasic認証をかける方法をまとめました。

Cloudflareのミドルウェアで何ができるのか

基本的にはAstroのミドルウェアと同じような機能があります。

  • 認証
  • キャッシュ制御
  • リダイレクト
  • ヘッダーやクッキーの操作

ミドルウェアの基本的な使用方法

Cloudflareのミドルフェアは、プロジェクト内の/functions/_middleware.jsまたは/functions/_middleware.tsを作成して記述します。
デプロイ時にCloudflare側で自動で/functions/_middleware.jsを処理してくれます。

export async function onRequest(context) {
try {
return await context.next();
} catch (err) {
return new Response(`${err.message}\n${err.stack}`, { status: 500 });
}
}

_middleware.jsを使ってBasic認証をかけてみる

/functions/_middleware.jsを作成して、以下のように記述します。

functions/_middleware.js
export async function onRequest(context) {
/* 認証ヘッダーを取得 */
const authHeader = context.request.headers.get("Authorization");
/* 認証ヘッダーがない場合は401を返す */
if (!authHeader || !authHeader.startsWith("Basic ")) {
return unauthorized();
}
/* Base64デコード */
const base64Credentials = authHeader.slice("Basic ".length);
let credentials;
/* Base64デコードに失敗した場合は401を返す */
try {
credentials = atob(base64Credentials);
} catch {
return unauthorized();
}
/* ユーザー名とパスワードを分割 */
const index = credentials.indexOf(":");
/* ユーザー名とパスワードを分割に失敗した場合は401を返す */
if (index === -1) {
return unauthorized();
}
/* ユーザー名とパスワードを分割 */
const id = credentials.substring(0, index);
const password = credentials.substring(index + 1);
/* 期待するユーザー名とパスワードを取得 */
const expectedUser = context.env.BASIC_AUTH_ID;
const expectedPassword = context.env.BASIC_AUTH_PASSWORD;
/* ユーザー名とパスワードが一致しない場合は401を返す */
if (
!id ||
!password ||
id !== expectedUser ||
password !== expectedPassword
) {
return unauthorized();
}
return context.next();
}
function unauthorized() {
return new Response("認証が必要です", {
status: 401,
headers: {
"WWW-Authenticate": 'Basic realm="Secure Area"',
},
});
}

Cloudflareのダッシュボードから、環境変数を設定します。

BASIC_AUTH_ID=your_id
BASIC_AUTH_PASSWORD=your_password

実際にアクセスをすると、Basic認証のダイアログが表示され、認証が成功するとページが表示されます。
確認できれば設定完了です。

Basic認証の動作を詳しく見てみる

  • onRequest
  • ヘッダーの取得
  • ヘッダーのデコード
  • ユーザー名とパスワードの検証
  • 認証が成功した場合は次のミドルウェアまたはレンダリング処理に渡す
  • 認証が失敗した場合は401を返す

onRequest

onRequestはCloudflare上で実行される関数で、リクエストされた時に実行される関数です。
以下のように扱うことができます。

export async function onRequest(context) {
return await context.next();
}

contextはリクエストの情報を含むオブジェクトで、nextは次のミドルウェアまたはレンダリング処理に渡すための関数です。

onRequest以外にもonRequestGetonRequestPostなどがあり、それぞれエクスポートする必要があり、GETリクエストに対してはonRequestGetが実行されるため、リクエストに応じて使い分ける必要があります。
Basic認証の場合は、リクエストの種類に関係なく実行を行う必要があるため、onRequestを使用します。

ヘッダーの取得

context.request.headersでリクエストのヘッダーを取得することができます。

/* 認証ヘッダーを取得 */
const authHeader = context.request.headers.get("Authorization");

onRequestの第一引数であるcontextには、リクエストの情報を含むオブジェクトが渡されます。
contextrequestオブジェクトがあり、リクエストに含まれるheaderを取得することができます。

ヘッダーのデコード

ヘッダーに載せられたBasic認証のユーザー名とパスワードは、Base64エンコードされています。
Base64 エンコーディングでエンコードされたデータの文字列をデコードするのがatobメソッドです。
また、今回はatobを使用しましたが、Uint8Arrayを使用してデコードする方法もあるようです。

/* Base64デコード */
const base64Credentials = authHeader.slice("Basic ".length);
let credentials;
/* Base64デコードに失敗した場合は401を返す */
try {
credentials = atob(base64Credentials);
} catch {
return unauthorized();
}

Memo

Base64とは、文字列をバイト列として表現するためのエンコード方式です。
Basic認証では、HTTPの通信規約により、ユーザー名とパスワードをBase64エンコードして符号化し、リクエストを送信するという決まりになっています。

Basic 認証方式

Memo

AstroのミドルウェアでBasic認証をかけた際には、Buffer.fromメソッドを使用して、Base64エンコードされた文字列をデコードしました。
Astroの場合はNode.js環境になるため、atobが非推奨になっているため、Buffer.fromを使用しました。

CloudflareではJavaScriptが標準で使用できるため、atobを使用することができますが、Node.js環境ではないため、Buffer.fromを使用することができません。

Web Standards

ユーザー名とパスワードの検証

ユーザー名とパスワードを分割するために、indexOfメソッドを使用します。 デコードした文字列には:が含まれているため、indexOfメソッドで:の位置を取得し、substringメソッドでユーザー名とパスワードを分割することができます。 ユーザー名とパスワードをcontext.envから取得して、一致しているかを検証します。

const index = credentials.indexOf(":");
/* ユーザー名とパスワードを分割に失敗した場合は401を返す */
if (index === -1) {
return unauthorized();
}
/* ユーザー名とパスワードを分割 */
const id = credentials.substring(0, index);
const password = credentials.substring(index + 1);
/* 期待するユーザー名とパスワードを取得 */
const expectedUser = context.env.BASIC_AUTH_ID;
const expectedPassword = context.env.BASIC_AUTH_PASSWORD;
/* ユーザー名とパスワードが一致しない場合は401を返す */
if (!id || !password || id !== expectedUser || password !== expectedPassword) {
return unauthorized();
}

認証が成功した場合は次のミドルウェアまたはレンダリング処理に渡す

認証が成功した場合は、次のミドルウェアまたはレンダリング処理に渡すために、context.next()を実行します。

return context.next();

認証が失敗した場合は401を返す

認証が失敗した場合は、Responseオブジェクトを返して、401ステータスコードを返します。

return new Response("認証が必要です", {
status: 401,
headers: {
"WWW-Authenticate": 'Basic realm="Secure Area"',
},
});
一覧に戻る