@spfn/auth 0.2.0-beta.10 → 0.2.0-beta.12

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.12
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,13 @@
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
+ - [OAuth Authentication](#oauth-authentication)
21
22
  - [Database Schema](#database-schema)
22
23
  - [RBAC System](#rbac-system)
23
24
  - [Next.js Adapter](#nextjs-adapter)
@@ -34,10 +35,11 @@
34
35
 
35
36
  - **Asymmetric JWT Authentication** - Client-signed tokens using ES256/RS256
36
37
  - **User Management** - Email/phone-based identity with bcrypt hashing
38
+ - **OAuth Authentication** - Google OAuth 2.0 (Authorization Code Flow), extensible to other providers
37
39
  - **Multi-Factor Authentication** - OTP verification via email/SMS
38
40
  - **Session Management** - Public key rotation with 90-day expiry
39
41
  - **Role-Based Access Control** - Flexible RBAC with runtime role/permission management
40
- - **Next.js Integration** - Session helpers and server-side guards
42
+ - **Next.js Integration** - Session helpers, server-side guards, and OAuth interceptors
41
43
 
42
44
  ### Design Principles
43
45
 
@@ -90,18 +92,26 @@ export const appRouter = defineRouter({
90
92
 
91
93
  ### 3. Configure Client (Next.js)
92
94
 
93
- #### Register Router Metadata and Errors in `api-client.ts`
95
+ #### Option A: Use the built-in `authApi` (Recommended)
96
+
97
+ ```typescript
98
+ import { authApi } from '@spfn/auth';
99
+
100
+ // Type-safe API calls for auth routes
101
+ const session = await authApi.getAuthSession.call({});
102
+ ```
103
+
104
+ #### Option B: Register Error Registry in Custom API Client
94
105
 
95
106
  ```typescript
96
107
  import { createApi } from '@spfn/core/nextjs';
97
108
  import type { AppRouter } from '@/server/router';
98
- import { appMetadata as authAppMetadata } from "@spfn/auth";
99
109
  import { authErrorRegistry } from "@spfn/auth/errors";
100
110
  import { appMetadata } from '@/server/router.metadata';
101
111
  import { errorRegistry } from "@spfn/core/errors";
102
112
 
103
113
  export const api = createApi<AppRouter>({
104
- metadata: { ...appMetadata, ...authAppMetadata },
114
+ metadata: appMetadata,
105
115
  errorRegistry: errorRegistry.concat(authErrorRegistry),
106
116
  });
107
117
  ```
@@ -122,6 +132,16 @@ SPFN_AUTH_JWT_EXPIRES_IN=7d
122
132
  SPFN_AUTH_BCRYPT_SALT_ROUNDS=10
123
133
  SPFN_AUTH_SESSION_TTL=7d
124
134
 
135
+ # Google OAuth
136
+ SPFN_AUTH_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
137
+ SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-...
138
+ SPFN_APP_URL=http://localhost:3000
139
+
140
+ # Google OAuth (Optional)
141
+ SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback
142
+ SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback
143
+ SPFN_AUTH_OAUTH_ERROR_URL=http://localhost:3000/auth/error?error={error}
144
+
125
145
  # AWS SES (Email)
126
146
  SPFN_AUTH_AWS_REGION=ap-northeast-2
127
147
  SPFN_AUTH_AWS_SES_ACCESS_KEY_ID=AKIA...
@@ -144,6 +164,83 @@ pnpm spfn db generate
144
164
  pnpm spfn db migrate
145
165
  ```
146
166
 
167
+ ### 6. Admin Account Setup
168
+
169
+ Admin accounts are automatically created on server startup via `createAuthLifecycle()`.
170
+ Choose one of the following methods:
171
+
172
+ #### Method 1: JSON Format (Recommended)
173
+
174
+ Best for multiple accounts with full configuration:
175
+
176
+ ```bash
177
+ SPFN_AUTH_ADMIN_ACCOUNTS='[
178
+ {"email": "superadmin@example.com", "password": "secure-pass-1", "role": "superadmin"},
179
+ {"email": "admin@example.com", "password": "secure-pass-2", "role": "admin"},
180
+ {"email": "manager@example.com", "password": "secure-pass-3", "role": "user"}
181
+ ]'
182
+ ```
183
+
184
+ **JSON Schema:**
185
+ ```typescript
186
+ interface AdminAccountConfig {
187
+ email: string; // Required
188
+ password: string; // Required
189
+ role?: string; // Default: 'user' (options: 'user', 'admin', 'superadmin')
190
+ phone?: string; // Optional
191
+ passwordChangeRequired?: boolean; // Default: true
192
+ }
193
+ ```
194
+
195
+ #### Method 2: CSV Format
196
+
197
+ For multiple accounts with simpler configuration:
198
+
199
+ ```bash
200
+ SPFN_AUTH_ADMIN_EMAILS=admin@example.com,manager@example.com
201
+ SPFN_AUTH_ADMIN_PASSWORDS=admin-pass,manager-pass
202
+ SPFN_AUTH_ADMIN_ROLES=superadmin,admin
203
+ ```
204
+
205
+ #### Method 3: Single Account (Legacy)
206
+
207
+ Simplest format for a single superadmin:
208
+
209
+ ```bash
210
+ SPFN_AUTH_ADMIN_EMAIL=admin@example.com
211
+ SPFN_AUTH_ADMIN_PASSWORD=secure-password
212
+ ```
213
+
214
+ > **Note:** This method always creates a `superadmin` role account.
215
+
216
+ #### Default Behavior
217
+
218
+ All admin accounts created via environment variables have:
219
+ - `emailVerifiedAt`: Auto-verified (current timestamp)
220
+ - `passwordChangeRequired`: `true` (must change on first login)
221
+ - `status`: `active`
222
+
223
+ #### Programmatic Creation
224
+
225
+ You can also create admin accounts programmatically:
226
+
227
+ ```typescript
228
+ import { usersRepository, getRoleByName, hashPassword } from '@spfn/auth/server';
229
+
230
+ // After initializeAuth() has been called
231
+ const role = await getRoleByName('admin');
232
+ const passwordHash = await hashPassword('secure-password');
233
+
234
+ await usersRepository.create({
235
+ email: 'admin@example.com',
236
+ passwordHash,
237
+ roleId: role.id,
238
+ emailVerifiedAt: new Date(),
239
+ passwordChangeRequired: true,
240
+ status: 'active',
241
+ });
242
+ ```
243
+
147
244
  ---
148
245
 
149
246
  ## Architecture
@@ -335,20 +432,15 @@ packages/auth/
335
432
 
336
433
  ### Common Module (`@spfn/auth`)
337
434
 
338
- **Entities:**
435
+ **API Client:**
339
436
  ```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';
437
+ import { authApi } from '@spfn/auth';
438
+
439
+ // Type-safe API calls
440
+ const session = await authApi.getAuthSession.call({});
441
+ const result = await authApi.login.call({
442
+ body: { email, password, fingerprint, publicKey, keyId }
443
+ });
352
444
  ```
353
445
 
354
446
  **Types:**
@@ -359,6 +451,9 @@ import type {
359
451
  VerificationCode,
360
452
  Role,
361
453
  Permission,
454
+ AuthSession,
455
+ UserProfile,
456
+ ProfileInfo,
362
457
  // ... etc
363
458
  } from '@spfn/auth';
364
459
  ```
@@ -380,6 +475,34 @@ import type {
380
475
  } from '@spfn/auth';
381
476
  ```
382
477
 
478
+ **Validation Patterns:**
479
+ ```typescript
480
+ import {
481
+ UUID_PATTERN,
482
+ EMAIL_PATTERN,
483
+ BASE64_PATTERN,
484
+ FINGERPRINT_PATTERN,
485
+ PHONE_PATTERN,
486
+ } from '@spfn/auth';
487
+ ```
488
+
489
+ **Route Map (for RPC Proxy):**
490
+ ```typescript
491
+ import { authRouteMap } from '@spfn/auth';
492
+
493
+ // Use in Next.js RPC proxy (app/api/rpc/[routeName]/route.ts)
494
+ import '@spfn/auth/nextjs/api'; // Auto-register auth interceptors
495
+ import { routeMap } from '@/generated/route-map';
496
+ import { authRouteMap } from '@spfn/auth';
497
+ import { createRpcProxy } from '@spfn/core/nextjs/proxy';
498
+
499
+ export const { GET, POST } = createRpcProxy({
500
+ routeMap: { ...routeMap, ...authRouteMap }
501
+ });
502
+ ```
503
+
504
+ > **Note:** Database entities (`users`, `userPublicKeys`, etc.) are exported from `@spfn/auth/server`, not the common module.
505
+
383
506
  ---
384
507
 
385
508
  ### Server Module (`@spfn/auth/server`)
@@ -456,22 +579,10 @@ import {
456
579
 
457
580
  // Session
458
581
  getAuthSessionService,
459
- getUserProfileService,
460
582
 
461
- // Email
462
- sendEmail,
463
- registerEmailProvider,
464
-
465
- // SMS
466
- sendSMS,
467
- registerSMSProvider,
468
-
469
- // Email Templates
470
- registerEmailTemplates,
471
- getVerificationCodeTemplate,
472
- getWelcomeTemplate,
473
- getPasswordResetTemplate,
474
- getInvitationTemplate,
583
+ // User Profile
584
+ getUserProfileService,
585
+ updateUserProfileService,
475
586
  } from '@spfn/auth/server';
476
587
  ```
477
588
 
@@ -495,10 +606,11 @@ import {
495
606
  import {
496
607
  authenticate,
497
608
  requirePermissions,
609
+ requireAnyPermission,
498
610
  requireRole,
499
611
  } from '@spfn/auth/server';
500
612
 
501
- // Usage
613
+ // Usage - all permissions required
502
614
  app.bind(
503
615
  myContract,
504
616
  [authenticate, requirePermissions('user:delete')],
@@ -506,6 +618,15 @@ app.bind(
506
618
  // Handler
507
619
  }
508
620
  );
621
+
622
+ // Usage - any of the permissions
623
+ app.bind(
624
+ myContract,
625
+ [authenticate, requireAnyPermission('content:read', 'admin:access')],
626
+ async (c) => {
627
+ // User has either content:read OR admin:access
628
+ }
629
+ );
509
630
  ```
510
631
 
511
632
  **Helpers:**
@@ -610,9 +731,11 @@ import {
610
731
  loginRegisterInterceptor,
611
732
  generalAuthInterceptor,
612
733
  keyRotationInterceptor,
734
+ oauthUrlInterceptor,
735
+ oauthFinalizeInterceptor,
613
736
  } from '@spfn/auth/nextjs/api';
614
737
 
615
- // Auto-registers interceptors on import
738
+ // Auto-registers interceptors on import (including OAuth)
616
739
  import '@spfn/auth/nextjs/api';
617
740
  ```
618
741
 
@@ -679,141 +802,24 @@ export default async function DashboardPage()
679
802
 
680
803
  ## Email & SMS Services
681
804
 
682
- ### Email Service
683
-
684
- The email service uses AWS SES by default, with fallback to console logging in development.
685
-
686
- **Send Email:**
687
- ```typescript
688
- import { sendEmail } from '@spfn/auth/server';
689
-
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
- });
697
- ```
698
-
699
- **Custom Email Provider:**
700
- ```typescript
701
- import { registerEmailProvider } from '@spfn/auth/server';
702
-
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
805
+ > **⚠️ DEPRECATED:** Email and SMS functionality has been moved to `@spfn/notification` package.
747
806
 
748
- ### Built-in Templates
807
+ ### Migration Guide
749
808
 
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
809
  ```typescript
759
- import { getVerificationCodeTemplate, sendEmail } from '@spfn/auth/server';
810
+ // Before (deprecated)
811
+ import { sendEmail, sendSMS } from '@spfn/auth/server';
760
812
 
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 });
813
+ // After (recommended)
814
+ import { sendEmail, sendSMS } from '@spfn/notification/server';
769
815
  ```
770
816
 
771
- ---
772
-
773
- ### Custom Templates
817
+ The `@spfn/notification` package provides:
818
+ - Multi-channel support (Email, SMS, Slack, Push)
819
+ - Template system with variable substitution
820
+ - Multiple provider support (AWS SES, SNS, SendGrid, Twilio, etc.)
774
821
 
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?` |
822
+ For documentation, see `@spfn/notification` package README.
817
823
 
818
824
  ---
819
825
 
@@ -1027,6 +1033,266 @@ Change password.
1027
1033
 
1028
1034
  ---
1029
1035
 
1036
+ ## OAuth Authentication
1037
+
1038
+ ### Overview
1039
+
1040
+ `@spfn/auth`는 OAuth 2.0 Authorization Code Flow를 지원합니다. 현재 Google OAuth가 구현되어 있으며, 다른 provider (GitHub, Kakao, Naver)는 동일한 패턴으로 확장 가능합니다.
1041
+
1042
+ **핵심 설계:**
1043
+ - 환경 변수만으로 설정 (`SPFN_AUTH_GOOGLE_CLIENT_ID`, `SPFN_AUTH_GOOGLE_CLIENT_SECRET`)
1044
+ - Next.js 인터셉터 기반 자동 세션 관리 (키쌍 생성 → pending session → full session)
1045
+ - 기존 이메일 계정과 자동 연결 (Google verified_email 확인 시에만)
1046
+
1047
+ ---
1048
+
1049
+ ### Authentication Flow
1050
+
1051
+ ```
1052
+ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
1053
+ │ Client │ │ Next.js RPC │ │ Backend │ │ Google │
1054
+ │ (Browser)│ │ (Interceptor)│ │ (SPFN) │ │ OAuth │
1055
+ └────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
1056
+ │ │ │ │
1057
+ │ 1. Click Login │ │ │
1058
+ ├──────────────────>│ │ │
1059
+ │ │ │ │
1060
+ │ 2. Generate keypair (ES256) │ │
1061
+ │ 3. Create encrypted state │ │
1062
+ │ (publicKey, keyId in JWE) │ │
1063
+ │ 4. Save privateKey to │ │
1064
+ │ pending session cookie │ │
1065
+ │ │ │ │
1066
+ │ │ 5. Forward with │ │
1067
+ │ │ state in body │ │
1068
+ │ ├─────────────────>│ │
1069
+ │ │ │ │
1070
+ │ │ 6. Return Google │ │
1071
+ │ │ Auth URL │ │
1072
+ │ │<─────────────────┤ │
1073
+ │ │ │ │
1074
+ │ 7. Redirect to Google │ │
1075
+ │<──────────────────┤ │ │
1076
+ │ │ │ │
1077
+ │ 8. User consents │ │ │
1078
+ ├───────────────────┼──────────────────┼────────────────>│
1079
+ │ │ │ │
1080
+ │ │ 9. Callback with code + state │
1081
+ │ │ │<────────────────┤
1082
+ │ │ │ │
1083
+ │ │ 10. Verify state, exchange code │
1084
+ │ │ Create/link user account │
1085
+ │ │ Register publicKey │
1086
+ │ │ │ │
1087
+ │ 11. Redirect to /auth/callback │ │
1088
+ │ ?userId=X&keyId=Y&returnUrl=/ │ │
1089
+ │<─────────────────────────────────────┤ │
1090
+ │ │ │ │
1091
+ │ 12. OAuthCallback │ │ │
1092
+ │ component │ │ │
1093
+ │ calls finalize│ │ │
1094
+ ├──────────────────>│ │ │
1095
+ │ │ │ │
1096
+ │ 13. Interceptor reads pending │ │
1097
+ │ session cookie, verifies │ │
1098
+ │ keyId match, creates full │ │
1099
+ │ session cookie │ │
1100
+ │ │ │ │
1101
+ │ 14. Session set, │ │ │
1102
+ │ redirect to │ │ │
1103
+ │ returnUrl │ │ │
1104
+ │<──────────────────┤ │ │
1105
+ │ │ │ │
1106
+ ```
1107
+
1108
+ ---
1109
+
1110
+ ### Setup
1111
+
1112
+ #### 1. Google Cloud Console
1113
+
1114
+ 1. [Google Cloud Console](https://console.cloud.google.com/) > APIs & Services > Credentials
1115
+ 2. Create OAuth 2.0 Client ID (Web application)
1116
+ 3. Add Authorized redirect URI: `http://localhost:8790/_auth/oauth/google/callback`
1117
+ 4. Copy Client ID and Client Secret
1118
+
1119
+ #### 2. Environment Variables
1120
+
1121
+ ```bash
1122
+ # Required
1123
+ SPFN_AUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
1124
+ SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
1125
+
1126
+ # Next.js app URL (for OAuth callback redirect)
1127
+ SPFN_APP_URL=http://localhost:3000
1128
+
1129
+ # Optional
1130
+ SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback # default
1131
+ SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback # default
1132
+ ```
1133
+
1134
+ #### 3. Next.js Callback Page
1135
+
1136
+ ```tsx
1137
+ // app/auth/callback/page.tsx
1138
+ export { OAuthCallback as default } from '@spfn/auth/nextjs/client';
1139
+ ```
1140
+
1141
+ #### 4. Login Button
1142
+
1143
+ ```typescript
1144
+ import { authApi } from '@spfn/auth';
1145
+
1146
+ const handleGoogleLogin = async () =>
1147
+ {
1148
+ const response = await authApi.getGoogleOAuthUrl.call({
1149
+ body: { returnUrl: '/dashboard' },
1150
+ });
1151
+ window.location.href = response.authUrl;
1152
+ };
1153
+ ```
1154
+
1155
+ ---
1156
+
1157
+ ### OAuth Routes
1158
+
1159
+ #### `GET /_auth/oauth/google`
1160
+
1161
+ Google OAuth 시작 (리다이렉트 방식). 브라우저를 Google 로그인 페이지로 직접 리다이렉트합니다.
1162
+
1163
+ **Query:**
1164
+ ```typescript
1165
+ {
1166
+ state: string; // Encrypted OAuth state (JWE)
1167
+ }
1168
+ ```
1169
+
1170
+ ---
1171
+
1172
+ #### `POST /_auth/oauth/google/url`
1173
+
1174
+ Google OAuth URL 획득 (인터셉터 방식). 인터셉터가 state를 자동 생성하여 주입합니다.
1175
+
1176
+ **Request:**
1177
+ ```typescript
1178
+ {
1179
+ returnUrl?: string; // Default: '/'
1180
+ }
1181
+ ```
1182
+
1183
+ **Response:**
1184
+ ```typescript
1185
+ {
1186
+ authUrl: string; // Google OAuth URL
1187
+ }
1188
+ ```
1189
+
1190
+ ---
1191
+
1192
+ #### `GET /_auth/oauth/google/callback`
1193
+
1194
+ Google에서 리다이렉트되는 콜백. code를 token으로 교환하고 사용자를 생성/연결합니다.
1195
+
1196
+ **Query (from Google):**
1197
+ ```typescript
1198
+ {
1199
+ code?: string; // Authorization code
1200
+ state?: string; // OAuth state
1201
+ error?: string; // Error code
1202
+ error_description?: string; // Error description
1203
+ }
1204
+ ```
1205
+
1206
+ **Result:** Next.js 콜백 페이지로 리다이렉트 (`/auth/callback?userId=X&keyId=Y&returnUrl=/`)
1207
+
1208
+ ---
1209
+
1210
+ #### `POST /_auth/oauth/finalize`
1211
+
1212
+ OAuth 세션 완료. 인터셉터가 pending session에서 full session을 생성합니다.
1213
+
1214
+ **Request:**
1215
+ ```typescript
1216
+ {
1217
+ userId: string;
1218
+ keyId: string;
1219
+ returnUrl?: string;
1220
+ }
1221
+ ```
1222
+
1223
+ **Response:**
1224
+ ```typescript
1225
+ {
1226
+ success: boolean;
1227
+ returnUrl: string;
1228
+ }
1229
+ ```
1230
+
1231
+ ---
1232
+
1233
+ #### `GET /_auth/oauth/providers`
1234
+
1235
+ 활성화된 OAuth provider 목록을 반환합니다.
1236
+
1237
+ **Response:**
1238
+ ```typescript
1239
+ {
1240
+ providers: ('google' | 'github' | 'kakao' | 'naver')[];
1241
+ }
1242
+ ```
1243
+
1244
+ ---
1245
+
1246
+ ### Security
1247
+
1248
+ - **State 암호화**: JWE (A256GCM)로 state 파라미터 암호화. CSRF 방지용 nonce 포함.
1249
+ - **Pending Session**: OAuth 리다이렉트 중 privateKey를 JWE로 암호화한 HttpOnly 쿠키에 저장. 10분 TTL.
1250
+ - **KeyId 검증**: finalize 시 pending session의 keyId와 응답의 keyId 일치 확인.
1251
+ - **Email 검증**: `verified_email`이 true인 경우에만 기존 계정에 자동 연결. 미검증 이메일로 기존 계정 연결 시도 시 에러.
1252
+ - **Session Cookie**: `HttpOnly`, `Secure` (production), `SameSite=strict`.
1253
+
1254
+ ---
1255
+
1256
+ ### OAuthCallback Component
1257
+
1258
+ `@spfn/auth/nextjs/client`에서 제공하는 클라이언트 컴포넌트입니다.
1259
+
1260
+ ```tsx
1261
+ import { OAuthCallback } from '@spfn/auth/nextjs/client';
1262
+
1263
+ // 기본 사용
1264
+ export default function CallbackPage()
1265
+ {
1266
+ return <OAuthCallback />;
1267
+ }
1268
+
1269
+ // 커스터마이징
1270
+ export default function CallbackPage()
1271
+ {
1272
+ return (
1273
+ <OAuthCallback
1274
+ apiBasePath="/api/rpc"
1275
+ loadingComponent={<MySpinner />}
1276
+ errorComponent={(error) => <MyError message={error} />}
1277
+ onSuccess={(userId) => console.log('Logged in:', userId)}
1278
+ onError={(error) => console.error(error)}
1279
+ />
1280
+ );
1281
+ }
1282
+ ```
1283
+
1284
+ **Props:**
1285
+
1286
+ | Prop | Type | Default | Description |
1287
+ |------|------|---------|-------------|
1288
+ | `apiBasePath` | `string` | `'/api/rpc'` | RPC API base path |
1289
+ | `loadingComponent` | `ReactNode` | Built-in | 로딩 중 표시할 컴포넌트 |
1290
+ | `errorComponent` | `(error: string) => ReactNode` | Built-in | 에러 표시 컴포넌트 |
1291
+ | `onSuccess` | `(userId: string) => void` | - | 성공 콜백 |
1292
+ | `onError` | `(error: string) => void` | - | 에러 콜백 |
1293
+
1294
+ ---
1295
+
1030
1296
  ## Database Schema
1031
1297
 
1032
1298
  ### Core Tables
@@ -1167,7 +1433,7 @@ CREATE TABLE permissions (
1167
1433
 
1168
1434
  **Built-in Permissions:**
1169
1435
  - `auth:self:manage`
1170
- - `user:read`, `user:write`, `user:delete`
1436
+ - `user:read`, `user:write`, `user:delete`, `user:invite`
1171
1437
  - `rbac:role:manage`, `rbac:permission:manage`
1172
1438
 
1173
1439
  ---
@@ -1260,7 +1526,7 @@ CREATE TABLE user_profiles (
1260
1526
 
1261
1527
  #### `user_social_accounts`
1262
1528
 
1263
- OAuth provider accounts (future feature).
1529
+ OAuth provider accounts (Google, GitHub, etc.).
1264
1530
 
1265
1531
  ```sql
1266
1532
  CREATE TABLE user_social_accounts (
@@ -1327,7 +1593,7 @@ await initializeAuth({
1327
1593
 
1328
1594
  **Permissions:**
1329
1595
  - `auth:self:manage` - Change password, rotate keys
1330
- - `user:read`, `user:write`, `user:delete`
1596
+ - `user:read`, `user:write`, `user:delete`, `user:invite`
1331
1597
  - `rbac:role:manage`, `rbac:permission:manage`
1332
1598
 
1333
1599
  ---
@@ -1335,7 +1601,7 @@ await initializeAuth({
1335
1601
  ### Middleware Usage
1336
1602
 
1337
1603
  ```typescript
1338
- import { authenticate, requirePermissions, requireRole } from '@spfn/auth/server';
1604
+ import { authenticate, requirePermissions, requireAnyPermission, requireRole } from '@spfn/auth/server';
1339
1605
 
1340
1606
  // Single permission
1341
1607
  app.bind(
@@ -1355,6 +1621,15 @@ app.bind(
1355
1621
  }
1356
1622
  );
1357
1623
 
1624
+ // Any of the permissions (at least one required)
1625
+ app.bind(
1626
+ viewContentContract,
1627
+ [authenticate, requireAnyPermission('content:read', 'admin:access')],
1628
+ async (c) => {
1629
+ // User has either content:read OR admin:access
1630
+ }
1631
+ );
1632
+
1358
1633
  // Role-based
1359
1634
  app.bind(
1360
1635
  adminDashboardContract,
@@ -1468,10 +1743,22 @@ import '@spfn/auth/nextjs/api';
1468
1743
  **Target Routes:**
1469
1744
  - `/_auth/login`, `/_auth/register` - Login/register interceptor
1470
1745
  - `/_auth/keys/rotate` - Key rotation interceptor
1746
+ - `/_auth/oauth/:provider/url` - OAuth URL interceptor (keypair + state generation)
1747
+ - `/_auth/oauth/finalize` - OAuth finalize interceptor (pending session → full session)
1471
1748
  - All other authenticated routes - General auth interceptor
1472
1749
 
1473
1750
  ---
1474
1751
 
1752
+ ### OAuth Client Component (`@spfn/auth/nextjs/client`)
1753
+
1754
+ ```typescript
1755
+ import { OAuthCallback, type OAuthCallbackProps } from '@spfn/auth/nextjs/client';
1756
+ ```
1757
+
1758
+ OAuth 콜백 페이지용 `'use client'` 컴포넌트. 자세한 사용법은 [OAuth Authentication](#oauth-authentication) 섹션 참조.
1759
+
1760
+ ---
1761
+
1475
1762
  ## Testing
1476
1763
 
1477
1764
  ### Setup Test Environment
@@ -1817,7 +2104,7 @@ ls migrations/
1817
2104
 
1818
2105
  - [ ] **React hooks** - useAuth, useSession, usePermissions
1819
2106
  - [ ] **UI components** - LoginForm, RegisterForm, AuthProvider
1820
- - [ ] **OAuth integration** - Google, GitHub, etc.
2107
+ - [x] **OAuth integration** - Google (implemented), GitHub/Kakao/Naver (planned)
1821
2108
  - [ ] **2FA support** - TOTP/authenticator apps
1822
2109
  - [ ] **Password reset flow** - Complete email-based reset
1823
2110
  - [ ] **Email change flow** - Verification for email updates
@@ -1957,6 +2244,6 @@ MIT License - See LICENSE file for details.
1957
2244
 
1958
2245
  ---
1959
2246
 
1960
- **Last Updated:** 2025-12-07
1961
- **Document Version:** 2.2.0 (Technical Documentation)
1962
- **Package Version:** 0.1.0-alpha.88
2247
+ **Last Updated:** 2026-01-27
2248
+ **Document Version:** 2.4.0 (Technical Documentation)
2249
+ **Package Version:** 0.2.0-beta.12