EmDash는 패스키 인증을 주요 로그인 방법으로 사용합니다. 패스키는 피싱에 강하고 비밀번호가 필요 없으며, 브라우저나 비밀번호 관리자를 통해 기기 전반에서 작동합니다.
패스키 외에도 플러그형 로그인 프로바이더를 추가할 수 있습니다. GitHub와 Google은 EmDash에 포함되어 있습니다. 별도로 설치하는 Atmosphere 프로바이더는 AT Protocol 계정을 추가하며, 동일한 프로바이더 인터페이스는 다른 패키지에도 열려 있습니다. 문서화된 GitHub, Google, Atmosphere 프로바이더는 첫 관리자 계정을 만들거나 연결된 EmDash 사용자로 로그인할 수 있습니다.
Cloudflare 배포에서는 Cloudflare Access가 프로덕션에서 별개의 독점 인증 모드입니다. EmDash 로그인 방법을 표시하는 대신 보호된 EmDash 경로에서 Access 자격 증명을 검증합니다.
인증 모드 선택
패스키는 WebAuthn을 사용합니다. 기기에 저장되거나 비밀번호 관리자를 통해 동기화되는 공개 키 자격 증명을 만드는 웹 표준입니다. 로그인할 때 기기는 네트워크로 비밀번호를 보내지 않고 자격 증명 소유를 증명합니다.
패스키가 기본값입니다. GitHub, Google, Atmosphere 프로바이더는 추가 로그인 방법입니다. 각각 사용자를 인증하고 EmDash 계정을 연결하거나 만들고, 패스키 로그인과 동일한 EmDash 세션을 설정합니다.
패스키 인증이 제공하는 것:
- 기억하거나 유출될 비밀번호 없음
- 피싱 저항 — 자격 증명이 사이트 도메인에 바인딩됨
- 기기 간 동기화 — iCloud Keychain, Google Password Manager, 1Password 등과 동작
- 빠른 로그인 — 생체 인식 또는 PIN으로 한 번 탭
Cloudflare Access는 authProviders 대신 auth 옵션을 사용합니다. 프로덕션에서는 보호된 /_emdash 경로의 권위가 됩니다. 역할, 소유권, 비활성 사용자 검사가 계속 작동하도록 EmDash는 여전히 로컬 사용자를 저장합니다.
첫 사용자 설정
관리 패널에 처음 접근하면 설정 마법사가 관리자 계정 생성을 안내합니다.
-
http://localhost:4321/_emdash/admin으로 이동합니다 -
Set up your site에서 사이트 제목과 선택적 슬로건을 입력합니다. 템플릿이 샘플 콘텐츠도 제공할 수 있습니다. Continue를 선택합니다.
-
Create your account에서 이메일 주소와 선택적 이름을 입력합니다. Continue를 선택합니다.
-
Secure your account에서 패스키를 만들거나 구성된 로그인 프로바이더 중 하나를 선택합니다. 패스키를 선택하면 브라우저가 저장 위치를 묻습니다:
- macOS: Touch ID, 기기 비밀번호 또는 보안 키
- Windows: Windows Hello 또는 보안 키
- 모바일: Face ID, 지문 또는 PIN
-
브라우저 또는 프로바이더 흐름을 완료합니다. EmDash는 첫 사용자를 Admin으로 만들고 대시보드를 엽니다.
패스키로 로그인
설정 후 관리 패널로 돌아오면 패스키 인증이 시작됩니다:
-
/_emdash/admin을 방문합니다 -
로그인하지 않은 경우 로그인 페이지가 표시됩니다
-
Sign in을 클릭하여 인증합니다
-
브라우저가 패스키(생체 인식, PIN 또는 보안 키)를 요청합니다
-
검증 후 관리 대시보드로 리디렉션됩니다
매직 링크로 로그인
패스키를 사용할 수 없으면 매직 링크가 대안을 제공합니다. EmDash가 링크를 보내려면 사이트에 이메일 프로바이더가 구성되어 있어야 합니다 — 이메일 설정을 참조하세요.
-
로그인 페이지에서 Sign in with email을 클릭합니다
-
이메일 주소를 입력합니다
-
받은편지함에서 로그인 링크를 확인합니다
-
링크(15분 유효)를 클릭한 다음 확인 페이지에서 Continue를 선택합니다
링크는 Continue를 선택할 때만 사용되므로, 미리 링크를 여는 이메일 보안 스캐너가 링크를 소진하지 않습니다.
로그인 프로바이더 구성
패스키 외에도 EmDash는 로그인 페이지와 설정 마법사에 나타나는 플러그형 로그인 프로바이더를 지원합니다. GitHub와 Google은 EmDash에 포함되어 있습니다. Atmosphere 및 서드파티 프로바이더는 동일한 인터페이스를 통해 등록하는 별도 패키지입니다.
프로바이더는 추가적입니다. 프로바이더가 활성화되어도 패스키는 계속 작동합니다. GitHub와 Google은 프로바이더가 동일한 검증된 이메일 주소를 제공할 때만 기존 EmDash 사용자를 자동으로 연결합니다. Atmosphere 계정은 분산 식별자(DID)로 연결됩니다. EmDash Atmosphere 흐름은 이메일 주소를 받지 않기 때문입니다. 포함된 각 프로바이더는 첫 사용자를 만들 수 있어, 새 설치에서는 패스키를 완전히 건너뛸 수 있습니다.
Astro에 프로바이더 추가
EmDash 통합의 authProviders 배열에 프로바이더를 전달합니다. 다음 예는 GitHub, Google, Atmosphere를 활성화합니다:
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는 접두사가 있는 이름을 먼저 확인하고 접두사 없는 이름으로 폴백합니다:
| Variable | Purpose |
|---|---|
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 프로바이더를 활성화합니다:
import { google } from "emdash/auth/providers/google";
emdash({ authProviders: [google()] });
환경 변수로 자격 증명을 설정합니다. EmDash는 접두사가 있는 이름을 먼저 확인하고 접두사 없는 이름으로 폴백합니다:
| Variable | Purpose |
|---|---|
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 네트워크 뒤의 사용자 소유 신원 — 을 가진 사이트의 경우 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: {},
},
};
}
Atmosphere 패키지(@emdash-cms/auth-atproto)는 사용자 지정 로그인 양식, OAuth 라우트 핸들러, 영구 스토리지가 필요한 프로바이더의 가장 완성도 높은 실제 참조입니다.
사용자 역할
EmDash는 다섯 수준의 역할 기반 접근 제어를 사용합니다:
| Role | Level | Description |
|---|---|---|
| Subscriber | 10 | 게시된 콘텐츠 읽기(초안 접근 없음) |
| Contributor | 20 | 콘텐츠 만들기(게시에 승인 필요) |
| Author | 30 | 자신의 콘텐츠 만들기/편집/게시 |
| Editor | 40 | 모든 콘텐츠 관리 |
| Admin | 50 | 설정 포함 전체 접근 |
각 역할은 모든 하위 수준의 권한을 상속합니다. 첫 사용자는 항상 Admin으로 생성됩니다.
구독자와 초안 콘텐츠
구독자는 content:read 권한을 가져 회원 전용 게시 콘텐츠를 인증된 독자에게 제공할 수 있습니다. 초안, 예약 항목, 휴지통 항목, 리비전, 미리보기 URL은 볼 수 없습니다 — 이는 Contributor 이상에 부여되는 content:read_drafts로 게이트됩니다. list 및 get 엔드포인트는 구독자에 대해 투명하게 status=published로 필터링합니다. 편집자 전용 보기(/compare, /revisions, /trash, /preview-url)는 구독자 요청을 즉시 거부합니다.
사용자 초대
관리자는 관리 패널에서 새 사용자를 초대할 수 있습니다:
-
Settings > Users로 이동합니다
-
Invite User를 클릭합니다
-
사용자 이메일을 입력하고 역할을 선택합니다
-
Send Invite를 클릭합니다
-
이메일이 구성되어 있으면 EmDash가 초대를 보냅니다. 그렇지 않으면 생성된 링크를 복사해 직접 사용자에게 보내세요.
-
링크를 열고 초대 페이지에서 제공되는 패스키 또는 로그인 프로바이더로 계정을 만듭니다.
초대 링크는 일회용이며 7일 후 만료됩니다.
패스키 관리
사용자는 계정 설정에서 패스키를 관리할 수 있습니다:
- Add passkey — 백업이나 다른 기기용으로 추가 패스키 등록
- Remove passkey — 더 이상 사용하지 않는 패스키 삭제
- Rename passkey — 패스키에 설명적인 이름 지정
각 사용자는 최대 10개의 패스키를 등록할 수 있습니다.
EmDash는 사용자가 마지막 패스키를 제거하지 못하도록 합니다. 이전 것을 삭제하기 전에 대체 패스키를 추가하세요.
초대 없이 그룹 로그인 허용
각 사용자를 초대하지 않고 그룹이 로그인하게 하려면 허용 목록이 있는 로그인 프로바이더를 구성하세요. Atmosphere 프로바이더는 allowedHandles와 allowedDIDs를 받습니다(Atmosphere 로그인 참조). Cloudflare Access 어댑터는 autoProvision과 roleMapping을 통해 ID 프로바이더에서 사용자를 프로비저닝합니다. 문서화된 GitHub, Google, Atmosphere 프로바이더도 초기 관리자 계정을 만들 수 있습니다.
세션
패스키, 매직 링크, 초대, 로그인 프로바이더 콜백은 Astro 세션 스토어에 EmDash 사용자 ID를 저장합니다. 브라우저는 Astro의 불투명한 astro-session 식별자를 받습니다. 사용자 및 자격 증명 레코드는 EmDash 데이터베이스에 남습니다.
Cloudflare Access도 해석된 EmDash 사용자를 Astro 세션에 씁니다. 이를 통해 공개 페이지는 Astro.locals.user를 읽을 때 로그인된 사용자를 식별할 수 있습니다. 세션은 보호된 /_emdash 경로의 Access 인증을 대체하지 않습니다. EmDash는 해당 요청에서 Access JSON Web Token(JWT)을 다시 검증합니다.
인증 속도 제한
EmDash는 인증되지 않은 로그인 또는 가입 흐름을 시작하는 엔드포인트를 제한합니다. 제한은 각 엔드포인트와 신뢰할 수 있는 클라이언트 IP별로 분리됩니다:
| Endpoint | Limit |
|---|---|
POST /_emdash/api/auth/passkey/options | 분당 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에서 작동하지 않습니다. 각 도메인에 새 패스키를 등록하세요.
모든 패스키 분실
등록된 모든 패스키에 대한 접근을 잃은 경우:
- 다른 관리자에게 복구 매직 링크 전송을 요청합니다. 사이트에 이메일이 구성되어 있어야 합니다.
- 15분 이내에 링크를 열고 Continue를 선택하여 로그인합니다.
- 계정 설정에서 새 패스키를 등록합니다.
유일한 관리자이고 이메일이 구성되지 않은 경우, 데이터베이스를 통해 사이트의 인증을 재설정해야 합니다.
Cloudflare Access
Cloudflare에 배포할 때 내장 로그인 방법 대신 Cloudflare Access를 사용할 수 있습니다. Access는 엣지에서 ID 프로바이더로 사용자를 인증합니다. EmDash는 서명된 Access JWT를 검증하고, 해당 인물의 신원과 그룹을 로드하며, 그 신원을 로컬 EmDash 사용자에 매핑합니다.
Cloudflare Access를 사용할 때
- 싱글 사인온 — 사용자가 회사 IdP로 인증
- 중앙 집중식 접근 제어 — Cloudflare 대시보드에서 관리 접근 권한 관리
- 패스키 관리 불필요 — 패스키 등록·관리 필요 없음
- 그룹 기반 역할 — IdP 그룹을 EmDash 역할에 자동 매핑
Access 설정
- 사이트의
/_emdash/*경로에 대한 Cloudflare Access 애플리케이션과 정책을 만듭니다./_emdash/admin/*만 보호하면 EmDash가 기대하는 JWT 없이 REST API가 남습니다. - 애플리케이션의 Application Audience (AUD) Tag를 복사합니다.
- 태그를 런타임 환경 변수
CF_ACCESS_AUDIENCE에 저장합니다. 로컬 및 배포 값은 EmDash 시크릿 가이드를 따르세요. - 런타임에 해당 값을 읽도록 EmDash를 구성합니다:
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",
}),
}),
],
});
애플리케이션 audience는 어떤 Access 애플리케이션이 JWT를 발급했는지 식별합니다. EmDash는 발급자와 서명과 함께 검증합니다. 다른 Access 애플리케이션용 토큰은 거부됩니다.
구성 옵션
| Option | Type | Default | Description |
|---|---|---|---|
teamDomain | string | required | Access 팀 도메인(예: myteam.cloudflareaccess.com) |
audience | string | — | Application Audience (AUD) 태그를 직접 제공. Workers에서는 audienceEnvVar 권장. |
autoProvision | boolean | true | 첫 Access 로그인 시 EmDash 사용자 생성 |
defaultRole | number | 30 | 어떤 그룹에도 일치하지 않는 사용자의 역할(30 = Author) |
syncRoles | boolean | false | 각 로그인 시 IdP 그룹에 따라 역할 업데이트 |
roleMapping | object | — | IdP 그룹 이름을 역할 수준에 매핑 |
audienceEnvVar | string | "CF_ACCESS_AUDIENCE" | audience 태그가 들어 있는 환경 변수. audience 생략 시 사용. |
audience 또는 audienceEnvVar 아래의 환경 값 중 하나를 제공하세요.
역할 매핑
IdP 그룹을 EmDash 역할에 매핑합니다:
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
}),
});
사용자가 여러 그룹에 속하면 첫 번째 일치 그룹이 이깁니다. 사이트에 접근하는 첫 사용자는 그룹과 관계없이 항상 Admin이 됩니다.
역할 동기화 동작
기본값(syncRoles: false)에서는 사용자의 역할이 첫 로그인 시 설정되고 이후 변경되지 않습니다. 관리자가 EmDash에서 역할을 수동으로 조정할 수 있습니다.
IdP 그룹을 권위 있게 하려면 syncRoles: true를 설정하세요 — 사용자의 역할이 현재 그룹에 따라 매 로그인마다 업데이트됩니다.
요청 및 세션 흐름
- 사용자가 Access 애플리케이션으로 보호된 경로를 방문합니다.
- Access 세션이 없으면 Cloudflare Access가 사용자를 ID 프로바이더로 리디렉션합니다.
- 인증 후 Access는 서명된 JWT를
Cf-Access-Jwt-Assertion으로 오리진에 보냅니다. - EmDash는 토큰의 서명, 발급자, audience를 검증한 다음 Access 신원과 그룹을 읽습니다.
- EmDash는 로컬 사용자를 찾거나 프로비저닝하고, 구성된 역할 동작을 적용하며, Astro 세션에 사용자를 기록합니다.
- 보호된 EmDash 경로에 대한 이후 요청은 Access 검증을 반복합니다. 공개 페이지는 새 Access 요청의 증명으로 취급하지 않고 EmDash 세션으로 사용자를 식별할 수 있습니다.
Access로 대체되는 기능
Access가 활성화되면 다음 기능을 사용할 수 없습니다:
- 로그인 페이지(
/_emdash/admin/login) - 패스키 등록 및 관리
- GitHub, Google, Atmosphere 로그인
- 매직 링크 로그인
- 자가 가입
- 사용자 초대
Access 정책이 누가 EmDash에 도달하는지 결정합니다. EmDash는 여전히 로컬 역할, 콘텐츠 소유권, 비활성 사용자 플래그를 소유합니다. syncRoles: false일 때 관리자는 EmDash에서 프로비저닝된 사용자의 역할을 변경할 수 있습니다. syncRoles: true일 때 매핑된 Access 그룹이 매 로그인마다 그 역할을 대체합니다.
문제 해결
”No Access JWT present”
요청이 Access JWT 없이 EmDash에 도달했습니다. 이는 다음을 의미합니다:
- Access가 애플리케이션을 보호하도록 구성되지 않음
- Access 정책이 관리 경로와 일치하지 않음
Access 애플리케이션이 전체 /_emdash/* 경로를 다루고 정책에 사용자가 포함되어 있는지 확인하세요.
”JWT audience mismatch”
설정의 audience가 JWT와 일치하지 않습니다. Access 애플리케이션 설정의 Application Audience Tag를 다시 확인하세요.
”User not authorized”
사용자가 Access를 통해 인증되었지만 autoProvision이 false이고 EmDash에 존재하지 않습니다. 다음 중 하나입니다:
autoProvision: true를 설정하거나- 로그인하기 전에 수동으로 사용자를 만들기