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 }); },};/** * 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 を参照してください。 */function timingSafeEqual(a: string, b: string) { 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);}
interface Env { PASSWORD: string;}export default { async fetch(request, env): Promise<Response> { 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 }); },} satisfies ExportedHandler<Env>;