Cloudflare PagesでBasic認証を設定する方法
#Cloudflare
#Cloudflare Pages
#TypeScript
#Node.js
#Basic認証
にメンテナンス済み
結論
Pages Functions を使用した場合
cloudflare-pagesを使用したBasic認証の設定
import type { Crypto } from '@cloudflare/workers-types';
const BASIC_USER = 'admin';
const BASIC_PASS = 'admin';
const encoder = new TextEncoder();
const timingSafeEqual = (a: string, b: string) => {
const aBytes = encoder.encode(a);
const bBytes = encoder.encode(b);
if (aBytes.byteLength !== bBytes.byteLength) {
return false;
}
return (crypto as Crypto).subtle.timingSafeEqual(aBytes, bBytes);
};
const errorHandling: PagesFunction = async (context) => {
try {
return await context.next();
} catch (err: any) {
return new Response(`${err.message}\n${err.stack}`, { status: 500 });
}
};
const handleRequest: PagesFunction = async ({ next, request }) => {
const authorization = request.headers.get('Authorization');
if (!authorization) {
return new Response('You need to login.', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
},
});
}
const [scheme, encoded] = authorization.split(' ');
if (!encoded || scheme !== 'Basic') {
return new Response(`The Authorization header must start with Basic`, {
status: 400,
});
}
const [user, password] = Buffer.from(encoded, 'base64').toString().split(':');
if (
!timingSafeEqual(user, BASIC_USER) ||
!timingSafeEqual(password, BASIC_PASS)
) {
return new Response('Invalid authorization value.', { status: 400 });
}
return await next();
};
export const onRequest = [errorHandling, handleRequest];
next-on-pages を使用した場合
next-on-pages を使用している場合、middleware はfunctions/_middleware.ts
ではなく、Next.js の Middleware に準拠したディレクトリに配置する必要があります。
next-on-pagesを使用したBasic認証の設定
import type { Crypto } from '@cloudflare/workers-types';
import { NextResponse, type NextRequest } from 'next/server';
const BASIC_USER = 'admin';
const BASIC_PASS = 'admin';
const encoder = new TextEncoder();
const timingSafeEqual = (a: string, b: string) => {
const aBytes = encoder.encode(a);
const bBytes = encoder.encode(b);
if (aBytes.byteLength !== bBytes.byteLength) {
return false;
}
return (crypto as Crypto).subtle.timingSafeEqual(aBytes, bBytes);
};
export function middleware(request: NextRequest) {
const authorization = request.headers.get('Authorization');
if (!authorization) {
return NextResponse.json(
{ error: 'Please enter credentials' },
{ headers: { 'WWW-Authenticate': 'Basic realm="Secure Area"' }, status: 401 }
);
}
const [scheme, encoded] = authorization.split(' ');
if (!encoded || scheme !== 'Basic') {
return new Response('Malformed authorization header.', {
status: 400,
});
}
const [user, password] = Buffer.from(encoded, 'base64').toString().split(':');
if (!timingSafeEqual(user, BASIC_USER) || !timingSafeEqual(password, BASIC_PASS)) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ headers: { 'WWW-Authenticate': 'Basic realm="Secure Area"' }, status: 401 }
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*'],
};
解説
timingSafeEqual 関数
紹介したいずれのコードでも、timingSafeEqual
関数を使用しています。
timingSafeEqual
import type { Crypto } from '@cloudflare/workers-types';
// ~ 省略 ~
const timingSafeEqual = (a: string, b: string) => {
const aBytes = encoder.encode(a);
const bBytes = encoder.encode(b);
if (aBytes.byteLength !== bBytes.byteLength) {
return false;
}
return (crypto as Crypto).subtle.timingSafeEqual(aBytes, bBytes);
};
最小限の実装であれば、以下のようにデコードしたユーザー名とパスワードを比較するだけで十分なように思えます。
if (user !== BASIC_USER || password !== BASIC_PASS) {
return new Response('Invalid authorization value.', { status: 400 });
}
しかし、この方法はタイミング攻撃に対して脆弱です。
タイミング攻撃によって、認証にかかった時間によってパスワードの文字列長を推測される可能性があります。
そのため、現在では Node.js に組み込まれているcrypto.timingSafeEqual
を使用することが推奨されています。
Cloudflare のドキュメントでも、timingSafeEqual
を使用することが推奨されています。
timingSafeEqual を使うで紹介されています。
ただし、crypto.subtle.timingSafeEqual
は Cloudflare workers でのみ使用可能です。
開発環境の場合は、そもそも認証を行わないか、代わりとしてcrypto.timingSafeEqual
を使用する必要があります。