logo Ribbit's works

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 });
}

しかし、この方法はタイミング攻撃に対して脆弱です。

タイミング攻撃 - wikipedia

タイミング攻撃によって、認証にかかった時間によってパスワードの文字列長を推測される可能性があります。

そのため、現在では Node.js に組み込まれているcrypto.timingSafeEqualを使用することが推奨されています。

Cloudflare のドキュメントでも、timingSafeEqualを使用することが推奨されています。

timingSafeEqual を使うで紹介されています。

ただし、crypto.subtle.timingSafeEqualは Cloudflare workers でのみ使用可能です。

開発環境の場合は、そもそも認証を行わないか、代わりとしてcrypto.timingSafeEqualを使用する必要があります。