コンテンツにスキップ

HTTP Basic 認証

Last reviewed: 9 months ago

HTTP Basic スキーマを使用してアクセスを制限する方法を示します。

/**
* HTTP Basic スキーマを使用してアクセスを制限する方法を示します。
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
* @see https://tools.ietf.org/html/rfc7617
*
*/
import { Buffer } from "node:buffer";
const encoder = new TextEncoder();
/**
* `timingSafeEqual` を使用して値を安全に比較することで、タイミング攻撃から保護します。
* 詳細については、https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#timingsafeequal を参照してください。
* @param {string} a
* @param {string} b
* @returns {boolean}
*/
function timingSafeEqual(a, b) {
const aBytes = encoder.encode(a);
const bBytes = encoder.encode(b);
if (aBytes.byteLength !== bBytes.byteLength) {
// 文字列は比較するために同じ長さである必要があります
// crypto.subtle.timingSafeEqual で比較するために
return false;
}
return crypto.subtle.timingSafeEqual(aBytes, bBytes);
}
export default {
/**
*
* @param {Request} request
* @param {{PASSWORD: string}} env
* @returns
*/
async fetch(request, env) {
const BASIC_USER = "admin";
// 管理者パスワードが必要です。これは
// 暗号化された秘密として Worker に添付する必要があります。
// https://developers.cloudflare.com/workers/configuration/secrets/ を参照してください。
const BASIC_PASS = env.PASSWORD ?? "password";
const url = new URL(request.url);
switch (url.pathname) {
case "/":
return new Response("誰でもホームページにアクセスできます。");
case "/logout":
// "Authorization" ヘッダーを無効にするために HTTP 401 を返します。
// "WWW-Authenticate" ヘッダーは送信しません。これにより
// ブラウザでポップアップが表示され、すぐに資格情報を再度要求されます。
return new Response("ログアウトしました。", { status: 401 });
case "/admin": {
// 認証されたときに "Authorization" ヘッダーが送信されます。
const authorization = request.headers.get("Authorization");
if (!authorization) {
return new Response("ログインする必要があります。", {
status: 401,
headers: {
// ユーザーに資格情報を要求します。
"WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"',
},
});
}
const [scheme, encoded] = authorization.split(" ");
// Authorization ヘッダーは Basic で始まり、その後にスペースが必要です。
if (!encoded || scheme !== "Basic") {
return new Response("不正な認証ヘッダーです。", {
status: 400,
});
}
const credentials = Buffer.from(encoded, "base64").toString();
// ユーザー名とパスワードは最初のコロンで分割されます。
//=> 例: "username:password"
const index = credentials.indexOf(":");
const user = credentials.substring(0, index);
const pass = credentials.substring(index + 1);
if (
!timingSafeEqual(BASIC_USER, user) ||
!timingSafeEqual(BASIC_PASS, pass)
) {
return new Response("ログインする必要があります。", {
status: 401,
headers: {
// ユーザーに資格情報を要求します。
"WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"',
},
});
}
return new Response("🎉 プライベートアクセスが可能です!", {
status: 200,
headers: {
"Cache-Control": "no-store",
},
});
}
}
return new Response("見つかりませんでした。", { status: 404 });
},
};