sumor 3.2.3 → 3.3.0
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 +372 -413
- package/README.zh-CN.md +576 -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 +151 -0
- package/dist/server/mock/mockApiRoutes.js.map +1 -0
- package/dist/server/mock/mockConfig.d.ts +38 -0
- package/dist/server/mock/mockConfig.d.ts.map +1 -0
- package/dist/server/mock/mockConfig.js +51 -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 +103 -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,573 @@
|
|
|
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
|
+
[中文文档](README.zh-CN.md)
|
|
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
|
+
import { oauthStore } from 'sumor/web'
|
|
276
277
|
|
|
277
|
-
|
|
278
|
+
// Read current user
|
|
279
|
+
const user = oauthStore.getUser()
|
|
280
|
+
// { id, isVerified, roles, permissions } or null
|
|
278
281
|
|
|
279
|
-
|
|
280
|
-
|
|
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:**
|
|
287
293
|
|
|
288
|
-
|
|
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 |
|
|
300
|
+
|
|
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.
|
|
335
|
+
|
|
336
|
+
### Making Authenticated HTTP Requests
|
|
317
337
|
|
|
318
|
-
|
|
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
|
|
367
379
|
|
|
368
|
-
Sumor
|
|
380
|
+
Sumor reads all configuration from environment variables. No explicit constructor arguments are required.
|
|
381
|
+
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
- JWT tokens are verified using JWKS public keys from the OAuth provider (no local secret needed)
|
|
385
|
-
|
|
386
|
-
**Example configurations:**
|
|
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
|
|
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) |
|
|
394
397
|
|
|
395
|
-
|
|
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
|
-
```
|
|
398
|
+
JWT tokens are verified using JWKS public keys fetched from `{OAUTH_ENDPOINT}/api/oauth/jwks` — no local secret is required.
|
|
401
399
|
|
|
402
|
-
|
|
400
|
+
---
|
|
403
401
|
|
|
404
|
-
|
|
402
|
+
## Mock Mode
|
|
405
403
|
|
|
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(',') || []
|
|
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.
|
|
411
405
|
|
|
412
|
-
|
|
413
|
-
return res.status(403).json({ error: 'Insufficient permissions' })
|
|
414
|
-
}
|
|
406
|
+
### How It Works
|
|
415
407
|
|
|
416
|
-
|
|
417
|
-
res.json({ postId: 123 })
|
|
418
|
-
})
|
|
408
|
+
When `OAUTH_MOCK=true`:
|
|
419
409
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
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
|
|
428
415
|
|
|
429
|
-
|
|
430
|
-
<button v-if="canCreatePost()" @click="createPost">Create Post</button>
|
|
431
|
-
```
|
|
416
|
+
### Mock Server Environment Variables
|
|
432
417
|
|
|
433
|
-
|
|
418
|
+
```bash
|
|
419
|
+
# Enable mock mode
|
|
420
|
+
OAUTH_MOCK=true
|
|
434
421
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
438
427
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
428
|
+
# URLs returned by the mock token endpoint
|
|
429
|
+
OAUTH_MOCK_ENDPOINT=http://localhost
|
|
430
|
+
OAUTH_MOCK_REDIRECT_URI=http://localhost
|
|
442
431
|
```
|
|
443
432
|
|
|
444
|
-
|
|
433
|
+
| Variable | Default | Description |
|
|
434
|
+
| ----------------------------- | ------------------ | ----------------------------------------- |
|
|
435
|
+
| `OAUTH_MOCK` | `false` | Set to `true` to enable mock mode |
|
|
436
|
+
| `OAUTH_MOCK_USER_ID` | `mock-user-001` | Mock user ID |
|
|
437
|
+
| `OAUTH_MOCK_USER_ROLES` | `admin` | Comma-separated mock roles |
|
|
438
|
+
| `OAUTH_MOCK_USER_PERMISSIONS` | _(empty)_ | Comma-separated mock permissions |
|
|
439
|
+
| `OAUTH_MOCK_USER_IS_VERIFIED` | `1` | Mock verification status (`0` or `1`) |
|
|
440
|
+
| `OAUTH_MOCK_ENDPOINT` | `http://localhost` | Endpoint value returned in token response |
|
|
441
|
+
| `OAUTH_MOCK_REDIRECT_URI` | `http://localhost` | Origin to redirect after mock login |
|
|
445
442
|
|
|
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
|
-
```
|
|
443
|
+
### Mock Routes (registered only when `OAUTH_MOCK=true`)
|
|
464
444
|
|
|
465
|
-
|
|
445
|
+
| Method | Path | Description |
|
|
446
|
+
| ------ | ----------------------------- | ----------------------------------------------------------- |
|
|
447
|
+
| `POST` | `/api/oauth/mock/login` | Issue mock tokens and return user info (no redirect needed) |
|
|
448
|
+
| `POST` | `/api/oauth/mock/logout` | Clear mock token cookies |
|
|
449
|
+
| `GET` | `/api/oauth/mock/avatar/:id` | Returns 404 (no avatar service in mock mode) |
|
|
450
|
+
| `GET` | `/api/oauth/mock/nav/:target` | Placeholder page for OAuth provider navigation links |
|
|
466
451
|
|
|
467
|
-
|
|
468
|
-
app.get('/api/search/users', async (req: any, res) => {
|
|
469
|
-
const { q, limit = 20 } = req.query
|
|
452
|
+
### Example Mock Setup
|
|
470
453
|
|
|
471
|
-
|
|
472
|
-
return res.status(400).json({ error: 'Search term required' })
|
|
473
|
-
}
|
|
454
|
+
**.env.development:**
|
|
474
455
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
})
|
|
456
|
+
```bash
|
|
457
|
+
OAUTH_MOCK=true
|
|
458
|
+
OAUTH_MOCK_USER_ID=dev-user-1
|
|
459
|
+
OAUTH_MOCK_USER_ROLES=admin
|
|
460
|
+
OAUTH_MOCK_USER_PERMISSIONS=posts:view,posts:edit,posts:delete
|
|
461
|
+
OAUTH_MOCK_USER_IS_VERIFIED=1
|
|
482
462
|
```
|
|
483
463
|
|
|
484
|
-
|
|
464
|
+
No changes are needed in application code — the same `oauthRoutes`, `loadJwtUserMiddleware`, `login()`, and `logout()` calls work in both mock and production modes.
|
|
485
465
|
|
|
486
|
-
###
|
|
466
|
+
### Security Notice
|
|
487
467
|
|
|
488
|
-
|
|
489
|
-
- Always use HTTPS in production to prevent token interception
|
|
490
|
-
- Refresh tokens should be rotated regularly
|
|
468
|
+
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
469
|
|
|
492
|
-
|
|
470
|
+
---
|
|
493
471
|
|
|
494
|
-
|
|
472
|
+
## Usage Examples
|
|
495
473
|
|
|
496
|
-
|
|
497
|
-
const userPermissions = req.jwtUser.permissions?.split(',') || []
|
|
474
|
+
### Server — Protect a Route with Permission Check
|
|
498
475
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Session Revocation
|
|
476
|
+
```typescript
|
|
477
|
+
app.post('/api/posts', (req, res) => {
|
|
478
|
+
const permissions = req.jwtUser.permissions?.split(',') ?? []
|
|
505
479
|
|
|
506
|
-
|
|
480
|
+
if (!permissions.includes('posts:create')) {
|
|
481
|
+
return res.status(403).json({ code: 'FORBIDDEN', message: 'Insufficient permissions' })
|
|
482
|
+
}
|
|
507
483
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Token is added to blacklist and becomes invalid
|
|
484
|
+
// Create post...
|
|
485
|
+
res.json({ code: 'OK', data: { id: 'new-post-id' } })
|
|
486
|
+
})
|
|
512
487
|
```
|
|
513
488
|
|
|
514
|
-
###
|
|
515
|
-
|
|
516
|
-
Implement CSRF tokens for state-changing operations:
|
|
489
|
+
### Server — Fetch User Details from OAuth Provider
|
|
517
490
|
|
|
518
491
|
```typescript
|
|
519
|
-
|
|
520
|
-
const
|
|
492
|
+
app.get('/api/posts/:id/author', async (req, res) => {
|
|
493
|
+
const post = await db.findPost(req.params.id)
|
|
494
|
+
const author = await oauthService.getUserInfo(post.authorId)
|
|
521
495
|
|
|
522
|
-
|
|
523
|
-
// Handle POST with CSRF protection
|
|
496
|
+
res.json({ code: 'OK', data: author })
|
|
524
497
|
})
|
|
525
498
|
```
|
|
526
499
|
|
|
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`
|
|
500
|
+
### Web — Vue Component with Login/Logout
|
|
534
501
|
|
|
535
|
-
|
|
502
|
+
```typescript
|
|
503
|
+
import { login, logout, oauthStore, hasPermission } from 'sumor/web'
|
|
504
|
+
import { ref, onMounted } from 'vue'
|
|
536
505
|
|
|
537
|
-
|
|
506
|
+
const user = ref(oauthStore.getUser())
|
|
538
507
|
|
|
539
|
-
|
|
508
|
+
onMounted(() => {
|
|
509
|
+
oauthStore.onUserChange(u => {
|
|
510
|
+
user.value = u
|
|
511
|
+
})
|
|
512
|
+
})
|
|
540
513
|
|
|
514
|
+
const canEdit = () => hasPermission('posts', 'edit')
|
|
541
515
|
```
|
|
542
|
-
|
|
516
|
+
|
|
517
|
+
```html
|
|
518
|
+
<template>
|
|
519
|
+
<button v-if="!user" @click="login">Login</button>
|
|
520
|
+
<button v-else @click="logout">Logout</button>
|
|
521
|
+
<button v-if="canEdit()" @click="editPost">Edit</button>
|
|
522
|
+
</template>
|
|
543
523
|
```
|
|
544
524
|
|
|
545
|
-
###
|
|
525
|
+
### Web — Vue Router Guard
|
|
546
526
|
|
|
547
|
-
|
|
527
|
+
```typescript
|
|
528
|
+
import { refreshToken, hasPermission } from 'sumor/web'
|
|
548
529
|
|
|
549
|
-
|
|
530
|
+
// Restore user state before mounting router
|
|
531
|
+
await refreshToken()
|
|
550
532
|
|
|
551
|
-
|
|
533
|
+
router.beforeEach(to => {
|
|
534
|
+
if (to.meta.requiresPermission) {
|
|
535
|
+
const [module, operation] = (to.meta.requiresPermission as string).split(':')
|
|
536
|
+
if (!hasPermission(module, operation)) {
|
|
537
|
+
return '/403'
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
})
|
|
541
|
+
```
|
|
552
542
|
|
|
553
|
-
|
|
543
|
+
---
|
|
554
544
|
|
|
555
|
-
|
|
545
|
+
## Security Considerations
|
|
556
546
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
547
|
+
- Tokens are stored in **HTTP-only cookies** and never exposed to JavaScript (XSS protection)
|
|
548
|
+
- Always use **HTTPS** in production
|
|
549
|
+
- JWT signatures are verified against JWKS public keys fetched from the OAuth provider
|
|
550
|
+
- Logout immediately **blacklists** the session on the OAuth provider
|
|
551
|
+
- Never enable `OAUTH_MOCK=true` outside of local development
|
|
560
552
|
|
|
561
|
-
|
|
553
|
+
---
|
|
562
554
|
|
|
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
|
-
```
|
|
555
|
+
## Troubleshooting
|
|
591
556
|
|
|
592
|
-
|
|
557
|
+
### `req.jwtUser` is `undefined`
|
|
593
558
|
|
|
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`
|
|
559
|
+
Ensure that `loadJwtUserMiddleware` is registered before the routes that access `req.jwtUser`.
|
|
603
560
|
|
|
604
|
-
|
|
561
|
+
### Token verification fails
|
|
605
562
|
|
|
606
|
-
|
|
563
|
+
Ensure `OAUTH_ENDPOINT` points to the correct OAuth provider. Sumor fetches JWKS from `{OAUTH_ENDPOINT}/api/oauth/jwks`.
|
|
607
564
|
|
|
608
|
-
|
|
565
|
+
### `refreshToken()` returns without setting a user
|
|
609
566
|
|
|
610
|
-
|
|
567
|
+
The refresh token cookie may be absent or expired. The user needs to log in again via `login()`.
|
|
611
568
|
|
|
612
|
-
|
|
569
|
+
### 401 on protected routes in mock mode
|
|
613
570
|
|
|
614
|
-
|
|
571
|
+
Ensure `OAUTH_MOCK=true` is set both when the server starts and when checking tokens. Restart the server after changing this variable.
|
|
615
572
|
|
|
616
573
|
---
|
|
617
574
|
|
|
618
|
-
|
|
575
|
+
## License
|
|
576
|
+
|
|
577
|
+
MIT License — see [LICENSE](LICENSE) for details.
|