인증

이 페이지

EmDash는 패스키 인증을 기본 로그인 방법으로 사용합니다. 패스키는 피싱에 강하고, 비밀번호가 필요 없으며, 브라우저나 비밀번호 관리자를 통해 기기 간에 작동합니다.

패스키 외에도 플러그 가능한 로그인 프로바이더를 추가할 수 있습니다. GitHub과 Google은 EmDash에 포함되어 있습니다. 별도로 설치하는 Atmosphere 프로바이더는 AT Protocol 계정을 추가하며, 동일한 프로바이더 인터페이스는 다른 패키지에도 열려 있습니다. 문서화된 GitHub, Google, Atmosphere 프로바이더는 첫 번째 관리자 계정을 생성하거나 연결된 EmDash 사용자로 로그인할 수 있습니다.

Cloudflare 배포의 경우, Cloudflare Access가 프로덕션에서 별도의 독점적 인증 모드입니다. EmDash 로그인 방법을 표시하는 대신 보호된 EmDash 경로에서 Access 자격 증명을 검증합니다.

인증 모드 선택

패스키는 WebAuthn을 사용합니다. 이는 기기에 저장되거나 비밀번호 관리자를 통해 동기화되는 공개 키 자격 증명을 생성하는 웹 표준입니다. 로그인할 때 기기는 네트워크를 통해 비밀번호를 보내지 않고 자격 증명의 소유를 증명합니다.

패스키가 기본값입니다. GitHub, Google, Atmosphere 프로바이더는 추가 로그인 방법입니다: 각각 사용자를 인증하고, EmDash 계정을 연결하거나 생성하고, 패스키 로그인에서 사용되는 것과 동일한 EmDash 세션을 설정합니다.

패스키 인증이 제공하는 것:

  • 기억하거나 유출될 비밀번호 없음
  • 피싱 방지 — 자격 증명이 사이트 도메인에 바인딩
  • 기기 간 동기화 — iCloud 키체인, Google 비밀번호 관리자, 1Password 등과 작동
  • 빠른 로그인 — 생체 인증 또는 PIN으로 한 번 탭

Cloudflare Access는 authProviders 대신 auth 옵션을 사용합니다. 프로덕션에서는 보호된 /_emdash 경로의 권한이 됩니다. EmDash는 역할, 소유권, 비활성화된 사용자 확인이 계속 작동하도록 로컬 사용자를 계속 저장합니다.

첫 번째 사용자 설정

관리 패널에 처음 접근하면 설정 마법사가 관리자 계정 생성을 안내합니다.

  1. http://localhost:4321/_emdash/admin으로 이동합니다

  2. Set up your site에서 사이트 제목과 선택적 태그라인을 입력합니다. 템플릿이 샘플 콘텐츠도 제공할 수 있습니다. Continue를 선택합니다.

  3. Create your account에서 이메일 주소와 선택적 이름을 입력합니다. Continue를 선택합니다.

  4. Secure your account에서 패스키를 생성하거나 설정된 로그인 프로바이더 중 하나를 선택합니다. 패스키를 선택하면 브라우저가 저장 위치를 묻습니다:

    • macOS: Touch ID, 기기 비밀번호 또는 보안 키
    • Windows: Windows Hello 또는 보안 키
    • 모바일: Face ID, 지문 또는 PIN
  5. 브라우저 또는 프로바이더 플로우를 완료합니다. EmDash는 첫 번째 사용자를 Admin으로 생성하고 대시보드를 엽니다.

패스키로 로그인

설정 후 관리 패널로 돌아오면 패스키 인증이 트리거됩니다:

  1. /_emdash/admin을 방문합니다

  2. 로그인하지 않은 경우 로그인 페이지가 표시됩니다

  3. Sign in을 클릭하여 인증합니다

  4. 브라우저가 패스키를 요청합니다(생체 인증, PIN 또는 보안 키)

  5. 검증 후 관리 대시보드로 리디렉션됩니다

매직 링크로 로그인

패스키를 사용할 수 없는 경우 매직 링크가 대안을 제공합니다. EmDash가 링크를 보내려면 사이트에 이메일 프로바이더가 설정되어 있어야 합니다.

  1. 로그인 페이지에서 Sign in with email을 클릭합니다

  2. 이메일 주소를 입력합니다

  3. 받은 편지함에서 로그인 링크를 확인합니다

  4. 링크를 클릭하여 인증합니다(15분간 유효)

로그인 프로바이더 설정

패스키 외에도 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는 접두사가 붙은 이름을 먼저 확인하고 접두사 없는 이름으로 폴백합니다:

변수용도
EMDASH_OAUTH_GITHUB_CLIENT_ID / GITHUB_CLIENT_IDOAuth 앱 클라이언트 ID
EMDASH_OAUTH_GITHUB_CLIENT_SECRET / GITHUB_CLIENT_SECRETOAuth 앱 시크릿

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_IDOAuth 앱 클라이언트 ID
EMDASH_OAUTH_GOOGLE_CLIENT_SECRET / GOOGLE_CLIENT_SECRETOAuth 앱 시크릿

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", // 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는 5개 레벨의 역할 기반 접근 제어를 사용합니다:

역할레벨설명
Subscriber10게시된 콘텐츠 읽기(초안 접근 없음)
Contributor20콘텐츠 생성(게시에 승인 필요)
Author30자신의 콘텐츠 생성/편집/게시
Editor40모든 콘텐츠 관리
Admin50설정 포함 전체 접근

각 역할은 모든 하위 레벨의 권한을 상속합니다. 첫 번째 사용자는 항상 Admin으로 생성됩니다.

Subscriber와 초안 콘텐츠

Subscriber는 content:read 권한을 가지고 있어 회원 전용 게시된 콘텐츠를 인증된 독자에게 제공할 수 있습니다. 초안, 예약된 항목, 삭제된 항목, 리비전, 미리보기 URL은 볼 수 없습니다 — 이들은 Contributor 이상에게 부여되는 content:read_drafts로 제한됩니다. 목록 및 조회 엔드포인트는 Subscriber에 대해 status=published로 투명하게 필터링합니다. 편집자 전용 뷰(/compare, /revisions, /trash, /preview-url)는 Subscriber 요청을 직접 거부합니다.

사용자 초대

Admin은 관리 패널을 통해 새 사용자를 초대할 수 있습니다:

  1. Settings > Users로 이동합니다

  2. Invite User를 클릭합니다

  3. 사용자의 이메일을 입력하고 역할을 선택합니다

  4. Send Invite를 클릭합니다

  5. 이메일이 설정된 경우 EmDash가 초대를 보냅니다. 그렇지 않으면 생성된 링크를 복사하여 직접 사용자에게 보냅니다.

  6. 사용자는 링크를 열고 패스키 또는 초대 페이지에서 제공되는 로그인 프로바이더로 계정을 생성합니다.

초대 링크는 일회용이며 7일 후에 만료됩니다.

패스키 관리

사용자는 계정 설정에서 패스키를 관리할 수 있습니다:

  • 패스키 추가 — 백업 또는 다른 기기용으로 추가 패스키 등록
  • 패스키 제거 — 더 이상 사용하지 않는 패스키 삭제
  • 패스키 이름 변경 — 패스키에 설명적인 이름 부여

각 사용자는 최대 10개의 패스키를 등록할 수 있습니다.

EmDash는 사용자가 마지막 패스키를 제거하는 것을 허용하지 않습니다. 이전 것을 삭제하기 전에 대체를 추가하세요.

초대 없이 그룹이 로그인하도록 허용

각 사용자를 초대하지 않고 그룹이 로그인하도록 하려면 허용 목록이 있는 로그인 프로바이더를 설정합니다. 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분당 10 요청
POST /_emdash/api/auth/magic-link/send5분당 3 요청
POST /_emdash/api/auth/signup/request5분당 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. 계정 설정에서 새 패스키를 등록합니다.

유일한 관리자이고 이메일이 설정되지 않은 경우 데이터베이스를 통해 사이트의 인증을 재설정해야 합니다.

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가 런타임에 해당 값을 읽도록 설정합니다:
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 애플리케이션의 토큰은 거부됩니다.

설정 옵션

옵션타입기본값설명
teamDomainstring필수Access 팀 도메인(예: myteam.cloudflareaccess.com)
audiencestringApplication Audience (AUD) 태그 직접 제공. Workers에서는 audienceEnvVar 권장.
autoProvisionbooleantrue첫 Access 로그인 시 EmDash 사용자 생성
defaultRolenumber30어떤 그룹에도 매칭되지 않는 사용자의 역할(30 = Author)
syncRolesbooleanfalseIdP 그룹 기반으로 로그인마다 역할 업데이트
roleMappingobjectIdP 그룹 이름을 역할 레벨에 매핑
audienceEnvVarstring"CF_ACCESS_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
	}),
});

사용자가 여러 그룹에 속하면 첫 번째로 매칭되는 그룹이 우선합니다. 사이트에 접근하는 첫 번째 사용자는 그룹에 관계없이 항상 Admin이 됩니다.

역할 동기화 동작

기본값(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 검증을 반복합니다. 공개 페이지는 새로운 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를 통해 인증되었지만 autoProvisionfalse이고 EmDash에 존재하지 않습니다. 해결 방법:

  • autoProvision: true로 설정, 또는
  • 로그인 전에 수동으로 사용자 생성