sumor 3.2.3 → 3.3.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/README.md +367 -414
- package/README.zh-CN.md +570 -0
- package/dist/server/index.d.ts +2 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/middlewares/loadJwtUserMiddleware.d.ts +0 -1
- package/dist/server/middlewares/loadJwtUserMiddleware.d.ts.map +1 -1
- package/dist/server/middlewares/loadJwtUserMiddleware.js +4 -2
- package/dist/server/middlewares/loadJwtUserMiddleware.js.map +1 -1
- package/dist/server/mock/mockApiRoutes.d.ts +14 -0
- package/dist/server/mock/mockApiRoutes.d.ts.map +1 -0
- package/dist/server/mock/mockApiRoutes.js +149 -0
- package/dist/server/mock/mockApiRoutes.js.map +1 -0
- package/dist/server/mock/mockConfig.d.ts +34 -0
- package/dist/server/mock/mockConfig.d.ts.map +1 -0
- package/dist/server/mock/mockConfig.js +47 -0
- package/dist/server/mock/mockConfig.js.map +1 -0
- package/dist/server/mock/mockRoutes.d.ts +9 -0
- package/dist/server/mock/mockRoutes.d.ts.map +1 -0
- package/dist/server/mock/mockRoutes.js +97 -0
- package/dist/server/mock/mockRoutes.js.map +1 -0
- package/dist/server/mock/mockTokenUtils.d.ts +30 -0
- package/dist/server/mock/mockTokenUtils.d.ts.map +1 -0
- package/dist/server/mock/mockTokenUtils.js +81 -0
- package/dist/server/mock/mockTokenUtils.js.map +1 -0
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +29 -25
- package/dist/server/routes.js.map +1 -1
- package/dist/server/services/oauthService.d.ts +0 -8
- package/dist/server/services/oauthService.d.ts.map +1 -1
- package/dist/server/services/oauthService.js +0 -24
- package/dist/server/services/oauthService.js.map +1 -1
- package/dist/server/types/oauth.d.ts +0 -1
- package/dist/server/types/oauth.d.ts.map +1 -1
- package/dist/server/utils/config.d.ts.map +1 -1
- package/dist/server/utils/config.js +13 -0
- package/dist/server/utils/config.js.map +1 -1
- package/dist/web/OAuthStore.d.ts +11 -5
- package/dist/web/OAuthStore.d.ts.map +1 -1
- package/dist/web/OAuthStore.js +43 -64
- package/dist/web/OAuthStore.js.map +1 -1
- package/dist/web/UrlHelper.d.ts +1 -0
- package/dist/web/UrlHelper.d.ts.map +1 -1
- package/dist/web/UrlHelper.js +11 -0
- package/dist/web/UrlHelper.js.map +1 -1
- package/dist/web/api/login.d.ts +2 -2
- package/dist/web/api/login.d.ts.map +1 -1
- package/dist/web/api/login.js +12 -2
- package/dist/web/api/login.js.map +1 -1
- package/dist/web/api/logout.d.ts +1 -1
- package/dist/web/api/logout.d.ts.map +1 -1
- package/dist/web/api/logout.js +3 -2
- package/dist/web/api/logout.js.map +1 -1
- package/package.json +2 -1
- package/dist/server/middlewares/isLoggedMiddleware.d.ts +0 -15
- package/dist/server/middlewares/isLoggedMiddleware.d.ts.map +0 -1
- package/dist/server/middlewares/isLoggedMiddleware.js +0 -35
- package/dist/server/middlewares/isLoggedMiddleware.js.map +0 -1
- package/dist/server/middlewares/isVerifiedMiddleware.d.ts +0 -16
- package/dist/server/middlewares/isVerifiedMiddleware.d.ts.map +0 -1
- package/dist/server/middlewares/isVerifiedMiddleware.js +0 -44
- package/dist/server/middlewares/isVerifiedMiddleware.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,614 +5,567 @@
|
|
|
5
5
|
|
|
6
6
|
A comprehensive OAuth 2.0 authentication framework for Express.js applications with role-based access control (RBAC). Sumor simplifies OAuth integration, token management, and permission-based route protection in multi-service architectures.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
[中文文档](https://www.npmjs.com/package/sumor?activeTab=code)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- **🔑 Session & Token Management**: Secure token exchange, refresh, and blacklisting
|
|
12
|
-
- **🛡️ JWT Verification**: Built-in JWT validation using JWKS (JSON Web Key Set)
|
|
13
|
-
- **👥 Role-Based Access Control (RBAC)**: Permission-based route protection and middleware
|
|
14
|
-
- **📝 TypeScript First**: Full TypeScript support with complete type definitions
|
|
15
|
-
- **🚀 Express Integration**: Drop-in middleware and route setup
|
|
16
|
-
- **🎯 Permission Sync**: Automatic permission synchronization with OAuth provider
|
|
17
|
-
- **💾 Session Revocation**: Token blacklist support for logout and session management
|
|
18
|
-
- **🌐 Multi-Domain Support**: Built-in domain and origin handling
|
|
19
|
-
- **⚡ Request Context**: Access user info and OAuth service in Express request object
|
|
10
|
+
## Key Features
|
|
20
11
|
|
|
21
|
-
|
|
12
|
+
- **OAuth 2.0 Complete Flow**: Full authorization code flow with automatic token exchange
|
|
13
|
+
- **Session & Token Management**: Secure token exchange, refresh via HTTP-only cookies
|
|
14
|
+
- **JWT Verification**: Built-in JWT validation using JWKS (JSON Web Key Set)
|
|
15
|
+
- **Role-Based Access Control (RBAC)**: Permission sync and runtime permission/role checks
|
|
16
|
+
- **TypeScript First**: Full TypeScript support with complete type definitions
|
|
17
|
+
- **Express Integration**: Drop-in middleware and pre-configured route setup
|
|
18
|
+
- **Permission Sync**: Register application permissions to the OAuth provider at startup
|
|
19
|
+
- **Mock Mode**: Zero-dependency local development without a real OAuth server
|
|
20
|
+
- **Web Client SDK**: Browser-side token refresh, user state, and permission utilities
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
npm install sumor
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
##
|
|
28
|
+
## Architecture Overview
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
Sumor is split into two entry points:
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
| Entry | Purpose |
|
|
33
|
+
| ----------- | ------------------------------- |
|
|
34
|
+
| `sumor` | Server-side (Node.js / Express) |
|
|
35
|
+
| `sumor/web` | Client-side (browser) |
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await oauthService.updatePermissions(permissionConfig)
|
|
49
|
-
|
|
50
|
-
// Register JWT middleware and OAuth routes
|
|
51
|
-
app.use('/api', loadJwtUserMiddleware)
|
|
52
|
-
app.use('/api/oauth', oauthRoutes)
|
|
37
|
+
```
|
|
38
|
+
Browser App
|
|
39
|
+
│ (1) refreshToken() on init
|
|
40
|
+
│ (2) login() → redirect to OAuth provider
|
|
41
|
+
│ (3) OAuth callback → /api/oauth/callback
|
|
42
|
+
│ (4) Subsequent requests carry HttpOnly cookie
|
|
43
|
+
▼
|
|
44
|
+
Your Express App
|
|
45
|
+
├── oauthRoutes ← /api/oauth/* (token refresh, callback, logout)
|
|
46
|
+
├── loadJwtUserMiddleware ← validates JWT, injects req.jwtUser
|
|
47
|
+
└── Your routes ← access req.jwtUser, call OAuthService methods
|
|
48
|
+
▼
|
|
49
|
+
OAuth Provider
|
|
50
|
+
└── issues JWT, manages users & permissions, exposes JWKS
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
---
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
// User info from JWT token is available in req.jwtUser
|
|
59
|
-
app.get('/api/user/profile', (req: any, res) => {
|
|
60
|
-
const user = req.jwtUser
|
|
55
|
+
## Server-Side Usage
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
userId: user.userId,
|
|
64
|
-
roles: user.roles?.split(','),
|
|
65
|
-
permissions: user.permissions?.split(','),
|
|
66
|
-
verified: user.isVerified
|
|
67
|
-
})
|
|
68
|
-
})
|
|
57
|
+
### Install
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// Fetch user info from OAuth provider
|
|
74
|
-
const userInfo = await req.sumor.getUserInfo(req.params.userId)
|
|
75
|
-
res.json(userInfo)
|
|
76
|
-
} catch (error) {
|
|
77
|
-
res.status(400).json({ error: error.message })
|
|
78
|
-
}
|
|
79
|
-
})
|
|
59
|
+
```typescript
|
|
60
|
+
import { OAuthService, loadJwtUserMiddleware, oauthRoutes } from 'sumor'
|
|
80
61
|
```
|
|
81
62
|
|
|
82
|
-
|
|
63
|
+
### Exports
|
|
83
64
|
|
|
84
|
-
|
|
65
|
+
| Export | Type | Description |
|
|
66
|
+
| ----------------------- | -------------- | ------------------------------------- |
|
|
67
|
+
| `OAuthService` | Class | Interact with the OAuth provider API |
|
|
68
|
+
| `loadJwtUserMiddleware` | Middleware | Validate JWT and inject `req.jwtUser` |
|
|
69
|
+
| `oauthRoutes` | Express Router | Pre-configured OAuth routes |
|
|
85
70
|
|
|
86
|
-
|
|
71
|
+
### Setup
|
|
87
72
|
|
|
88
|
-
|
|
73
|
+
Register the OAuth routes and middleware in your Express app:
|
|
89
74
|
|
|
90
|
-
|
|
75
|
+
```typescript
|
|
76
|
+
import express from 'express'
|
|
77
|
+
import { OAuthService, loadJwtUserMiddleware, oauthRoutes } from 'sumor'
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
- `permissions` (string[]) - List of permission strings in format `<module>:<operation>`
|
|
94
|
-
- `permissionLabels` (Array) - Labels with `module`, `zh`, `en` fields
|
|
79
|
+
const app = express()
|
|
95
80
|
|
|
96
|
-
|
|
81
|
+
// Register pre-configured OAuth routes (callback, token refresh, logout)
|
|
82
|
+
app.use('/api/oauth', oauthRoutes)
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
module: 'posts',
|
|
105
|
-
zh: '帖子管理',
|
|
106
|
-
en: 'Posts Management'
|
|
107
|
-
}
|
|
108
|
-
]
|
|
109
|
-
})
|
|
84
|
+
// JWT middleware: validates token and injects req.jwtUser for all routes below
|
|
85
|
+
app.use(loadJwtUserMiddleware)
|
|
86
|
+
|
|
87
|
+
// Your protected routes go here
|
|
88
|
+
app.use('/api/user', userRoutes)
|
|
110
89
|
```
|
|
111
90
|
|
|
112
|
-
|
|
91
|
+
> **Note:** Register `oauthRoutes` before `loadJwtUserMiddleware` so that OAuth callback endpoints (`/api/oauth/callback`) are publicly accessible.
|
|
113
92
|
|
|
114
|
-
|
|
93
|
+
### Sync Permissions on Startup
|
|
115
94
|
|
|
116
|
-
|
|
95
|
+
Register your application's permission definitions with the OAuth provider once on startup:
|
|
117
96
|
|
|
118
97
|
```typescript
|
|
119
|
-
|
|
98
|
+
const oauthService = new OAuthService()
|
|
99
|
+
|
|
100
|
+
await oauthService.updatePermissions({
|
|
101
|
+
permissions: ['posts:view', 'posts:create', 'posts:edit', 'posts:delete'],
|
|
102
|
+
permissionLabels: [{ module: 'posts', zh: '文章管理', en: 'Posts Management' }]
|
|
103
|
+
})
|
|
120
104
|
```
|
|
121
105
|
|
|
122
|
-
|
|
106
|
+
Permission strings follow the format `<module>:<operation>`.
|
|
123
107
|
|
|
124
|
-
|
|
108
|
+
### Access User Info in Routes
|
|
125
109
|
|
|
126
|
-
|
|
127
|
-
- `PUT /api/oauth/token` - Refresh token and get user info + authorization URL
|
|
128
|
-
- `POST /api/oauth/logout` - Logout and revoke session
|
|
110
|
+
After `loadJwtUserMiddleware`, `req.jwtUser` contains the decoded JWT payload:
|
|
129
111
|
|
|
130
112
|
```typescript
|
|
131
|
-
app.
|
|
132
|
-
|
|
113
|
+
app.get('/api/profile', (req, res) => {
|
|
114
|
+
const { userId, roles, permissions, isVerified } = req.jwtUser
|
|
133
115
|
|
|
134
|
-
|
|
116
|
+
res.json({
|
|
117
|
+
userId,
|
|
118
|
+
roles: roles?.split(',') ?? [],
|
|
119
|
+
permissions: permissions?.split(',') ?? [],
|
|
120
|
+
isVerified: isVerified === 1
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
```
|
|
135
124
|
|
|
136
|
-
|
|
125
|
+
**`req.jwtUser` properties:**
|
|
137
126
|
|
|
138
|
-
|
|
127
|
+
| Property | Type | Description |
|
|
128
|
+
| ------------- | -------- | ---------------------------------- |
|
|
129
|
+
| `userId` | `string` | Unique user identifier |
|
|
130
|
+
| `roles` | `string` | Comma-separated role IDs |
|
|
131
|
+
| `permissions` | `string` | Comma-separated permission strings |
|
|
132
|
+
| `isVerified` | `number` | `1` if verified, `0` otherwise |
|
|
133
|
+
| `tenantId` | `string` | Multi-tenant identifier |
|
|
134
|
+
| `jti` | `string` | JWT ID (session identifier) |
|
|
135
|
+
| `exp` | `number` | Token expiration timestamp |
|
|
136
|
+
| `iat` | `number` | Token issued-at timestamp |
|
|
139
137
|
|
|
140
|
-
|
|
141
|
-
- `roles` (string) - Comma-separated role IDs
|
|
142
|
-
- `permissions` (string) - Comma-separated user permissions
|
|
143
|
-
- `isVerified` (number) - Verification status
|
|
144
|
-
- `tenantId` (string) - Multi-tenant identifier
|
|
145
|
-
- `exp` (number) - Token expiration timestamp
|
|
146
|
-
- `iat` (number) - Token issued at timestamp
|
|
138
|
+
### OAuthService Methods
|
|
147
139
|
|
|
148
|
-
|
|
140
|
+
Create an instance anywhere in your server code — it reads configuration from environment variables automatically:
|
|
149
141
|
|
|
150
142
|
```typescript
|
|
151
|
-
|
|
152
|
-
const { userId, roles, permissions } = req.jwtUser
|
|
153
|
-
res.json({ userId, roles: roles?.split(','), permissions: permissions?.split(',') })
|
|
154
|
-
})
|
|
143
|
+
const oauthService = new OAuthService()
|
|
155
144
|
```
|
|
156
145
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Available in all route handlers. Provides methods to call the OAuth provider's API.
|
|
160
|
-
|
|
161
|
-
**Methods:**
|
|
162
|
-
|
|
163
|
-
#### getUserInfo(userId)
|
|
146
|
+
#### `updatePermissions(config)`
|
|
164
147
|
|
|
165
|
-
|
|
148
|
+
Synchronize the application's permission definitions to the OAuth provider.
|
|
166
149
|
|
|
167
150
|
```typescript
|
|
168
|
-
|
|
169
|
-
|
|
151
|
+
await oauthService.updatePermissions({
|
|
152
|
+
permissions: ['resource:view', 'resource:edit'],
|
|
153
|
+
permissionLabels: [{ module: 'resource', zh: '资源管理', en: 'Resource Management' }]
|
|
154
|
+
})
|
|
170
155
|
```
|
|
171
156
|
|
|
172
|
-
####
|
|
157
|
+
#### `getUserInfo(userId)`
|
|
173
158
|
|
|
174
|
-
|
|
159
|
+
Fetch detailed information for a single user.
|
|
175
160
|
|
|
176
161
|
```typescript
|
|
177
|
-
const
|
|
178
|
-
// Returns:
|
|
162
|
+
const userInfo = await oauthService.getUserInfo('user-123')
|
|
163
|
+
// Returns: { userId, username, nickname, email, avatar, ... }
|
|
179
164
|
```
|
|
180
165
|
|
|
181
|
-
####
|
|
166
|
+
#### `getUsersInfo(userIds)`
|
|
182
167
|
|
|
183
|
-
|
|
168
|
+
Fetch information for multiple users in one call.
|
|
184
169
|
|
|
185
170
|
```typescript
|
|
186
|
-
const
|
|
187
|
-
// Returns: [{ userId,
|
|
171
|
+
const users = await oauthService.getUsersInfo(['user-1', 'user-2', 'user-3'])
|
|
172
|
+
// Returns: [{ userId, username, ... }, ...]
|
|
188
173
|
```
|
|
189
174
|
|
|
190
|
-
####
|
|
175
|
+
#### `searchUsers(searchTerm, limit)`
|
|
191
176
|
|
|
192
|
-
|
|
177
|
+
Search users by name or email.
|
|
193
178
|
|
|
194
179
|
```typescript
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
authCode,
|
|
198
|
-
'http://localhost:3000/callback',
|
|
199
|
-
codeVerifier
|
|
200
|
-
)
|
|
201
|
-
// Returns: { accessToken, refreshToken, expiresIn, tokenType }
|
|
180
|
+
const results = await oauthService.searchUsers('alice', 20)
|
|
181
|
+
// Returns: [{ userId, username, email, ... }, ...]
|
|
202
182
|
```
|
|
203
183
|
|
|
204
|
-
####
|
|
184
|
+
#### `revokeSession(sessionId)`
|
|
205
185
|
|
|
206
|
-
|
|
186
|
+
Revoke (blacklist) a session on logout.
|
|
207
187
|
|
|
208
188
|
```typescript
|
|
209
|
-
|
|
189
|
+
await oauthService.revokeSession(req.jwtUser.jti)
|
|
210
190
|
```
|
|
211
191
|
|
|
212
|
-
####
|
|
192
|
+
#### `checkBlacklist(sessionId)`
|
|
213
193
|
|
|
214
|
-
|
|
194
|
+
Check whether a session has been revoked.
|
|
215
195
|
|
|
216
196
|
```typescript
|
|
217
|
-
await
|
|
197
|
+
const isRevoked = await oauthService.checkBlacklist(sessionId)
|
|
218
198
|
```
|
|
219
199
|
|
|
220
|
-
### OAuth Routes
|
|
200
|
+
### Pre-configured OAuth Routes
|
|
221
201
|
|
|
222
|
-
|
|
202
|
+
`oauthRoutes` registers the following endpoints automatically:
|
|
223
203
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
204
|
+
| Method | Path | Auth required | Description |
|
|
205
|
+
| ------ | --------------------- | ------------- | ------------------------------------------------------ |
|
|
206
|
+
| `GET` | `/api/oauth/callback` | No | Exchange authorization code for tokens |
|
|
207
|
+
| `PUT` | `/api/oauth/token` | No | Refresh access token; returns user info and OAuth URLs |
|
|
208
|
+
| `POST` | `/api/oauth/logout` | Yes | Revoke session and clear cookies |
|
|
228
209
|
|
|
229
|
-
|
|
210
|
+
---
|
|
230
211
|
|
|
231
|
-
|
|
212
|
+
## Client-Side Usage (`sumor/web`)
|
|
232
213
|
|
|
233
|
-
###
|
|
214
|
+
### Install
|
|
234
215
|
|
|
235
216
|
```typescript
|
|
236
|
-
import {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
217
|
+
import {
|
|
218
|
+
refreshToken,
|
|
219
|
+
login,
|
|
220
|
+
logout,
|
|
221
|
+
hasPermission,
|
|
222
|
+
hasRole,
|
|
223
|
+
oauthUrl,
|
|
224
|
+
oauthStore,
|
|
225
|
+
axios
|
|
226
|
+
} from 'sumor/web'
|
|
227
|
+
import type { ApiResponse } from 'sumor/web'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Exports
|
|
231
|
+
|
|
232
|
+
| Export | Type | Description |
|
|
233
|
+
| --------------- | -------------- | ----------------------------------------------------- |
|
|
234
|
+
| `refreshToken` | Function | Refresh token and sync user state (call on app init) |
|
|
235
|
+
| `login` | Function | Redirect to OAuth login page |
|
|
236
|
+
| `logout` | Function | Call logout endpoint and clear user state |
|
|
237
|
+
| `hasPermission` | Function | Check if the current user has a specific permission |
|
|
238
|
+
| `hasRole` | Function | Check if the current user has a specific role |
|
|
239
|
+
| `oauthStore` | Singleton | In-memory user and OAuth state store |
|
|
240
|
+
| `oauthUrl` | Object | Helpers to generate OAuth provider navigation URLs |
|
|
241
|
+
| `axios` | Axios instance | Pre-configured Axios with automatic 401 token refresh |
|
|
242
|
+
| `ApiResponse` | Type | Standard API response wrapper type |
|
|
243
|
+
|
|
244
|
+
### Initialize on App Start
|
|
245
|
+
|
|
246
|
+
Call `refreshToken()` once during application initialization to restore user state from the stored refresh token cookie:
|
|
240
247
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
### Sumor Client Properties
|
|
248
|
+
```typescript
|
|
249
|
+
// main.ts (or App.vue onMounted)
|
|
250
|
+
import { refreshToken } from 'sumor/web'
|
|
246
251
|
|
|
247
|
-
|
|
248
|
-
- `authorizeUrl` - Authorization URL for login redirect
|
|
249
|
-
- `user` - Current user object or null
|
|
250
|
-
- `id` - User ID
|
|
251
|
-
- `isVerified` - Verification status
|
|
252
|
-
- `roles` - Comma-separated role list
|
|
253
|
-
- `permissions` - Comma-separated permission list
|
|
252
|
+
await refreshToken()
|
|
254
253
|
|
|
255
|
-
|
|
254
|
+
// User state is now available via oauthStore
|
|
255
|
+
```
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
> In SSR environments, `refreshToken()` is a no-op and returns immediately.
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
### Login and Logout
|
|
260
260
|
|
|
261
261
|
```typescript
|
|
262
|
-
|
|
263
|
-
await window.sumor.refresh()
|
|
262
|
+
import { login, logout } from 'sumor/web'
|
|
264
263
|
|
|
265
|
-
//
|
|
266
|
-
|
|
264
|
+
// Redirect to OAuth authorization page
|
|
265
|
+
login()
|
|
266
|
+
|
|
267
|
+
// Logout: revokes session and clears user state
|
|
268
|
+
await logout()
|
|
267
269
|
```
|
|
268
270
|
|
|
269
|
-
|
|
271
|
+
### Subscribe to User Changes
|
|
270
272
|
|
|
271
|
-
|
|
273
|
+
Use `oauthStore` to read the current user and react to state changes:
|
|
272
274
|
|
|
273
275
|
```typescript
|
|
274
|
-
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
#### logout()
|
|
276
|
+
import { oauthStore } from 'sumor/web'
|
|
278
277
|
|
|
279
|
-
|
|
278
|
+
// Read current user
|
|
279
|
+
const user = oauthStore.getUser()
|
|
280
|
+
// { id, isVerified, roles, permissions } or null
|
|
280
281
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
// Subscribe to login/logout events
|
|
283
|
+
oauthStore.onUserChange(user => {
|
|
284
|
+
if (user) {
|
|
285
|
+
console.log('Logged in:', user.id)
|
|
286
|
+
} else {
|
|
287
|
+
console.log('Logged out')
|
|
288
|
+
}
|
|
289
|
+
})
|
|
284
290
|
```
|
|
285
291
|
|
|
286
|
-
|
|
292
|
+
**`UserInfo` properties:**
|
|
293
|
+
|
|
294
|
+
| Property | Type | Description |
|
|
295
|
+
| ------------- | -------- | ---------------------------------- |
|
|
296
|
+
| `id` | `string` | User ID |
|
|
297
|
+
| `isVerified` | `number` | `1` if verified |
|
|
298
|
+
| `roles` | `string` | Comma-separated role IDs |
|
|
299
|
+
| `permissions` | `string` | Comma-separated permission strings |
|
|
287
300
|
|
|
288
|
-
|
|
301
|
+
### Permission and Role Checks
|
|
289
302
|
|
|
290
303
|
```typescript
|
|
291
|
-
|
|
292
|
-
|
|
304
|
+
import { hasPermission, hasRole } from 'sumor/web'
|
|
305
|
+
|
|
306
|
+
// Check a specific permission (module + operation)
|
|
307
|
+
if (hasPermission('posts', 'edit')) {
|
|
293
308
|
// User can edit posts
|
|
294
309
|
}
|
|
295
310
|
|
|
296
|
-
// Check module (
|
|
297
|
-
if (
|
|
311
|
+
// Check any permission in a module (wildcard)
|
|
312
|
+
if (hasPermission('posts', '*')) {
|
|
298
313
|
// User has any posts permission
|
|
299
314
|
}
|
|
300
315
|
|
|
301
|
-
|
|
302
|
-
|
|
316
|
+
// Check role
|
|
317
|
+
if (hasRole('admin')) {
|
|
318
|
+
// User is an admin
|
|
303
319
|
}
|
|
304
320
|
```
|
|
305
321
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
Check if user has a specific role.
|
|
322
|
+
### OAuth URL Helpers
|
|
309
323
|
|
|
310
324
|
```typescript
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
325
|
+
import { oauthUrl } from 'sumor/web'
|
|
326
|
+
|
|
327
|
+
oauthUrl.avatar(userId) // URL for a user's avatar image
|
|
328
|
+
oauthUrl.user() // URL for the user profile page
|
|
329
|
+
oauthUrl.home() // URL for the OAuth provider home page
|
|
330
|
+
oauthUrl.site() // URL for the site management page
|
|
331
|
+
oauthUrl.feedback() // URL for the feedback page
|
|
314
332
|
```
|
|
315
333
|
|
|
316
|
-
|
|
334
|
+
In [Mock Mode](#mock-mode), these URLs point to placeholder pages served locally.
|
|
317
335
|
|
|
318
|
-
|
|
336
|
+
### Making Authenticated HTTP Requests
|
|
337
|
+
|
|
338
|
+
The exported `axios` instance automatically handles 401 responses by refreshing the token and retrying:
|
|
319
339
|
|
|
320
340
|
```typescript
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
341
|
+
import { axios } from 'sumor/web'
|
|
342
|
+
import type { ApiResponse } from 'sumor/web'
|
|
343
|
+
|
|
344
|
+
// GET
|
|
345
|
+
const { data } = await axios.get<ApiResponse<UserInfo>>('/api/user/info')
|
|
346
|
+
|
|
347
|
+
// POST
|
|
348
|
+
const { data } = await axios.post<ApiResponse<Item>>('/api/items', { name: 'My Item' })
|
|
349
|
+
|
|
350
|
+
// PUT
|
|
351
|
+
const { data } = await axios.put<ApiResponse<Item>>(`/api/items/${id}`, updates)
|
|
352
|
+
|
|
353
|
+
// DELETE
|
|
354
|
+
const { data } = await axios.delete<ApiResponse<null>>(`/api/items/${id}`)
|
|
355
|
+
|
|
356
|
+
// File upload with progress
|
|
357
|
+
await axios.post<ApiResponse<UploadResult>>('/api/upload', formData, {
|
|
358
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
359
|
+
onUploadProgress: e => {
|
|
360
|
+
const percent = Math.round((e.loaded * 100) / (e.total ?? e.loaded))
|
|
361
|
+
console.log(`Upload progress: ${percent}%`)
|
|
326
362
|
}
|
|
327
363
|
})
|
|
328
364
|
```
|
|
329
365
|
|
|
330
|
-
|
|
366
|
+
**`ApiResponse<T>` type:**
|
|
331
367
|
|
|
332
368
|
```typescript
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
setup() {
|
|
338
|
-
const userInfo = ref(null)
|
|
339
|
-
const isLoggedIn = ref(false)
|
|
340
|
-
|
|
341
|
-
onMounted(async () => {
|
|
342
|
-
await setupSumor()
|
|
343
|
-
|
|
344
|
-
// Get initial user state
|
|
345
|
-
userInfo.value = window.sumor.user
|
|
346
|
-
isLoggedIn.value = !!window.sumor.user
|
|
347
|
-
|
|
348
|
-
// Subscribe to user changes
|
|
349
|
-
window.sumor.onUserChange(user => {
|
|
350
|
-
userInfo.value = user
|
|
351
|
-
isLoggedIn.value = !!user
|
|
352
|
-
})
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
userInfo,
|
|
357
|
-
isLoggedIn,
|
|
358
|
-
login: () => window.sumor.login(),
|
|
359
|
-
logout: () => window.sumor.logout(),
|
|
360
|
-
canEdit: () => window.sumor.hasPermission('posts', 'edit')
|
|
361
|
-
}
|
|
362
|
-
}
|
|
369
|
+
interface ApiResponse<T> {
|
|
370
|
+
code: string // 'OK' on success
|
|
371
|
+
message: string
|
|
372
|
+
data: T
|
|
363
373
|
}
|
|
364
374
|
```
|
|
365
375
|
|
|
366
|
-
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Configuration
|
|
379
|
+
|
|
380
|
+
Sumor reads all configuration from environment variables. No explicit constructor arguments are required.
|
|
367
381
|
|
|
368
|
-
|
|
382
|
+
### Required Environment Variables
|
|
369
383
|
|
|
370
384
|
```bash
|
|
371
|
-
# OAuth Provider Configuration
|
|
372
385
|
OAUTH_ENDPOINT=https://auth.example.com
|
|
373
386
|
OAUTH_CLIENT_KEY=your-app-client-id
|
|
374
387
|
OAUTH_CLIENT_SECRET=your-app-client-secret
|
|
375
388
|
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
|
|
376
389
|
```
|
|
377
390
|
|
|
378
|
-
|
|
391
|
+
| Variable | Description |
|
|
392
|
+
| --------------------- | ------------------------------------------------------ |
|
|
393
|
+
| `OAUTH_ENDPOINT` | Base URL of the OAuth provider |
|
|
394
|
+
| `OAUTH_CLIENT_KEY` | OAuth application client ID |
|
|
395
|
+
| `OAUTH_CLIENT_SECRET` | OAuth application client secret |
|
|
396
|
+
| `OAUTH_REDIRECT_URI` | Callback URL (must match OAuth provider configuration) |
|
|
379
397
|
|
|
380
|
-
|
|
381
|
-
- OAuth endpoints are automatically derived: `{OAUTH_ENDPOINT}/api/oauth/...`
|
|
382
|
-
- `OAUTH_CLIENT_KEY` and `OAUTH_CLIENT_SECRET` - OAuth application credentials
|
|
383
|
-
- `OAUTH_REDIRECT_URI` - Callback URL that matches your OAuth provider configuration
|
|
384
|
-
- JWT tokens are verified using JWKS public keys from the OAuth provider (no local secret needed)
|
|
398
|
+
JWT tokens are verified using JWKS public keys fetched from `{OAUTH_ENDPOINT}/api/oauth/jwks` — no local secret is required.
|
|
385
399
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
```bash
|
|
389
|
-
# Local development
|
|
390
|
-
OAUTH_ENDPOINT=http://localhost:3001
|
|
391
|
-
OAUTH_CLIENT_KEY=myapp-dev
|
|
392
|
-
OAUTH_CLIENT_SECRET=myapp-dev-secret
|
|
393
|
-
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
|
|
394
|
-
|
|
395
|
-
# Production
|
|
396
|
-
OAUTH_ENDPOINT=https://auth.mycompany.com
|
|
397
|
-
OAUTH_CLIENT_KEY=myapp-prod
|
|
398
|
-
OAUTH_CLIENT_SECRET=<secure-secret>
|
|
399
|
-
OAUTH_REDIRECT_URI=https://app.mycompany.com/api/oauth/callback
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
## 📚 Usage Examples
|
|
403
|
-
|
|
404
|
-
### Example 1: Protect Routes with Permission Checks
|
|
400
|
+
---
|
|
405
401
|
|
|
406
|
-
|
|
407
|
-
// Server-side: Express route with permission check
|
|
408
|
-
app.post('/api/posts', (req: any, res) => {
|
|
409
|
-
// Check if user has permission
|
|
410
|
-
const permissions = req.jwtUser.permissions?.split(',') || []
|
|
402
|
+
## Mock Mode
|
|
411
403
|
|
|
412
|
-
|
|
413
|
-
return res.status(403).json({ error: 'Insufficient permissions' })
|
|
414
|
-
}
|
|
404
|
+
Mock Mode allows local development without a running OAuth server. Enable it by setting `OAUTH_MOCK=true`. In this mode, Sumor issues locally signed JWT tokens using HS256 and exposes additional mock-only endpoints.
|
|
415
405
|
|
|
416
|
-
|
|
417
|
-
res.json({ postId: 123 })
|
|
418
|
-
})
|
|
406
|
+
### How It Works
|
|
419
407
|
|
|
420
|
-
|
|
421
|
-
export default {
|
|
422
|
-
setup() {
|
|
423
|
-
return {
|
|
424
|
-
canCreatePost: () => window.sumor.hasPermission('posts', 'create')
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
408
|
+
When `OAUTH_MOCK=true`:
|
|
428
409
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
410
|
+
- `oauthRoutes` registers extra sub-routes under `/api/oauth/mock/`
|
|
411
|
+
- The `PUT /api/oauth/token` route validates mock tokens locally instead of calling the OAuth provider
|
|
412
|
+
- `login()` on the web client calls `POST /api/oauth/mock/login` directly instead of redirecting to the OAuth provider
|
|
413
|
+
- `logout()` calls `POST /api/oauth/mock/logout` to clear cookies locally
|
|
414
|
+
- `oauthUrl.*` helpers return local placeholder pages instead of external OAuth URLs
|
|
432
415
|
|
|
433
|
-
###
|
|
416
|
+
### Mock Server Environment Variables
|
|
434
417
|
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
|
|
418
|
+
```bash
|
|
419
|
+
# Enable mock mode
|
|
420
|
+
OAUTH_MOCK=true
|
|
438
421
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
422
|
+
# Mock user configuration (all optional — defaults shown)
|
|
423
|
+
OAUTH_MOCK_USER_ID=mock-user-001
|
|
424
|
+
OAUTH_MOCK_USER_ROLES=admin
|
|
425
|
+
OAUTH_MOCK_USER_PERMISSIONS=
|
|
426
|
+
OAUTH_MOCK_USER_IS_VERIFIED=1
|
|
442
427
|
```
|
|
443
428
|
|
|
444
|
-
|
|
429
|
+
| Variable | Default | Description |
|
|
430
|
+
| ----------------------------- | --------------- | ------------------------------------- |
|
|
431
|
+
| `OAUTH_MOCK` | `false` | Set to `true` to enable mock mode |
|
|
432
|
+
| `OAUTH_MOCK_USER_ID` | `mock-user-001` | Mock user ID |
|
|
433
|
+
| `OAUTH_MOCK_USER_ROLES` | `admin` | Comma-separated mock roles |
|
|
434
|
+
| `OAUTH_MOCK_USER_PERMISSIONS` | _(empty)_ | Comma-separated mock permissions |
|
|
435
|
+
| `OAUTH_MOCK_USER_IS_VERIFIED` | `1` | Mock verification status (`0` or `1`) |
|
|
445
436
|
|
|
446
|
-
|
|
447
|
-
app.get('/api/followers', async (req: any, res) => {
|
|
448
|
-
try {
|
|
449
|
-
// Get list of follower IDs from your local database
|
|
450
|
-
const followerIds = await db.query(
|
|
451
|
-
'SELECT follower_id FROM relationships WHERE leader_id = ?',
|
|
452
|
-
[req.jwtUser.userId]
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
// Get detailed info for all followers from OAuth provider
|
|
456
|
-
const followerInfo = await req.sumor.getUsersInfo(followerIds.map(r => r.follower_id))
|
|
457
|
-
|
|
458
|
-
res.json(followerInfo)
|
|
459
|
-
} catch (error) {
|
|
460
|
-
res.status(500).json({ error: error.message })
|
|
461
|
-
}
|
|
462
|
-
})
|
|
463
|
-
```
|
|
437
|
+
### Mock Routes (registered only when `OAUTH_MOCK=true`)
|
|
464
438
|
|
|
465
|
-
|
|
439
|
+
| Method | Path | Description |
|
|
440
|
+
| ------ | ----------------------------- | ----------------------------------------------------------- |
|
|
441
|
+
| `POST` | `/api/oauth/mock/login` | Issue mock tokens and return user info (no redirect needed) |
|
|
442
|
+
| `POST` | `/api/oauth/mock/logout` | Clear mock token cookies |
|
|
443
|
+
| `GET` | `/api/oauth/mock/avatar/:id` | Returns 404 (no avatar service in mock mode) |
|
|
444
|
+
| `GET` | `/api/oauth/mock/nav/:target` | Placeholder page for OAuth provider navigation links |
|
|
466
445
|
|
|
467
|
-
|
|
468
|
-
app.get('/api/search/users', async (req: any, res) => {
|
|
469
|
-
const { q, limit = 20 } = req.query
|
|
446
|
+
### Example Mock Setup
|
|
470
447
|
|
|
471
|
-
|
|
472
|
-
return res.status(400).json({ error: 'Search term required' })
|
|
473
|
-
}
|
|
448
|
+
**.env.development:**
|
|
474
449
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
})
|
|
450
|
+
```bash
|
|
451
|
+
OAUTH_MOCK=true
|
|
452
|
+
OAUTH_MOCK_USER_ID=dev-user-1
|
|
453
|
+
OAUTH_MOCK_USER_ROLES=admin
|
|
454
|
+
OAUTH_MOCK_USER_PERMISSIONS=posts:view,posts:edit,posts:delete
|
|
455
|
+
OAUTH_MOCK_USER_IS_VERIFIED=1
|
|
482
456
|
```
|
|
483
457
|
|
|
484
|
-
|
|
458
|
+
No changes are needed in application code — the same `oauthRoutes`, `loadJwtUserMiddleware`, `login()`, and `logout()` calls work in both mock and production modes.
|
|
485
459
|
|
|
486
|
-
###
|
|
460
|
+
### Security Notice
|
|
487
461
|
|
|
488
|
-
|
|
489
|
-
- Always use HTTPS in production to prevent token interception
|
|
490
|
-
- Refresh tokens should be rotated regularly
|
|
462
|
+
Mock tokens are signed with a fixed HS256 secret and are identified by `iss: 'mock-oauth'` in the payload. **Never use `OAUTH_MOCK=true` in production.**
|
|
491
463
|
|
|
492
|
-
|
|
464
|
+
---
|
|
493
465
|
|
|
494
|
-
|
|
466
|
+
## Usage Examples
|
|
495
467
|
|
|
496
|
-
|
|
497
|
-
const userPermissions = req.jwtUser.permissions?.split(',') || []
|
|
468
|
+
### Server — Protect a Route with Permission Check
|
|
498
469
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Session Revocation
|
|
470
|
+
```typescript
|
|
471
|
+
app.post('/api/posts', (req, res) => {
|
|
472
|
+
const permissions = req.jwtUser.permissions?.split(',') ?? []
|
|
505
473
|
|
|
506
|
-
|
|
474
|
+
if (!permissions.includes('posts:create')) {
|
|
475
|
+
return res.status(403).json({ code: 'FORBIDDEN', message: 'Insufficient permissions' })
|
|
476
|
+
}
|
|
507
477
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Token is added to blacklist and becomes invalid
|
|
478
|
+
// Create post...
|
|
479
|
+
res.json({ code: 'OK', data: { id: 'new-post-id' } })
|
|
480
|
+
})
|
|
512
481
|
```
|
|
513
482
|
|
|
514
|
-
###
|
|
515
|
-
|
|
516
|
-
Implement CSRF tokens for state-changing operations:
|
|
483
|
+
### Server — Fetch User Details from OAuth Provider
|
|
517
484
|
|
|
518
485
|
```typescript
|
|
519
|
-
|
|
520
|
-
const
|
|
486
|
+
app.get('/api/posts/:id/author', async (req, res) => {
|
|
487
|
+
const post = await db.findPost(req.params.id)
|
|
488
|
+
const author = await oauthService.getUserInfo(post.authorId)
|
|
521
489
|
|
|
522
|
-
|
|
523
|
-
// Handle POST with CSRF protection
|
|
490
|
+
res.json({ code: 'OK', data: author })
|
|
524
491
|
})
|
|
525
492
|
```
|
|
526
493
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
### "Token verification failed"
|
|
530
|
-
|
|
531
|
-
**Cause:** JWT signature doesn't match JWKS public key
|
|
532
|
-
|
|
533
|
-
**Solution:** Ensure `OAUTH_ENDPOINT` is correctly configured. Sumor automatically fetches JWKS from `{OAUTH_ENDPOINT}/api/oauth/jwks`
|
|
494
|
+
### Web — Vue Component with Login/Logout
|
|
534
495
|
|
|
535
|
-
|
|
496
|
+
```typescript
|
|
497
|
+
import { login, logout, oauthStore, hasPermission } from 'sumor/web'
|
|
498
|
+
import { ref, onMounted } from 'vue'
|
|
536
499
|
|
|
537
|
-
|
|
500
|
+
const user = ref(oauthStore.getUser())
|
|
538
501
|
|
|
539
|
-
|
|
502
|
+
onMounted(() => {
|
|
503
|
+
oauthStore.onUserChange(u => {
|
|
504
|
+
user.value = u
|
|
505
|
+
})
|
|
506
|
+
})
|
|
540
507
|
|
|
508
|
+
const canEdit = () => hasPermission('posts', 'edit')
|
|
541
509
|
```
|
|
542
|
-
|
|
510
|
+
|
|
511
|
+
```html
|
|
512
|
+
<template>
|
|
513
|
+
<button v-if="!user" @click="login">Login</button>
|
|
514
|
+
<button v-else @click="logout">Logout</button>
|
|
515
|
+
<button v-if="canEdit()" @click="editPost">Edit</button>
|
|
516
|
+
</template>
|
|
543
517
|
```
|
|
544
518
|
|
|
545
|
-
###
|
|
519
|
+
### Web — Vue Router Guard
|
|
546
520
|
|
|
547
|
-
|
|
521
|
+
```typescript
|
|
522
|
+
import { refreshToken, hasPermission } from 'sumor/web'
|
|
548
523
|
|
|
549
|
-
|
|
524
|
+
// Restore user state before mounting router
|
|
525
|
+
await refreshToken()
|
|
550
526
|
|
|
551
|
-
|
|
527
|
+
router.beforeEach(to => {
|
|
528
|
+
if (to.meta.requiresPermission) {
|
|
529
|
+
const [module, operation] = (to.meta.requiresPermission as string).split(':')
|
|
530
|
+
if (!hasPermission(module, operation)) {
|
|
531
|
+
return '/403'
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
})
|
|
535
|
+
```
|
|
552
536
|
|
|
553
|
-
|
|
537
|
+
---
|
|
554
538
|
|
|
555
|
-
|
|
539
|
+
## Security Considerations
|
|
556
540
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
541
|
+
- Tokens are stored in **HTTP-only cookies** and never exposed to JavaScript (XSS protection)
|
|
542
|
+
- Always use **HTTPS** in production
|
|
543
|
+
- JWT signatures are verified against JWKS public keys fetched from the OAuth provider
|
|
544
|
+
- Logout immediately **blacklists** the session on the OAuth provider
|
|
545
|
+
- Never enable `OAUTH_MOCK=true` outside of local development
|
|
560
546
|
|
|
561
|
-
|
|
547
|
+
---
|
|
562
548
|
|
|
563
|
-
|
|
564
|
-
┌──────────────────────────────────────┐
|
|
565
|
-
│ Browser / Mobile Client │
|
|
566
|
-
└───────────────┬──────────────────────┘
|
|
567
|
-
│ (1) Click Login
|
|
568
|
-
▼
|
|
569
|
-
┌──────────────────────────────────────┐
|
|
570
|
-
│ Your Express App (Port 3000) │
|
|
571
|
-
│ ┌────────────────────────────────┐ │
|
|
572
|
-
│ │ PUT /api/oauth/token │ │ (2) Get OAuth URL & User Info
|
|
573
|
-
│ │ GET /api/oauth/callback │ │
|
|
574
|
-
│ │ POST /api/oauth/logout │ │
|
|
575
|
-
│ └────────────────────────────────┘ │
|
|
576
|
-
└───────────┬──────────────────────────┘
|
|
577
|
-
│ (3) Redirect to OAuth
|
|
578
|
-
▼
|
|
579
|
-
┌──────────────────────────────────────┐
|
|
580
|
-
│ OAuth Provider │
|
|
581
|
-
│ {OAUTH_ENDPOINT}/api/oauth/... │
|
|
582
|
-
│ - Issue JWT tokens │
|
|
583
|
-
│ - Manage users & permissions │
|
|
584
|
-
│ - Provide JWKS public keys │
|
|
585
|
-
└──────────────────────────────────────┘
|
|
586
|
-
▲
|
|
587
|
-
│ (4) Verify JWT
|
|
588
|
-
│
|
|
589
|
-
req.sumor
|
|
590
|
-
```
|
|
549
|
+
## Troubleshooting
|
|
591
550
|
|
|
592
|
-
|
|
551
|
+
### `req.jwtUser` is `undefined`
|
|
593
552
|
|
|
594
|
-
|
|
595
|
-
2. App calls `PUT /api/oauth/token` with refresh token to get OAuth provider URL and user info
|
|
596
|
-
3. User redirected to OAuth provider
|
|
597
|
-
4. OAuth provider authenticates and redirects back to `/api/oauth/callback` with authorization code
|
|
598
|
-
5. Server exchanges code for JWT token via OAuth API
|
|
599
|
-
6. Server verifies JWT signature using OAuth JWKS public keys
|
|
600
|
-
7. Subsequent requests include JWT in Authorization header
|
|
601
|
-
8. Middleware validates JWT and extracts user info (userId, roles, permissions)
|
|
602
|
-
9. Routes access user info via `req.jwtUser` and call OAuth service via `req.sumor`
|
|
553
|
+
Ensure that `loadJwtUserMiddleware` is registered before the routes that access `req.jwtUser`.
|
|
603
554
|
|
|
604
|
-
|
|
555
|
+
### Token verification fails
|
|
605
556
|
|
|
606
|
-
|
|
557
|
+
Ensure `OAUTH_ENDPOINT` points to the correct OAuth provider. Sumor fetches JWKS from `{OAUTH_ENDPOINT}/api/oauth/jwks`.
|
|
607
558
|
|
|
608
|
-
|
|
559
|
+
### `refreshToken()` returns without setting a user
|
|
609
560
|
|
|
610
|
-
|
|
561
|
+
The refresh token cookie may be absent or expired. The user needs to log in again via `login()`.
|
|
611
562
|
|
|
612
|
-
|
|
563
|
+
### 401 on protected routes in mock mode
|
|
613
564
|
|
|
614
|
-
|
|
565
|
+
Ensure `OAUTH_MOCK=true` is set both when the server starts and when checking tokens. Restart the server after changing this variable.
|
|
615
566
|
|
|
616
567
|
---
|
|
617
568
|
|
|
618
|
-
|
|
569
|
+
## License
|
|
570
|
+
|
|
571
|
+
MIT License — see [LICENSE](LICENSE) for details.
|