@qlover/oauth-wrapper 0.2.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/CHANGELOG.md +27 -0
- package/README.md +313 -0
- package/README_EN.md +313 -0
- package/dist/client.cjs +835 -0
- package/dist/client.d.ts +341 -0
- package/dist/client.js +791 -0
- package/dist/core.cjs +301 -0
- package/dist/core.d.ts +460 -0
- package/dist/core.js +250 -0
- package/dist/index.cjs +1043 -0
- package/dist/index.d.ts +194 -0
- package/dist/index.js +977 -0
- package/package.json +72 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
OAuthAuthorizationCodeRowSchema: () => OAuthAuthorizationCodeRowSchema,
|
|
24
|
+
OAuthAuthorizeQuerySchema: () => OAuthAuthorizeQuerySchema,
|
|
25
|
+
OAuthClientCreateResponseSchema: () => OAuthClientCreateResponseSchema,
|
|
26
|
+
OAuthClientCreateSchema: () => OAuthClientCreateSchema,
|
|
27
|
+
OAuthClientDetailSchema: () => OAuthClientDetailSchema,
|
|
28
|
+
OAuthClientListItemSchema: () => OAuthClientListItemSchema,
|
|
29
|
+
OAuthClientRowSchema: () => OAuthClientRowSchema,
|
|
30
|
+
OAuthClientSecretRotateResponseSchema: () => OAuthClientSecretRotateResponseSchema,
|
|
31
|
+
OAuthClientUpdateSchema: () => OAuthClientUpdateSchema,
|
|
32
|
+
OAuthClientsService: () => OAuthClientsService,
|
|
33
|
+
OAuthConsentBodySchema: () => OAuthConsentBodySchema,
|
|
34
|
+
OAuthRefreshTokenSchema: () => OAuthRefreshTokenSchema,
|
|
35
|
+
OAuthRfcCodes: () => OAuthRfcCodes,
|
|
36
|
+
OAuthTokenAuthorizationCodeSchema: () => OAuthTokenAuthorizationCodeSchema,
|
|
37
|
+
OAuthTokenErrorResponseSchema: () => OAuthTokenErrorResponseSchema,
|
|
38
|
+
OAuthTokenRefreshSchema: () => OAuthTokenRefreshSchema,
|
|
39
|
+
OAuthTokenRequestSchema: () => OAuthTokenRequestSchema,
|
|
40
|
+
OAuthTokenResponseSchema: () => OAuthTokenResponseSchema,
|
|
41
|
+
OAuthTokenRevokeSchema: () => OAuthTokenRevokeSchema,
|
|
42
|
+
OAuthTokenService: () => OAuthTokenService,
|
|
43
|
+
OAuthUserCredentialsSchema: () => OAuthUserCredentialsSchema,
|
|
44
|
+
OAuthUserInfoErrorResponseSchema: () => OAuthUserInfoErrorResponseSchema,
|
|
45
|
+
OAuthUserInfoResponseSchema: () => OAuthUserInfoResponseSchema,
|
|
46
|
+
OAuthWrapperError: () => OAuthWrapperError,
|
|
47
|
+
OAuthWrapperService: () => OAuthWrapperService,
|
|
48
|
+
buildOAuthRedirectUrl: () => buildOAuthRedirectUrl,
|
|
49
|
+
computePkceS256Challenge: () => computePkceS256Challenge,
|
|
50
|
+
computeS256CodeChallenge: () => computeS256CodeChallenge,
|
|
51
|
+
generatePkceVerifier: () => generatePkceVerifier,
|
|
52
|
+
hashClientSecret: () => hashClientSecret,
|
|
53
|
+
isOAuthRedirectUri: () => isOAuthRedirectUri,
|
|
54
|
+
isRedirectUriAllowed: () => isRedirectUriAllowed,
|
|
55
|
+
isValidCodeChallenge: () => isValidCodeChallenge,
|
|
56
|
+
isValidCodeVerifier: () => isValidCodeVerifier,
|
|
57
|
+
normalizeQuery: () => normalizeQuery,
|
|
58
|
+
parseScopeList: () => parseScopeList,
|
|
59
|
+
randomOAuthState: () => randomOAuthState,
|
|
60
|
+
validatePkceParams: () => validatePkceParams,
|
|
61
|
+
verifyClientSecret: () => verifyClientSecret,
|
|
62
|
+
verifyPkceS256: () => verifyPkceS256
|
|
63
|
+
});
|
|
64
|
+
module.exports = __toCommonJS(server_exports);
|
|
65
|
+
|
|
66
|
+
// src/server/services/OAuthClientsService.ts
|
|
67
|
+
var OAuthClientsService = class {
|
|
68
|
+
constructor(clientsRepo) {
|
|
69
|
+
this.clientsRepo = clientsRepo;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* List all OAuth clients owned by a user
|
|
73
|
+
* @override
|
|
74
|
+
*/
|
|
75
|
+
async listForOwner(ownerUserId) {
|
|
76
|
+
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get detailed information about a specific OAuth client
|
|
80
|
+
* @override
|
|
81
|
+
*/
|
|
82
|
+
async getByClientId(ownerUserId, clientId) {
|
|
83
|
+
const client = await this.clientsRepo.findClientById(clientId);
|
|
84
|
+
if (!client) {
|
|
85
|
+
throw new Error("Client not found");
|
|
86
|
+
}
|
|
87
|
+
if (client.owner_user_id !== ownerUserId) {
|
|
88
|
+
throw new Error("Access denied");
|
|
89
|
+
}
|
|
90
|
+
return this.mapToDetail(client);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create a new OAuth client
|
|
94
|
+
* @override
|
|
95
|
+
*/
|
|
96
|
+
async create(ownerUserId, input) {
|
|
97
|
+
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
98
|
+
return {
|
|
99
|
+
client_id: result.client.client_id,
|
|
100
|
+
client_secret: result.clientSecret,
|
|
101
|
+
confidential: result.client.confidential,
|
|
102
|
+
client_name: result.client.client_name,
|
|
103
|
+
client_uri: result.client.client_uri,
|
|
104
|
+
redirect_uris: result.client.redirect_uris,
|
|
105
|
+
created_at: result.client.created_at
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Update an existing OAuth client
|
|
110
|
+
* @override
|
|
111
|
+
*/
|
|
112
|
+
async update(ownerUserId, clientId, input) {
|
|
113
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
114
|
+
if (!existing) {
|
|
115
|
+
throw new Error("Client not found");
|
|
116
|
+
}
|
|
117
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
118
|
+
throw new Error("Access denied");
|
|
119
|
+
}
|
|
120
|
+
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Rotate the client secret
|
|
124
|
+
* @override
|
|
125
|
+
*/
|
|
126
|
+
async rotateSecret(ownerUserId, clientId) {
|
|
127
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
128
|
+
if (!existing) {
|
|
129
|
+
throw new Error("Client not found");
|
|
130
|
+
}
|
|
131
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
132
|
+
throw new Error("Access denied");
|
|
133
|
+
}
|
|
134
|
+
if (!existing.confidential) {
|
|
135
|
+
throw new Error("Public clients do not have a client_secret");
|
|
136
|
+
}
|
|
137
|
+
const result = await this.clientsRepo.rotateClientSecret(
|
|
138
|
+
ownerUserId,
|
|
139
|
+
clientId
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
client_id: clientId,
|
|
143
|
+
client_secret: result.clientSecret
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Delete an OAuth client
|
|
148
|
+
* @override
|
|
149
|
+
*/
|
|
150
|
+
async delete(ownerUserId, clientId) {
|
|
151
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
152
|
+
if (!existing) {
|
|
153
|
+
throw new Error("Client not found");
|
|
154
|
+
}
|
|
155
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
156
|
+
throw new Error("Access denied");
|
|
157
|
+
}
|
|
158
|
+
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
159
|
+
}
|
|
160
|
+
mapToDetail(row) {
|
|
161
|
+
return {
|
|
162
|
+
client_id: row.client_id,
|
|
163
|
+
client_name: row.client_name,
|
|
164
|
+
client_uri: row.client_uri,
|
|
165
|
+
logo_uri: row.logo_uri,
|
|
166
|
+
redirect_uris: row.redirect_uris,
|
|
167
|
+
grant_types: row.grant_types,
|
|
168
|
+
scopes: row.scopes,
|
|
169
|
+
confidential: row.confidential,
|
|
170
|
+
created_at: row.created_at,
|
|
171
|
+
updated_at: row.updated_at
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/server/services/OAuthTokenService.ts
|
|
177
|
+
var import_crypto2 = require("crypto");
|
|
178
|
+
|
|
179
|
+
// src/core/schema/OAuthAuthorizeSchema.ts
|
|
180
|
+
var import_zod = require("zod");
|
|
181
|
+
function isOAuthRedirectUri(value) {
|
|
182
|
+
const trimmed = value.trim();
|
|
183
|
+
if (!trimmed) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const parsed = new URL(trimmed);
|
|
188
|
+
return Boolean(parsed.protocol && parsed.protocol.endsWith(":"));
|
|
189
|
+
} catch {
|
|
190
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
var oauthRedirectUriSchema = import_zod.z.string().min(1).refine(isOAuthRedirectUri, { message: "Invalid redirect URI" });
|
|
194
|
+
var OAuthClientRowSchema = import_zod.z.object({
|
|
195
|
+
id: import_zod.z.number(),
|
|
196
|
+
client_id: import_zod.z.string(),
|
|
197
|
+
client_secret_hash: import_zod.z.string().nullable().optional(),
|
|
198
|
+
client_name: import_zod.z.string(),
|
|
199
|
+
client_uri: import_zod.z.string().nullable().optional(),
|
|
200
|
+
logo_uri: import_zod.z.string().nullable().optional(),
|
|
201
|
+
redirect_uris: import_zod.z.array(import_zod.z.string()),
|
|
202
|
+
grant_types: import_zod.z.array(import_zod.z.string()),
|
|
203
|
+
scopes: import_zod.z.array(import_zod.z.string()),
|
|
204
|
+
confidential: import_zod.z.boolean(),
|
|
205
|
+
owner_user_id: import_zod.z.number(),
|
|
206
|
+
created_at: import_zod.z.string(),
|
|
207
|
+
updated_at: import_zod.z.string()
|
|
208
|
+
});
|
|
209
|
+
var OAuthClientListItemSchema = import_zod.z.object({
|
|
210
|
+
client_id: import_zod.z.string(),
|
|
211
|
+
client_name: import_zod.z.string(),
|
|
212
|
+
client_uri: import_zod.z.string().nullable().optional(),
|
|
213
|
+
logo_uri: import_zod.z.string().nullable().optional(),
|
|
214
|
+
redirect_uris: import_zod.z.array(import_zod.z.string()),
|
|
215
|
+
confidential: import_zod.z.boolean(),
|
|
216
|
+
created_at: import_zod.z.string(),
|
|
217
|
+
updated_at: import_zod.z.string()
|
|
218
|
+
});
|
|
219
|
+
var OAuthClientDetailSchema = import_zod.z.object({
|
|
220
|
+
client_id: import_zod.z.string(),
|
|
221
|
+
client_name: import_zod.z.string(),
|
|
222
|
+
client_uri: import_zod.z.string().nullable().optional(),
|
|
223
|
+
logo_uri: import_zod.z.string().nullable().optional(),
|
|
224
|
+
redirect_uris: import_zod.z.array(import_zod.z.string()),
|
|
225
|
+
grant_types: import_zod.z.array(import_zod.z.string()),
|
|
226
|
+
scopes: import_zod.z.array(import_zod.z.string()),
|
|
227
|
+
confidential: import_zod.z.boolean(),
|
|
228
|
+
created_at: import_zod.z.string(),
|
|
229
|
+
updated_at: import_zod.z.string()
|
|
230
|
+
});
|
|
231
|
+
var OAuthClientCreateSchema = import_zod.z.object({
|
|
232
|
+
client_name: import_zod.z.string().min(1).max(100),
|
|
233
|
+
client_uri: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
234
|
+
redirect_uris: import_zod.z.array(oauthRedirectUriSchema).min(1),
|
|
235
|
+
/** `true` = confidential (client_secret); `false` = public (PKCE required). */
|
|
236
|
+
confidential: import_zod.z.boolean().default(true)
|
|
237
|
+
});
|
|
238
|
+
var OAuthClientUpdateSchema = import_zod.z.object({
|
|
239
|
+
client_name: import_zod.z.string().min(1).max(100),
|
|
240
|
+
client_uri: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
241
|
+
redirect_uris: import_zod.z.array(oauthRedirectUriSchema).min(1)
|
|
242
|
+
});
|
|
243
|
+
var OAuthClientCreateResponseSchema = import_zod.z.object({
|
|
244
|
+
client_id: import_zod.z.string(),
|
|
245
|
+
client_secret: import_zod.z.string().optional(),
|
|
246
|
+
confidential: import_zod.z.boolean(),
|
|
247
|
+
client_name: import_zod.z.string(),
|
|
248
|
+
client_uri: import_zod.z.string().nullable().optional(),
|
|
249
|
+
redirect_uris: import_zod.z.array(import_zod.z.string()),
|
|
250
|
+
created_at: import_zod.z.string()
|
|
251
|
+
});
|
|
252
|
+
var OAuthClientSecretRotateResponseSchema = import_zod.z.object({
|
|
253
|
+
client_id: import_zod.z.string(),
|
|
254
|
+
client_secret: import_zod.z.string()
|
|
255
|
+
});
|
|
256
|
+
var OAuthAuthorizeQuerySchema = import_zod.z.object({
|
|
257
|
+
response_type: import_zod.z.literal("code"),
|
|
258
|
+
client_id: import_zod.z.string().min(1),
|
|
259
|
+
redirect_uri: oauthRedirectUriSchema,
|
|
260
|
+
scope: import_zod.z.string().optional(),
|
|
261
|
+
state: import_zod.z.string().optional(),
|
|
262
|
+
code_challenge: import_zod.z.string().min(43).max(128).optional(),
|
|
263
|
+
code_challenge_method: import_zod.z.literal("S256").optional()
|
|
264
|
+
});
|
|
265
|
+
var OAuthConsentBodySchema = import_zod.z.object({
|
|
266
|
+
action: import_zod.z.enum(["allow", "deny"]),
|
|
267
|
+
client_id: import_zod.z.string().min(1),
|
|
268
|
+
redirect_uri: oauthRedirectUriSchema,
|
|
269
|
+
scope: import_zod.z.string().optional(),
|
|
270
|
+
state: import_zod.z.string().optional(),
|
|
271
|
+
trust: import_zod.z.boolean().optional(),
|
|
272
|
+
code_challenge: import_zod.z.string().min(43).max(128).optional(),
|
|
273
|
+
code_challenge_method: import_zod.z.literal("S256").optional()
|
|
274
|
+
});
|
|
275
|
+
var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
|
|
276
|
+
code: import_zod.z.string(),
|
|
277
|
+
client_id: import_zod.z.string(),
|
|
278
|
+
user_id: import_zod.z.number(),
|
|
279
|
+
redirect_uri: import_zod.z.string(),
|
|
280
|
+
scope: import_zod.z.string().nullable().optional(),
|
|
281
|
+
code_challenge: import_zod.z.string().nullable().optional(),
|
|
282
|
+
code_challenge_method: import_zod.z.string().nullable().optional(),
|
|
283
|
+
expires_at: import_zod.z.string(),
|
|
284
|
+
used: import_zod.z.boolean(),
|
|
285
|
+
created_at: import_zod.z.string()
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// src/core/schema/OAuthClientSchema.ts
|
|
289
|
+
var import_zod2 = require("zod");
|
|
290
|
+
var OAuthUserCredentialsSchema = import_zod2.z.object({
|
|
291
|
+
user_id: import_zod2.z.number(),
|
|
292
|
+
provider_refresh_token: import_zod2.z.string().nullable().optional(),
|
|
293
|
+
provider_session_token: import_zod2.z.string().nullable().optional(),
|
|
294
|
+
updated_at: import_zod2.z.string()
|
|
295
|
+
});
|
|
296
|
+
var OAuthRefreshTokenSchema = import_zod2.z.object({
|
|
297
|
+
id: import_zod2.z.number(),
|
|
298
|
+
refresh_token: import_zod2.z.string(),
|
|
299
|
+
client_id: import_zod2.z.string(),
|
|
300
|
+
user_id: import_zod2.z.number(),
|
|
301
|
+
expires_at: import_zod2.z.string(),
|
|
302
|
+
revoked: import_zod2.z.boolean(),
|
|
303
|
+
created_at: import_zod2.z.string()
|
|
304
|
+
});
|
|
305
|
+
var OAuthTokenResponseSchema = import_zod2.z.object({
|
|
306
|
+
access_token: import_zod2.z.string(),
|
|
307
|
+
token_type: import_zod2.z.literal("Bearer"),
|
|
308
|
+
expires_in: import_zod2.z.number(),
|
|
309
|
+
refresh_token: import_zod2.z.string().optional(),
|
|
310
|
+
scope: import_zod2.z.string().optional()
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// src/core/schema/OAuthUserInfoSchema.ts
|
|
314
|
+
var import_zod3 = require("zod");
|
|
315
|
+
var OAuthUserInfoResponseSchema = import_zod3.z.object({
|
|
316
|
+
sub: import_zod3.z.string(),
|
|
317
|
+
email: import_zod3.z.string().email(),
|
|
318
|
+
name: import_zod3.z.string(),
|
|
319
|
+
roles: import_zod3.z.array(import_zod3.z.string()).optional()
|
|
320
|
+
});
|
|
321
|
+
var OAuthUserInfoErrorResponseSchema = import_zod3.z.object({
|
|
322
|
+
error: import_zod3.z.literal("invalid_token"),
|
|
323
|
+
error_id: import_zod3.z.string().optional()
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// src/core/schema/OAuthTokenSchema.ts
|
|
327
|
+
var import_zod4 = require("zod");
|
|
328
|
+
var OAuthTokenAuthorizationCodeSchema = import_zod4.z.object({
|
|
329
|
+
grant_type: import_zod4.z.literal("authorization_code"),
|
|
330
|
+
code: import_zod4.z.string().min(1),
|
|
331
|
+
redirect_uri: import_zod4.z.string().min(1),
|
|
332
|
+
client_id: import_zod4.z.string().min(1),
|
|
333
|
+
client_secret: import_zod4.z.string().min(1).optional(),
|
|
334
|
+
code_verifier: import_zod4.z.string().min(43).max(128).optional()
|
|
335
|
+
});
|
|
336
|
+
var OAuthTokenRefreshSchema = import_zod4.z.object({
|
|
337
|
+
grant_type: import_zod4.z.literal("refresh_token"),
|
|
338
|
+
refresh_token: import_zod4.z.string().min(1),
|
|
339
|
+
client_id: import_zod4.z.string().min(1),
|
|
340
|
+
client_secret: import_zod4.z.string().min(1).optional()
|
|
341
|
+
});
|
|
342
|
+
var OAuthTokenRequestSchema = import_zod4.z.discriminatedUnion("grant_type", [
|
|
343
|
+
OAuthTokenAuthorizationCodeSchema,
|
|
344
|
+
OAuthTokenRefreshSchema
|
|
345
|
+
]);
|
|
346
|
+
var OAuthTokenErrorResponseSchema = import_zod4.z.object({
|
|
347
|
+
error: import_zod4.z.string(),
|
|
348
|
+
error_id: import_zod4.z.string().optional(),
|
|
349
|
+
error_description: import_zod4.z.string().optional()
|
|
350
|
+
});
|
|
351
|
+
var OAuthTokenRevokeSchema = import_zod4.z.object({
|
|
352
|
+
token: import_zod4.z.string().min(1),
|
|
353
|
+
token_type_hint: import_zod4.z.enum(["access_token", "refresh_token"]).optional(),
|
|
354
|
+
client_id: import_zod4.z.string().min(1),
|
|
355
|
+
client_secret: import_zod4.z.string().min(1).optional()
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/core/config.ts
|
|
359
|
+
var OAuthRfcCodes = {
|
|
360
|
+
INVALID_REQUEST: "invalid_request",
|
|
361
|
+
INVALID_CLIENT: "invalid_client",
|
|
362
|
+
INVALID_GRANT: "invalid_grant",
|
|
363
|
+
INVALID_TOKEN: "invalid_token",
|
|
364
|
+
UNAUTHORIZED_CLIENT: "unauthorized_client",
|
|
365
|
+
INVALID_SCOPE: "invalid_scope",
|
|
366
|
+
ACCESS_DENIED: "access_denied",
|
|
367
|
+
UNSUPPORTED_RESPONSE_TYPE: "unsupported_response_type",
|
|
368
|
+
UNSUPPORTED_GRANT_TYPE: "unsupported_grant_type",
|
|
369
|
+
OAUTH_ERROR: "oauth_error"
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/core/utils/pkce.ts
|
|
373
|
+
var PKCE_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
374
|
+
function generatePkceVerifier(length = 64) {
|
|
375
|
+
const size = Math.min(128, Math.max(43, length));
|
|
376
|
+
const values = new Uint8Array(size);
|
|
377
|
+
crypto.getRandomValues(values);
|
|
378
|
+
let result = "";
|
|
379
|
+
for (let i = 0; i < size; i++) {
|
|
380
|
+
result += PKCE_CHARSET[values[i] % PKCE_CHARSET.length];
|
|
381
|
+
}
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
function base64UrlEncode(buffer) {
|
|
385
|
+
const bytes = new Uint8Array(buffer);
|
|
386
|
+
let binary = "";
|
|
387
|
+
for (const byte of bytes) {
|
|
388
|
+
binary += String.fromCharCode(byte);
|
|
389
|
+
}
|
|
390
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
391
|
+
}
|
|
392
|
+
async function computePkceS256Challenge(verifier) {
|
|
393
|
+
const data = new TextEncoder().encode(verifier);
|
|
394
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
395
|
+
return base64UrlEncode(digest);
|
|
396
|
+
}
|
|
397
|
+
function randomOAuthState() {
|
|
398
|
+
const bytes = new Uint8Array(16);
|
|
399
|
+
crypto.getRandomValues(bytes);
|
|
400
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/server/utils/OAuthWrapperError.ts
|
|
404
|
+
var import_fe_corekit = require("@qlover/fe-corekit");
|
|
405
|
+
var OAuthWrapperError = class extends import_fe_corekit.ExecutorError {
|
|
406
|
+
constructor(id, status, cause) {
|
|
407
|
+
super(id, cause);
|
|
408
|
+
this.status = status;
|
|
409
|
+
}
|
|
410
|
+
name = "OAuthWrapperError";
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/server/utils/pkce.ts
|
|
414
|
+
var import_crypto = require("crypto");
|
|
415
|
+
var PKCE_VERIFIER_MIN = 43;
|
|
416
|
+
var PKCE_VERIFIER_MAX = 128;
|
|
417
|
+
var UNRESERVED = /^[A-Za-z0-9\-._~]+$/;
|
|
418
|
+
function isValidCodeVerifier(verifier) {
|
|
419
|
+
const len = verifier.length;
|
|
420
|
+
if (len < PKCE_VERIFIER_MIN || len > PKCE_VERIFIER_MAX) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return UNRESERVED.test(verifier);
|
|
424
|
+
}
|
|
425
|
+
function isValidCodeChallenge(challenge) {
|
|
426
|
+
if (challenge.length < PKCE_VERIFIER_MIN || challenge.length > PKCE_VERIFIER_MAX) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return UNRESERVED.test(challenge);
|
|
430
|
+
}
|
|
431
|
+
function computeS256CodeChallenge(verifier) {
|
|
432
|
+
return (0, import_crypto.createHash)("sha256").update(verifier).digest("base64url");
|
|
433
|
+
}
|
|
434
|
+
function verifyPkceS256(verifier, challenge) {
|
|
435
|
+
if (!isValidCodeVerifier(verifier) || !isValidCodeChallenge(challenge)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const computed = computeS256CodeChallenge(verifier);
|
|
439
|
+
try {
|
|
440
|
+
return (0, import_crypto.timingSafeEqual)(Buffer.from(computed), Buffer.from(challenge));
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/server/services/OAuthTokenService.ts
|
|
447
|
+
function hashOpaqueToken(token) {
|
|
448
|
+
return (0, import_crypto2.createHash)("sha256").update(token).digest("hex");
|
|
449
|
+
}
|
|
450
|
+
var OAuthTokenService = class _OAuthTokenService {
|
|
451
|
+
constructor(tokenEncryption, userAdapter, oauthRepo) {
|
|
452
|
+
this.tokenEncryption = tokenEncryption;
|
|
453
|
+
this.userAdapter = userAdapter;
|
|
454
|
+
this.oauthRepo = oauthRepo;
|
|
455
|
+
}
|
|
456
|
+
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
457
|
+
/**
|
|
458
|
+
* @override
|
|
459
|
+
*/
|
|
460
|
+
async exchangeToken(rawFields) {
|
|
461
|
+
let request;
|
|
462
|
+
try {
|
|
463
|
+
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
464
|
+
} catch {
|
|
465
|
+
throw new OAuthWrapperError(
|
|
466
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
467
|
+
400,
|
|
468
|
+
"Malformed token request"
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
let client;
|
|
472
|
+
try {
|
|
473
|
+
client = await this.oauthRepo.verifyClientCredentials(
|
|
474
|
+
request.client_id,
|
|
475
|
+
request.client_secret
|
|
476
|
+
);
|
|
477
|
+
} catch {
|
|
478
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
479
|
+
}
|
|
480
|
+
if (!client.grant_types.includes(request.grant_type)) {
|
|
481
|
+
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
482
|
+
}
|
|
483
|
+
if (request.grant_type === "authorization_code") {
|
|
484
|
+
return await this.exchangeAuthorizationCode(request, client.client_id);
|
|
485
|
+
}
|
|
486
|
+
return await this.exchangeRefreshToken(request, client.client_id);
|
|
487
|
+
}
|
|
488
|
+
async exchangeAuthorizationCode(request, verifiedClientId) {
|
|
489
|
+
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
490
|
+
if (!authCode) {
|
|
491
|
+
throw new OAuthWrapperError(
|
|
492
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
493
|
+
400,
|
|
494
|
+
"Authorization code is invalid, expired, or already used"
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (authCode.client_id !== verifiedClientId) {
|
|
498
|
+
throw new OAuthWrapperError(
|
|
499
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
500
|
+
400,
|
|
501
|
+
"client_id mismatch"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
505
|
+
throw new OAuthWrapperError(
|
|
506
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
507
|
+
400,
|
|
508
|
+
"redirect_uri mismatch"
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
this.assertPkceForAuthorizationCode(request, authCode);
|
|
512
|
+
const providerTokens = await this.fetchProviderAccessToken(
|
|
513
|
+
authCode.user_id
|
|
514
|
+
);
|
|
515
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
516
|
+
authCode.client_id,
|
|
517
|
+
authCode.user_id
|
|
518
|
+
);
|
|
519
|
+
return {
|
|
520
|
+
access_token: providerTokens.access_token,
|
|
521
|
+
token_type: "Bearer",
|
|
522
|
+
expires_in: providerTokens.expires_in,
|
|
523
|
+
refresh_token: middlewareRefresh,
|
|
524
|
+
scope: authCode.scope ?? void 0
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
assertPkceForAuthorizationCode(request, authCode) {
|
|
528
|
+
const storedChallenge = authCode.code_challenge?.trim();
|
|
529
|
+
if (storedChallenge) {
|
|
530
|
+
const verifier = request.code_verifier?.trim();
|
|
531
|
+
if (!verifier) {
|
|
532
|
+
throw new OAuthWrapperError(
|
|
533
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
534
|
+
400,
|
|
535
|
+
"code_verifier is required"
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (authCode.code_challenge_method !== "S256") {
|
|
539
|
+
throw new OAuthWrapperError(
|
|
540
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
541
|
+
400,
|
|
542
|
+
"Unsupported PKCE method"
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
if (!verifyPkceS256(verifier, storedChallenge)) {
|
|
546
|
+
throw new OAuthWrapperError(
|
|
547
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
548
|
+
400,
|
|
549
|
+
"code_verifier mismatch"
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (!request.client_secret?.trim()) {
|
|
555
|
+
throw new OAuthWrapperError(
|
|
556
|
+
OAuthRfcCodes.INVALID_CLIENT,
|
|
557
|
+
401,
|
|
558
|
+
"client_secret is required when PKCE is not used"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async exchangeRefreshToken(request, verifiedClientId) {
|
|
563
|
+
const tokenHash = hashOpaqueToken(request.refresh_token);
|
|
564
|
+
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
565
|
+
if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
566
|
+
throw new OAuthWrapperError(
|
|
567
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
568
|
+
400,
|
|
569
|
+
"Refresh token is invalid"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
|
|
573
|
+
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
574
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
575
|
+
stored.client_id,
|
|
576
|
+
stored.user_id
|
|
577
|
+
);
|
|
578
|
+
return {
|
|
579
|
+
access_token: providerTokens.access_token,
|
|
580
|
+
token_type: "Bearer",
|
|
581
|
+
expires_in: providerTokens.expires_in,
|
|
582
|
+
refresh_token: middlewareRefresh
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
async fetchProviderAccessToken(userId) {
|
|
586
|
+
const credentials = await this.oauthRepo.getUserCredentials(userId);
|
|
587
|
+
const sessionToken = credentials?.provider_session_token?.trim();
|
|
588
|
+
if (!sessionToken) {
|
|
589
|
+
throw new OAuthWrapperError(
|
|
590
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
591
|
+
400,
|
|
592
|
+
"User credentials expired. Re-authorization required."
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const access = await this.userAdapter.exchangeAccessToken({
|
|
597
|
+
token: sessionToken
|
|
598
|
+
});
|
|
599
|
+
if (access.refresh_token) {
|
|
600
|
+
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
601
|
+
provider_refresh_token: this.tokenEncryption.encrypt(
|
|
602
|
+
access.refresh_token
|
|
603
|
+
)
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
access_token: access.access_token,
|
|
608
|
+
expires_in: access.expires_in
|
|
609
|
+
};
|
|
610
|
+
} catch {
|
|
611
|
+
throw new OAuthWrapperError(
|
|
612
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
613
|
+
400,
|
|
614
|
+
"Failed to obtain access token from user provider"
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
619
|
+
const plain = (0, import_crypto2.randomBytes)(32).toString("base64url");
|
|
620
|
+
const expiresAt = new Date(
|
|
621
|
+
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
622
|
+
).toISOString();
|
|
623
|
+
await this.oauthRepo.createRefreshToken({
|
|
624
|
+
refresh_token: hashOpaqueToken(plain),
|
|
625
|
+
client_id: clientId,
|
|
626
|
+
user_id: userId,
|
|
627
|
+
expires_at: expiresAt
|
|
628
|
+
});
|
|
629
|
+
return plain;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* @override
|
|
633
|
+
* RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
|
|
634
|
+
*/
|
|
635
|
+
async revokeToken(rawFields) {
|
|
636
|
+
let request;
|
|
637
|
+
try {
|
|
638
|
+
request = OAuthTokenRevokeSchema.parse(rawFields);
|
|
639
|
+
} catch {
|
|
640
|
+
throw new OAuthWrapperError(
|
|
641
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
642
|
+
400,
|
|
643
|
+
"Malformed revocation request"
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
await this.oauthRepo.verifyClientCredentials(
|
|
648
|
+
request.client_id,
|
|
649
|
+
request.client_secret
|
|
650
|
+
);
|
|
651
|
+
} catch {
|
|
652
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
653
|
+
}
|
|
654
|
+
if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const tokenHash = hashOpaqueToken(request.token);
|
|
658
|
+
const stored = await this.oauthRepo.findByTokenHash(tokenHash);
|
|
659
|
+
if (!stored || stored.client_id !== request.client_id) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
await this.oauthRepo.revokeByTokenHash(tokenHash);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/server/services/OAuthWrapperService.ts
|
|
667
|
+
var import_crypto3 = require("crypto");
|
|
668
|
+
var import_fe_corekit2 = require("@qlover/fe-corekit");
|
|
669
|
+
|
|
670
|
+
// src/server/utils/authorizeUtil.ts
|
|
671
|
+
function validatePkceParams(parsed, confidential) {
|
|
672
|
+
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
673
|
+
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
674
|
+
if (!confidential) {
|
|
675
|
+
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
676
|
+
return {
|
|
677
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
678
|
+
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
682
|
+
return {
|
|
683
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
684
|
+
message: "Invalid code_challenge."
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
if (hasChallenge !== hasMethod) {
|
|
690
|
+
return {
|
|
691
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
692
|
+
message: "code_challenge and code_challenge_method must be sent together."
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (hasChallenge) {
|
|
696
|
+
if (parsed.code_challenge_method !== "S256") {
|
|
697
|
+
return {
|
|
698
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
699
|
+
message: "Only code_challenge_method=S256 is supported."
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
703
|
+
return {
|
|
704
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
705
|
+
message: "Invalid code_challenge."
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
function normalizeQuery(raw) {
|
|
712
|
+
const result = {};
|
|
713
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
714
|
+
if (Array.isArray(value)) {
|
|
715
|
+
result[key] = value[0];
|
|
716
|
+
} else {
|
|
717
|
+
result[key] = value;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (!result.response_type) {
|
|
721
|
+
result.response_type = "code";
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
function isRedirectUriAllowed(redirectUri, client) {
|
|
726
|
+
return client.redirect_uris.includes(redirectUri);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/server/utils/oauthRedirectUtils.ts
|
|
730
|
+
function buildOAuthRedirectUrl(redirectUri, params) {
|
|
731
|
+
const url = new URL(redirectUri);
|
|
732
|
+
for (const [key, value] of Object.entries(params)) {
|
|
733
|
+
if (value != null && value !== "") {
|
|
734
|
+
url.searchParams.set(key, value);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return url.toString();
|
|
738
|
+
}
|
|
739
|
+
function parseScopeList(scope) {
|
|
740
|
+
if (!scope?.trim()) {
|
|
741
|
+
return ["openid", "profile", "email"];
|
|
742
|
+
}
|
|
743
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/server/services/OAuthWrapperService.ts
|
|
747
|
+
var AUTH_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
748
|
+
var OAuthWrapperService = class {
|
|
749
|
+
constructor(oauthSession, userAdapter, tokenService, oauthRepo) {
|
|
750
|
+
this.oauthSession = oauthSession;
|
|
751
|
+
this.userAdapter = userAdapter;
|
|
752
|
+
this.tokenService = tokenService;
|
|
753
|
+
this.oauthRepo = oauthRepo;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* @override
|
|
757
|
+
*/
|
|
758
|
+
getOAuthSession() {
|
|
759
|
+
return this.oauthSession;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* @override
|
|
763
|
+
*/
|
|
764
|
+
getOAuthAdapter() {
|
|
765
|
+
return this.userAdapter;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* @override
|
|
769
|
+
*/
|
|
770
|
+
getOAuthTokenService() {
|
|
771
|
+
return this.tokenService;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* @override
|
|
775
|
+
*/
|
|
776
|
+
getOAuthRepo() {
|
|
777
|
+
return this.oauthRepo;
|
|
778
|
+
}
|
|
779
|
+
isQuery(query) {
|
|
780
|
+
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
781
|
+
}
|
|
782
|
+
isValidateConsent(value) {
|
|
783
|
+
return OAuthConsentBodySchema.safeParse(value).success;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* @override
|
|
787
|
+
*/
|
|
788
|
+
async resolveAuthorizePage(rawQuery) {
|
|
789
|
+
const query = normalizeQuery(rawQuery);
|
|
790
|
+
if (!this.isQuery(query)) {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
error: {
|
|
794
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
795
|
+
message: "Missing or invalid authorization request parameters."
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (query.response_type !== "code") {
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
error: {
|
|
803
|
+
errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
|
|
804
|
+
message: "Only response_type=code is supported."
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const client = await this.oauthRepo.findClientById(query.client_id);
|
|
809
|
+
if (!client) {
|
|
810
|
+
return {
|
|
811
|
+
ok: false,
|
|
812
|
+
error: {
|
|
813
|
+
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
814
|
+
message: "Unknown client_id."
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
if (!isRedirectUriAllowed(query.redirect_uri, client)) {
|
|
819
|
+
return {
|
|
820
|
+
ok: false,
|
|
821
|
+
error: {
|
|
822
|
+
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
823
|
+
message: "redirect_uri is not registered for this client."
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const requestedScopes = parseScopeList(query.scope);
|
|
828
|
+
const invalidScope = requestedScopes.find(
|
|
829
|
+
(scope) => !client.scopes.includes(scope)
|
|
830
|
+
);
|
|
831
|
+
if (invalidScope) {
|
|
832
|
+
return {
|
|
833
|
+
ok: false,
|
|
834
|
+
error: {
|
|
835
|
+
errorKey: OAuthRfcCodes.INVALID_SCOPE,
|
|
836
|
+
message: `Scope "${invalidScope}" is not allowed for this client.`
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const pkceError = validatePkceParams(query, client.confidential);
|
|
841
|
+
if (pkceError) {
|
|
842
|
+
return { ok: false, error: pkceError };
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
ok: true,
|
|
846
|
+
data: {
|
|
847
|
+
clientId: client.client_id,
|
|
848
|
+
clientName: client.client_name,
|
|
849
|
+
clientUri: client.client_uri ?? null,
|
|
850
|
+
logoUri: client.logo_uri ?? null,
|
|
851
|
+
redirectUri: query.redirect_uri,
|
|
852
|
+
scopes: requestedScopes,
|
|
853
|
+
state: query.state,
|
|
854
|
+
responseType: "code",
|
|
855
|
+
codeChallenge: query.code_challenge,
|
|
856
|
+
codeChallengeMethod: query.code_challenge_method,
|
|
857
|
+
confidential: client.confidential
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* @override
|
|
863
|
+
*/
|
|
864
|
+
async processConsent(requestBody) {
|
|
865
|
+
if (!this.isValidateConsent(requestBody)) {
|
|
866
|
+
throw new import_fe_corekit2.ExecutorError(
|
|
867
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
868
|
+
"Invalid consent request body"
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const session = await this.oauthSession.getSession();
|
|
872
|
+
if (!session) {
|
|
873
|
+
throw new import_fe_corekit2.ExecutorError(
|
|
874
|
+
OAuthRfcCodes.ACCESS_DENIED,
|
|
875
|
+
"User session expired. Please sign in again."
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const pageResult = await this.resolveAuthorizePage({
|
|
879
|
+
response_type: "code",
|
|
880
|
+
client_id: requestBody.client_id,
|
|
881
|
+
redirect_uri: requestBody.redirect_uri,
|
|
882
|
+
scope: requestBody.scope,
|
|
883
|
+
state: requestBody.state,
|
|
884
|
+
code_challenge: requestBody.code_challenge,
|
|
885
|
+
code_challenge_method: requestBody.code_challenge_method
|
|
886
|
+
});
|
|
887
|
+
if (!pageResult.ok) {
|
|
888
|
+
throw new import_fe_corekit2.ExecutorError(
|
|
889
|
+
pageResult.error.errorKey,
|
|
890
|
+
pageResult.error.message
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const { data } = pageResult;
|
|
894
|
+
if (requestBody.action === "deny") {
|
|
895
|
+
return {
|
|
896
|
+
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
897
|
+
error: OAuthRfcCodes.ACCESS_DENIED,
|
|
898
|
+
error_description: "The resource owner denied the request",
|
|
899
|
+
state: data.state
|
|
900
|
+
})
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const code = (0, import_crypto3.randomBytes)(32).toString("base64url");
|
|
904
|
+
const expiresAt = new Date(Date.now() + AUTH_CODE_TTL_MS).toISOString();
|
|
905
|
+
await this.oauthRepo.create({
|
|
906
|
+
code,
|
|
907
|
+
client_id: data.clientId,
|
|
908
|
+
user_id: session.userId,
|
|
909
|
+
redirect_uri: data.redirectUri,
|
|
910
|
+
scope: data.scopes.join(" ") || null,
|
|
911
|
+
code_challenge: data.codeChallenge ?? null,
|
|
912
|
+
code_challenge_method: data.codeChallengeMethod ?? null,
|
|
913
|
+
expires_at: expiresAt
|
|
914
|
+
});
|
|
915
|
+
void requestBody.trust;
|
|
916
|
+
return {
|
|
917
|
+
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
918
|
+
code,
|
|
919
|
+
state: data.state
|
|
920
|
+
})
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* @override
|
|
925
|
+
*/
|
|
926
|
+
async exchangeToken(rawFields) {
|
|
927
|
+
return await this.tokenService.exchangeToken(rawFields);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* @override
|
|
931
|
+
*/
|
|
932
|
+
async revokeToken(rawFields) {
|
|
933
|
+
return await this.tokenService.revokeToken(rawFields);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* @override
|
|
937
|
+
*/
|
|
938
|
+
async logoutUser(userId) {
|
|
939
|
+
await this.oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
940
|
+
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
941
|
+
provider_refresh_token: null,
|
|
942
|
+
provider_session_token: null
|
|
943
|
+
});
|
|
944
|
+
await this.oauthSession.clearSession();
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* @override
|
|
948
|
+
*/
|
|
949
|
+
async getUserInfo(accessToken) {
|
|
950
|
+
try {
|
|
951
|
+
const profile = await this.userAdapter.getUserInfoByAccessToken(accessToken);
|
|
952
|
+
return this.toUserInfoResponse(profile);
|
|
953
|
+
} catch {
|
|
954
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
toUserInfoResponse(profile) {
|
|
958
|
+
const sub = String(profile.id);
|
|
959
|
+
if (!sub || sub === "NaN") {
|
|
960
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
961
|
+
}
|
|
962
|
+
const email = profile.email?.trim();
|
|
963
|
+
if (!email) {
|
|
964
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
965
|
+
}
|
|
966
|
+
const nameFromParts = [profile.first_name, profile.last_name].filter(Boolean).join(" ");
|
|
967
|
+
const name = profile.name?.trim() || nameFromParts || email;
|
|
968
|
+
return {
|
|
969
|
+
sub,
|
|
970
|
+
email,
|
|
971
|
+
name,
|
|
972
|
+
...profile.roles?.length ? { roles: profile.roles } : {}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// src/server/utils/clientSecretHash.ts
|
|
978
|
+
var import_crypto4 = require("crypto");
|
|
979
|
+
var import_util = require("util");
|
|
980
|
+
var scryptAsync = (0, import_util.promisify)(import_crypto4.scrypt);
|
|
981
|
+
var PREFIX = "scrypt";
|
|
982
|
+
var KEY_LEN = 64;
|
|
983
|
+
async function hashClientSecret(secret) {
|
|
984
|
+
const salt = (0, import_crypto4.randomBytes)(16);
|
|
985
|
+
const derived = await scryptAsync(secret, salt, KEY_LEN);
|
|
986
|
+
return `${PREFIX}$${salt.toString("base64")}$${derived.toString("base64")}`;
|
|
987
|
+
}
|
|
988
|
+
async function verifyClientSecret(secret, storedHash) {
|
|
989
|
+
const parts = storedHash.split("$");
|
|
990
|
+
if (parts.length !== 3 || parts[0] !== PREFIX) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
const salt = Buffer.from(parts[1], "base64");
|
|
994
|
+
const expected = Buffer.from(parts[2], "base64");
|
|
995
|
+
const derived = await scryptAsync(secret, salt, expected.length);
|
|
996
|
+
if (derived.length !== expected.length) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
return (0, import_crypto4.timingSafeEqual)(derived, expected);
|
|
1000
|
+
}
|
|
1001
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1002
|
+
0 && (module.exports = {
|
|
1003
|
+
OAuthAuthorizationCodeRowSchema,
|
|
1004
|
+
OAuthAuthorizeQuerySchema,
|
|
1005
|
+
OAuthClientCreateResponseSchema,
|
|
1006
|
+
OAuthClientCreateSchema,
|
|
1007
|
+
OAuthClientDetailSchema,
|
|
1008
|
+
OAuthClientListItemSchema,
|
|
1009
|
+
OAuthClientRowSchema,
|
|
1010
|
+
OAuthClientSecretRotateResponseSchema,
|
|
1011
|
+
OAuthClientUpdateSchema,
|
|
1012
|
+
OAuthClientsService,
|
|
1013
|
+
OAuthConsentBodySchema,
|
|
1014
|
+
OAuthRefreshTokenSchema,
|
|
1015
|
+
OAuthRfcCodes,
|
|
1016
|
+
OAuthTokenAuthorizationCodeSchema,
|
|
1017
|
+
OAuthTokenErrorResponseSchema,
|
|
1018
|
+
OAuthTokenRefreshSchema,
|
|
1019
|
+
OAuthTokenRequestSchema,
|
|
1020
|
+
OAuthTokenResponseSchema,
|
|
1021
|
+
OAuthTokenRevokeSchema,
|
|
1022
|
+
OAuthTokenService,
|
|
1023
|
+
OAuthUserCredentialsSchema,
|
|
1024
|
+
OAuthUserInfoErrorResponseSchema,
|
|
1025
|
+
OAuthUserInfoResponseSchema,
|
|
1026
|
+
OAuthWrapperError,
|
|
1027
|
+
OAuthWrapperService,
|
|
1028
|
+
buildOAuthRedirectUrl,
|
|
1029
|
+
computePkceS256Challenge,
|
|
1030
|
+
computeS256CodeChallenge,
|
|
1031
|
+
generatePkceVerifier,
|
|
1032
|
+
hashClientSecret,
|
|
1033
|
+
isOAuthRedirectUri,
|
|
1034
|
+
isRedirectUriAllowed,
|
|
1035
|
+
isValidCodeChallenge,
|
|
1036
|
+
isValidCodeVerifier,
|
|
1037
|
+
normalizeQuery,
|
|
1038
|
+
parseScopeList,
|
|
1039
|
+
randomOAuthState,
|
|
1040
|
+
validatePkceParams,
|
|
1041
|
+
verifyClientSecret,
|
|
1042
|
+
verifyPkceS256
|
|
1043
|
+
});
|