@spfn/auth 0.2.0-beta.4 → 0.2.0-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @spfn/auth - Technical Documentation
2
2
 
3
- **Version:** 0.1.0-alpha.88
3
+ **Version:** 0.2.0-beta.15
4
4
  **Status:** Alpha - Internal Development
5
5
 
6
6
  > **Note:** This is a technical documentation for developers working on the @spfn/auth package.
@@ -12,12 +12,14 @@
12
12
 
13
13
  - [Overview](#overview)
14
14
  - [Installation](#installation)
15
+ - [Admin Account Setup](#6-admin-account-setup)
15
16
  - [Architecture](#architecture)
16
17
  - [Package Structure](#package-structure)
17
18
  - [Module Exports](#module-exports)
18
19
  - [Email & SMS Services](#email--sms-services)
19
- - [Email Templates](#email-templates)
20
20
  - [Server-Side API](#server-side-api)
21
+ - [Events](#events)
22
+ - [OAuth Authentication](#oauth-authentication)
21
23
  - [Database Schema](#database-schema)
22
24
  - [RBAC System](#rbac-system)
23
25
  - [Next.js Adapter](#nextjs-adapter)
@@ -34,10 +36,11 @@
34
36
 
35
37
  - **Asymmetric JWT Authentication** - Client-signed tokens using ES256/RS256
36
38
  - **User Management** - Email/phone-based identity with bcrypt hashing
39
+ - **OAuth Authentication** - Google OAuth 2.0 (Authorization Code Flow), extensible to other providers
37
40
  - **Multi-Factor Authentication** - OTP verification via email/SMS
38
41
  - **Session Management** - Public key rotation with 90-day expiry
39
42
  - **Role-Based Access Control** - Flexible RBAC with runtime role/permission management
40
- - **Next.js Integration** - Session helpers and server-side guards
43
+ - **Next.js Integration** - Session helpers, server-side guards, and OAuth interceptors
41
44
 
42
45
  ### Design Principles
43
46
 
@@ -90,18 +93,26 @@ export const appRouter = defineRouter({
90
93
 
91
94
  ### 3. Configure Client (Next.js)
92
95
 
93
- #### Register Router Metadata and Errors in `api-client.ts`
96
+ #### Option A: Use the built-in `authApi` (Recommended)
97
+
98
+ ```typescript
99
+ import { authApi } from '@spfn/auth';
100
+
101
+ // Type-safe API calls for auth routes
102
+ const session = await authApi.getAuthSession.call({});
103
+ ```
104
+
105
+ #### Option B: Register Error Registry in Custom API Client
94
106
 
95
107
  ```typescript
96
108
  import { createApi } from '@spfn/core/nextjs';
97
109
  import type { AppRouter } from '@/server/router';
98
- import { appMetadata as authAppMetadata } from "@spfn/auth";
99
110
  import { authErrorRegistry } from "@spfn/auth/errors";
100
111
  import { appMetadata } from '@/server/router.metadata';
101
112
  import { errorRegistry } from "@spfn/core/errors";
102
113
 
103
114
  export const api = createApi<AppRouter>({
104
- metadata: { ...appMetadata, ...authAppMetadata },
115
+ metadata: appMetadata,
105
116
  errorRegistry: errorRegistry.concat(authErrorRegistry),
106
117
  });
107
118
  ```
@@ -122,16 +133,19 @@ SPFN_AUTH_JWT_EXPIRES_IN=7d
122
133
  SPFN_AUTH_BCRYPT_SALT_ROUNDS=10
123
134
  SPFN_AUTH_SESSION_TTL=7d
124
135
 
125
- # AWS SES (Email)
126
- SPFN_AUTH_AWS_REGION=ap-northeast-2
127
- SPFN_AUTH_AWS_SES_ACCESS_KEY_ID=AKIA...
128
- SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY=...
129
- SPFN_AUTH_AWS_SES_FROM_EMAIL=noreply@yourdomain.com
136
+ # Google OAuth
137
+ SPFN_AUTH_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
138
+ SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-...
139
+ SPFN_APP_URL=http://localhost:3000
140
+
141
+ # Google OAuth (Optional)
142
+ SPFN_AUTH_GOOGLE_SCOPES=email,profile,https://www.googleapis.com/auth/gmail.readonly
143
+ SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback
144
+ SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback
145
+ SPFN_AUTH_OAUTH_ERROR_URL=http://localhost:3000/auth/error?error={error}
130
146
 
131
- # AWS SNS (SMS)
132
- SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID=AKIA...
133
- SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY=...
134
- SPFN_AUTH_AWS_SNS_SENDER_ID=MyApp
147
+ # Email/SMS configure via @spfn/notification
148
+ # See @spfn/notification README for AWS SES/SNS settings
135
149
  ```
136
150
 
137
151
  ### 5. Run Migrations
@@ -144,6 +158,83 @@ pnpm spfn db generate
144
158
  pnpm spfn db migrate
145
159
  ```
146
160
 
161
+ ### 6. Admin Account Setup
162
+
163
+ Admin accounts are automatically created on server startup via `createAuthLifecycle()`.
164
+ Choose one of the following methods:
165
+
166
+ #### Method 1: JSON Format (Recommended)
167
+
168
+ Best for multiple accounts with full configuration:
169
+
170
+ ```bash
171
+ SPFN_AUTH_ADMIN_ACCOUNTS='[
172
+ {"email": "superadmin@example.com", "password": "secure-pass-1", "role": "superadmin"},
173
+ {"email": "admin@example.com", "password": "secure-pass-2", "role": "admin"},
174
+ {"email": "manager@example.com", "password": "secure-pass-3", "role": "user"}
175
+ ]'
176
+ ```
177
+
178
+ **JSON Schema:**
179
+ ```typescript
180
+ interface AdminAccountConfig {
181
+ email: string; // Required
182
+ password: string; // Required
183
+ role?: string; // Default: 'user' (options: 'user', 'admin', 'superadmin')
184
+ phone?: string; // Optional
185
+ passwordChangeRequired?: boolean; // Default: true
186
+ }
187
+ ```
188
+
189
+ #### Method 2: CSV Format
190
+
191
+ For multiple accounts with simpler configuration:
192
+
193
+ ```bash
194
+ SPFN_AUTH_ADMIN_EMAILS=admin@example.com,manager@example.com
195
+ SPFN_AUTH_ADMIN_PASSWORDS=admin-pass,manager-pass
196
+ SPFN_AUTH_ADMIN_ROLES=superadmin,admin
197
+ ```
198
+
199
+ #### Method 3: Single Account (Legacy)
200
+
201
+ Simplest format for a single superadmin:
202
+
203
+ ```bash
204
+ SPFN_AUTH_ADMIN_EMAIL=admin@example.com
205
+ SPFN_AUTH_ADMIN_PASSWORD=secure-password
206
+ ```
207
+
208
+ > **Note:** This method always creates a `superadmin` role account.
209
+
210
+ #### Default Behavior
211
+
212
+ All admin accounts created via environment variables have:
213
+ - `emailVerifiedAt`: Auto-verified (current timestamp)
214
+ - `passwordChangeRequired`: `true` (must change on first login)
215
+ - `status`: `active`
216
+
217
+ #### Programmatic Creation
218
+
219
+ You can also create admin accounts programmatically:
220
+
221
+ ```typescript
222
+ import { usersRepository, getRoleByName, hashPassword } from '@spfn/auth/server';
223
+
224
+ // After initializeAuth() has been called
225
+ const role = await getRoleByName('admin');
226
+ const passwordHash = await hashPassword('secure-password');
227
+
228
+ await usersRepository.create({
229
+ email: 'admin@example.com',
230
+ passwordHash,
231
+ roleId: role.id,
232
+ emailVerifiedAt: new Date(),
233
+ passwordChangeRequired: true,
234
+ status: 'active',
235
+ });
236
+ ```
237
+
147
238
  ---
148
239
 
149
240
  ## Architecture
@@ -335,20 +426,15 @@ packages/auth/
335
426
 
336
427
  ### Common Module (`@spfn/auth`)
337
428
 
338
- **Entities:**
429
+ **API Client:**
339
430
  ```typescript
340
- import {
341
- users,
342
- userPublicKeys,
343
- verificationCodes,
344
- roles,
345
- permissions,
346
- rolePermissions,
347
- userPermissions,
348
- userInvitations,
349
- userSocialAccounts,
350
- userProfiles
351
- } from '@spfn/auth';
431
+ import { authApi } from '@spfn/auth';
432
+
433
+ // Type-safe API calls
434
+ const session = await authApi.getAuthSession.call({});
435
+ const result = await authApi.login.call({
436
+ body: { email, password, fingerprint, publicKey, keyId }
437
+ });
352
438
  ```
353
439
 
354
440
  **Types:**
@@ -359,6 +445,9 @@ import type {
359
445
  VerificationCode,
360
446
  Role,
361
447
  Permission,
448
+ AuthSession,
449
+ UserProfile,
450
+ ProfileInfo,
362
451
  // ... etc
363
452
  } from '@spfn/auth';
364
453
  ```
@@ -380,6 +469,34 @@ import type {
380
469
  } from '@spfn/auth';
381
470
  ```
382
471
 
472
+ **Validation Patterns:**
473
+ ```typescript
474
+ import {
475
+ UUID_PATTERN,
476
+ EMAIL_PATTERN,
477
+ BASE64_PATTERN,
478
+ FINGERPRINT_PATTERN,
479
+ PHONE_PATTERN,
480
+ } from '@spfn/auth';
481
+ ```
482
+
483
+ **Route Map (for RPC Proxy):**
484
+ ```typescript
485
+ import { authRouteMap } from '@spfn/auth';
486
+
487
+ // Use in Next.js RPC proxy (app/api/rpc/[routeName]/route.ts)
488
+ import '@spfn/auth/nextjs/api'; // Auto-register auth interceptors
489
+ import { routeMap } from '@/generated/route-map';
490
+ import { authRouteMap } from '@spfn/auth';
491
+ import { createRpcProxy } from '@spfn/core/nextjs/proxy';
492
+
493
+ export const { GET, POST } = createRpcProxy({
494
+ routeMap: { ...routeMap, ...authRouteMap }
495
+ });
496
+ ```
497
+
498
+ > **Note:** Database entities (`users`, `userPublicKeys`, etc.) are exported from `@spfn/auth/server`, not the common module.
499
+
383
500
  ---
384
501
 
385
502
  ### Server Module (`@spfn/auth/server`)
@@ -456,22 +573,13 @@ import {
456
573
 
457
574
  // Session
458
575
  getAuthSessionService,
459
- getUserProfileService,
460
-
461
- // Email
462
- sendEmail,
463
- registerEmailProvider,
464
576
 
465
- // SMS
466
- sendSMS,
467
- registerSMSProvider,
577
+ // User Profile
578
+ getUserProfileService,
579
+ updateUserProfileService,
468
580
 
469
- // Email Templates
470
- registerEmailTemplates,
471
- getVerificationCodeTemplate,
472
- getWelcomeTemplate,
473
- getPasswordResetTemplate,
474
- getInvitationTemplate,
581
+ // OAuth - Google API Access
582
+ getGoogleAccessToken,
475
583
  } from '@spfn/auth/server';
476
584
  ```
477
585
 
@@ -494,11 +602,13 @@ import {
494
602
  ```typescript
495
603
  import {
496
604
  authenticate,
605
+ optionalAuth,
497
606
  requirePermissions,
607
+ requireAnyPermission,
498
608
  requireRole,
499
609
  } from '@spfn/auth/server';
500
610
 
501
- // Usage
611
+ // Usage - all permissions required
502
612
  app.bind(
503
613
  myContract,
504
614
  [authenticate, requirePermissions('user:delete')],
@@ -506,6 +616,27 @@ app.bind(
506
616
  // Handler
507
617
  }
508
618
  );
619
+
620
+ // Usage - any of the permissions
621
+ app.bind(
622
+ myContract,
623
+ [authenticate, requireAnyPermission('content:read', 'admin:access')],
624
+ async (c) => {
625
+ // User has either content:read OR admin:access
626
+ }
627
+ );
628
+
629
+ // Usage - optional auth (public route with optional user context)
630
+ // Auto-skips global 'auth' middleware — no .skip(['auth']) needed
631
+ export const getProducts = route.get('/products')
632
+ .use([optionalAuth])
633
+ .handler(async (c) => {
634
+ const auth = getOptionalAuth(c); // AuthContext | undefined
635
+ if (auth) {
636
+ return getPersonalizedProducts(auth.userId);
637
+ }
638
+ return getPublicProducts();
639
+ });
509
640
  ```
510
641
 
511
642
  **Helpers:**
@@ -513,6 +644,7 @@ app.bind(
513
644
  import {
514
645
  // Context
515
646
  getAuth,
647
+ getOptionalAuth,
516
648
  getUser,
517
649
  getUserId,
518
650
  getKeyId,
@@ -610,9 +742,11 @@ import {
610
742
  loginRegisterInterceptor,
611
743
  generalAuthInterceptor,
612
744
  keyRotationInterceptor,
745
+ oauthUrlInterceptor,
746
+ oauthFinalizeInterceptor,
613
747
  } from '@spfn/auth/nextjs/api';
614
748
 
615
- // Auto-registers interceptors on import
749
+ // Auto-registers interceptors on import (including OAuth)
616
750
  import '@spfn/auth/nextjs/api';
617
751
  ```
618
752
 
@@ -679,141 +813,24 @@ export default async function DashboardPage()
679
813
 
680
814
  ## Email & SMS Services
681
815
 
682
- ### Email Service
816
+ > **⚠️ DEPRECATED:** Email and SMS functionality has been moved to `@spfn/notification` package.
683
817
 
684
- The email service uses AWS SES by default, with fallback to console logging in development.
818
+ ### Migration Guide
685
819
 
686
- **Send Email:**
687
820
  ```typescript
688
- import { sendEmail } from '@spfn/auth/server';
821
+ // Before (deprecated)
822
+ import { sendEmail, sendSMS } from '@spfn/auth/server';
689
823
 
690
- await sendEmail({
691
- to: 'user@example.com',
692
- subject: 'Welcome!',
693
- text: 'Plain text content',
694
- html: '<h1>HTML content</h1>',
695
- purpose: 'welcome', // for logging
696
- });
824
+ // After (recommended)
825
+ import { sendEmail, sendSMS } from '@spfn/notification/server';
697
826
  ```
698
827
 
699
- **Custom Email Provider:**
700
- ```typescript
701
- import { registerEmailProvider } from '@spfn/auth/server';
828
+ The `@spfn/notification` package provides:
829
+ - Multi-channel support (Email, SMS, Slack, Push)
830
+ - Template system with variable substitution
831
+ - Multiple provider support (AWS SES, SNS, SendGrid, Twilio, etc.)
702
832
 
703
- // Register SendGrid provider
704
- registerEmailProvider({
705
- name: 'sendgrid',
706
- sendEmail: async ({ to, subject, text, html }) => {
707
- // Your SendGrid implementation
708
- return { success: true, messageId: '...' };
709
- },
710
- });
711
- ```
712
-
713
- ---
714
-
715
- ### SMS Service
716
-
717
- The SMS service uses AWS SNS by default.
718
-
719
- **Send SMS:**
720
- ```typescript
721
- import { sendSMS } from '@spfn/auth/server';
722
-
723
- await sendSMS({
724
- phone: '+821012345678', // E.164 format
725
- message: 'Your code is: 123456',
726
- purpose: 'verification',
727
- });
728
- ```
729
-
730
- **Custom SMS Provider:**
731
- ```typescript
732
- import { registerSMSProvider } from '@spfn/auth/server';
733
-
734
- // Register Twilio provider
735
- registerSMSProvider({
736
- name: 'twilio',
737
- sendSMS: async ({ phone, message }) => {
738
- // Your Twilio implementation
739
- return { success: true, messageId: '...' };
740
- },
741
- });
742
- ```
743
-
744
- ---
745
-
746
- ## Email Templates
747
-
748
- ### Built-in Templates
749
-
750
- | Template | Function | Purpose |
751
- |----------|----------|---------|
752
- | `verificationCode` | `getVerificationCodeTemplate` | Verification codes (registration, login, password reset) |
753
- | `welcome` | `getWelcomeTemplate` | Welcome email after registration |
754
- | `passwordReset` | `getPasswordResetTemplate` | Password reset link |
755
- | `invitation` | `getInvitationTemplate` | User invitation |
756
-
757
- **Usage:**
758
- ```typescript
759
- import { getVerificationCodeTemplate, sendEmail } from '@spfn/auth/server';
760
-
761
- const { subject, text, html } = getVerificationCodeTemplate({
762
- code: '123456',
763
- purpose: 'registration',
764
- expiresInMinutes: 5,
765
- appName: 'MyApp',
766
- });
767
-
768
- await sendEmail({ to: 'user@example.com', subject, text, html });
769
- ```
770
-
771
- ---
772
-
773
- ### Custom Templates
774
-
775
- Register custom templates to override defaults with your brand design:
776
-
777
- ```typescript
778
- import { registerEmailTemplates } from '@spfn/auth/server';
779
-
780
- // Register at app initialization (e.g., server.config.ts)
781
- registerEmailTemplates({
782
- // Override verification code template
783
- verificationCode: ({ code, purpose, expiresInMinutes, appName }) => ({
784
- subject: `[${appName}] Your verification code`,
785
- text: `Your code: ${code}\nExpires in ${expiresInMinutes} minutes.`,
786
- html: `
787
- <div style="font-family: Arial, sans-serif;">
788
- <img src="https://myapp.com/logo.png" alt="Logo" />
789
- <h1>Verification Code</h1>
790
- <div style="font-size: 32px; font-weight: bold;">${code}</div>
791
- <p>This code expires in ${expiresInMinutes} minutes.</p>
792
- </div>
793
- `,
794
- }),
795
-
796
- // Override invitation template
797
- invitation: ({ inviteLink, inviterName, roleName, appName }) => ({
798
- subject: `${inviterName} invited you to ${appName}`,
799
- text: `Accept invitation: ${inviteLink}`,
800
- html: `
801
- <h1>You're Invited!</h1>
802
- <p>${inviterName} invited you to join ${appName} as ${roleName}.</p>
803
- <a href="${inviteLink}">Accept Invitation</a>
804
- `,
805
- }),
806
- });
807
- ```
808
-
809
- **Template Parameters:**
810
-
811
- | Template | Parameters |
812
- |----------|------------|
813
- | `verificationCode` | `code`, `purpose`, `expiresInMinutes?`, `appName?` |
814
- | `welcome` | `email`, `appName?` |
815
- | `passwordReset` | `resetLink`, `expiresInMinutes?`, `appName?` |
816
- | `invitation` | `inviteLink`, `inviterName?`, `roleName?`, `appName?` |
833
+ For documentation, see `@spfn/notification` package README.
817
834
 
818
835
  ---
819
836
 
@@ -1027,6 +1044,499 @@ Change password.
1027
1044
 
1028
1045
  ---
1029
1046
 
1047
+ #### `GET /_auth/users/username/check`
1048
+
1049
+ Check if a username is available.
1050
+
1051
+ **Query:**
1052
+ ```typescript
1053
+ {
1054
+ username: string; // Min 1 char
1055
+ }
1056
+ ```
1057
+
1058
+ **Response:**
1059
+ ```typescript
1060
+ {
1061
+ available: boolean;
1062
+ }
1063
+ ```
1064
+
1065
+ ---
1066
+
1067
+ #### `PATCH /_auth/users/username`
1068
+
1069
+ Update authenticated user's username. Validates uniqueness before updating.
1070
+
1071
+ **Request:**
1072
+ ```typescript
1073
+ {
1074
+ username: string | null; // New username or null to clear
1075
+ }
1076
+ ```
1077
+
1078
+ **Response:** Updated user object.
1079
+
1080
+ **Errors:**
1081
+ - `409 UsernameAlreadyTakenError` - Username is already in use by another user
1082
+
1083
+ ---
1084
+
1085
+ ## Events
1086
+
1087
+ `@spfn/auth`는 `@spfn/core/event`를 사용하여 인증 관련 이벤트를 발행합니다. 이를 통해 로그인/회원가입 시 추가 로직(환영 이메일, 분석, 알림 등)을 디커플링된 방식으로 처리할 수 있습니다.
1088
+
1089
+ ### Available Events
1090
+
1091
+ | Event | Description | Trigger |
1092
+ |-------|-------------|---------|
1093
+ | `auth.login` | 로그인 성공 | 이메일/전화 로그인, OAuth 기존 사용자 |
1094
+ | `auth.register` | 회원가입 성공 | 이메일/전화 회원가입, OAuth 신규 사용자 |
1095
+
1096
+ ---
1097
+
1098
+ ### Event Payloads
1099
+
1100
+ #### `auth.login`
1101
+
1102
+ ```typescript
1103
+ {
1104
+ userId: string;
1105
+ provider: 'email' | 'phone' | 'google';
1106
+ email?: string;
1107
+ phone?: string;
1108
+ }
1109
+ ```
1110
+
1111
+ #### `auth.register`
1112
+
1113
+ ```typescript
1114
+ {
1115
+ userId: string;
1116
+ provider: 'email' | 'phone' | 'google';
1117
+ email?: string;
1118
+ phone?: string;
1119
+ metadata?: Record<string, unknown>; // 가입 시 전달된 커스텀 메타데이터
1120
+ }
1121
+ ```
1122
+
1123
+ `metadata`는 클라이언트가 register/OAuth 요청 body에 포함한 값이 그대로 전달됩니다.
1124
+ 레퍼럴 코드, UTM 파라미터 등 앱 고유 데이터를 이벤트 구독자에게 전달할 때 사용합니다.
1125
+
1126
+ ---
1127
+
1128
+ ### Subscribing to Events
1129
+
1130
+ ```typescript
1131
+ import { authLoginEvent, authRegisterEvent } from '@spfn/auth/server';
1132
+
1133
+ // 로그인 이벤트 구독
1134
+ authLoginEvent.subscribe(async (payload) => {
1135
+ console.log('User logged in:', payload.userId, payload.provider);
1136
+ await analytics.trackLogin(payload.userId);
1137
+ });
1138
+
1139
+ // 회원가입 이벤트 구독 (metadata 활용)
1140
+ authRegisterEvent.subscribe(async (payload) => {
1141
+ console.log('New user registered:', payload.userId);
1142
+ if (payload.email) {
1143
+ await emailService.sendWelcome(payload.email);
1144
+ }
1145
+
1146
+ // 레퍼럴 코드 처리
1147
+ const refCode = payload.metadata?.refCode as string;
1148
+ if (refCode) {
1149
+ await referralService.link(payload.userId, refCode);
1150
+ }
1151
+ });
1152
+ ```
1153
+
1154
+ 클라이언트에서 metadata를 전달하는 방법:
1155
+
1156
+ ```typescript
1157
+ // 이메일/전화 가입
1158
+ authApi.register.call({
1159
+ body: { email, password, metadata: { refCode: 'CODE', utm_source: 'google' } }
1160
+ });
1161
+
1162
+ // OAuth 가입
1163
+ authApi.oauthStart.call({
1164
+ body: { provider: 'google', returnUrl: '/dashboard', metadata: { refCode: 'CODE' } }
1165
+ });
1166
+ ```
1167
+
1168
+ ---
1169
+
1170
+ ### Job Integration
1171
+
1172
+ `@spfn/core/job`과 연동하여 백그라운드 작업을 실행할 수 있습니다.
1173
+
1174
+ ```typescript
1175
+ import { job, defineJobRouter } from '@spfn/core/job';
1176
+ import { authRegisterEvent } from '@spfn/auth/server';
1177
+
1178
+ // 회원가입 시 환영 이메일 발송 Job
1179
+ const sendWelcomeEmailJob = job('send-welcome-email')
1180
+ .on(authRegisterEvent)
1181
+ .handler(async ({ userId, email }) => {
1182
+ if (email) {
1183
+ await emailService.sendWelcome(email);
1184
+ }
1185
+ });
1186
+
1187
+ // 회원가입 시 기본 설정 생성 Job
1188
+ const createDefaultSettingsJob = job('create-default-settings')
1189
+ .on(authRegisterEvent)
1190
+ .handler(async ({ userId }) => {
1191
+ await settingsService.createDefaults(userId);
1192
+ });
1193
+
1194
+ export const jobRouter = defineJobRouter({
1195
+ sendWelcomeEmailJob,
1196
+ createDefaultSettingsJob,
1197
+ });
1198
+ ```
1199
+
1200
+ ---
1201
+
1202
+ ### Event Flow
1203
+
1204
+ ```
1205
+ ┌─────────────────────────────────────────────────────────────────┐
1206
+ │ loginService() / registerService() │
1207
+ │ oauthCallbackService() │
1208
+ └─────────────────────────────────────────────────────────────────┘
1209
+
1210
+
1211
+ authLoginEvent.emit()
1212
+ authRegisterEvent.emit()
1213
+
1214
+ ┌───────────────────┼───────────────────┐
1215
+ ▼ ▼ ▼
1216
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
1217
+ │ Backend │ │ Job │ │ SSE │
1218
+ │ Handler │ │ Queue │ │ Stream │
1219
+ └──────────┘ └──────────┘ └──────────┘
1220
+ .subscribe() .on(event) (optional)
1221
+ │ │
1222
+ ▼ ▼
1223
+ [Analytics, [Background
1224
+ Logging] Processing]
1225
+ ```
1226
+
1227
+ ---
1228
+
1229
+ ### Type Exports
1230
+
1231
+ ```typescript
1232
+ import type {
1233
+ AuthLoginPayload,
1234
+ AuthRegisterPayload,
1235
+ } from '@spfn/auth/server';
1236
+ ```
1237
+
1238
+ ---
1239
+
1240
+ ## OAuth Authentication
1241
+
1242
+ ### Overview
1243
+
1244
+ `@spfn/auth`는 OAuth 2.0 Authorization Code Flow를 지원합니다. 현재 Google OAuth가 구현되어 있으며, 다른 provider (GitHub, Kakao, Naver)는 동일한 패턴으로 확장 가능합니다.
1245
+
1246
+ **핵심 설계:**
1247
+ - 환경 변수만으로 설정 (`SPFN_AUTH_GOOGLE_CLIENT_ID`, `SPFN_AUTH_GOOGLE_CLIENT_SECRET`)
1248
+ - Next.js 인터셉터 기반 자동 세션 관리 (키쌍 생성 → pending session → full session)
1249
+ - 기존 이메일 계정과 자동 연결 (Google verified_email 확인 시에만)
1250
+
1251
+ ---
1252
+
1253
+ ### Authentication Flow
1254
+
1255
+ ```
1256
+ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
1257
+ │ Client │ │ Next.js RPC │ │ Backend │ │ Google │
1258
+ │ (Browser)│ │ (Interceptor)│ │ (SPFN) │ │ OAuth │
1259
+ └────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
1260
+ │ │ │ │
1261
+ │ 1. Click Login │ │ │
1262
+ ├──────────────────>│ │ │
1263
+ │ │ │ │
1264
+ │ 2. Generate keypair (ES256) │ │
1265
+ │ 3. Create encrypted state │ │
1266
+ │ (publicKey, keyId in JWE) │ │
1267
+ │ 4. Save privateKey to │ │
1268
+ │ pending session cookie │ │
1269
+ │ │ │ │
1270
+ │ │ 5. Forward with │ │
1271
+ │ │ state in body │ │
1272
+ │ ├─────────────────>│ │
1273
+ │ │ │ │
1274
+ │ │ 6. Return Google │ │
1275
+ │ │ Auth URL │ │
1276
+ │ │<─────────────────┤ │
1277
+ │ │ │ │
1278
+ │ 7. Redirect to Google │ │
1279
+ │<──────────────────┤ │ │
1280
+ │ │ │ │
1281
+ │ 8. User consents │ │ │
1282
+ ├───────────────────┼──────────────────┼────────────────>│
1283
+ │ │ │ │
1284
+ │ │ 9. Callback with code + state │
1285
+ │ │ │<────────────────┤
1286
+ │ │ │ │
1287
+ │ │ 10. Verify state, exchange code │
1288
+ │ │ Create/link user account │
1289
+ │ │ Register publicKey │
1290
+ │ │ │ │
1291
+ │ 11. Redirect to /auth/callback │ │
1292
+ │ ?userId=X&keyId=Y&returnUrl=/ │ │
1293
+ │<─────────────────────────────────────┤ │
1294
+ │ │ │ │
1295
+ │ 12. OAuthCallback │ │ │
1296
+ │ component │ │ │
1297
+ │ calls finalize│ │ │
1298
+ ├──────────────────>│ │ │
1299
+ │ │ │ │
1300
+ │ 13. Interceptor reads pending │ │
1301
+ │ session cookie, verifies │ │
1302
+ │ keyId match, creates full │ │
1303
+ │ session cookie │ │
1304
+ │ │ │ │
1305
+ │ 14. Session set, │ │ │
1306
+ │ redirect to │ │ │
1307
+ │ returnUrl │ │ │
1308
+ │<──────────────────┤ │ │
1309
+ │ │ │ │
1310
+ ```
1311
+
1312
+ ---
1313
+
1314
+ ### Setup
1315
+
1316
+ #### 1. Google Cloud Console
1317
+
1318
+ 1. [Google Cloud Console](https://console.cloud.google.com/) > APIs & Services > Credentials
1319
+ 2. Create OAuth 2.0 Client ID (Web application)
1320
+ 3. Add Authorized redirect URI: `http://localhost:8790/_auth/oauth/google/callback`
1321
+ 4. Copy Client ID and Client Secret
1322
+
1323
+ #### 2. Environment Variables
1324
+
1325
+ ```bash
1326
+ # Required
1327
+ SPFN_AUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
1328
+ SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
1329
+
1330
+ # Next.js app URL (for OAuth callback redirect)
1331
+ SPFN_APP_URL=http://localhost:3000
1332
+
1333
+ # Optional
1334
+ SPFN_AUTH_GOOGLE_SCOPES=email,profile # default (comma-separated)
1335
+ SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback # default
1336
+ SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback # default
1337
+ ```
1338
+
1339
+ #### 3. Next.js Callback Page
1340
+
1341
+ ```tsx
1342
+ // app/auth/callback/page.tsx
1343
+ export { OAuthCallback as default } from '@spfn/auth/nextjs/client';
1344
+ ```
1345
+
1346
+ #### 4. Login Button
1347
+
1348
+ ```typescript
1349
+ import { authApi } from '@spfn/auth';
1350
+
1351
+ const handleGoogleLogin = async () =>
1352
+ {
1353
+ const response = await authApi.getGoogleOAuthUrl.call({
1354
+ body: { returnUrl: '/dashboard' },
1355
+ });
1356
+ window.location.href = response.authUrl;
1357
+ };
1358
+ ```
1359
+
1360
+ ---
1361
+
1362
+ ### OAuth Routes
1363
+
1364
+ #### `GET /_auth/oauth/google`
1365
+
1366
+ Google OAuth 시작 (리다이렉트 방식). 브라우저를 Google 로그인 페이지로 직접 리다이렉트합니다.
1367
+
1368
+ **Query:**
1369
+ ```typescript
1370
+ {
1371
+ state: string; // Encrypted OAuth state (JWE)
1372
+ }
1373
+ ```
1374
+
1375
+ ---
1376
+
1377
+ #### `POST /_auth/oauth/google/url`
1378
+
1379
+ Google OAuth URL 획득 (인터셉터 방식). 인터셉터가 state를 자동 생성하여 주입합니다.
1380
+
1381
+ **Request:**
1382
+ ```typescript
1383
+ {
1384
+ returnUrl?: string; // Default: '/'
1385
+ }
1386
+ ```
1387
+
1388
+ **Response:**
1389
+ ```typescript
1390
+ {
1391
+ authUrl: string; // Google OAuth URL
1392
+ }
1393
+ ```
1394
+
1395
+ ---
1396
+
1397
+ #### `GET /_auth/oauth/google/callback`
1398
+
1399
+ Google에서 리다이렉트되는 콜백. code를 token으로 교환하고 사용자를 생성/연결합니다.
1400
+
1401
+ **Query (from Google):**
1402
+ ```typescript
1403
+ {
1404
+ code?: string; // Authorization code
1405
+ state?: string; // OAuth state
1406
+ error?: string; // Error code
1407
+ error_description?: string; // Error description
1408
+ }
1409
+ ```
1410
+
1411
+ **Result:** Next.js 콜백 페이지로 리다이렉트 (`/auth/callback?userId=X&keyId=Y&returnUrl=/`)
1412
+
1413
+ ---
1414
+
1415
+ #### `POST /_auth/oauth/finalize`
1416
+
1417
+ OAuth 세션 완료. 인터셉터가 pending session에서 full session을 생성합니다.
1418
+
1419
+ **Request:**
1420
+ ```typescript
1421
+ {
1422
+ userId: string;
1423
+ keyId: string;
1424
+ returnUrl?: string;
1425
+ }
1426
+ ```
1427
+
1428
+ **Response:**
1429
+ ```typescript
1430
+ {
1431
+ success: boolean;
1432
+ returnUrl: string;
1433
+ }
1434
+ ```
1435
+
1436
+ ---
1437
+
1438
+ #### `GET /_auth/oauth/providers`
1439
+
1440
+ 활성화된 OAuth provider 목록을 반환합니다.
1441
+
1442
+ **Response:**
1443
+ ```typescript
1444
+ {
1445
+ providers: ('google' | 'github' | 'kakao' | 'naver')[];
1446
+ }
1447
+ ```
1448
+
1449
+ ---
1450
+
1451
+ ### Google API Access
1452
+
1453
+ OAuth 로그인 후 저장된 access token으로 Google API를 호출할 수 있습니다.
1454
+
1455
+ #### Custom Scopes 설정
1456
+
1457
+ `SPFN_AUTH_GOOGLE_SCOPES` 환경변수로 추가 스코프를 요청합니다. 미설정 시 `email,profile`이 기본값입니다.
1458
+
1459
+ ```bash
1460
+ # Gmail + Calendar 읽기 권한 추가
1461
+ SPFN_AUTH_GOOGLE_SCOPES=email,profile,https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/calendar.readonly
1462
+ ```
1463
+
1464
+ > **Note:** Google Cloud Console에서 해당 API를 활성화해야 합니다.
1465
+
1466
+ #### Access Token 사용
1467
+
1468
+ `getGoogleAccessToken(userId)`은 유효한 access token을 반환합니다. 토큰이 만료 임박(5분 이내) 또는 만료 상태이면 자동으로 refresh token을 사용하여 갱신합니다.
1469
+
1470
+ ```typescript
1471
+ import { getGoogleAccessToken } from '@spfn/auth/server';
1472
+
1473
+ // 항상 유효한 토큰 반환 (만료 시 자동 갱신)
1474
+ const token = await getGoogleAccessToken(userId);
1475
+
1476
+ // Gmail API 호출
1477
+ const response = await fetch(
1478
+ 'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10',
1479
+ { headers: { Authorization: `Bearer ${token}` } }
1480
+ );
1481
+ const data = await response.json();
1482
+ ```
1483
+
1484
+ **에러 케이스:**
1485
+ - Google 계정 미연결 → `'No Google account linked'`
1486
+ - Refresh token 없음 → `'Google refresh token not available'` (재로그인 필요)
1487
+
1488
+ ---
1489
+
1490
+ ### Security
1491
+
1492
+ - **State 암호화**: JWE (A256GCM)로 state 파라미터 암호화. CSRF 방지용 nonce 포함.
1493
+ - **Pending Session**: OAuth 리다이렉트 중 privateKey를 JWE로 암호화한 HttpOnly 쿠키에 저장. 10분 TTL.
1494
+ - **KeyId 검증**: finalize 시 pending session의 keyId와 응답의 keyId 일치 확인.
1495
+ - **Email 검증**: `verified_email`이 true인 경우에만 기존 계정에 자동 연결. 미검증 이메일로 기존 계정 연결 시도 시 에러.
1496
+ - **Session Cookie**: `HttpOnly`, `Secure` (production), `SameSite=strict`.
1497
+
1498
+ ---
1499
+
1500
+ ### OAuthCallback Component
1501
+
1502
+ `@spfn/auth/nextjs/client`에서 제공하는 클라이언트 컴포넌트입니다.
1503
+
1504
+ ```tsx
1505
+ import { OAuthCallback } from '@spfn/auth/nextjs/client';
1506
+
1507
+ // 기본 사용
1508
+ export default function CallbackPage()
1509
+ {
1510
+ return <OAuthCallback />;
1511
+ }
1512
+
1513
+ // 커스터마이징
1514
+ export default function CallbackPage()
1515
+ {
1516
+ return (
1517
+ <OAuthCallback
1518
+ apiBasePath="/api/rpc"
1519
+ loadingComponent={<MySpinner />}
1520
+ errorComponent={(error) => <MyError message={error} />}
1521
+ onSuccess={(userId) => console.log('Logged in:', userId)}
1522
+ onError={(error) => console.error(error)}
1523
+ />
1524
+ );
1525
+ }
1526
+ ```
1527
+
1528
+ **Props:**
1529
+
1530
+ | Prop | Type | Default | Description |
1531
+ |------|------|---------|-------------|
1532
+ | `apiBasePath` | `string` | `'/api/rpc'` | RPC API base path |
1533
+ | `loadingComponent` | `ReactNode` | Built-in | 로딩 중 표시할 컴포넌트 |
1534
+ | `errorComponent` | `(error: string) => ReactNode` | Built-in | 에러 표시 컴포넌트 |
1535
+ | `onSuccess` | `(userId: string) => void` | - | 성공 콜백 |
1536
+ | `onError` | `(error: string) => void` | - | 에러 콜백 |
1537
+
1538
+ ---
1539
+
1030
1540
  ## Database Schema
1031
1541
 
1032
1542
  ### Core Tables
@@ -1040,6 +1550,7 @@ CREATE TABLE users (
1040
1550
  id BIGSERIAL PRIMARY KEY,
1041
1551
  email TEXT UNIQUE,
1042
1552
  phone TEXT UNIQUE,
1553
+ username TEXT UNIQUE,
1043
1554
  password_hash TEXT NOT NULL,
1044
1555
  password_change_required BOOLEAN DEFAULT false,
1045
1556
  role_id BIGINT REFERENCES roles(id) NOT NULL,
@@ -1058,6 +1569,7 @@ CREATE TABLE users (
1058
1569
 
1059
1570
  **Key Points:**
1060
1571
  - At least one of `email` OR `phone` required
1572
+ - `username` is unique and nullable (optional display/mention identifier)
1061
1573
  - `passwordHash` is bcrypt ($2b$10$..., 60 chars)
1062
1574
  - `roleId` references roles table (NOT NULL)
1063
1575
 
@@ -1167,7 +1679,7 @@ CREATE TABLE permissions (
1167
1679
 
1168
1680
  **Built-in Permissions:**
1169
1681
  - `auth:self:manage`
1170
- - `user:read`, `user:write`, `user:delete`
1682
+ - `user:read`, `user:write`, `user:delete`, `user:invite`
1171
1683
  - `rbac:role:manage`, `rbac:permission:manage`
1172
1684
 
1173
1685
  ---
@@ -1260,7 +1772,7 @@ CREATE TABLE user_profiles (
1260
1772
 
1261
1773
  #### `user_social_accounts`
1262
1774
 
1263
- OAuth provider accounts (future feature).
1775
+ OAuth provider accounts (Google, GitHub, etc.).
1264
1776
 
1265
1777
  ```sql
1266
1778
  CREATE TABLE user_social_accounts (
@@ -1327,7 +1839,7 @@ await initializeAuth({
1327
1839
 
1328
1840
  **Permissions:**
1329
1841
  - `auth:self:manage` - Change password, rotate keys
1330
- - `user:read`, `user:write`, `user:delete`
1842
+ - `user:read`, `user:write`, `user:delete`, `user:invite`
1331
1843
  - `rbac:role:manage`, `rbac:permission:manage`
1332
1844
 
1333
1845
  ---
@@ -1335,7 +1847,7 @@ await initializeAuth({
1335
1847
  ### Middleware Usage
1336
1848
 
1337
1849
  ```typescript
1338
- import { authenticate, requirePermissions, requireRole } from '@spfn/auth/server';
1850
+ import { authenticate, requirePermissions, requireAnyPermission, requireRole } from '@spfn/auth/server';
1339
1851
 
1340
1852
  // Single permission
1341
1853
  app.bind(
@@ -1355,6 +1867,15 @@ app.bind(
1355
1867
  }
1356
1868
  );
1357
1869
 
1870
+ // Any of the permissions (at least one required)
1871
+ app.bind(
1872
+ viewContentContract,
1873
+ [authenticate, requireAnyPermission('content:read', 'admin:access')],
1874
+ async (c) => {
1875
+ // User has either content:read OR admin:access
1876
+ }
1877
+ );
1878
+
1358
1879
  // Role-based
1359
1880
  app.bind(
1360
1881
  adminDashboardContract,
@@ -1468,10 +1989,22 @@ import '@spfn/auth/nextjs/api';
1468
1989
  **Target Routes:**
1469
1990
  - `/_auth/login`, `/_auth/register` - Login/register interceptor
1470
1991
  - `/_auth/keys/rotate` - Key rotation interceptor
1992
+ - `/_auth/oauth/:provider/url` - OAuth URL interceptor (keypair + state generation)
1993
+ - `/_auth/oauth/finalize` - OAuth finalize interceptor (pending session → full session)
1471
1994
  - All other authenticated routes - General auth interceptor
1472
1995
 
1473
1996
  ---
1474
1997
 
1998
+ ### OAuth Client Component (`@spfn/auth/nextjs/client`)
1999
+
2000
+ ```typescript
2001
+ import { OAuthCallback, type OAuthCallbackProps } from '@spfn/auth/nextjs/client';
2002
+ ```
2003
+
2004
+ OAuth 콜백 페이지용 `'use client'` 컴포넌트. 자세한 사용법은 [OAuth Authentication](#oauth-authentication) 섹션 참조.
2005
+
2006
+ ---
2007
+
1475
2008
  ## Testing
1476
2009
 
1477
2010
  ### Setup Test Environment
@@ -1817,7 +2350,7 @@ ls migrations/
1817
2350
 
1818
2351
  - [ ] **React hooks** - useAuth, useSession, usePermissions
1819
2352
  - [ ] **UI components** - LoginForm, RegisterForm, AuthProvider
1820
- - [ ] **OAuth integration** - Google, GitHub, etc.
2353
+ - [x] **OAuth integration** - Google (implemented), GitHub/Kakao/Naver (planned)
1821
2354
  - [ ] **2FA support** - TOTP/authenticator apps
1822
2355
  - [ ] **Password reset flow** - Complete email-based reset
1823
2356
  - [ ] **Email change flow** - Verification for email updates
@@ -1957,6 +2490,6 @@ MIT License - See LICENSE file for details.
1957
2490
 
1958
2491
  ---
1959
2492
 
1960
- **Last Updated:** 2025-12-07
1961
- **Document Version:** 2.2.0 (Technical Documentation)
1962
- **Package Version:** 0.1.0-alpha.88
2493
+ **Last Updated:** 2026-02-23
2494
+ **Document Version:** 2.6.0 (Technical Documentation)
2495
+ **Package Version:** 0.2.0-beta.15