@55387.ai/uniauth-server 1.2.1 → 1.2.4
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 +92 -171
- package/ai-prompts/backend-protection.md +197 -0
- package/dist/index.cjs +18 -0
- package/dist/index.js +18 -0
- package/package.json +3 -3
- package/INTEGRATION.md +0 -1273
package/README.md
CHANGED
|
@@ -1,244 +1,151 @@
|
|
|
1
1
|
# @55387.ai/uniauth-server
|
|
2
2
|
|
|
3
|
-
UniAuth
|
|
3
|
+
> UniAuth Backend SDK — Token verification & middleware for Node.js servers.
|
|
4
|
+
>
|
|
5
|
+
> UniAuth 后端 SDK — Node.js 服务端令牌验证和中间件。
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Version / 版本:** 1.2.2
|
|
8
|
+
|
|
9
|
+
## Install / 安装
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
npm install @55387.ai/uniauth-server
|
|
9
|
-
# or
|
|
13
|
+
# or / 或
|
|
10
14
|
pnpm add @55387.ai/uniauth-server
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
## 快速开始
|
|
17
|
+
## Quick Start / 快速开始
|
|
14
18
|
|
|
15
19
|
```typescript
|
|
16
20
|
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
17
21
|
|
|
18
22
|
const auth = new UniAuthServer({
|
|
19
23
|
baseUrl: 'https://sso.55387.xyz',
|
|
20
|
-
clientId:
|
|
21
|
-
clientSecret:
|
|
24
|
+
clientId: process.env.UNIAUTH_CLIENT_ID!,
|
|
25
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET!,
|
|
22
26
|
});
|
|
23
27
|
|
|
24
|
-
// 验证令牌
|
|
28
|
+
// Verify token / 验证令牌
|
|
25
29
|
const payload = await auth.verifyToken(accessToken);
|
|
26
30
|
console.log('User ID:', payload.sub);
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
##
|
|
33
|
+
## Middleware / 中间件
|
|
34
|
+
|
|
35
|
+
### Express
|
|
30
36
|
|
|
31
37
|
```typescript
|
|
32
38
|
import express from 'express';
|
|
33
|
-
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
34
|
-
|
|
35
39
|
const app = express();
|
|
36
|
-
const auth = new UniAuthServer({ ... });
|
|
37
40
|
|
|
38
|
-
// 保护 API 路由
|
|
39
41
|
app.use('/api/*', auth.middleware());
|
|
40
42
|
|
|
41
|
-
// 在路由中使用用户信息
|
|
42
43
|
app.get('/api/profile', (req, res) => {
|
|
43
44
|
res.json({ user: req.user, payload: req.authPayload });
|
|
44
45
|
});
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
### Hono
|
|
48
49
|
|
|
49
50
|
```typescript
|
|
50
51
|
import { Hono } from 'hono';
|
|
51
|
-
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
52
|
-
|
|
53
52
|
const app = new Hono();
|
|
54
|
-
const auth = new UniAuthServer({ ... });
|
|
55
53
|
|
|
56
|
-
// 保护 API 路由
|
|
57
54
|
app.use('/api/*', auth.honoMiddleware());
|
|
58
55
|
|
|
59
|
-
// 在路由中使用用户信息
|
|
60
56
|
app.get('/api/profile', (c) => {
|
|
61
|
-
|
|
62
|
-
return c.json({ user });
|
|
57
|
+
return c.json({ user: c.get('user') });
|
|
63
58
|
});
|
|
64
59
|
```
|
|
65
60
|
|
|
66
|
-
## SSO
|
|
61
|
+
## SSO Backend Proxy / SSO 后端代理
|
|
67
62
|
|
|
68
|
-
|
|
63
|
+
When your app is a **Confidential Client**, token exchange must happen on the server.
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
当应用配置为 **机密客户端** 时,Token 交换必须在服务端完成。
|
|
71
66
|
|
|
72
67
|
```
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
User → Frontend → /api/auth/login → Backend → redirect to UniAuth SSO
|
|
69
|
+
↓
|
|
70
|
+
User ← Frontend ← redirect ← Backend (set cookie) ← SSO callback
|
|
71
|
+
↑
|
|
72
|
+
Backend exchanges code with client_secret
|
|
78
73
|
```
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| 端点 | URL |
|
|
83
|
-
|------|-----|
|
|
84
|
-
| 授权端点 | `https://sso.55387.xyz/api/v1/oauth2/authorize` |
|
|
85
|
-
| Token 端点 | `https://sso.55387.xyz/api/v1/oauth2/token` |
|
|
86
|
-
| 用户信息端点 | `https://sso.55387.xyz/api/v1/oauth2/userinfo` |
|
|
87
|
-
|
|
88
|
-
### 实现示例(Hono)
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { Hono } from 'hono';
|
|
92
|
-
import { setCookie, getCookie } from 'hono/cookie';
|
|
93
|
-
|
|
94
|
-
const app = new Hono();
|
|
95
|
-
|
|
96
|
-
// 登录端点 - 重定向到 SSO
|
|
97
|
-
app.get('/api/auth/login', (c) => {
|
|
98
|
-
const origin = c.req.header('origin') || 'http://localhost:3000';
|
|
99
|
-
const redirectUri = `${origin}/api/auth/callback`;
|
|
100
|
-
|
|
101
|
-
const params = new URLSearchParams({
|
|
102
|
-
client_id: process.env.UNIAUTH_CLIENT_ID,
|
|
103
|
-
redirect_uri: redirectUri,
|
|
104
|
-
response_type: 'code',
|
|
105
|
-
scope: 'openid profile email phone',
|
|
106
|
-
state: generateRandomState(), // 生成随机 state 防止 CSRF
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return c.redirect(`https://sso.55387.xyz/api/v1/oauth2/authorize?${params}`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// 回调端点 - 交换 Token
|
|
113
|
-
app.get('/api/auth/callback', async (c) => {
|
|
114
|
-
const code = c.req.query('code');
|
|
115
|
-
const origin = c.req.header('referer')?.replace(/\/api\/auth\/callback.*$/, '') || 'http://localhost:3000';
|
|
116
|
-
|
|
117
|
-
// 用授权码交换 Token
|
|
118
|
-
const response = await fetch('https://sso.55387.xyz/api/v1/oauth2/token', {
|
|
119
|
-
method: 'POST',
|
|
120
|
-
headers: { 'Content-Type': 'application/json' },
|
|
121
|
-
body: JSON.stringify({
|
|
122
|
-
client_id: process.env.UNIAUTH_CLIENT_ID,
|
|
123
|
-
client_secret: process.env.UNIAUTH_CLIENT_SECRET,
|
|
124
|
-
code,
|
|
125
|
-
grant_type: 'authorization_code',
|
|
126
|
-
redirect_uri: `${origin}/api/auth/callback`,
|
|
127
|
-
}),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const { access_token, id_token } = await response.json();
|
|
131
|
-
|
|
132
|
-
// 将 Token 存储到 httpOnly Cookie
|
|
133
|
-
setCookie(c, 'auth_token', id_token, {
|
|
134
|
-
httpOnly: true,
|
|
135
|
-
secure: true,
|
|
136
|
-
sameSite: 'Lax',
|
|
137
|
-
maxAge: 60 * 60 * 24 * 7, // 7 天
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
return c.redirect('/');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// 检查登录状态
|
|
144
|
-
app.get('/api/auth/status', async (c) => {
|
|
145
|
-
const token = getCookie(c, 'auth_token');
|
|
146
|
-
if (!token) {
|
|
147
|
-
return c.json({ authenticated: false });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 验证 Token
|
|
151
|
-
try {
|
|
152
|
-
const payload = await auth.verifyToken(token);
|
|
153
|
-
return c.json({ authenticated: true, userId: payload.sub });
|
|
154
|
-
} catch {
|
|
155
|
-
return c.json({ authenticated: false });
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
```
|
|
75
|
+
See full implementation: [AI Integration Guide](../../docs/AI_INTEGRATION_GUIDE.md#2b-backend-proxy-confidential-client)
|
|
159
76
|
|
|
160
|
-
|
|
77
|
+
完整实现见: [集成指南](../../docs/AI_INTEGRATION_GUIDE.md#2b-backend-proxy-confidential-client)
|
|
161
78
|
|
|
162
|
-
|
|
163
|
-
// 触发登录
|
|
164
|
-
const handleLogin = () => {
|
|
165
|
-
window.location.href = '/api/auth/login';
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// 检查登录状态
|
|
169
|
-
const checkAuth = async () => {
|
|
170
|
-
const response = await fetch('/api/auth/status', { credentials: 'include' });
|
|
171
|
-
const data = await response.json();
|
|
172
|
-
return data.authenticated;
|
|
173
|
-
};
|
|
174
|
-
```
|
|
79
|
+
## Token Introspection / 令牌内省
|
|
175
80
|
|
|
176
|
-
|
|
81
|
+
RFC 7662 compliant token introspection:
|
|
177
82
|
|
|
178
83
|
```typescript
|
|
179
|
-
// 内省令牌(资源服务器标准验证方式)
|
|
180
84
|
const result = await auth.introspectToken(accessToken);
|
|
181
85
|
|
|
182
86
|
if (result.active) {
|
|
183
|
-
console.log('
|
|
184
|
-
console.log('
|
|
185
|
-
console.log('权限:', result.scope);
|
|
186
|
-
} else {
|
|
187
|
-
console.log('Token 无效或已过期');
|
|
87
|
+
console.log('User:', result.sub);
|
|
88
|
+
console.log('Scope:', result.scope);
|
|
188
89
|
}
|
|
189
90
|
```
|
|
190
91
|
|
|
191
|
-
## API 参考
|
|
92
|
+
## API Reference / API 参考
|
|
192
93
|
|
|
193
|
-
###
|
|
94
|
+
### Config / 配置
|
|
194
95
|
|
|
195
96
|
```typescript
|
|
196
97
|
interface UniAuthServerConfig {
|
|
197
|
-
baseUrl: string; // UniAuth
|
|
198
|
-
clientId: string; // OAuth2
|
|
199
|
-
clientSecret: string; // OAuth2
|
|
200
|
-
jwtPublicKey?: string; // JWT
|
|
98
|
+
baseUrl: string; // UniAuth server URL
|
|
99
|
+
clientId: string; // OAuth2 client ID
|
|
100
|
+
clientSecret: string; // OAuth2 client secret
|
|
101
|
+
jwtPublicKey?: string; // JWT public key (local verification)
|
|
201
102
|
}
|
|
202
103
|
```
|
|
203
104
|
|
|
204
|
-
### 方法
|
|
105
|
+
### Methods / 方法
|
|
205
106
|
|
|
206
|
-
|
|
|
207
|
-
|
|
208
|
-
| `verifyToken(token)` | 验证访问令牌 |
|
|
209
|
-
| `introspectToken(token)` | RFC 7662 令牌内省 |
|
|
210
|
-
| `isTokenActive(token)` |
|
|
211
|
-
| `getUser(userId)` | 获取用户信息 |
|
|
212
|
-
| `middleware()` | Express/
|
|
213
|
-
| `honoMiddleware()` | Hono 中间件 |
|
|
214
|
-
| `clearCache()` | 清除令牌缓存 |
|
|
107
|
+
| Method | Description / 说明 |
|
|
108
|
+
|--------|-----------|
|
|
109
|
+
| `verifyToken(token)` | Verify access token / 验证访问令牌 |
|
|
110
|
+
| `introspectToken(token)` | RFC 7662 introspection / 令牌内省 |
|
|
111
|
+
| `isTokenActive(token)` | Check if token is active / 检查令牌状态 |
|
|
112
|
+
| `getUser(userId)` | Get user info / 获取用户信息 |
|
|
113
|
+
| `middleware()` | Express middleware / Express 中间件 |
|
|
114
|
+
| `honoMiddleware()` | Hono middleware / Hono 中间件 |
|
|
115
|
+
| `clearCache()` | Clear token cache / 清除令牌缓存 |
|
|
215
116
|
|
|
216
|
-
###
|
|
117
|
+
### Token Verification Flow / 令牌验证流程
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
verifyToken(token)
|
|
121
|
+
│
|
|
122
|
+
├─ 1. POST /api/v1/auth/verify (App Key + Secret)
|
|
123
|
+
│ ↓ success → return payload
|
|
124
|
+
│ ↓ 404 or network error
|
|
125
|
+
│
|
|
126
|
+
├─ 2. POST /api/v1/oauth2/introspect (Basic Auth, RFC 7662)
|
|
127
|
+
│ ↓ active:true → return payload
|
|
128
|
+
│ ↓ fail
|
|
129
|
+
│
|
|
130
|
+
└─ 3. Local JWT verification (if jwtPublicKey configured)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Types / 类型
|
|
217
134
|
|
|
218
135
|
```typescript
|
|
219
136
|
interface TokenPayload {
|
|
220
|
-
sub: string;
|
|
221
|
-
iss?: string;
|
|
222
|
-
aud?: string | string[];
|
|
223
|
-
exp: number;
|
|
224
|
-
iat: number;
|
|
225
|
-
scope?: string;
|
|
226
|
-
phone?: string;
|
|
227
|
-
email?: string;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
interface UserInfo {
|
|
231
|
-
id: string;
|
|
232
|
-
phone?: string;
|
|
233
|
-
email?: string;
|
|
234
|
-
nickname?: string;
|
|
235
|
-
avatar_url?: string;
|
|
236
|
-
phone_verified?: boolean;
|
|
237
|
-
email_verified?: boolean;
|
|
137
|
+
sub: string; // User ID
|
|
138
|
+
iss?: string; // Issuer
|
|
139
|
+
aud?: string | string[]; // Audience
|
|
140
|
+
exp: number; // Expiration
|
|
141
|
+
iat: number; // Issued at
|
|
142
|
+
scope?: string; // Scopes
|
|
143
|
+
phone?: string; // Phone number
|
|
144
|
+
email?: string; // Email
|
|
238
145
|
}
|
|
239
146
|
```
|
|
240
147
|
|
|
241
|
-
## 错误处理
|
|
148
|
+
## Error Handling / 错误处理
|
|
242
149
|
|
|
243
150
|
```typescript
|
|
244
151
|
import { ServerAuthError, ServerErrorCode } from '@55387.ai/uniauth-server';
|
|
@@ -248,17 +155,31 @@ try {
|
|
|
248
155
|
} catch (error) {
|
|
249
156
|
if (error instanceof ServerAuthError) {
|
|
250
157
|
switch (error.code) {
|
|
251
|
-
case ServerErrorCode.INVALID_TOKEN:
|
|
252
|
-
|
|
253
|
-
break;
|
|
254
|
-
case ServerErrorCode.TOKEN_EXPIRED:
|
|
255
|
-
// 令牌已过期
|
|
256
|
-
break;
|
|
158
|
+
case ServerErrorCode.INVALID_TOKEN: // Invalid / 令牌无效
|
|
159
|
+
case ServerErrorCode.TOKEN_EXPIRED: // Expired / 令牌过期
|
|
257
160
|
}
|
|
258
161
|
}
|
|
259
162
|
}
|
|
260
163
|
```
|
|
261
164
|
|
|
165
|
+
## 🤖 AI Agent Prompts / AI 智能体提示词
|
|
166
|
+
|
|
167
|
+
This package includes an AI-ready integration prompt. Copy it into your AI coding assistant (Claude, Cursor, Copilot, etc.) to generate a complete backend protection setup automatically.
|
|
168
|
+
|
|
169
|
+
本包附带 AI 集成提示词。将其复制到 AI 编程助手中,即可自动生成完整的后端保护代码。
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# After install, find the prompt at:
|
|
173
|
+
# 安装后,提示词文件位于:
|
|
174
|
+
cat node_modules/@55387.ai/uniauth-server/ai-prompts/backend-protection.md
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> [!TIP]
|
|
178
|
+
> Replace placeholders like `YOUR_UNIAUTH_URL` and `YOUR_CLIENT_SECRET` before pasting into your AI assistant.
|
|
179
|
+
> 粘贴到 AI 助手前,请替换 `YOUR_UNIAUTH_URL` 和 `YOUR_CLIENT_SECRET` 等占位符。
|
|
180
|
+
|
|
181
|
+
See all prompts: [docs/ai-prompts/](../../docs/ai-prompts/README.md)
|
|
182
|
+
|
|
262
183
|
## License
|
|
263
184
|
|
|
264
185
|
MIT
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# 🤖 AI Prompt: Backend API Protection
|
|
2
|
+
|
|
3
|
+
> Copy everything below the line and paste into your AI coding assistant.
|
|
4
|
+
>
|
|
5
|
+
> 复制下方分割线以下的全部内容,粘贴到你的 AI 编程助手中。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are an expert Node.js/TypeScript backend developer. Help me protect my API routes using UniAuth token verification with the official `@55387.ai/uniauth-server` SDK.
|
|
10
|
+
|
|
11
|
+
## Project Context
|
|
12
|
+
|
|
13
|
+
- **UniAuth Server URL**: `YOUR_UNIAUTH_URL` (e.g. `https://auth.55387.xyz`)
|
|
14
|
+
- **Client ID**: `YOUR_CLIENT_ID`
|
|
15
|
+
- **Client Secret**: `YOUR_CLIENT_SECRET`
|
|
16
|
+
- **Framework**: Express / Hono / Next.js API Routes (choose based on my project)
|
|
17
|
+
|
|
18
|
+
## SDK Reference
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @55387.ai/uniauth-server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### UniAuthServer Class
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
30
|
+
|
|
31
|
+
const auth = new UniAuthServer({
|
|
32
|
+
baseUrl: 'YOUR_UNIAUTH_URL',
|
|
33
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
34
|
+
clientSecret: 'YOUR_CLIENT_SECRET',
|
|
35
|
+
jwtPublicKey: process.env.JWT_PUBLIC_KEY, // optional, for local verification
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Core Methods
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// Verify an access token (remote → introspection → local JWT fallback)
|
|
43
|
+
const payload = await auth.verifyToken(accessToken);
|
|
44
|
+
// Returns: TokenPayload { sub, iss, aud, iat, exp, scope, azp, phone, email }
|
|
45
|
+
|
|
46
|
+
// Token introspection (RFC 7662)
|
|
47
|
+
const result = await auth.introspectToken(token, 'access_token');
|
|
48
|
+
// Returns: IntrospectionResult { active, scope, client_id, sub, exp, ... }
|
|
49
|
+
|
|
50
|
+
// Check if token is active
|
|
51
|
+
const isActive = await auth.isTokenActive(token);
|
|
52
|
+
|
|
53
|
+
// Get user info by ID
|
|
54
|
+
const user = await auth.getUser(userId);
|
|
55
|
+
// Returns: UserInfo { id, phone, email, nickname, avatar_url, ... }
|
|
56
|
+
|
|
57
|
+
// Cache management
|
|
58
|
+
auth.clearCache();
|
|
59
|
+
auth.getCacheStats(); // { size, entries }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Express Middleware
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import express from 'express';
|
|
66
|
+
|
|
67
|
+
const app = express();
|
|
68
|
+
|
|
69
|
+
// Protect all /api routes
|
|
70
|
+
app.use('/api/*', auth.middleware());
|
|
71
|
+
|
|
72
|
+
// Access user info in route handlers
|
|
73
|
+
app.get('/api/profile', (req, res) => {
|
|
74
|
+
// req.authPayload — TokenPayload (always available)
|
|
75
|
+
// req.user — UserInfo (fetched automatically, may be undefined)
|
|
76
|
+
res.json({ user: req.user, payload: req.authPayload });
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Hono Middleware
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Hono } from 'hono';
|
|
84
|
+
|
|
85
|
+
const app = new Hono();
|
|
86
|
+
|
|
87
|
+
// Protect all /api routes
|
|
88
|
+
app.use('/api/*', auth.honoMiddleware());
|
|
89
|
+
|
|
90
|
+
// Access user info in route handlers
|
|
91
|
+
app.get('/api/profile', (c) => {
|
|
92
|
+
const user = c.get('user'); // UserInfo
|
|
93
|
+
const payload = c.get('authPayload'); // TokenPayload
|
|
94
|
+
return c.json({ user, payload });
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Next.js API Route Protection
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// middleware.ts or in each API route
|
|
102
|
+
import { UniAuthServer } from '@55387.ai/uniauth-server';
|
|
103
|
+
|
|
104
|
+
const auth = new UniAuthServer({
|
|
105
|
+
baseUrl: process.env.UNIAUTH_URL!,
|
|
106
|
+
clientId: process.env.UNIAUTH_CLIENT_ID!,
|
|
107
|
+
clientSecret: process.env.UNIAUTH_CLIENT_SECRET!,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export async function protectRoute(req: Request) {
|
|
111
|
+
const authHeader = req.headers.get('authorization');
|
|
112
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
113
|
+
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
|
|
114
|
+
}
|
|
115
|
+
const payload = await auth.verifyToken(authHeader.substring(7));
|
|
116
|
+
return payload; // Use in your handler
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Types
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
interface TokenPayload {
|
|
124
|
+
sub: string; // User ID or Client ID (for M2M tokens)
|
|
125
|
+
iss?: string; // Issuer
|
|
126
|
+
aud?: string | string[];
|
|
127
|
+
iat: number; // Issued at
|
|
128
|
+
exp: number; // Expiration
|
|
129
|
+
scope?: string; // Scopes (space-separated)
|
|
130
|
+
azp?: string; // Authorized party (client_id)
|
|
131
|
+
phone?: string;
|
|
132
|
+
email?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface UserInfo {
|
|
136
|
+
id: string;
|
|
137
|
+
phone?: string | null;
|
|
138
|
+
email?: string | null;
|
|
139
|
+
nickname?: string | null;
|
|
140
|
+
avatar_url?: string | null;
|
|
141
|
+
phone_verified?: boolean;
|
|
142
|
+
email_verified?: boolean;
|
|
143
|
+
created_at?: string;
|
|
144
|
+
updated_at?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Error handling
|
|
148
|
+
import { ServerAuthError, ServerErrorCode } from '@55387.ai/uniauth-server';
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await auth.verifyToken(token);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof ServerAuthError) {
|
|
154
|
+
// error.code: 'INVALID_TOKEN' | 'TOKEN_EXPIRED' | 'UNAUTHORIZED' | ...
|
|
155
|
+
// error.statusCode: 401
|
|
156
|
+
// error.message: human-readable message
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Error Codes
|
|
162
|
+
|
|
163
|
+
| Code | Meaning |
|
|
164
|
+
|------|---------|
|
|
165
|
+
| `INVALID_TOKEN` | Token is malformed or invalid |
|
|
166
|
+
| `TOKEN_EXPIRED` | Token has expired |
|
|
167
|
+
| `VERIFICATION_FAILED` | Remote verification failed |
|
|
168
|
+
| `USER_NOT_FOUND` | User ID from token not found |
|
|
169
|
+
| `UNAUTHORIZED` | Missing or invalid Authorization header |
|
|
170
|
+
| `NO_PUBLIC_KEY` | JWT public key not configured for local verification |
|
|
171
|
+
| `NETWORK_ERROR` | Cannot reach UniAuth server |
|
|
172
|
+
|
|
173
|
+
## Requirements
|
|
174
|
+
|
|
175
|
+
1. **Middleware Setup**: Configure `UniAuthServer` and apply middleware to protect API routes.
|
|
176
|
+
|
|
177
|
+
2. **Public vs Protected**: Some routes should be public (e.g. health check, login). Apply middleware selectively.
|
|
178
|
+
|
|
179
|
+
3. **Error Handling**: Return proper JSON error responses with appropriate HTTP status codes. Never expose internal errors.
|
|
180
|
+
|
|
181
|
+
4. **M2M Support**: Handle both user tokens (`sub` = user ID) and machine-to-machine tokens (`sub` = client ID, no user info).
|
|
182
|
+
|
|
183
|
+
5. **CORS**: Configure CORS to allow requests from your frontend origin with `Authorization` header.
|
|
184
|
+
|
|
185
|
+
6. **Rate Limiting**: Add basic rate limiting to prevent abuse.
|
|
186
|
+
|
|
187
|
+
7. **Environment Variables**:
|
|
188
|
+
```env
|
|
189
|
+
UNIAUTH_URL=YOUR_UNIAUTH_URL
|
|
190
|
+
UNIAUTH_CLIENT_ID=YOUR_CLIENT_ID
|
|
191
|
+
UNIAUTH_CLIENT_SECRET=YOUR_CLIENT_SECRET
|
|
192
|
+
JWT_PUBLIC_KEY=optional_for_local_verification
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
8. **Testing**: Include unit tests using Vitest. Mock the `UniAuthServer` for testing.
|
|
196
|
+
|
|
197
|
+
Generate the complete implementation based on my project's framework.
|
package/dist/index.cjs
CHANGED
|
@@ -113,6 +113,24 @@ var UniAuthServer = class {
|
|
|
113
113
|
if (error instanceof ServerAuthError) {
|
|
114
114
|
throw error;
|
|
115
115
|
}
|
|
116
|
+
try {
|
|
117
|
+
const introspectionResult = await this.introspectToken(token);
|
|
118
|
+
if (introspectionResult.active) {
|
|
119
|
+
const payload = {
|
|
120
|
+
sub: introspectionResult.sub || "",
|
|
121
|
+
iss: introspectionResult.iss || "",
|
|
122
|
+
aud: introspectionResult.aud || "",
|
|
123
|
+
iat: introspectionResult.iat || 0,
|
|
124
|
+
exp: introspectionResult.exp || 0,
|
|
125
|
+
scope: introspectionResult.scope,
|
|
126
|
+
azp: introspectionResult.client_id
|
|
127
|
+
};
|
|
128
|
+
const cacheExpiry = Math.min(payload.exp * 1e3, Date.now() + 60 * 1e3);
|
|
129
|
+
this.tokenCache.set(token, { payload, expiresAt: cacheExpiry });
|
|
130
|
+
return payload;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
116
134
|
if (this.config.jwtPublicKey) {
|
|
117
135
|
return this.verifyTokenLocally(token);
|
|
118
136
|
}
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,24 @@ var UniAuthServer = class {
|
|
|
76
76
|
if (error instanceof ServerAuthError) {
|
|
77
77
|
throw error;
|
|
78
78
|
}
|
|
79
|
+
try {
|
|
80
|
+
const introspectionResult = await this.introspectToken(token);
|
|
81
|
+
if (introspectionResult.active) {
|
|
82
|
+
const payload = {
|
|
83
|
+
sub: introspectionResult.sub || "",
|
|
84
|
+
iss: introspectionResult.iss || "",
|
|
85
|
+
aud: introspectionResult.aud || "",
|
|
86
|
+
iat: introspectionResult.iat || 0,
|
|
87
|
+
exp: introspectionResult.exp || 0,
|
|
88
|
+
scope: introspectionResult.scope,
|
|
89
|
+
azp: introspectionResult.client_id
|
|
90
|
+
};
|
|
91
|
+
const cacheExpiry = Math.min(payload.exp * 1e3, Date.now() + 60 * 1e3);
|
|
92
|
+
this.tokenCache.set(token, { payload, expiresAt: cacheExpiry });
|
|
93
|
+
return payload;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
79
97
|
if (this.config.jwtPublicKey) {
|
|
80
98
|
return this.verifyTokenLocally(token);
|
|
81
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@55387.ai/uniauth-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "UniAuth Server SDK - Token verification for Node.js backends",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"README.md",
|
|
12
|
-
"
|
|
12
|
+
"ai-prompts"
|
|
13
13
|
],
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"jwt",
|
|
46
46
|
"sdk"
|
|
47
47
|
]
|
|
48
|
-
}
|
|
48
|
+
}
|