@55387.ai/uniauth-server 1.2.0 β 1.2.1
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/INTEGRATION.md +1273 -0
- package/package.json +6 -1
- package/src/index.ts +0 -581
- package/src/server.test.ts +0 -231
- package/tsconfig.json +0 -15
package/INTEGRATION.md
ADDED
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
# UniAuth Integration Guide for AI Assistants
|
|
2
|
+
|
|
3
|
+
> **Purpose**: This document provides everything an AI coding assistant needs to integrate UniAuth into any project. It covers all authentication methods, SDK usage, OAuth2/OIDC flows, API reference, and complete code examples.
|
|
4
|
+
>
|
|
5
|
+
> **UniAuth Service URL (Production)**: `https://sso.55387.xyz`
|
|
6
|
+
>
|
|
7
|
+
> **SDK Packages**:
|
|
8
|
+
> - Frontend: `@55387.ai/uniauth-client` (v1.2.0)
|
|
9
|
+
> - Backend: `@55387.ai/uniauth-server` (v1.2.0)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
1. [Overview](#1-overview)
|
|
16
|
+
2. [Prerequisites](#2-prerequisites)
|
|
17
|
+
3. [Integration Methods Decision Tree](#3-integration-methods-decision-tree)
|
|
18
|
+
4. [Method A: SDK Integration (Recommended)](#4-method-a-sdk-integration-recommended)
|
|
19
|
+
5. [Method B: SSO / OAuth2 Authorization Code Flow](#5-method-b-sso--oauth2-authorization-code-flow)
|
|
20
|
+
6. [Method C: OIDC Standard Integration](#6-method-c-oidc-standard-integration)
|
|
21
|
+
7. [Method D: Direct API Integration (No SDK)](#7-method-d-direct-api-integration-no-sdk)
|
|
22
|
+
8. [Complete API Reference](#8-complete-api-reference)
|
|
23
|
+
9. [Data Types & Interfaces](#9-data-types--interfaces)
|
|
24
|
+
10. [Error Handling](#10-error-handling)
|
|
25
|
+
11. [Security Best Practices](#11-security-best-practices)
|
|
26
|
+
12. [Framework-Specific Examples](#12-framework-specific-examples)
|
|
27
|
+
13. [Troubleshooting](#13-troubleshooting)
|
|
28
|
+
14. [FAQ](#14-faq)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Overview
|
|
33
|
+
|
|
34
|
+
UniAuth is a unified authentication platform providing centralized user authentication and authorization. It supports:
|
|
35
|
+
|
|
36
|
+
| Feature | Description |
|
|
37
|
+
|---------|-------------|
|
|
38
|
+
| π± Phone Login | Phone + SMS verification code (Tencent Cloud SMS) |
|
|
39
|
+
| π§ Email Login | Email + verification code / Email + password |
|
|
40
|
+
| π OAuth2/OIDC | Acts as an OAuth 2.0 / OpenID Connect Provider |
|
|
41
|
+
| π JWT Tokens | Access Token (1h) + Refresh Token (30d) with rotation |
|
|
42
|
+
| π SSO | Single Sign-On across multiple applications |
|
|
43
|
+
| π‘οΈ MFA | TOTP-based multi-factor authentication |
|
|
44
|
+
| π€ M2M | Machine-to-Machine authentication via Client Credentials |
|
|
45
|
+
| π Social Login | Google, GitHub, WeChat OAuth |
|
|
46
|
+
|
|
47
|
+
### Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
ββββββββββββββββββββββββββββββββ
|
|
51
|
+
β Your Application β
|
|
52
|
+
β ββββββββββ ββββββββββββββ β
|
|
53
|
+
β βFrontendβ β Backend β β
|
|
54
|
+
β β(Client β β (Server β β
|
|
55
|
+
β β SDK) β β SDK) β β
|
|
56
|
+
β βββββ¬βββββ βββββββ¬βββββββ β
|
|
57
|
+
ββββββββΌβββββββββββββββΌβββββββββ
|
|
58
|
+
β β
|
|
59
|
+
β HTTPS β HTTPS
|
|
60
|
+
βΌ βΌ
|
|
61
|
+
ββββββββββββββββββββββββββββββββ
|
|
62
|
+
β UniAuth Server β
|
|
63
|
+
β https://sso.55387.xyz β
|
|
64
|
+
β (Hono + Supabase + Redis) β
|
|
65
|
+
ββββββββββββββββββββββββββββββββ
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 2. Prerequisites
|
|
71
|
+
|
|
72
|
+
### 2.1 Register Your Application
|
|
73
|
+
|
|
74
|
+
Go to the **UniAuth Developer Console** (`https://sso.55387.xyz/developer`) and register your app to get:
|
|
75
|
+
|
|
76
|
+
| Credential | Description | Example |
|
|
77
|
+
|------------|-------------|---------|
|
|
78
|
+
| `Client ID` | Unique app identifier | `ua_xxxxxxxxxxxx` |
|
|
79
|
+
| `Client Secret` | App secret key (keep secure, never in frontend) | `xxxxxxxx` |
|
|
80
|
+
| `Redirect URIs` | Allowed OAuth callback URLs (multiple supported) | `http://localhost:3000/callback` |
|
|
81
|
+
| `Client Type` | **Public** (frontend SPA) or **Confidential** (backend) | Depends on your architecture |
|
|
82
|
+
| `Scopes` | Authorization scopes | `openid profile email phone` |
|
|
83
|
+
|
|
84
|
+
### 2.2 Environment Variables
|
|
85
|
+
|
|
86
|
+
Your project needs these environment variables:
|
|
87
|
+
|
|
88
|
+
```env
|
|
89
|
+
# Required
|
|
90
|
+
UNIAUTH_URL=https://sso.55387.xyz
|
|
91
|
+
UNIAUTH_CLIENT_ID=ua_xxxxxxxxxxxx
|
|
92
|
+
UNIAUTH_CLIENT_SECRET=your_client_secret # Backend only, NEVER in frontend
|
|
93
|
+
|
|
94
|
+
# Optional
|
|
95
|
+
UNIAUTH_REDIRECT_URI=http://localhost:3000/callback
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 3. Integration Methods Decision Tree
|
|
101
|
+
|
|
102
|
+
Choose the best method based on your scenario:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Is your app a Node.js / TypeScript project?
|
|
106
|
+
βββ YES β Do you want embedded login UI (your own login form)?
|
|
107
|
+
β βββ YES β Use Method A: SDK Integration (Trusted Client)
|
|
108
|
+
β βββ NO β Do you need SSO (redirect to UniAuth login page)?
|
|
109
|
+
β βββ YES β Is your backend a Confidential Client?
|
|
110
|
+
β β βββ YES β Use Method B: Backend-proxy SSO flow
|
|
111
|
+
β β βββ NO β Use Method A: Client SDK SSO
|
|
112
|
+
β βββ NO β Use Method A: SDK Integration
|
|
113
|
+
βββ NO β Are you using Python, Java, Go, etc.?
|
|
114
|
+
β βββ YES β Use Method C: OIDC Standard Integration
|
|
115
|
+
β βββ NO β Use Method D: Direct API Integration
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Method | When to Use | Complexity |
|
|
119
|
+
|--------|-------------|------------|
|
|
120
|
+
| **A: SDK** | Node.js/TS projects, fastest integration | β Low |
|
|
121
|
+
| **B: SSO/OAuth2** | Cross-domain SSO, redirecting to UniAuth login page | ββ Medium |
|
|
122
|
+
| **C: OIDC** | Non-Node.js projects, using standard OIDC libraries | ββ Medium |
|
|
123
|
+
| **D: Direct API** | Any language, no SDK dependency, full control | βββ High |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 4. Method A: SDK Integration (Recommended)
|
|
128
|
+
|
|
129
|
+
### 4.1 Installation
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Frontend SDK
|
|
133
|
+
npm install @55387.ai/uniauth-client
|
|
134
|
+
|
|
135
|
+
# Backend SDK
|
|
136
|
+
npm install @55387.ai/uniauth-server
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### 4.2 Frontend SDK β Embedded Login (Trusted Client)
|
|
142
|
+
|
|
143
|
+
> **Requirement**: Your app must be registered as a **Trusted Client** (`trusted_client` grant type) in the Developer Console.
|
|
144
|
+
|
|
145
|
+
#### Initialize
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { UniAuthClient } from '@55387.ai/uniauth-client';
|
|
149
|
+
|
|
150
|
+
const auth = new UniAuthClient({
|
|
151
|
+
baseUrl: 'https://sso.55387.xyz',
|
|
152
|
+
// clientId: 'ua_xxxx', // Optional for trusted client
|
|
153
|
+
// storage: 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
|
|
154
|
+
// enableRetry: true, // Auto-retry on failure (default: true)
|
|
155
|
+
// timeout: 30000, // Request timeout in ms (default: 30000)
|
|
156
|
+
onTokenRefresh: (tokens) => {
|
|
157
|
+
console.log('Tokens refreshed automatically');
|
|
158
|
+
},
|
|
159
|
+
onAuthError: (error) => {
|
|
160
|
+
console.error('Auth error:', error);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Phone + SMS Login
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Step 1: Send verification code
|
|
169
|
+
const sendResult = await auth.sendCode('+8613800138000');
|
|
170
|
+
// sendResult: { success: true, data: { expires_in: 300, retry_after: 60 } }
|
|
171
|
+
|
|
172
|
+
// Step 2: Login with code
|
|
173
|
+
const loginResult = await auth.loginWithCode('+8613800138000', '123456');
|
|
174
|
+
// loginResult: { success: true, data: { user, access_token, refresh_token, expires_in, is_new_user } }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Email + Code Login (Passwordless)
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Step 1: Send email code
|
|
181
|
+
await auth.sendEmailCode('user@example.com');
|
|
182
|
+
|
|
183
|
+
// Step 2: Login
|
|
184
|
+
const result = await auth.loginWithEmailCode('user@example.com', '123456');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Email + Password Login
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const result = await auth.loginWithEmail('user@example.com', 'password123');
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Email Registration
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const result = await auth.registerWithEmail('user@example.com', 'password123', 'Nickname');
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### MFA (Multi-Factor Authentication)
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const result = await auth.loginWithCode(phone, code);
|
|
203
|
+
|
|
204
|
+
if (result.mfa_required) {
|
|
205
|
+
// Prompt user for TOTP code from authenticator app
|
|
206
|
+
const mfaCode = '123456';
|
|
207
|
+
const mfaResult = await auth.verifyMFA(result.mfa_token!, mfaCode);
|
|
208
|
+
// mfaResult contains final access_token and refresh_token
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### User Management
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Get current user
|
|
216
|
+
const user = await auth.getCurrentUser();
|
|
217
|
+
// user: { id, phone, email, nickname, avatar_url }
|
|
218
|
+
|
|
219
|
+
// Update profile
|
|
220
|
+
await auth.updateProfile({ nickname: 'New Name', avatar_url: 'https://...' });
|
|
221
|
+
|
|
222
|
+
// Check auth status
|
|
223
|
+
const isLoggedIn = auth.isAuthenticated();
|
|
224
|
+
|
|
225
|
+
// Get access token (auto-refreshes if expired)
|
|
226
|
+
const token = await auth.getAccessToken();
|
|
227
|
+
|
|
228
|
+
// Get cached user (synchronous, no API call)
|
|
229
|
+
const cachedUser = auth.getCachedUser();
|
|
230
|
+
|
|
231
|
+
// Listen to auth state changes
|
|
232
|
+
const unsubscribe = auth.onAuthStateChange((user, isAuthenticated) => {
|
|
233
|
+
if (isAuthenticated) {
|
|
234
|
+
console.log('Logged in:', user);
|
|
235
|
+
} else {
|
|
236
|
+
console.log('Logged out');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Logout
|
|
241
|
+
await auth.logout(); // Current device
|
|
242
|
+
await auth.logoutAll(); // All devices
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Social Login
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Get available OAuth providers
|
|
249
|
+
const providers = await auth.getOAuthProviders();
|
|
250
|
+
// providers: ['google', 'github', 'wechat']
|
|
251
|
+
|
|
252
|
+
// Start social login (redirects user)
|
|
253
|
+
auth.startSocialLogin('google');
|
|
254
|
+
auth.startSocialLogin('github');
|
|
255
|
+
auth.startSocialLogin('wechat');
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### 4.3 Frontend SDK β SSO Login (Public Client)
|
|
261
|
+
|
|
262
|
+
> For apps that redirect to UniAuth's login page instead of building their own.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Step 1: Configure SSO
|
|
266
|
+
auth.configureSso({
|
|
267
|
+
ssoUrl: 'https://sso.55387.xyz',
|
|
268
|
+
clientId: 'ua_xxxxxxxxxxxx',
|
|
269
|
+
redirectUri: window.location.origin + '/callback',
|
|
270
|
+
scope: 'openid profile email phone', // default: 'openid profile email'
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Step 2: Trigger SSO login (redirects to UniAuth login page)
|
|
274
|
+
auth.loginWithSSO();
|
|
275
|
+
|
|
276
|
+
// With PKCE (recommended for public clients):
|
|
277
|
+
auth.loginWithSSO({ usePKCE: true });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Step 3: Handle callback (on your /callback page)
|
|
282
|
+
// React example:
|
|
283
|
+
function CallbackPage() {
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const handleCallback = async () => {
|
|
286
|
+
if (auth.isSSOCallback()) {
|
|
287
|
+
try {
|
|
288
|
+
const result = await auth.handleSSOCallback();
|
|
289
|
+
if (result) {
|
|
290
|
+
// result: { access_token, refresh_token?, expires_in?, token_type, id_token? }
|
|
291
|
+
localStorage.setItem('access_token', result.access_token);
|
|
292
|
+
if (result.refresh_token) {
|
|
293
|
+
localStorage.setItem('refresh_token', result.refresh_token);
|
|
294
|
+
}
|
|
295
|
+
window.location.href = '/';
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error('SSO callback error:', error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
handleCallback();
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
return <div>Logging in...</div>;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
> β οΈ **Important**: If your app is a **Confidential Client**, the frontend SDK cannot call the token endpoint directly (missing `client_secret`). Use **Method B** (backend-proxy flow) instead.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### 4.4 Backend SDK
|
|
314
|
+
|
|
315
|
+
#### Initialize
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// lib/auth.ts
|
|
319
|
+
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
320
|
+
|
|
321
|
+
export const uniauth = new UniAuthServer({
|
|
322
|
+
baseUrl: process.env.UNIAUTH_URL || 'https://sso.55387.xyz',
|
|
323
|
+
clientId: process.env.UNIAUTH_CLIENT_ID!,
|
|
324
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET!,
|
|
325
|
+
// jwtPublicKey: '...', // Optional: for local JWT verification (faster)
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### Express Middleware
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import express from 'express';
|
|
333
|
+
import { uniauth } from './lib/auth';
|
|
334
|
+
|
|
335
|
+
const app = express();
|
|
336
|
+
|
|
337
|
+
// Protect all /api routes
|
|
338
|
+
app.use('/api/*', uniauth.middleware());
|
|
339
|
+
|
|
340
|
+
// Access user info in routes
|
|
341
|
+
app.get('/api/profile', (req, res) => {
|
|
342
|
+
// req.user β full user info (UserInfo)
|
|
343
|
+
// req.authPayload β JWT payload (TokenPayload)
|
|
344
|
+
res.json({
|
|
345
|
+
userId: req.user?.id,
|
|
346
|
+
email: req.user?.email,
|
|
347
|
+
phone: req.user?.phone,
|
|
348
|
+
tokenExp: req.authPayload?.exp,
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Hono Middleware
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { Hono } from 'hono';
|
|
357
|
+
import { uniauth } from './lib/auth';
|
|
358
|
+
|
|
359
|
+
const app = new Hono();
|
|
360
|
+
|
|
361
|
+
// Protect all /api routes
|
|
362
|
+
app.use('/api/*', uniauth.honoMiddleware());
|
|
363
|
+
|
|
364
|
+
// Access user info
|
|
365
|
+
app.get('/api/profile', (c) => {
|
|
366
|
+
const user = c.get('user');
|
|
367
|
+
return c.json({ user });
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
#### Manual Token Verification
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { uniauth } from './lib/auth';
|
|
375
|
+
|
|
376
|
+
async function handleRequest(req: Request) {
|
|
377
|
+
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
|
|
378
|
+
|
|
379
|
+
if (!token) {
|
|
380
|
+
return new Response('Unauthorized', { status: 401 });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const payload = await uniauth.verifyToken(token);
|
|
385
|
+
// payload.sub = User ID
|
|
386
|
+
// payload.email = Email
|
|
387
|
+
// payload.phone = Phone
|
|
388
|
+
// payload.exp = Expiration (Unix timestamp)
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return new Response('Invalid token', { status: 401 });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### Token Introspection (RFC 7662)
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
const result = await uniauth.introspectToken(accessToken);
|
|
399
|
+
|
|
400
|
+
if (result.active) {
|
|
401
|
+
console.log('User:', result.sub);
|
|
402
|
+
console.log('Scopes:', result.scope);
|
|
403
|
+
} else {
|
|
404
|
+
console.log('Token is invalid or expired');
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### Quick Token Check
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
const isValid = await uniauth.isTokenActive(token);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## 5. Method B: SSO / OAuth2 Authorization Code Flow
|
|
417
|
+
|
|
418
|
+
### When to Use
|
|
419
|
+
|
|
420
|
+
Use this when your backend is a **Confidential Client** and you want SSO (redirect to UniAuth login page with `client_secret` exchange on the backend).
|
|
421
|
+
|
|
422
|
+
### Flow Diagram
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
User β Frontend β /api/auth/login β Backend generates auth URL β Redirect to SSO
|
|
426
|
+
β
|
|
427
|
+
User β Frontend β / β Backend sets Cookie β SSO callback to /api/auth/callback
|
|
428
|
+
β
|
|
429
|
+
Backend exchanges code for tokens with client_secret
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### OAuth2 Endpoints
|
|
433
|
+
|
|
434
|
+
| Endpoint | URL |
|
|
435
|
+
|----------|-----|
|
|
436
|
+
| Authorization | `https://sso.55387.xyz/api/v1/oauth2/authorize` |
|
|
437
|
+
| Token | `https://sso.55387.xyz/api/v1/oauth2/token` |
|
|
438
|
+
| UserInfo | `https://sso.55387.xyz/api/v1/oauth2/userinfo` |
|
|
439
|
+
| JWKS | `https://sso.55387.xyz/.well-known/jwks.json` |
|
|
440
|
+
| OIDC Discovery | `https://sso.55387.xyz/.well-known/openid-configuration` |
|
|
441
|
+
|
|
442
|
+
### Complete Backend Implementation (Hono)
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { Hono } from 'hono';
|
|
446
|
+
import { setCookie, getCookie, deleteCookie } from 'hono/cookie';
|
|
447
|
+
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
448
|
+
import crypto from 'crypto';
|
|
449
|
+
|
|
450
|
+
const app = new Hono();
|
|
451
|
+
|
|
452
|
+
const auth = new UniAuthServer({
|
|
453
|
+
baseUrl: 'https://sso.55387.xyz',
|
|
454
|
+
clientId: process.env.UNIAUTH_CLIENT_ID!,
|
|
455
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET!,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Helper: generate random state for CSRF protection
|
|
459
|
+
function generateState(): string {
|
|
460
|
+
return crypto.randomBytes(32).toString('hex');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 1. Login endpoint β redirects to UniAuth SSO
|
|
464
|
+
app.get('/api/auth/login', (c) => {
|
|
465
|
+
const origin = c.req.header('origin') || c.req.header('referer')?.replace(/\/+$/, '') || 'http://localhost:3000';
|
|
466
|
+
const redirectUri = `${origin}/api/auth/callback`;
|
|
467
|
+
const state = generateState();
|
|
468
|
+
|
|
469
|
+
// TODO: Store state in Redis/session for CSRF validation
|
|
470
|
+
|
|
471
|
+
const params = new URLSearchParams({
|
|
472
|
+
client_id: process.env.UNIAUTH_CLIENT_ID!,
|
|
473
|
+
redirect_uri: redirectUri,
|
|
474
|
+
response_type: 'code',
|
|
475
|
+
scope: 'openid profile email phone',
|
|
476
|
+
state,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return c.redirect(`https://sso.55387.xyz/api/v1/oauth2/authorize?${params.toString()}`);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// 2. Callback endpoint β exchanges code for tokens
|
|
483
|
+
app.get('/api/auth/callback', async (c) => {
|
|
484
|
+
const code = c.req.query('code');
|
|
485
|
+
const state = c.req.query('state');
|
|
486
|
+
|
|
487
|
+
if (!code) {
|
|
488
|
+
return c.json({ error: 'Missing authorization code' }, 400);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// TODO: Validate state against stored value
|
|
492
|
+
|
|
493
|
+
const origin = c.req.header('referer')?.replace(/\/api\/auth\/callback.*$/, '') || 'http://localhost:3000';
|
|
494
|
+
const redirectUri = `${origin}/api/auth/callback`;
|
|
495
|
+
|
|
496
|
+
// Exchange code for tokens
|
|
497
|
+
const response = await fetch('https://sso.55387.xyz/api/v1/oauth2/token', {
|
|
498
|
+
method: 'POST',
|
|
499
|
+
headers: { 'Content-Type': 'application/json' },
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
client_id: process.env.UNIAUTH_CLIENT_ID,
|
|
502
|
+
client_secret: process.env.UNIAUTH_CLIENT_SECRET,
|
|
503
|
+
code,
|
|
504
|
+
grant_type: 'authorization_code',
|
|
505
|
+
redirect_uri: redirectUri,
|
|
506
|
+
}),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (!response.ok) {
|
|
510
|
+
const error = await response.json();
|
|
511
|
+
return c.json({ error: 'Token exchange failed', details: error }, 400);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const { access_token, id_token, refresh_token } = await response.json();
|
|
515
|
+
|
|
516
|
+
// Store token in httpOnly cookie (secure!)
|
|
517
|
+
setCookie(c, 'auth_token', id_token || access_token, {
|
|
518
|
+
httpOnly: true,
|
|
519
|
+
secure: true,
|
|
520
|
+
sameSite: 'Lax',
|
|
521
|
+
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
522
|
+
path: '/',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Redirect to frontend home
|
|
526
|
+
return c.redirect('/');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// 3. Auth status endpoint
|
|
530
|
+
app.get('/api/auth/status', async (c) => {
|
|
531
|
+
const token = getCookie(c, 'auth_token');
|
|
532
|
+
if (!token) {
|
|
533
|
+
return c.json({ authenticated: false });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const payload = await auth.verifyToken(token);
|
|
538
|
+
return c.json({
|
|
539
|
+
authenticated: true,
|
|
540
|
+
userId: payload.sub,
|
|
541
|
+
email: payload.email,
|
|
542
|
+
});
|
|
543
|
+
} catch {
|
|
544
|
+
return c.json({ authenticated: false });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// 4. Logout endpoint
|
|
549
|
+
app.post('/api/auth/logout', (c) => {
|
|
550
|
+
deleteCookie(c, 'auth_token', { path: '/' });
|
|
551
|
+
return c.json({ success: true });
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Frontend Code for SSO (Non-SDK)
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Trigger login
|
|
559
|
+
const handleLogin = () => {
|
|
560
|
+
window.location.href = '/api/auth/login';
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Check login status
|
|
564
|
+
const checkAuth = async (): Promise<boolean> => {
|
|
565
|
+
const response = await fetch('/api/auth/status', { credentials: 'include' });
|
|
566
|
+
const data = await response.json();
|
|
567
|
+
return data.authenticated === true;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Logout
|
|
571
|
+
const handleLogout = async () => {
|
|
572
|
+
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
|
573
|
+
window.location.href = '/login';
|
|
574
|
+
};
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## 6. Method C: OIDC Standard Integration
|
|
580
|
+
|
|
581
|
+
UniAuth is a **fully OIDC-compliant Identity Provider**. Any standard OIDC client library works.
|
|
582
|
+
|
|
583
|
+
### OIDC Discovery URL
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
https://sso.55387.xyz/.well-known/openid-configuration
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Discovery Response (Key Fields)
|
|
590
|
+
|
|
591
|
+
```json
|
|
592
|
+
{
|
|
593
|
+
"issuer": "https://sso.55387.xyz",
|
|
594
|
+
"authorization_endpoint": "https://sso.55387.xyz/oauth2/authorize",
|
|
595
|
+
"token_endpoint": "https://sso.55387.xyz/api/v1/oauth2/token",
|
|
596
|
+
"userinfo_endpoint": "https://sso.55387.xyz/api/v1/oauth2/userinfo",
|
|
597
|
+
"jwks_uri": "https://sso.55387.xyz/.well-known/jwks.json",
|
|
598
|
+
"scopes_supported": ["openid", "profile", "email", "phone"],
|
|
599
|
+
"response_types_supported": ["code"],
|
|
600
|
+
"grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
|
|
601
|
+
"id_token_signing_alg_values_supported": ["RS256"]
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Node.js + Passport
|
|
606
|
+
|
|
607
|
+
```javascript
|
|
608
|
+
import passport from 'passport';
|
|
609
|
+
import { Strategy as OpenIDStrategy } from 'passport-openidconnect';
|
|
610
|
+
|
|
611
|
+
passport.use(new OpenIDStrategy({
|
|
612
|
+
issuer: 'https://sso.55387.xyz',
|
|
613
|
+
authorizationURL: 'https://sso.55387.xyz/oauth2/authorize',
|
|
614
|
+
tokenURL: 'https://sso.55387.xyz/api/v1/oauth2/token',
|
|
615
|
+
userInfoURL: 'https://sso.55387.xyz/api/v1/oauth2/userinfo',
|
|
616
|
+
clientID: process.env.UNIAUTH_CLIENT_ID,
|
|
617
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET,
|
|
618
|
+
callbackURL: 'https://myapp.com/callback',
|
|
619
|
+
scope: ['openid', 'profile', 'email']
|
|
620
|
+
},
|
|
621
|
+
(issuer, profile, done) => {
|
|
622
|
+
return done(null, profile);
|
|
623
|
+
}
|
|
624
|
+
));
|
|
625
|
+
|
|
626
|
+
app.get('/auth/uniauth', passport.authenticate('openidconnect'));
|
|
627
|
+
app.get('/callback',
|
|
628
|
+
passport.authenticate('openidconnect', { failureRedirect: '/login' }),
|
|
629
|
+
(req, res) => res.redirect('/dashboard')
|
|
630
|
+
);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Next.js + NextAuth
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// pages/api/auth/[...nextauth].ts or app/api/auth/[...nextauth]/route.ts
|
|
637
|
+
import NextAuth from 'next-auth';
|
|
638
|
+
|
|
639
|
+
export const authOptions = {
|
|
640
|
+
providers: [
|
|
641
|
+
{
|
|
642
|
+
id: 'uniauth',
|
|
643
|
+
name: 'UniAuth',
|
|
644
|
+
type: 'oauth',
|
|
645
|
+
wellKnown: 'https://sso.55387.xyz/.well-known/openid-configuration',
|
|
646
|
+
authorization: { params: { scope: 'openid profile email phone' } },
|
|
647
|
+
idToken: true,
|
|
648
|
+
profile(profile) {
|
|
649
|
+
return {
|
|
650
|
+
id: profile.sub,
|
|
651
|
+
name: profile.name,
|
|
652
|
+
email: profile.email,
|
|
653
|
+
image: profile.picture,
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
clientId: process.env.UNIAUTH_CLIENT_ID,
|
|
657
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET,
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
export default NextAuth(authOptions);
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Python + Authlib (Flask)
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
from authlib.integrations.flask_client import OAuth
|
|
669
|
+
|
|
670
|
+
oauth = OAuth(app)
|
|
671
|
+
|
|
672
|
+
uniauth = oauth.register(
|
|
673
|
+
'uniauth',
|
|
674
|
+
client_id='ua_abc123',
|
|
675
|
+
client_secret='SECRET',
|
|
676
|
+
server_metadata_url='https://sso.55387.xyz/.well-known/openid-configuration',
|
|
677
|
+
client_kwargs={'scope': 'openid profile email'}
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
@app.route('/login')
|
|
681
|
+
def login():
|
|
682
|
+
redirect_uri = url_for('callback', _external=True)
|
|
683
|
+
return uniauth.authorize_redirect(redirect_uri)
|
|
684
|
+
|
|
685
|
+
@app.route('/callback')
|
|
686
|
+
def callback():
|
|
687
|
+
token = uniauth.authorize_access_token()
|
|
688
|
+
user_info = uniauth.parse_id_token(token)
|
|
689
|
+
# user_info['email'], user_info['name'], user_info['sub']
|
|
690
|
+
return redirect('/dashboard')
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
## 7. Method D: Direct API Integration (No SDK)
|
|
696
|
+
|
|
697
|
+
### 7.1 Phone SMS Login
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
# Step 1: Send code
|
|
701
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/phone/send-code \
|
|
702
|
+
-H "Content-Type: application/json" \
|
|
703
|
+
-d '{"phone": "+8613800138000"}'
|
|
704
|
+
|
|
705
|
+
# Response: {"success": true, "data": {"expires_in": 300, "retry_after": 60}}
|
|
706
|
+
|
|
707
|
+
# Step 2: Verify code and login
|
|
708
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/phone/verify \
|
|
709
|
+
-H "Content-Type: application/json" \
|
|
710
|
+
-d '{"phone": "+8613800138000", "code": "123456"}'
|
|
711
|
+
|
|
712
|
+
# Response: {"success": true, "data": {"user": {...}, "access_token": "eyJ...", "refresh_token": "...", "expires_in": 3600}}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### 7.2 Email Login
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
# Email + Code
|
|
719
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/email/send-code \
|
|
720
|
+
-H "Content-Type: application/json" \
|
|
721
|
+
-d '{"email": "user@example.com", "type": "login"}'
|
|
722
|
+
|
|
723
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/email/verify \
|
|
724
|
+
-H "Content-Type: application/json" \
|
|
725
|
+
-d '{"email": "user@example.com", "code": "123456"}'
|
|
726
|
+
|
|
727
|
+
# Email + Password
|
|
728
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/email/login \
|
|
729
|
+
-H "Content-Type: application/json" \
|
|
730
|
+
-d '{"email": "user@example.com", "password": "password123"}'
|
|
731
|
+
|
|
732
|
+
# Email Registration
|
|
733
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/email/register \
|
|
734
|
+
-H "Content-Type: application/json" \
|
|
735
|
+
-d '{"email": "user@example.com", "password": "password123", "code": "123456"}'
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### 7.3 Token Management
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
# Refresh token
|
|
742
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/refresh \
|
|
743
|
+
-H "Content-Type: application/json" \
|
|
744
|
+
-d '{"refresh_token": "your_refresh_token"}'
|
|
745
|
+
|
|
746
|
+
# Verify token
|
|
747
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/verify \
|
|
748
|
+
-H "Content-Type: application/json" \
|
|
749
|
+
-d '{"token": "your_access_token"}'
|
|
750
|
+
|
|
751
|
+
# Logout
|
|
752
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/logout \
|
|
753
|
+
-H "Authorization: Bearer your_access_token" \
|
|
754
|
+
-H "Content-Type: application/json" \
|
|
755
|
+
-d '{"refresh_token": "your_refresh_token"}'
|
|
756
|
+
|
|
757
|
+
# Logout all devices
|
|
758
|
+
curl -X POST https://sso.55387.xyz/api/v1/auth/logout-all \
|
|
759
|
+
-H "Authorization: Bearer your_access_token"
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### 7.4 User Info
|
|
763
|
+
|
|
764
|
+
```bash
|
|
765
|
+
# Get current user
|
|
766
|
+
curl https://sso.55387.xyz/api/v1/user/me \
|
|
767
|
+
-H "Authorization: Bearer your_access_token"
|
|
768
|
+
|
|
769
|
+
# Update profile
|
|
770
|
+
curl -X PATCH https://sso.55387.xyz/api/v1/user/me \
|
|
771
|
+
-H "Authorization: Bearer your_access_token" \
|
|
772
|
+
-H "Content-Type: application/json" \
|
|
773
|
+
-d '{"nickname": "NewName", "avatar_url": "https://..."}'
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### 7.5 OAuth2 Authorization Code Flow (cURL)
|
|
777
|
+
|
|
778
|
+
```bash
|
|
779
|
+
# Step 1: Redirect user to (open in browser)
|
|
780
|
+
https://sso.55387.xyz/api/v1/oauth2/authorize?client_id=ua_abc123&redirect_uri=https://myapp.com/callback&response_type=code&scope=openid%20profile%20email&state=random_string
|
|
781
|
+
|
|
782
|
+
# Step 2: Exchange code for tokens
|
|
783
|
+
curl -X POST https://sso.55387.xyz/api/v1/oauth2/token \
|
|
784
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
785
|
+
-d "grant_type=authorization_code" \
|
|
786
|
+
-d "code=AUTH_CODE" \
|
|
787
|
+
-d "redirect_uri=https://myapp.com/callback" \
|
|
788
|
+
-d "client_id=ua_abc123" \
|
|
789
|
+
-d "client_secret=SECRET"
|
|
790
|
+
|
|
791
|
+
# Step 3: Get user info
|
|
792
|
+
curl https://sso.55387.xyz/api/v1/oauth2/userinfo \
|
|
793
|
+
-H "Authorization: Bearer ACCESS_TOKEN"
|
|
794
|
+
|
|
795
|
+
# Refresh OAuth2 tokens
|
|
796
|
+
curl -X POST https://sso.55387.xyz/api/v1/oauth2/token \
|
|
797
|
+
-d "grant_type=refresh_token" \
|
|
798
|
+
-d "refresh_token=REFRESH_TOKEN" \
|
|
799
|
+
-d "client_id=ua_abc123" \
|
|
800
|
+
-d "client_secret=SECRET"
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## 8. Complete API Reference
|
|
806
|
+
|
|
807
|
+
### Authentication APIs
|
|
808
|
+
|
|
809
|
+
| Method | Endpoint | Auth Required | Description |
|
|
810
|
+
|--------|----------|:---:|-------------|
|
|
811
|
+
| POST | `/api/v1/auth/phone/send-code` | β | Send phone SMS code |
|
|
812
|
+
| POST | `/api/v1/auth/phone/verify` | β | Login with phone + code |
|
|
813
|
+
| POST | `/api/v1/auth/email/send-code` | β | Send email verification code |
|
|
814
|
+
| POST | `/api/v1/auth/email/verify` | β | Login with email + code |
|
|
815
|
+
| POST | `/api/v1/auth/email/login` | β | Login with email + password |
|
|
816
|
+
| POST | `/api/v1/auth/email/register` | β | Register with email + password |
|
|
817
|
+
| POST | `/api/v1/auth/refresh` | β | Refresh access token |
|
|
818
|
+
| POST | `/api/v1/auth/verify` | β | Verify a token |
|
|
819
|
+
| POST | `/api/v1/auth/logout` | β
| Logout current device |
|
|
820
|
+
| POST | `/api/v1/auth/logout-all` | β
| Logout all devices |
|
|
821
|
+
| GET | `/api/v1/auth/google` | β | Google OAuth redirect |
|
|
822
|
+
| GET | `/api/v1/auth/github` | β | GitHub OAuth redirect |
|
|
823
|
+
| GET | `/api/v1/auth/wechat` | β | WeChat OAuth redirect |
|
|
824
|
+
|
|
825
|
+
### User APIs
|
|
826
|
+
|
|
827
|
+
| Method | Endpoint | Auth Required | Description |
|
|
828
|
+
|--------|----------|:---:|-------------|
|
|
829
|
+
| GET | `/api/v1/user/me` | β
| Get current user |
|
|
830
|
+
| PATCH | `/api/v1/user/me` | β
| Update profile |
|
|
831
|
+
| GET | `/api/v1/user/sessions` | β
| Get active sessions |
|
|
832
|
+
| DELETE | `/api/v1/user/sessions/:id` | β
| Revoke a session |
|
|
833
|
+
| GET | `/api/v1/user/bindings` | β
| Get OAuth account bindings |
|
|
834
|
+
| DELETE | `/api/v1/user/unbind/:provider` | β
| Unbind OAuth account |
|
|
835
|
+
| POST | `/api/v1/user/bind/phone` | β
| Bind phone number |
|
|
836
|
+
| POST | `/api/v1/user/bind/email` | β
| Bind email |
|
|
837
|
+
| GET | `/api/v1/user/authorized-apps` | β
| Get authorized apps |
|
|
838
|
+
| DELETE | `/api/v1/user/authorized-apps/:clientId` | β
| Revoke app authorization |
|
|
839
|
+
|
|
840
|
+
### MFA APIs
|
|
841
|
+
|
|
842
|
+
| Method | Endpoint | Auth Required | Description |
|
|
843
|
+
|--------|----------|:---:|-------------|
|
|
844
|
+
| GET | `/api/v1/mfa/status` | β
| Get MFA status |
|
|
845
|
+
| POST | `/api/v1/mfa/setup` | β
| Start MFA setup (returns QR code) |
|
|
846
|
+
| POST | `/api/v1/mfa/verify-setup` | β
| Confirm MFA setup |
|
|
847
|
+
| POST | `/api/v1/mfa/verify` | β
| Verify MFA code during login |
|
|
848
|
+
| POST | `/api/v1/mfa/disable` | β
| Disable MFA |
|
|
849
|
+
| POST | `/api/v1/mfa/regenerate-recovery` | β
| Regenerate recovery codes |
|
|
850
|
+
|
|
851
|
+
### OAuth2 Provider APIs
|
|
852
|
+
|
|
853
|
+
| Method | Endpoint | Auth Required | Description |
|
|
854
|
+
|--------|----------|:---:|-------------|
|
|
855
|
+
| GET | `/api/v1/oauth2/validate` | β | Validate client & redirect_uri |
|
|
856
|
+
| POST | `/api/v1/oauth2/authorize` | β
| Generate authorization code |
|
|
857
|
+
| POST | `/api/v1/oauth2/token` | β | Exchange code for tokens |
|
|
858
|
+
| GET | `/api/v1/oauth2/userinfo` | β
(OAuth token) | Get user info (OIDC-compatible) |
|
|
859
|
+
|
|
860
|
+
### Health APIs
|
|
861
|
+
|
|
862
|
+
| Method | Endpoint | Description |
|
|
863
|
+
|--------|----------|-------------|
|
|
864
|
+
| GET | `/health` | Simple health check |
|
|
865
|
+
| GET | `/health/ready` | Deep readiness check (DB, Redis, memory) |
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
## 9. Data Types & Interfaces
|
|
870
|
+
|
|
871
|
+
### Login Response
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
interface LoginResponse {
|
|
875
|
+
success: boolean;
|
|
876
|
+
data: {
|
|
877
|
+
user: User;
|
|
878
|
+
access_token: string;
|
|
879
|
+
refresh_token: string;
|
|
880
|
+
expires_in: number; // seconds (3600 = 1 hour)
|
|
881
|
+
is_new_user: boolean;
|
|
882
|
+
// MFA fields (only present if MFA required)
|
|
883
|
+
mfa_required?: boolean;
|
|
884
|
+
mfa_token?: string;
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### User
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
interface User {
|
|
893
|
+
id: string; // UUID
|
|
894
|
+
phone?: string | null; // e.g. "+8613800138000"
|
|
895
|
+
email?: string | null;
|
|
896
|
+
nickname?: string | null;
|
|
897
|
+
avatar_url?: string | null;
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### Token Payload (JWT Claims)
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
interface TokenPayload {
|
|
905
|
+
sub: string; // User ID
|
|
906
|
+
iss?: string; // Issuer ("https://sso.55387.xyz")
|
|
907
|
+
aud?: string | string[]; // Audience (your client_id)
|
|
908
|
+
exp: number; // Expiration (Unix timestamp)
|
|
909
|
+
iat: number; // Issued at (Unix timestamp)
|
|
910
|
+
scope?: string; // Space-separated scopes
|
|
911
|
+
email?: string;
|
|
912
|
+
phone?: string;
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### UserInfo (from Server SDK / OAuth2 userinfo endpoint)
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
interface UserInfo {
|
|
920
|
+
id: string; // (same as `sub`)
|
|
921
|
+
phone?: string;
|
|
922
|
+
email?: string;
|
|
923
|
+
nickname?: string;
|
|
924
|
+
avatar_url?: string;
|
|
925
|
+
phone_verified?: boolean;
|
|
926
|
+
email_verified?: boolean;
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### OAuth2 Token Response
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
interface OAuth2TokenResponse {
|
|
934
|
+
access_token: string;
|
|
935
|
+
token_type: string; // "Bearer"
|
|
936
|
+
expires_in: number; // 3600
|
|
937
|
+
refresh_token: string;
|
|
938
|
+
id_token?: string; // Present when scope includes "openid"
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### SSO Callback Result
|
|
943
|
+
|
|
944
|
+
```typescript
|
|
945
|
+
interface SSOResult {
|
|
946
|
+
access_token: string;
|
|
947
|
+
refresh_token?: string;
|
|
948
|
+
expires_in?: number;
|
|
949
|
+
token_type: string; // "Bearer"
|
|
950
|
+
id_token?: string;
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### Error Response
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
interface ErrorResponse {
|
|
958
|
+
success: false;
|
|
959
|
+
error: {
|
|
960
|
+
code: string; // e.g. "INVALID_CODE", "TOKEN_EXPIRED"
|
|
961
|
+
message: string; // Human-readable error message
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
## 10. Error Handling
|
|
969
|
+
|
|
970
|
+
### Frontend SDK Errors
|
|
971
|
+
|
|
972
|
+
```typescript
|
|
973
|
+
import { UniAuthError, AuthErrorCode } from '@55387.ai/uniauth-client';
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
await auth.loginWithCode(phone, code);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
if (error instanceof UniAuthError) {
|
|
979
|
+
switch (error.code) {
|
|
980
|
+
case AuthErrorCode.MFA_REQUIRED:
|
|
981
|
+
// Handle MFA flow
|
|
982
|
+
break;
|
|
983
|
+
case AuthErrorCode.VERIFY_FAILED:
|
|
984
|
+
// Wrong verification code
|
|
985
|
+
break;
|
|
986
|
+
default:
|
|
987
|
+
console.error(error.message);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### Backend SDK Errors
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
import { ServerAuthError, ServerErrorCode } from '@55387.ai/uniauth-server';
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
await uniauth.verifyToken(token);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
if (error instanceof ServerAuthError) {
|
|
1002
|
+
switch (error.code) {
|
|
1003
|
+
case ServerErrorCode.INVALID_TOKEN:
|
|
1004
|
+
// Token format invalid
|
|
1005
|
+
break;
|
|
1006
|
+
case ServerErrorCode.TOKEN_EXPIRED:
|
|
1007
|
+
// Token expired β frontend should use refresh_token
|
|
1008
|
+
break;
|
|
1009
|
+
case ServerErrorCode.UNAUTHORIZED:
|
|
1010
|
+
// User not authenticated
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Common HTTP Error Codes
|
|
1018
|
+
|
|
1019
|
+
| Status | Meaning | Action |
|
|
1020
|
+
|--------|---------|--------|
|
|
1021
|
+
| 400 | Bad Request | Check request body / parameters |
|
|
1022
|
+
| 401 | Unauthorized | Token invalid/expired, refresh or re-login |
|
|
1023
|
+
| 404 | Not Found | Check endpoint URL (must include `/api/v1/`) |
|
|
1024
|
+
| 429 | Too Many Requests | Rate limited, wait and retry |
|
|
1025
|
+
| 500 | Server Error | UniAuth server issue, retry later |
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
## 11. Security Best Practices
|
|
1030
|
+
|
|
1031
|
+
### Token Strategy
|
|
1032
|
+
|
|
1033
|
+
| Token | Lifetime | Storage Location |
|
|
1034
|
+
|-------|----------|------------------|
|
|
1035
|
+
| Access Token | 1 hour | Memory or localStorage (frontend) |
|
|
1036
|
+
| Refresh Token | 30 days | httpOnly cookie or secure storage |
|
|
1037
|
+
| ID Token | 24 hours | httpOnly cookie (backend SSO flow) |
|
|
1038
|
+
| Authorization Code | 10 minutes | Single-use, never store |
|
|
1039
|
+
|
|
1040
|
+
### Rules
|
|
1041
|
+
|
|
1042
|
+
1. **NEVER expose `client_secret` in frontend code** β use backend-proxy flow for confidential clients
|
|
1043
|
+
2. **Always use HTTPS** in production
|
|
1044
|
+
3. **Use `state` parameter** for CSRF protection in OAuth2 flows
|
|
1045
|
+
4. **Use PKCE** for all public clients (SPA, mobile apps)
|
|
1046
|
+
5. **Validate `redirect_uri`** β must match exactly what's registered
|
|
1047
|
+
6. **Validate JWT tokens** β check signature, issuer, audience, expiration
|
|
1048
|
+
7. **Store sensitive tokens in httpOnly cookies** when possible
|
|
1049
|
+
8. **Implement token rotation** β UniAuth automatically rotates refresh tokens
|
|
1050
|
+
9. **Use rate limiting** β UniAuth enforces: SMS 1/min, verify 5/15min
|
|
1051
|
+
|
|
1052
|
+
---
|
|
1053
|
+
|
|
1054
|
+
## 12. Framework-Specific Examples
|
|
1055
|
+
|
|
1056
|
+
### React (with Client SDK)
|
|
1057
|
+
|
|
1058
|
+
```tsx
|
|
1059
|
+
// contexts/AuthContext.tsx
|
|
1060
|
+
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
1061
|
+
import { UniAuthClient } from '@55387.ai/uniauth-client';
|
|
1062
|
+
|
|
1063
|
+
interface AuthContextType {
|
|
1064
|
+
user: any | null;
|
|
1065
|
+
isAuthenticated: boolean;
|
|
1066
|
+
isLoading: boolean;
|
|
1067
|
+
login: (phone: string, code: string) => Promise<void>;
|
|
1068
|
+
logout: () => Promise<void>;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
1072
|
+
|
|
1073
|
+
const auth = new UniAuthClient({
|
|
1074
|
+
baseUrl: 'https://sso.55387.xyz',
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
1078
|
+
const [user, setUser] = useState<any>(null);
|
|
1079
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1080
|
+
|
|
1081
|
+
useEffect(() => {
|
|
1082
|
+
// Check existing auth state on mount
|
|
1083
|
+
if (auth.isAuthenticated()) {
|
|
1084
|
+
auth.getCurrentUser().then(setUser).catch(() => setUser(null)).finally(() => setIsLoading(false));
|
|
1085
|
+
} else {
|
|
1086
|
+
setIsLoading(false);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Listen for auth state changes
|
|
1090
|
+
const unsubscribe = auth.onAuthStateChange((u, isAuth) => {
|
|
1091
|
+
setUser(isAuth ? u : null);
|
|
1092
|
+
});
|
|
1093
|
+
return unsubscribe;
|
|
1094
|
+
}, []);
|
|
1095
|
+
|
|
1096
|
+
const login = async (phone: string, code: string) => {
|
|
1097
|
+
const result = await auth.loginWithCode(phone, code);
|
|
1098
|
+
if (result.mfa_required) {
|
|
1099
|
+
throw new Error('MFA_REQUIRED');
|
|
1100
|
+
}
|
|
1101
|
+
setUser(result.data?.user);
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const logout = async () => {
|
|
1105
|
+
await auth.logout();
|
|
1106
|
+
setUser(null);
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
return (
|
|
1110
|
+
<AuthContext.Provider value={{ user, isAuthenticated: !!user, isLoading, login, logout }}>
|
|
1111
|
+
{children}
|
|
1112
|
+
</AuthContext.Provider>
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export const useAuth = () => {
|
|
1117
|
+
const ctx = useContext(AuthContext);
|
|
1118
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
1119
|
+
return ctx;
|
|
1120
|
+
};
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
### Vue 3 (Composable)
|
|
1124
|
+
|
|
1125
|
+
```typescript
|
|
1126
|
+
// composables/useAuth.ts
|
|
1127
|
+
import { ref, onMounted } from 'vue';
|
|
1128
|
+
import { UniAuthClient } from '@55387.ai/uniauth-client';
|
|
1129
|
+
|
|
1130
|
+
const auth = new UniAuthClient({ baseUrl: 'https://sso.55387.xyz' });
|
|
1131
|
+
const user = ref<any>(null);
|
|
1132
|
+
const isAuthenticated = ref(false);
|
|
1133
|
+
const isLoading = ref(true);
|
|
1134
|
+
|
|
1135
|
+
export function useAuth() {
|
|
1136
|
+
onMounted(async () => {
|
|
1137
|
+
if (auth.isAuthenticated()) {
|
|
1138
|
+
try {
|
|
1139
|
+
user.value = await auth.getCurrentUser();
|
|
1140
|
+
isAuthenticated.value = true;
|
|
1141
|
+
} catch {
|
|
1142
|
+
user.value = null;
|
|
1143
|
+
isAuthenticated.value = false;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
isLoading.value = false;
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
const login = async (phone: string, code: string) => {
|
|
1150
|
+
const result = await auth.loginWithCode(phone, code);
|
|
1151
|
+
user.value = result.data?.user;
|
|
1152
|
+
isAuthenticated.value = true;
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
const logout = async () => {
|
|
1156
|
+
await auth.logout();
|
|
1157
|
+
user.value = null;
|
|
1158
|
+
isAuthenticated.value = false;
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
return { user, isAuthenticated, isLoading, login, logout, auth };
|
|
1162
|
+
}
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
### Express.js (Full Backend)
|
|
1166
|
+
|
|
1167
|
+
```typescript
|
|
1168
|
+
import express from 'express';
|
|
1169
|
+
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
1170
|
+
|
|
1171
|
+
const app = express();
|
|
1172
|
+
const auth = new UniAuthServer({
|
|
1173
|
+
baseUrl: process.env.UNIAUTH_URL || 'https://sso.55387.xyz',
|
|
1174
|
+
clientId: process.env.UNIAUTH_CLIENT_ID!,
|
|
1175
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET!,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Public routes
|
|
1179
|
+
app.get('/health', (req, res) => res.json({ status: 'ok' }));
|
|
1180
|
+
|
|
1181
|
+
// Protected routes
|
|
1182
|
+
app.use('/api/*', auth.middleware());
|
|
1183
|
+
|
|
1184
|
+
app.get('/api/profile', (req, res) => {
|
|
1185
|
+
res.json({ user: req.user });
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
app.get('/api/data', (req, res) => {
|
|
1189
|
+
const userId = req.authPayload?.sub;
|
|
1190
|
+
// Fetch user-specific data...
|
|
1191
|
+
res.json({ userId, data: [] });
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
app.listen(3000, () => console.log('Server running on :3000'));
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
---
|
|
1198
|
+
|
|
1199
|
+
## 13. Troubleshooting
|
|
1200
|
+
|
|
1201
|
+
| Error | Cause | Solution |
|
|
1202
|
+
|-------|-------|----------|
|
|
1203
|
+
| `invalid_client` | Wrong `client_id` | Verify credentials in Developer Console |
|
|
1204
|
+
| `Client authentication failed` | Wrong `client_secret` or using confidential client from frontend | Use backend-proxy flow, or switch to Public Client |
|
|
1205
|
+
| `invalid_grant` | Auth code expired (10 min) or already used | Codes are single-use, request a new one |
|
|
1206
|
+
| `redirect_uri mismatch` | Callback URL doesn't match registered URI | URLs must match exactly (protocol + domain + port + path) |
|
|
1207
|
+
| `PKCE required` | Public client must use PKCE | Use `auth.loginWithSSO({ usePKCE: true })` |
|
|
1208
|
+
| 404 on OAuth2 endpoints | Wrong endpoint path | Use `/api/v1/oauth2/authorize`, not `/oauth2/authorize` |
|
|
1209
|
+
| Token expired (401) | Access token expired | Use refresh token or `auth.getAccessToken()` (auto-refreshes) |
|
|
1210
|
+
| `ERR_SSL_PROTOCOL_ERROR` | Using HTTPS on localhost | Use `http://localhost:3000` for local development |
|
|
1211
|
+
|
|
1212
|
+
### Debugging Tips
|
|
1213
|
+
|
|
1214
|
+
1. Check the OIDC discovery document: `https://sso.55387.xyz/.well-known/openid-configuration`
|
|
1215
|
+
2. Decode JWTs at [jwt.io](https://jwt.io) to inspect claims
|
|
1216
|
+
3. Test API calls with cURL before implementing in code
|
|
1217
|
+
4. Enable `onAuthError` callback in the Client SDK for early error detection
|
|
1218
|
+
5. Check UniAuth API docs at `https://sso.55387.xyz/docs` (Swagger UI)
|
|
1219
|
+
|
|
1220
|
+
---
|
|
1221
|
+
|
|
1222
|
+
## 14. FAQ
|
|
1223
|
+
|
|
1224
|
+
**Q: Can I verify tokens locally without calling UniAuth?**
|
|
1225
|
+
A: Yes. Configure `jwtPublicKey` in the Server SDK, or fetch the JWKS from `/.well-known/jwks.json`. Default behavior is remote verification with 1-minute cache.
|
|
1226
|
+
|
|
1227
|
+
**Q: Who handles token refresh?**
|
|
1228
|
+
A: The frontend Client SDK handles auto-refresh via `getAccessToken()`. Backend only needs to verify the access token.
|
|
1229
|
+
|
|
1230
|
+
**Q: How long is the token cache?**
|
|
1231
|
+
A: Server SDK caches verification results for 1 minute. Call `clearCache()` to clear.
|
|
1232
|
+
|
|
1233
|
+
**Q: Can I use UniAuth without the SDK?**
|
|
1234
|
+
A: Yes, see Method D (Direct API) or Method C (OIDC standard library).
|
|
1235
|
+
|
|
1236
|
+
**Q: What scopes are available?**
|
|
1237
|
+
A: `openid`, `profile`, `email`, `phone`. Use `openid profile email phone` for full access.
|
|
1238
|
+
|
|
1239
|
+
**Q: Is M2M (machine-to-machine) authentication supported?**
|
|
1240
|
+
A: Yes, use Client Credentials flow:
|
|
1241
|
+
```typescript
|
|
1242
|
+
const result = await client.loginWithClientCredentials('scope1 scope2');
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## Quick Reference Card
|
|
1248
|
+
|
|
1249
|
+
```
|
|
1250
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1251
|
+
β UniAuth Quick Reference β
|
|
1252
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
1253
|
+
β Service URL: https://sso.55387.xyz β
|
|
1254
|
+
β Swagger: https://sso.55387.xyz/docs β
|
|
1255
|
+
β OIDC Discovery: https://sso.55387.xyz/.well-known/openid-configuration β
|
|
1256
|
+
β JWKS: https://sso.55387.xyz/.well-known/jwks.json β
|
|
1257
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
1258
|
+
β Frontend SDK: npm install @55387.ai/uniauth-client β
|
|
1259
|
+
β Backend SDK: npm install @55387.ai/uniauth-server β
|
|
1260
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
1261
|
+
β Access Token: 1h β Refresh Token: 30d β ID Token: 24hβ
|
|
1262
|
+
β Auth Code: 10min β SMS Rate: 1/min β PKCE: S256 β
|
|
1263
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
1264
|
+
β Key Endpoints: β
|
|
1265
|
+
β POST /api/v1/auth/phone/send-code β Send SMS code β
|
|
1266
|
+
β POST /api/v1/auth/phone/verify β SMS login β
|
|
1267
|
+
β POST /api/v1/auth/email/login β Email+password β
|
|
1268
|
+
β POST /api/v1/auth/refresh β Refresh token β
|
|
1269
|
+
β GET /api/v1/user/me β Get current user β
|
|
1270
|
+
β POST /api/v1/oauth2/token β OAuth2 token exchangeβ
|
|
1271
|
+
β GET /api/v1/oauth2/userinfo β OIDC userinfo β
|
|
1272
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1273
|
+
```
|