このページで分かること

  • パスキーによるログインと最初のユーザーの登録、マジックリンク、GitHub・Google・Atmosphereのログインプロバイダーの設定
  • 5段階のユーザーロール(閲覧者から管理者まで)、ユーザーの招待、パスキーの管理、セッションとレート制限
  • Cloudflare Accessを使う場合の設定とロールの対応づけ、代わりに使えなくなる機能、ログインできないときの対処
難易度
実践
読む時間
12分
このページの目次

EmDashは、主なログイン方法としてパスキー認証を使います。パスキーはフィッシングに強く、パスワードが不要で、ブラウザーやパスワードマネージャーを通じて複数のデバイスで使えます。

パスキーのほかに、追加できるログインプロバイダーを加えられます。GitHubとGoogleはEmDashに含まれています。別途インストールするAtmosphereプロバイダーはAT Protocolのアカウントを追加します。同じプロバイダーのインターフェースは、ほかのパッケージにも開かれています。このページで説明するGitHub、Google、Atmosphereのプロバイダーは、最初の管理者アカウントを作成することも、連携済みのEmDashユーザーとしてログインさせることもできます。

Cloudflareにデプロイする場合、Cloudflare Accessは、本番環境では別の、ほかの方法と併用できない認証モードになります。Cloudflare Accessは、EmDashのログイン方法を表示する代わりに、保護されたEmDashのルートでAccessの認証情報を検証します。

認証モードの選択

パスキーはWebAuthnを使います。WebAuthnは、デバイスに保存される、またはパスワードマネージャーで同期される公開鍵の認証情報を作成するWeb標準です。ログインするとき、デバイスは、パスワードをネットワークに一切送らずに、その認証情報を持っていることを証明します。

既定の認証方法はパスキーです。GitHub、Google、Atmosphereの各プロバイダーは、追加のログイン方法です。それぞれがユーザーを認証し、EmDashのアカウントと連携するかアカウントを作成して、パスキーでのログインと同じEmDashのセッションを確立します。

パスキー認証には次の利点があります。

  • 覚えたり漏えいしたりするパスワードがない
  • フィッシングに強い:認証情報はサイトのドメインに結び付けられます
  • デバイス間で同期できる:iCloudキーチェーン、Googleパスワードマネージャー、1Passwordなどで使えます
  • すばやくログインできる:生体認証やPINで1回タップするだけです

Cloudflare Accessは、authProviders ではなく auth オプションを使います。本番環境では、保護された /_emdash のルートに対する認証を担います。EmDashはそれでもローカルのユーザーを保存するため、ロール、所有者、無効化されたユーザーの確認は引き続き機能します。

本サイトの補足 やさしい解説

WordPressではユーザー名とパスワードで管理画面にログインしますが、EmDashの基本はパスワードを使わない「パスキー」です。パスキーは、パソコンやスマートフォンの指紋認証・顔認証・PINでログインする仕組みで、認証情報はそのサイトのドメインに結び付けられます。GitHubやGoogleのアカウントでのログインは、パスキーに追加して使える方法です。Cloudflare Accessを使う場合だけは、本番環境でこれらの方法がすべて使えなくなり、Accessがログインを受け持ちます。

最初のユーザーの設定

初めて管理画面にアクセスすると、セットアップウィザードが管理者アカウントの作成を案内します。

  1. http://localhost:4321/_emdash/admin に移動します。

  2. 「サイトをセットアップ」で、サイトのタイトルと、必要に応じてキャッチフレーズを入力します。テンプレートによってはサンプルコンテンツも用意されています。「続行」を選択します。

  3. 「アカウントを作成」で、メールアドレスと、必要に応じて名前を入力します。「続行」を選択します。

  4. 「アカウントを保護」で、パスキーを作成するか、設定済みのログインプロバイダーのどれかを選びます。パスキーを選ぶと、ブラウザーが保存先を尋ねます。

    • macOSの場合:Touch ID、デバイスのパスワード、またはセキュリティキー
    • Windowsの場合:Windows Helloまたはセキュリティキー
    • モバイルの場合:Face ID、指紋、またはPIN
  5. ブラウザーまたはプロバイダーの手順を完了します。EmDashは最初のユーザーを管理者として作成し、ダッシュボードを開きます。

パスキーでのログイン

セットアップのあとで管理画面に戻ると、パスキー認証が始まります。

  1. /_emdash/admin にアクセスします。

  2. ログインしていない場合は、ログインページが表示されます。

  3. 「ログイン」をクリックして認証します。

  4. ブラウザーがパスキー(生体認証、PIN、またはセキュリティキー)を求めます。

  5. 確認が終わると、管理画面のダッシュボードに移動します。

パスキーを使えない場合は、代わりにマジックリンクを使えます。EmDashがリンクを送信するには、サイトにメールプロバイダーが設定されている必要があります。

  1. ログインページで「メールでログイン」をクリックします。

  2. メールアドレスを入力します。

  3. 受信トレイでログイン用のリンクを確認します。

  4. リンクをクリックして認証します(有効期限は15分です)。

ログインプロバイダーの設定

EmDashは、パスキーに加えて、ログインページとセットアップウィザードに表示される、追加できるログインプロバイダーに対応しています。GitHubとGoogleはEmDashに含まれています。Atmosphereとサードパーティーのプロバイダーは別のパッケージで、同じインターフェースを通じて登録します。

プロバイダーは追加で使うもので、プロバイダーを有効にしてもパスキーは引き続き使えます。GitHubとGoogleは、プロバイダーが同じ確認済みのメールアドレスを提供した場合にだけ、既存のEmDashユーザーと自動で連携します。EmDashのAtmosphereの手順ではメールアドレスを受け取らないため、Atmosphereのアカウントは分散型識別子(DID)で連携します。含まれている各プロバイダーは最初のユーザーを作成できるため、新しくインストールしたサイトでは、パスキーをまったく使わずに進めることもできます。

Astroへのプロバイダーの追加

プロバイダーは、EmDashのインテグレーションの authProviders 配列に渡します。次の例は、GitHub、Google、Atmosphereを有効にします。

astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { github } from "emdash/auth/providers/github";
import { google } from "emdash/auth/providers/google";
import { atproto } from "@emdash-cms/auth-atproto";

export default defineConfig({
	integrations: [
		emdash({
			authProviders: [github(), google(), atproto()],
		}),
	],
});

ログインページでは順番に意味があります。プロバイダーは列挙した順番に描画されます。ボタンだけの小さなプロバイダーが先に表示され、独自のフォームが必要なプロバイダー(ハンドルを尋ねるAtmosphereなど)はそのあとに表示されます。

GitHub

次の例は、GitHubプロバイダーを有効にします。

import { github } from "emdash/auth/providers/github";

emdash({ authProviders: [github()] });

認証情報は環境変数で設定します。EmDashは、接頭辞付きの名前を先に確認し、なければ接頭辞なしの名前を使います。

変数 用途
EMDASH_OAUTH_GITHUB_CLIENT_ID / GITHUB_CLIENT_ID OAuthアプリのクライアントID
EMDASH_OAUTH_GITHUB_CLIENT_SECRET / GITHUB_CLIENT_SECRET OAuthアプリのシークレット

GitHubのOAuthアプリのコールバックURLには、https://your-site.example.com/_emdash/api/auth/oauth/github/callback を設定します。

Google

次の例は、Googleプロバイダーを有効にします。

import { google } from "emdash/auth/providers/google";

emdash({ authProviders: [google()] });

認証情報は環境変数で設定します。EmDashは、接頭辞付きの名前を先に確認し、なければ接頭辞なしの名前を使います。

変数 用途
EMDASH_OAUTH_GOOGLE_CLIENT_ID / GOOGLE_CLIENT_ID OAuthアプリのクライアントID
EMDASH_OAUTH_GOOGLE_CLIENT_SECRET / GOOGLE_CLIENT_SECRET OAuthアプリのシークレット

GoogleのOAuthクライアントのリダイレクトURIには、https://your-site.example.com/_emdash/api/auth/oauth/google/callback を設定します。

Atmosphere(AT Protocol)

寄稿者がすでにAtmosphereアカウント(BlueskyやAT Protocolのネットワーク全体で使われる、ユーザー自身が持つID)を持っているサイトでは、Atmosphereプロバイダーをインストールします。

pnpm add @emdash-cms/auth-atproto

次の例は、ハンドルの許可リスト付きでAtmosphereプロバイダーを有効にします。

import { atproto } from "@emdash-cms/auth-atproto";

emdash({
	authProviders: [
		atproto({
			allowedHandles: ["*.example.com"],
		}),
	],
});

クライアントシークレットや環境変数は必要ありません。ハンドルとDIDの許可リスト、ロールの対応づけ、AT ProtocolのOAuthプロファイルが求めるローカル開発の設定については、Atmosphereログインのガイドを参照してください。

プロバイダーの作成

プロバイダーは AuthProviderDescriptor です。id、人が読めるラベル、そしてログインの手順に必要な管理画面のコンポーネント、ルートハンドラー、公開ルートの接頭辞、保存用のコレクションを持ちます。最初のユーザーの設定中にプロバイダーを表示する場合は、adminEntry から SetupStep をエクスポートします。この型は emdash からエクスポートされています。

import type { AuthProviderDescriptor } from "emdash";

export function myProvider(): AuthProviderDescriptor {
	return {
		id: "my-provider",
		label: "My Provider",
		adminEntry: "my-provider/admin", // exports LoginButton / LoginForm / SetupStep
		routes: [
			{ pattern: "/_emdash/api/auth/my-provider/login", entrypoint: "my-provider/routes/login.ts" },
			{ pattern: "/_emdash/api/auth/my-provider/callback", entrypoint: "my-provider/routes/callback.ts" },
		],
		publicRoutes: ["/_emdash/api/auth/my-provider/"],
		storage: {
			sessions: {},
		},
	};
}

独自のログインフォーム、OAuthのルートハンドラー、永続的な保存領域が必要なプロバイダーの実例として最も充実しているのは、Atmosphereのパッケージ(@emdash-cms/auth-atproto)です。

本サイトの補足 やさしい解説

GitHubやGoogleでログインできるようにするには、astro.config.mjs にプロバイダーを1行追加し、各サービスで発行したクライアントIDとシークレットを環境変数に設定します。WordPressでソーシャルログインのプラグインを入れて設定するのに近い作業ですが、EmDashではGitHubとGoogleが最初から含まれています。GitHubやGoogleのログインが既存のユーザーに結び付くのは、そのサービスの確認済みメールアドレスがEmDashのユーザーと一致する場合だけです。

ユーザーロール

EmDashは、5段階のロールベースのアクセス制御を使います。

ロール レベル 説明
閲覧者(Subscriber) 10 公開済みのコンテンツを読む(下書きにはアクセスできない)
寄稿者(Contributor) 20 コンテンツを作成する(公開には承認が必要)
投稿者(Author) 30 自分のコンテンツを作成・編集・公開する
編集者(Editor) 40 すべてのコンテンツを管理する
管理者(Admin) 50 設定を含むすべてにアクセスできる

各ロールは、それより下のすべてのレベルの権限を引き継ぎます。最初のユーザーは必ず管理者として作成されます。

閲覧者と下書きのコンテンツ

閲覧者content:read 権限を持つため、会員限定の公開済みコンテンツを、認証済みの読者に配信できます。閲覧者は、下書き、予約公開の項目、ゴミ箱の項目、リビジョン、プレビューURLを見られません。これらは content:read_drafts で制限されており、この権限は寄稿者以上に与えられます。一覧と取得のエンドポイントは、閲覧者に対しては自動的に status=published に絞り込みます。編集者向けの画面(/compare/revisions/trash/preview-url)は、閲覧者のリクエストをそのまま拒否します。

本サイトの補足 やさしい解説

ロールは、WordPressの「購読者・寄稿者・投稿者・編集者・管理者」とほぼ同じ5段階です。管理画面では「閲覧者・寄稿者・投稿者・編集者・管理者」と表示されます。上のロールは下のロールができることをすべてでき、最初に登録したユーザーは必ず管理者になります。閲覧者は公開済みのコンテンツだけを読めるロールで、下書きやリビジョンは見られません。

ユーザーの招待

管理者は、管理画面から新しいユーザーを招待できます。

  1. 「設定」>「ユーザー」に移動します。

  2. 「ユーザーを招待」をクリックします。

  3. ユーザーのメールアドレスを入力し、ロールを選択します。

  4. 「招待を送信」をクリックします。

  5. メールが設定されている場合は、EmDashが招待を送信します。設定されていない場合は、生成されたリンクをコピーして、自分でユーザーに送ります。

  6. 招待されたユーザーはリンクを開き、パスキーか、招待ページに表示されたログインプロバイダーでアカウントを作成します。

招待リンクは1回しか使えず、7日で期限切れになります。

パスキーの管理

ユーザーは、アカウントの設定から自分のパスキーを管理できます。

  • 「パスキーを追加」:予備や別のデバイス用に、パスキーを追加で登録します
  • 「パスキーを削除」:使わなくなったパスキーを削除します
  • 「パスキーの名前を変更」:パスキーに分かりやすい名前を付けます

各ユーザーは、パスキーを10個まで登録できます。

EmDashでは、最後の1つのパスキーは削除できません。古いパスキーを削除する前に、代わりのパスキーを追加します。

招待なしでグループにログインさせる

ユーザーを1人ずつ招待せずにグループでログインできるようにするには、許可リスト付きでログインプロバイダーを設定します。Atmosphereプロバイダーは allowedHandlesallowedDIDs を受け付けます(Atmosphereログインを参照)。Cloudflare Accessのアダプターは、autoProvisionroleMapping によって、IDプロバイダーからユーザーを自動作成します。このページで説明するGitHub、Google、Atmosphereのプロバイダーは、最初の管理者アカウントを作成することもできます。

セッション

パスキー、マジックリンク、招待、ログインプロバイダーのコールバックは、EmDashのユーザーIDをAstroのセッションストアに保存します。ブラウザーが受け取るのは、中身の分からないAstroの astro-session 識別子です。ユーザーと認証情報のレコードは、EmDashのデータベースに残ります。

Cloudflare Accessも、解決したEmDashのユーザーをAstroのセッションに書き込みます。これにより、公開ページは Astro.locals.user を読んで、ログイン中のユーザーを識別できます。このセッションは、保護された /_emdash のルートでのAccessの認証の代わりにはなりません。EmDashは、それらのリクエストでAccessのJSON Web Token(JWT)をもう一度検証します。

認証のレート制限

EmDashは、未認証の状態からログインや登録を始めるエンドポイントに制限をかけています。制限は、エンドポイントごと、信頼できるクライアントIPごとに別々です。

エンドポイント 制限
POST /_emdash/api/auth/passkey/options 1分あたり10リクエスト
POST /_emdash/api/auth/magic-link/send 5分あたり3リクエスト
POST /_emdash/api/auth/signup/request 5分あたり3リクエスト

Cloudflareでは、EmDashはCloudflareのリクエストのメタデータからクライアントIPを読み取ります。リバースプロキシの後ろで動かす自己ホスト型のサイトでは、EmDashがプロキシのクライアントIPヘッダーを使えるように、先にtrustedProxyHeadersを設定する必要があります。信頼できるIPが得られない場合は、数える対象にできる安全なキーがないため、これらのIPごとの確認は省かれます。

パスキーは公開鍵の認証情報を保存し、秘密鍵はユーザーの認証器に残ります。マジックリンクのトークンはSHA-256のハッシュとして保存され、使用後に削除されます。

トラブルシューティング

「No passkeys registered」

ログイン時にこのエラーが表示される場合は、パスワードマネージャーからパスキーが削除された可能性があります。管理者に復旧用のマジックリンクを送ってもらいます。サイトにメールが設定されている必要があります。

「Passkey authentication failed」

通常、別のドメイン用に作成されたパスキーであることを意味します。パスキーはドメインに結び付けられています。localhost:4321 用のパスキーは example.com では使えません。ドメインごとに新しいパスキーを登録します。

すべてのパスキーを失った場合

登録したすべてのパスキーにアクセスできなくなった場合は、次のようにします。

  1. ほかの管理者に復旧用のマジックリンクを送ってもらいます。サイトにメールが設定されている必要があります。
  2. 15分以内にリンクを使ってログインします。
  3. アカウントの設定で新しいパスキーを登録します。

自分が唯一の管理者で、メールが設定されていない場合は、データベースを使ってサイトの認証をリセットする必要があります。

本サイトの補足 やさしい解説

パスキーは作成したドメインでしか使えないため、ローカル開発用(localhost:4321)に登録したパスキーは、本番のドメインでは使えません。本番では改めて登録します。パスキーをすべて失ったときの復旧手段はマジックリンクで、これにはメールの設定が必要です。管理者が1人だけのサイトでは、あらかじめメールを設定しておくか、複数のデバイスでパスキーを登録しておくと、データベースを直接触る作業を避けられます。

Cloudflare Access

Cloudflareにデプロイする場合は、組み込みのログイン方法の代わりにCloudflare Accessを使えます。Accessは、IDプロバイダーを使って、エッジでユーザーを認証します。EmDashは、署名されたAccessのJWTを検証し、その人のIDとグループを読み込んで、そのIDをローカルのEmDashユーザーに対応づけます。

Cloudflare Accessを使う場面

  • シングルサインオン:ユーザーは会社のIdPで認証します
  • アクセス制御の一元化:管理画面にアクセスできる人を、Cloudflareのダッシュボードで管理します
  • パスキーの管理が不要:パスキーを登録・管理する必要がありません
  • グループに基づくロール:IdPのグループを、EmDashのロールに自動で対応づけます

Accessの設定

  1. サイトの /_emdash/* のパスに対して、Cloudflare Accessのアプリケーションとポリシーを作成します。/_emdash/admin/* だけを保護すると、REST APIにはEmDashが必要とするJWTが届きません。
  2. アプリケーションの「Application Audience (AUD) Tag」をコピーします。
  3. そのタグを、実行時の環境変数 CF_ACCESS_AUDIENCE に保存します。ローカルとデプロイ先の値は、EmDashのシークレットのガイドに従って設定します。
  4. 実行時にその値を読み込むようにEmDashを設定します。
astro.config.mjs
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import emdash from "emdash/astro";
import { d1, access } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		emdash({
			database: d1({ binding: "DB" }),
			auth: access({
				teamDomain: "myteam.cloudflareaccess.com",
				audienceEnvVar: "CF_ACCESS_AUDIENCE",
			}),
		}),
	],
});

アプリケーションのオーディエンスは、どのAccessアプリケーションがJWTを発行したかを示します。EmDashは、発行者と署名とあわせてこれを検証します。別のAccessアプリケーション用のトークンは拒否されます。

設定オプション

オプション 既定値 説明
teamDomain string 必須 Accessのチームドメイン(例:myteam.cloudflareaccess.com
audience string 直接指定するApplication Audience(AUD)タグ。Workersでは audienceEnvVar の使用を推奨します。
autoProvision boolean true Accessで初めてログインしたときにEmDashのユーザーを作成する
defaultRole number 30 どのグループにも一致しないユーザーのロール(30=投稿者)
syncRoles boolean false ログインのたびに、IdPのグループに基づいてロールを更新する
roleMapping object IdPのグループ名をロールのレベルに対応づける
audienceEnvVar string "CF_ACCESS_AUDIENCE" オーディエンスのタグを含む環境変数。audience を省略した場合に使われます。

audience か、audienceEnvVar で指定した環境変数の値のどちらかを用意します。

ロールの対応づけ

IdPのグループを、EmDashのロールに対応づけます。

astro.config.mjs
emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audienceEnvVar: "CF_ACCESS_AUDIENCE",
		roleMapping: {
			Admins: 50, // Admin
			"Content Editors": 40, // Editor
			Writers: 30, // Author
		},
		defaultRole: 20, // Contributor for users not in any group
	}),
});

ユーザーが複数のグループに属している場合は、最初に一致したグループが使われます。サイトに最初にアクセスしたユーザーは、グループにかかわらず必ず管理者になります。

ロールの同期の動作

初期状態(syncRoles: false)では、ユーザーのロールは最初のログイン時に設定され、その後は変わりません。そのため、管理者はEmDashでロールを手動で調整できます。

IdPのグループを正とする場合は、syncRoles: true を設定します。ユーザーのロールは、ログインのたびに、その時点のグループに基づいて更新されます。

リクエストとセッションの流れ

  1. ユーザーが、Accessアプリケーションで保護されたパスにアクセスします。
  2. Accessのセッションがない場合、Cloudflare AccessはユーザーをIDプロバイダーにリダイレクトします。
  3. 認証が終わると、Accessは署名されたJWTを Cf-Access-Jwt-Assertion に入れてオリジンに送ります。
  4. EmDashはトークンの署名、発行者、オーディエンスを検証し、AccessのIDとグループを読み込みます。
  5. EmDashはローカルのユーザーを探すか自動作成し、設定されたロールの動作を適用して、ユーザーをAstroのセッションに記録します。
  6. その後の、保護されたEmDashのルートへのリクエストでも、毎回Accessの検証を繰り返します。公開ページは、EmDashのセッションを使ってユーザーを識別できますが、それを新しいAccessのリクエストの証明としては扱いません。

Accessで置き換えられる機能

Accessを有効にすると、次の機能は使えません。

  • ログインページ(/_emdash/admin/login
  • パスキーの登録と管理
  • GitHub、Google、Atmosphereでのログイン
  • マジックリンクでのログイン
  • 自己登録
  • ユーザーの招待

EmDashにたどり着ける人を決めるのは、Accessのポリシーです。ローカルのロール、コンテンツの所有者、無効化されたユーザーのフラグは、引き続きEmDashが管理します。syncRoles: false の場合、管理者は自動作成されたユーザーのロールをEmDashで変更できます。syncRoles: true の場合は、ログインのたびに、対応づけたAccessのグループでそのロールが置き換えられます。

本サイトの補足 やさしい解説

Cloudflare Accessは、管理画面の前に置く「会社の入館ゲート」のような仕組みです。社内のGoogle WorkspaceやOktaなどのIDプロバイダー(IdP)でログインした人だけが /_emdash/* に入れるようになり、EmDashはAccessから受け取った情報でユーザーを判別します。本番環境ではEmDashのログインページ、パスキー、招待などが使えなくなるため、ユーザーの追加はAccessのポリシーと autoProvision で管理します。ローカル開発中だけは、通常のパスキーでログインします。

トラブルシューティング

「No Access JWT present」

リクエストが、AccessのJWTを持たずにEmDashに届いています。これは次のどちらかを意味します。

  • Accessがアプリケーションを保護するように設定されていない
  • Accessのポリシーが管理画面のルートに一致していない

Accessアプリケーションが /_emdash/* のパス全体を対象にしていること、そのポリシーにユーザーが含まれていることを確認します。

「JWT audience mismatch」

設定の audience がJWTと一致していません。Accessアプリケーションの設定にあるApplication Audience Tagを確認し直します。

「User not authorized」

ユーザーはAccessで認証されていますが、autoProvisionfalse で、そのユーザーがEmDashに存在しません。次のどちらかで対処します。

  • autoProvision: true を設定する
  • ログインする前に、ユーザーを手動で作成する