@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 +459 -172
- package/dist/{dto-CRlgoCP5.d.ts → authenticate-xfEpwIjH.d.ts} +284 -182
- package/dist/config.d.ts +104 -0
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +187 -130
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +186 -0
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/client.js +80 -0
- package/dist/nextjs/client.js.map +1 -0
- package/dist/nextjs/server.d.ts +68 -2
- package/dist/nextjs/server.js +125 -3
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +243 -366
- package/dist/server.js +596 -476
- package/dist/server.js.map +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @spfn/auth - Technical Documentation
|
|
2
2
|
|
|
3
|
-
**Version:** 0.
|
|
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
|
|
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
|
-
####
|
|
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:
|
|
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
|
-
**
|
|
435
|
+
**API Client:**
|
|
339
436
|
```typescript
|
|
340
|
-
import {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
810
|
+
// Before (deprecated)
|
|
811
|
+
import { sendEmail, sendSMS } from '@spfn/auth/server';
|
|
760
812
|
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
- [
|
|
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:**
|
|
1961
|
-
**Document Version:** 2.
|
|
1962
|
-
**Package Version:** 0.
|
|
2247
|
+
**Last Updated:** 2026-01-27
|
|
2248
|
+
**Document Version:** 2.4.0 (Technical Documentation)
|
|
2249
|
+
**Package Version:** 0.2.0-beta.12
|