Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/atomic-api-key-rotation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@croco/auth-core": minor
"@croco/auth-drizzle": minor
"@croco/problems-core": patch
---

API key rotation now atomically revokes the old credential, replays the same protected replacement for idempotent retries, and durably recovers post-commit rotation events.

Custom `ApiKeyStore` adapters must implement atomic rotation plus event claim, completion, and release operations. Callers must provide an idempotency key and configure an `ApiKeyRotationProtector`.

Deploy the rotation schema first, pause rotation traffic, drain every instance using the legacy save-then-revoke path, deploy the new writers, and only then resume rotation. Mixed legacy and atomic rotation writers are not supported.
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ jobs:
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'packages/auth-core/**'
- 'packages/auth-drizzle/**'
- 'packages/events-core/**'
- 'packages/events-tx/**'
- 'packages/framework-context/**'
Expand Down Expand Up @@ -589,6 +591,13 @@ jobs:
MIGRATION_POSTGRES_URL: postgresql://postgres:postgres@127.0.0.1:5432/croco_membership
run: pnpm --filter @croco/migration-runner exec vitest run src/tests/MigrationStatusPostgres.spec.ts

- name: Verify API key rotation atomicity against PostgreSQL
env:
AUTH_POSTGRES_URL: postgresql://postgres:postgres@127.0.0.1:5432/croco_membership
run: |
pnpm build --filter=@croco/auth-drizzle...
pnpm --filter @croco/auth-drizzle exec vitest run src/tests/DrizzleApiKeyStore.postgres.spec.ts

- name: Verify typed TestKernel resources against real PostgreSQL and Redis
run: |
pnpm build --filter=@croco/testing-resources...
Expand Down
92 changes: 91 additions & 1 deletion docs/problem-code-registry.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "croco.problem-code-registry.v1",
"problemCount": 523,
"problemCount": 526,
"problems": [
{
"code": "ACCESS_DENIED",
Expand Down Expand Up @@ -1352,6 +1352,66 @@
}
]
},
{
"code": "auth-core/api-key-rotation-conflict",
"category": "Conflict",
"status": 409,
"title": "Conflict",
"cookbookPath": "/reference/problem-recovery-cookbook/#auth-core-api-key-rotation-conflict",
"recovery": {
"cause": "The request conflicts with current state or an idempotency constraint.",
"userAction": "Refresh state, resolve the conflict, and retry with the updated intent.",
"operatorAction": "Inspect concurrent writes, idempotency keys, and uniqueness constraints.",
"retryability": "conditional",
"redactionPolicy": "safe-message",
"telemetry": {
"eventName": "croco.problem.warning",
"severity": "warning",
"attributes": ["problem.code", "problem.category", "problem.status"]
}
},
"lifecycle": {
"status": "active"
},
"sources": [
{
"file": "packages/auth-core/src/libs/problems/AuthProblems.ts",
"line": 75,
"column": 3,
"kind": "problem-class"
}
]
},
{
"code": "auth-core/api-key-rotation-protection-failed",
"category": "InternalServerError",
"status": 500,
"title": "Internal Server Error",
"cookbookPath": "/reference/problem-recovery-cookbook/#auth-core-api-key-rotation-protection-failed",
"recovery": {
"cause": "Croco or an upstream dependency failed after accepting the request.",
"userAction": "Retry later only when the operation is idempotent or the caller owns retry safety.",
"operatorAction": "Use traces, logs, and upstream diagnostics to isolate the failing boundary.",
"retryability": "conditional",
"redactionPolicy": "operator-only",
"telemetry": {
"eventName": "croco.problem.error",
"severity": "error",
"attributes": ["problem.code", "problem.category", "problem.status"]
}
},
"lifecycle": {
"status": "active"
},
"sources": [
{
"file": "packages/auth-core/src/libs/apikey/ApiKeyRotationProtector.ts",
"line": 28,
"column": 5,
"kind": "problem-constructor"
}
]
},
{
"code": "auth-core/auth-provider-unavailable",
"category": "InternalServerError",
Expand Down Expand Up @@ -1382,6 +1442,36 @@
}
]
},
{
"code": "auth-core/invalid-api-key-rotation-idempotency-key",
"category": "ValidationError",
"status": 422,
"title": "Validation Error",
"cookbookPath": "/reference/problem-recovery-cookbook/#auth-core-invalid-api-key-rotation-idempotency-key",
"recovery": {
"cause": "The request or generated contract failed schema or semantic validation.",
"userAction": "Fix the invalid fields and retry with schema-conformant input.",
"operatorAction": "Inspect schema diagnostics, generated contracts, and validation metadata.",
"retryability": "not-retryable",
"redactionPolicy": "public",
"telemetry": {
"eventName": "croco.problem.info",
"severity": "info",
"attributes": ["problem.code", "problem.category", "problem.status"]
}
},
"lifecycle": {
"status": "active"
},
"sources": [
{
"file": "packages/auth-core/src/libs/problems/AuthProblems.ts",
"line": 83,
"column": 3,
"kind": "problem-class"
}
]
},
{
"code": "auth-core/invalid-permission-action",
"category": "ValidationError",
Expand Down
28 changes: 26 additions & 2 deletions packages/auth-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,27 @@ pnpm add @croco/auth-core
## 사용법

```ts
import { ApiKeyGenerator, ApiKeyHasher, ApiKeyManager } from "@croco/auth-core";
import {
AesGcmApiKeyRotationProtector,
ApiKeyGenerator,
ApiKeyHasher,
ApiKeyManager,
} from "@croco/auth-core";

const rotationProtector = new AesGcmApiKeyRotationProtector({
activeKeyId: "2026-07",
keys: {
"2026-07": rotationProtectionKey,
},
});

const manager = new ApiKeyManager(
apiKeyStore,
new ApiKeyGenerator(),
new ApiKeyHasher(),
eventBus,
logger,
rotationProtector,
);

const created = await manager.create({
Expand All @@ -28,8 +41,17 @@ const created = await manager.create({
});

const principal = await manager.verify(created.key);

const rotated = await manager.rotate(created.id, {
idempotencyKey: "deploy-2026-07-30",
});
```

`rotate()`는 호출자가 제공한 멱등성 키로 하나의 논리적 회전을 식별합니다. 저장소는 기존 키 폐기와 대체 키 저장을
원자적으로 처리해야 하며, 성공 응답이나 이벤트 발행이 유실되면 같은 멱등성 키로 재시도해 동일한 대체 키를 복구합니다.
복구 자료는 AES-256-GCM으로 보호되며, 회전 레코드가 재생 가능한 동안 해당 레코드를 암호화한 이전 보호 키도 설정에
유지해야 합니다.

```ts
import { RequirePermission, RbacEngine, RoleRegistry } from "@croco/auth-core";

Expand All @@ -52,6 +74,7 @@ class ProjectController {
- `ApiKeyManager`, API 키 생성, 검증, 폐기, 회전을 담당합니다.
- `ApiKeyGenerator`, 안전한 API 키를 생성하고 파싱합니다.
- `ApiKeyHasher`, API 키 해시와 검증을 담당합니다.
- `AesGcmApiKeyRotationProtector`, 재시도 가능한 회전 복구 자료를 보호합니다.
- `RbacEngine`, 사용자와 역할 기반 권한 검사를 수행합니다.
- `RoleRegistry`, 역할과 권한 집합을 관리합니다.
- `AuthGuard`, `ApiKeyGuard`, `PermissionGuard`, `UnifiedAuthGuard`, 라우트 보호를 담당합니다.
Expand All @@ -67,13 +90,14 @@ class ProjectController {

- `AuthUser`, `Principal`, `ApiKeyPrincipal`, `UserPrincipal`
- `AuthProvider`, `ApiKeyProvider`, `SessionProvider`, `TenantMappingProvider`
- `AuthRequest`, `ApiKey`, `CreateApiKeyOptions`, `CreateApiKeyResult`
- `AuthRequest`, `ApiKey`, `CreateApiKeyOptions`, `CreateApiKeyResult`, `RotateApiKeyOptions`

### 문제 타입

- `UnauthorizedProblem`, `ForbiddenProblem`
- `AuthProviderUnavailableProblem`
- `ApiKeyExpiredProblem`, `ApiKeyRevokedProblem`, `ApiKeyNotFoundProblem`
- `ApiKeyRotationConflictProblem`, `ApiKeyRotationProtectionProblem`
- `InvalidPermissionFormatProblem`, `InvalidPermissionActionProblem`

## AuthGuard conformance
Expand Down
21 changes: 21 additions & 0 deletions packages/auth-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ export { ApiKeyHasher } from "./libs/apikey/ApiKeyHasher";
*/
export { ApiKeyManager } from "./libs/apikey/ApiKeyManager";

/**
* API 키 회전 복구 자료를 보호하는 계약과 AES-GCM 구현입니다.
*/
export {
API_KEY_ROTATION_PROTECTOR_TOKEN,
AesGcmApiKeyRotationProtector,
ApiKeyRotationProtectionProblem,
} from "./libs/apikey/ApiKeyRotationProtector";
export type {
AesGcmApiKeyRotationProtectorOptions,
ApiKeyRotationProtectionContext,
ApiKeyRotationProtector,
} from "./libs/apikey/ApiKeyRotationProtector";

/**
* API 키 저장소 토큰과 추상 저장소 계약입니다.
*/
Expand Down Expand Up @@ -92,8 +106,13 @@ export { AbstractRoleRegistry } from "./libs/interfaces/AbstractRoleRegistry";
export type {
ApiKey,
ApiKeyRateLimit,
ApiKeyRotation,
ApiKeyRotationInput,
ApiKeyRotationPhaseStatus,
CreateApiKeyOptions,
CreateApiKeyResult,
RotateApiKeyOptions,
RotateApiKeyResult,
} from "./libs/interfaces/ApiKey";

/**
Expand Down Expand Up @@ -152,7 +171,9 @@ export type { TenantMappingProvider } from "./libs/interfaces/TenantMapping";
export {
ApiKeyCreationFailedProblem,
ApiKeyExpiredProblem,
ApiKeyRotationConflictProblem,
ApiKeyRevokedProblem,
InvalidApiKeyRotationIdempotencyKeyProblem,
AuthProviderUnavailableProblem,
ForbiddenProblem,
InvalidPermissionActionProblem,
Expand Down
Loading
Loading