인증

이 페이지

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

패스키를 넘어 플러그형 로그인 공급자를 추가할 수 있습니다 — GitHub, Google 및 Atmosphere(AT 프로토콜)가 기본 제공되며, 동일한 공급자 인터페이스가 서드파티 패키지에 개방되어 있습니다. 구성된 모든 공급자는 첫 번째 관리자 계정 생성, 로그인 또는 기존 사용자에 연결하는 데 사용할 수 있습니다.

Cloudflare 배포의 경우 Cloudflare Access도 전체 로그인 흐름을 인계받는 독점적인 인증 방법으로 사용할 수 있습니다.

작동 방식

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

패스키 인증의 이점:

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

첫 번째 사용자 설정

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

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

  2. 설정 마법사로 리디렉션됩니다. 입력:

    • 사이트 제목 — 사이트 이름
    • 태그라인 — 짧은 설명
    • 관리자 이메일 — 이메일 주소
  3. 사이트 만들기를 클릭하여 패스키 등록

  4. 브라우저에서 패스키를 만들라는 메시지가 표시됩니다:

    • macOS: Touch ID, 기기 비밀번호 또는 보안 키
    • Windows: Windows Hello 또는 보안 키
    • 모바일: Face ID, 지문 또는 PIN
  5. 패스키가 등록되면 로그인되고 관리자 대시보드로 리디렉션됩니다.

로그인

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

  1. /_emdash/admin 방문

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

  3. 로그인을 클릭하여 인증

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

  5. 확인 후 관리자 대시보드로 리디렉션됩니다

매직 링크 대체

패스키를 사용할 수 없는 경우(예: 분실된 기기) 매직 링크가 대안을 제공합니다. 이메일이 구성되어 있어야 합니다.

  1. 로그인 페이지에서 이메일로 로그인을 클릭

  2. 이메일 주소 입력

  3. 받은 편지함에서 로그인 링크 확인

  4. 링크를 클릭하여 인증(15분 동안 유효)

로그인 공급자

패스키 외에도 EmDash는 로그인 페이지와 설정 마법사에 표시되는 플러그형 로그인 공급자를 지원합니다. GitHub, Google 및 Atmosphere 공급자가 박스에 포함되어 있으며, 서드파티 패키지는 동일한 인터페이스를 사용하여 자체 공급자를 등록할 수 있습니다.

공급자는 추가적입니다 — 공급자가 활성화되어 있어도 패스키는 계속 작동하며, 사용자는 기존 패스키 전용 계정에 공급자를 연결할 수 있습니다. 첫 번째 사용자는 구성된 모든 공급자를 통해 생성할 수도 있으므로 원하는 경우 새 설치에서 패스키를 완전히 건너뛸 수 있습니다.

공급자 구성

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 프로토콜)

기여자가 이미 Atmosphere 계정을 가지고 있는 사이트의 경우 — Bluesky와 더 넓은 AT 프로토콜 네트워크 뒤에 있는 사용자 소유 ID — Atmosphere 공급자를 설치합니다:

pnpm add @emdash-cms/auth-atproto

다음 예제는 핸들 허용 목록이 있는 Atmosphere 공급자를 활성화합니다:

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

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

클라이언트 시크릿이나 환경 변수는 필요하지 않습니다. 핸들/DID 허용 목록, 역할 매핑 및 AT 프로토콜 OAuth 프로필이 요구하는 로컬 개발 설정에 대해서는 Atmosphere 로그인 가이드를 참조하세요.

자체 공급자 구축

공급자는 단순히 AuthProviderDescriptor입니다 — id, 사람이 읽을 수 있는 레이블 및 관리자 측 React 구성 요소, 라우트 핸들러, 공개 라우트 접두사 및 스토리지 컬렉션의 모든 조합입니다. 형태는 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는 5개 레벨의 역할 기반 액세스 제어를 사용합니다:

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

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

구독자 및 초안 콘텐츠

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

사용자 초대

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

  1. 설정 > 사용자로 이동

  2. 사용자 초대 클릭

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

  4. 초대 보내기 클릭

  5. 사용자는 초대 링크가 포함된 이메일을 받습니다

  6. 링크를 클릭하고 패스키를 등록합니다

초대는 7일 동안 유효합니다. 관리자는 사용자 페이지에서 초대를 다시 보내거나 취소할 수 있습니다.

패스키 관리

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

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

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

초대 없이 그룹 로그인 허용

각 사용자를 초대하지 않고 그룹이 로그인할 수 있도록 하려면 허용 목록이 있는 로그인 공급자를 구성합니다. Atmosphere 공급자는 allowedHandlesallowedDIDs를 허용합니다(Atmosphere 로그인 참조). Cloudflare Access 어댑터는 autoProvisionroleMapping을 통해 ID 공급자로부터 사용자를 프로비저닝합니다. 구성된 모든 공급자는 초기 관리자 계정을 생성할 수도 있습니다.

세션

세션은 안전한 HttpOnly, SameSite=Lax 쿠키를 사용하며 슬라이딩 만료로 30일 동안 지속됩니다 — 만료는 활동 시 재설정됩니다.

보안 참고사항

  • 패스키는 공개 키로 저장됩니다 — 개인 키는 기기를 떠나지 않습니다
  • 챌린지 검증은 재생 공격을 방지합니다
  • 속도 제한은 무차별 대입 공격으로부터 보호합니다(5회 시도/분/IP)
  • 세션은 HttpOnly, Secure, SameSite=Lax입니다 쿠키 보안을 위해
  • 매직 링크 토큰은 SHA-256으로 해시됩니다 — 원시 토큰은 저장되지 않습니다

문제 해결

”패스키가 등록되지 않음”

로그인 시 이 오류가 표시되면 비밀번호 관리자에서 패스키가 삭제되었을 수 있습니다. 관리자에게 매직 링크 또는 새 초대를 보내달라고 요청하세요.

”패스키 인증 실패”

이것은 일반적으로 패스키가 다른 도메인용으로 생성되었음을 의미합니다. 패스키는 도메인에 바인딩됩니다 — localhost:4321용 패스키는 example.com에서 작동하지 않습니다. 각 도메인에 대해 새 패스키를 등록하세요.

”세션 만료됨”

세션은 기본적으로 슬라이딩 만료로 30일 동안 지속됩니다. 예기치 않게 로그아웃되면 쿠키를 지우고 다시 로그인하세요.

모든 패스키 분실

등록된 모든 패스키에 대한 액세스 권한을 잃은 경우:

  1. 다른 관리자에게 매직 링크를 보내달라고 요청합니다(이메일 구성 필요)
  2. 매직 링크를 사용하여 로그인
  3. 계정 설정에서 새 패스키 등록

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

Cloudflare Access

Cloudflare에 배포할 때 패스키 대신 Cloudflare Access를 인증 공급자로 사용할 수 있습니다. Access는 기존 ID 공급자를 사용하여 에지에서 인증을 처리합니다.

Cloudflare Access를 사용하는 이유

  • 싱글 사인온 — 사용자가 회사의 IdP로 인증합니다
  • 중앙 집중식 액세스 제어 — Cloudflare 대시보드에서 관리자에 액세스할 수 있는 사람 관리
  • 패스키 관리 불필요 — 패스키를 등록하거나 관리할 필요가 없습니다
  • 그룹 기반 역할 — IdP 그룹을 EmDash 역할에 자동으로 매핑

설정

  1. EmDash 사이트용 Cloudflare Access 애플리케이션을 생성합니다
  2. 애플리케이션 설정에서 Application Audience(AUD) 태그를 기록합니다
  3. Access를 사용하도록 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",
				audience: "abc123def456...", // From Access app settings
			}),
		}),
	],
});

구성 옵션

옵션유형기본값설명
teamDomainstring필수Access 팀 도메인(예: myteam.cloudflareaccess.com)
audiencestring필수Access 설정의 Application Audience(AUD) 태그
autoProvisionbooleantrue첫 번째 Access 로그인 시 EmDash 사용자 생성
defaultRolenumber30그룹과 일치하지 않는 사용자의 역할(30 = Author)
syncRolesbooleanfalseIdP 그룹을 기반으로 로그인할 때마다 역할 업데이트
roleMappingobjectIdP 그룹 이름을 역할 레벨에 매핑
audienceEnvVarstring"CF_ACCESS_AUDIENCE"오디언스 태그의 환경 변수 이름(하드코딩 대안)

역할 매핑

IdP 그룹을 EmDash 역할에 매핑합니다:

emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audience: "abc123...",
		roleMapping: {
			Admins: 50, // Admin
			"Content Editors": 40, // Editor
			Writers: 30, // Author
		},
		defaultRole: 20, // 그룹에 속하지 않은 사용자를 위한 Contributor
	}),
});

사용자가 여러 그룹에 속한 경우 첫 번째로 일치하는 그룹이 우선합니다. 사이트에 액세스하는 첫 번째 사용자는 그룹과 관계없이 항상 관리자가 됩니다.

역할 동기화 동작

기본적으로(syncRoles: false) 사용자의 역할은 처음 로그인할 때 설정되며 나중에 변경되지 않습니다. 이를 통해 관리자가 EmDash에서 역할을 수동으로 조정할 수 있습니다.

IdP 그룹을 권위 있는 것으로 만들려면 syncRoles: true를 설정합니다 — 사용자의 역할은 현재 그룹을 기반으로 로그인할 때마다 업데이트됩니다.

작동 방식

  1. 사용자가 /_emdash/admin을 방문합니다
  2. Cloudflare Access가 가로채고 IdP로 리디렉션합니다
  3. 사용자가 인증합니다(SSO, MFA 등)
  4. Access가 요청에 서명된 JWT를 설정합니다
  5. EmDash가 JWT를 검증하고 사용자를 생성/인증합니다

비활성화된 기능

Access가 활성화되면 이러한 기능을 사용할 수 없습니다:

  • 로그인 페이지(/_emdash/admin/login)
  • 패스키 등록 및 관리
  • OAuth 로그인
  • 매직 링크 로그인
  • 자가 가입
  • 사용자 초대

사용자 관리는 Cloudflare Access 정책을 통해 전적으로 수행됩니다.

문제 해결

”Access JWT 없음”

요청이 Access JWT 없이 EmDash에 도달했습니다. 이는 다음을 의미합니다:

  • Access가 애플리케이션을 보호하도록 구성되지 않음
  • Access 정책이 관리자 경로와 일치하지 않음

Access 애플리케이션이 /_emdash/admin/*를 포함하는지 확인하세요.

”JWT 오디언스 불일치”

구성의 audience가 JWT와 일치하지 않습니다. Access 애플리케이션 설정에서 Application Audience 태그를 다시 확인하세요.

”사용자가 승인되지 않음”

사용자가 Access를 통해 인증되었지만 autoProvisionfalse이고 EmDash에 존재하지 않습니다. 다음 중 하나:

  • autoProvision: true를 설정하거나
  • 로그인하기 전에 사용자를 수동으로 생성합니다