@qlover/oauth-wrapper 0.2.1 → 0.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/dist/client.cjs +3 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +3 -1
- package/dist/core.cjs +20 -4
- package/dist/core.d.ts +92 -117
- package/dist/core.js +17 -3
- package/dist/index.cjs +513 -447
- package/dist/index.d.ts +97 -34
- package/dist/index.js +510 -447
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,116 +1,3 @@
|
|
|
1
|
-
// src/server/services/OAuthClientsService.ts
|
|
2
|
-
var OAuthClientsService = class {
|
|
3
|
-
constructor(clientsRepo) {
|
|
4
|
-
this.clientsRepo = clientsRepo;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* List all OAuth clients owned by a user
|
|
8
|
-
* @override
|
|
9
|
-
*/
|
|
10
|
-
async listForOwner(ownerUserId) {
|
|
11
|
-
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Get detailed information about a specific OAuth client
|
|
15
|
-
* @override
|
|
16
|
-
*/
|
|
17
|
-
async getByClientId(ownerUserId, clientId) {
|
|
18
|
-
const client = await this.clientsRepo.findClientById(clientId);
|
|
19
|
-
if (!client) {
|
|
20
|
-
throw new Error("Client not found");
|
|
21
|
-
}
|
|
22
|
-
if (client.owner_user_id !== ownerUserId) {
|
|
23
|
-
throw new Error("Access denied");
|
|
24
|
-
}
|
|
25
|
-
return this.mapToDetail(client);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Create a new OAuth client
|
|
29
|
-
* @override
|
|
30
|
-
*/
|
|
31
|
-
async create(ownerUserId, input) {
|
|
32
|
-
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
33
|
-
return {
|
|
34
|
-
client_id: result.client.client_id,
|
|
35
|
-
client_secret: result.clientSecret,
|
|
36
|
-
confidential: result.client.confidential,
|
|
37
|
-
client_name: result.client.client_name,
|
|
38
|
-
client_uri: result.client.client_uri,
|
|
39
|
-
redirect_uris: result.client.redirect_uris,
|
|
40
|
-
created_at: result.client.created_at
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Update an existing OAuth client
|
|
45
|
-
* @override
|
|
46
|
-
*/
|
|
47
|
-
async update(ownerUserId, clientId, input) {
|
|
48
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
49
|
-
if (!existing) {
|
|
50
|
-
throw new Error("Client not found");
|
|
51
|
-
}
|
|
52
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
53
|
-
throw new Error("Access denied");
|
|
54
|
-
}
|
|
55
|
-
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Rotate the client secret
|
|
59
|
-
* @override
|
|
60
|
-
*/
|
|
61
|
-
async rotateSecret(ownerUserId, clientId) {
|
|
62
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
63
|
-
if (!existing) {
|
|
64
|
-
throw new Error("Client not found");
|
|
65
|
-
}
|
|
66
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
67
|
-
throw new Error("Access denied");
|
|
68
|
-
}
|
|
69
|
-
if (!existing.confidential) {
|
|
70
|
-
throw new Error("Public clients do not have a client_secret");
|
|
71
|
-
}
|
|
72
|
-
const result = await this.clientsRepo.rotateClientSecret(
|
|
73
|
-
ownerUserId,
|
|
74
|
-
clientId
|
|
75
|
-
);
|
|
76
|
-
return {
|
|
77
|
-
client_id: clientId,
|
|
78
|
-
client_secret: result.clientSecret
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Delete an OAuth client
|
|
83
|
-
* @override
|
|
84
|
-
*/
|
|
85
|
-
async delete(ownerUserId, clientId) {
|
|
86
|
-
const existing = await this.clientsRepo.findClientById(clientId);
|
|
87
|
-
if (!existing) {
|
|
88
|
-
throw new Error("Client not found");
|
|
89
|
-
}
|
|
90
|
-
if (existing.owner_user_id !== ownerUserId) {
|
|
91
|
-
throw new Error("Access denied");
|
|
92
|
-
}
|
|
93
|
-
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
94
|
-
}
|
|
95
|
-
mapToDetail(row) {
|
|
96
|
-
return {
|
|
97
|
-
client_id: row.client_id,
|
|
98
|
-
client_name: row.client_name,
|
|
99
|
-
client_uri: row.client_uri,
|
|
100
|
-
logo_uri: row.logo_uri,
|
|
101
|
-
redirect_uris: row.redirect_uris,
|
|
102
|
-
grant_types: row.grant_types,
|
|
103
|
-
scopes: row.scopes,
|
|
104
|
-
confidential: row.confidential,
|
|
105
|
-
created_at: row.created_at,
|
|
106
|
-
updated_at: row.updated_at
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// src/server/services/OAuthTokenService.ts
|
|
112
|
-
import { createHash as createHash2, randomBytes } from "crypto";
|
|
113
|
-
|
|
114
1
|
// src/core/schema/OAuthAuthorizeSchema.ts
|
|
115
2
|
import { z } from "zod";
|
|
116
3
|
function isOAuthRedirectUri(value) {
|
|
@@ -219,6 +106,18 @@ var OAuthAuthorizationCodeRowSchema = z.object({
|
|
|
219
106
|
used: z.boolean(),
|
|
220
107
|
created_at: z.string()
|
|
221
108
|
});
|
|
109
|
+
var signWithPhoneOtpSchema = z.object({
|
|
110
|
+
/** The user's phone number. */
|
|
111
|
+
phone: z.string(),
|
|
112
|
+
/** The otp sent to the user's phone number. */
|
|
113
|
+
token: z.string().optional()
|
|
114
|
+
});
|
|
115
|
+
var signWithEmailOtpSchema = z.object({
|
|
116
|
+
/** The user's email address. */
|
|
117
|
+
email: z.email(),
|
|
118
|
+
/** The otp sent to the user's email address. */
|
|
119
|
+
token: z.string().optional()
|
|
120
|
+
});
|
|
222
121
|
|
|
223
122
|
// src/core/schema/OAuthClientSchema.ts
|
|
224
123
|
import { z as z2 } from "zod";
|
|
@@ -249,12 +148,12 @@ var OAuthTokenResponseSchema = z2.object({
|
|
|
249
148
|
import { z as z3 } from "zod";
|
|
250
149
|
var OAuthUserInfoResponseSchema = z3.object({
|
|
251
150
|
sub: z3.string(),
|
|
252
|
-
email: z3.
|
|
151
|
+
email: z3.email(),
|
|
253
152
|
name: z3.string(),
|
|
254
153
|
roles: z3.array(z3.string()).optional()
|
|
255
154
|
});
|
|
256
155
|
var OAuthUserInfoErrorResponseSchema = z3.object({
|
|
257
|
-
error: z3.
|
|
156
|
+
error: z3.string(),
|
|
258
157
|
error_id: z3.string().optional()
|
|
259
158
|
});
|
|
260
159
|
|
|
@@ -335,16 +234,6 @@ function randomOAuthState() {
|
|
|
335
234
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
336
235
|
}
|
|
337
236
|
|
|
338
|
-
// src/server/utils/OAuthWrapperError.ts
|
|
339
|
-
import { ExecutorError } from "@qlover/fe-corekit";
|
|
340
|
-
var OAuthWrapperError = class extends ExecutorError {
|
|
341
|
-
constructor(id, status, cause) {
|
|
342
|
-
super(id, cause);
|
|
343
|
-
this.status = status;
|
|
344
|
-
}
|
|
345
|
-
name = "OAuthWrapperError";
|
|
346
|
-
};
|
|
347
|
-
|
|
348
237
|
// src/server/utils/pkce.ts
|
|
349
238
|
import { createHash, timingSafeEqual } from "crypto";
|
|
350
239
|
var PKCE_VERIFIER_MIN = 43;
|
|
@@ -378,90 +267,467 @@ function verifyPkceS256(verifier, challenge) {
|
|
|
378
267
|
}
|
|
379
268
|
}
|
|
380
269
|
|
|
381
|
-
// src/server/
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
392
|
-
/**
|
|
393
|
-
* @override
|
|
394
|
-
*/
|
|
395
|
-
async exchangeToken(rawFields) {
|
|
396
|
-
let request;
|
|
397
|
-
try {
|
|
398
|
-
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
399
|
-
} catch {
|
|
400
|
-
throw new OAuthWrapperError(
|
|
401
|
-
OAuthRfcCodes.INVALID_REQUEST,
|
|
402
|
-
400,
|
|
403
|
-
"Malformed token request"
|
|
404
|
-
);
|
|
270
|
+
// src/server/utils/authorizeUtil.ts
|
|
271
|
+
function validatePkceParams(parsed, confidential) {
|
|
272
|
+
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
273
|
+
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
274
|
+
if (!confidential) {
|
|
275
|
+
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
276
|
+
return {
|
|
277
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
278
|
+
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
279
|
+
};
|
|
405
280
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
);
|
|
412
|
-
} catch {
|
|
413
|
-
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
281
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
282
|
+
return {
|
|
283
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
284
|
+
message: "Invalid code_challenge."
|
|
285
|
+
};
|
|
414
286
|
}
|
|
415
|
-
|
|
416
|
-
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (hasChallenge !== hasMethod) {
|
|
290
|
+
return {
|
|
291
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
292
|
+
message: "code_challenge and code_challenge_method must be sent together."
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (hasChallenge) {
|
|
296
|
+
if (parsed.code_challenge_method !== "S256") {
|
|
297
|
+
return {
|
|
298
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
299
|
+
message: "Only code_challenge_method=S256 is supported."
|
|
300
|
+
};
|
|
417
301
|
}
|
|
418
|
-
if (
|
|
419
|
-
return
|
|
302
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
303
|
+
return {
|
|
304
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
305
|
+
message: "Invalid code_challenge."
|
|
306
|
+
};
|
|
420
307
|
}
|
|
421
|
-
return await this.exchangeRefreshToken(request, client.client_id);
|
|
422
308
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (authCode.client_id !== verifiedClientId) {
|
|
433
|
-
throw new OAuthWrapperError(
|
|
434
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
435
|
-
400,
|
|
436
|
-
"client_id mismatch"
|
|
437
|
-
);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function normalizeQuery(raw) {
|
|
312
|
+
const result = {};
|
|
313
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
result[key] = value[0];
|
|
316
|
+
} else {
|
|
317
|
+
result[key] = value;
|
|
438
318
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
319
|
+
}
|
|
320
|
+
if (!result.response_type) {
|
|
321
|
+
result.response_type = "code";
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
function isRedirectUriAllowed(redirectUri, client) {
|
|
326
|
+
return client.redirect_uris.includes(redirectUri);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/server/utils/oauthRedirectUtils.ts
|
|
330
|
+
function buildOAuthRedirectUrl(redirectUri, params) {
|
|
331
|
+
const url = new URL(redirectUri);
|
|
332
|
+
for (const [key, value] of Object.entries(params)) {
|
|
333
|
+
if (value != null && value !== "") {
|
|
334
|
+
url.searchParams.set(key, value);
|
|
445
335
|
}
|
|
446
|
-
this.assertPkceForAuthorizationCode(request, authCode);
|
|
447
|
-
const providerTokens = await this.fetchProviderAccessToken(
|
|
448
|
-
authCode.user_id
|
|
449
|
-
);
|
|
450
|
-
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
451
|
-
authCode.client_id,
|
|
452
|
-
authCode.user_id
|
|
453
|
-
);
|
|
454
|
-
return {
|
|
455
|
-
access_token: providerTokens.access_token,
|
|
456
|
-
token_type: "Bearer",
|
|
457
|
-
expires_in: providerTokens.expires_in,
|
|
458
|
-
refresh_token: middlewareRefresh,
|
|
459
|
-
scope: authCode.scope ?? void 0
|
|
460
|
-
};
|
|
461
336
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
337
|
+
return url.toString();
|
|
338
|
+
}
|
|
339
|
+
function parseScopeList(scope) {
|
|
340
|
+
if (!scope?.trim()) {
|
|
341
|
+
return ["openid", "profile", "email"];
|
|
342
|
+
}
|
|
343
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/server/services/OAuthAbstractProvider.ts
|
|
347
|
+
import { ExecutorError } from "@qlover/fe-corekit";
|
|
348
|
+
import { randomBytes } from "crypto";
|
|
349
|
+
var OAuthAbstractProvider = class {
|
|
350
|
+
authCodeTTLMs = 5 * 60 * 1e3;
|
|
351
|
+
isQuery(query) {
|
|
352
|
+
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
353
|
+
}
|
|
354
|
+
isValidateConsent(value) {
|
|
355
|
+
return OAuthConsentBodySchema.safeParse(value).success;
|
|
356
|
+
}
|
|
357
|
+
generageSessionPayload(userInfo) {
|
|
358
|
+
return {
|
|
359
|
+
userId: String(userInfo.id),
|
|
360
|
+
email: userInfo.email,
|
|
361
|
+
name: userInfo.name ?? userInfo.email,
|
|
362
|
+
providerSessionToken: userInfo.sessionToken
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* @override
|
|
367
|
+
*/
|
|
368
|
+
async logout(userId) {
|
|
369
|
+
const oauthRepo = this.getOAuthRepo();
|
|
370
|
+
await oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
371
|
+
await oauthRepo.upsertUserCredentials(userId, {
|
|
372
|
+
provider_refresh_token: null,
|
|
373
|
+
provider_session_token: null
|
|
374
|
+
});
|
|
375
|
+
await this.clearSession();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* @override
|
|
379
|
+
*/
|
|
380
|
+
async resolveAuthorizePage(rawQuery) {
|
|
381
|
+
const query = normalizeQuery(rawQuery);
|
|
382
|
+
if (!this.isQuery(query)) {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
error: {
|
|
386
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
387
|
+
message: "Missing or invalid authorization request parameters."
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (query.response_type !== "code") {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
error: {
|
|
395
|
+
errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
|
|
396
|
+
message: "Only response_type=code is supported."
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const oauthRepo = this.getOAuthRepo();
|
|
401
|
+
const client = await oauthRepo.findClientById(query.client_id);
|
|
402
|
+
if (!client) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: {
|
|
406
|
+
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
407
|
+
message: "Unknown client_id."
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (!isRedirectUriAllowed(query.redirect_uri, client)) {
|
|
412
|
+
return {
|
|
413
|
+
ok: false,
|
|
414
|
+
error: {
|
|
415
|
+
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
416
|
+
message: "redirect_uri is not registered for this client."
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const requestedScopes = parseScopeList(query.scope);
|
|
421
|
+
const invalidScope = requestedScopes.find(
|
|
422
|
+
(scope) => !client.scopes.includes(scope)
|
|
423
|
+
);
|
|
424
|
+
if (invalidScope) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: {
|
|
428
|
+
errorKey: OAuthRfcCodes.INVALID_SCOPE,
|
|
429
|
+
message: `Scope "${invalidScope}" is not allowed for this client.`
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const pkceError = validatePkceParams(query, client.confidential);
|
|
434
|
+
if (pkceError) {
|
|
435
|
+
return { ok: false, error: pkceError };
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
data: {
|
|
440
|
+
clientId: client.client_id,
|
|
441
|
+
clientName: client.client_name,
|
|
442
|
+
clientUri: client.client_uri ?? null,
|
|
443
|
+
logoUri: client.logo_uri ?? null,
|
|
444
|
+
redirectUri: query.redirect_uri,
|
|
445
|
+
scopes: requestedScopes,
|
|
446
|
+
state: query.state,
|
|
447
|
+
responseType: "code",
|
|
448
|
+
codeChallenge: query.code_challenge,
|
|
449
|
+
codeChallengeMethod: query.code_challenge_method,
|
|
450
|
+
confidential: client.confidential
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* @override
|
|
456
|
+
*/
|
|
457
|
+
async processConsent(requestBody) {
|
|
458
|
+
if (!this.isValidateConsent(requestBody)) {
|
|
459
|
+
throw new ExecutorError(
|
|
460
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
461
|
+
"Invalid consent request body"
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const session = await this.getSession();
|
|
465
|
+
if (!session) {
|
|
466
|
+
throw new ExecutorError(
|
|
467
|
+
OAuthRfcCodes.ACCESS_DENIED,
|
|
468
|
+
"User session expired. Please sign in again."
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const pageResult = await this.resolveAuthorizePage({
|
|
472
|
+
response_type: "code",
|
|
473
|
+
client_id: requestBody.client_id,
|
|
474
|
+
redirect_uri: requestBody.redirect_uri,
|
|
475
|
+
scope: requestBody.scope,
|
|
476
|
+
state: requestBody.state,
|
|
477
|
+
code_challenge: requestBody.code_challenge,
|
|
478
|
+
code_challenge_method: requestBody.code_challenge_method
|
|
479
|
+
});
|
|
480
|
+
if (!pageResult.ok) {
|
|
481
|
+
throw new ExecutorError(
|
|
482
|
+
pageResult.error.errorKey,
|
|
483
|
+
pageResult.error.message
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const { data } = pageResult;
|
|
487
|
+
if (requestBody.action === "deny") {
|
|
488
|
+
return {
|
|
489
|
+
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
490
|
+
error: OAuthRfcCodes.ACCESS_DENIED,
|
|
491
|
+
error_description: "The resource owner denied the request",
|
|
492
|
+
state: data.state
|
|
493
|
+
})
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const code = randomBytes(32).toString("base64url");
|
|
497
|
+
const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
|
|
498
|
+
const oauthRepo = this.getOAuthRepo();
|
|
499
|
+
await oauthRepo.create({
|
|
500
|
+
code,
|
|
501
|
+
client_id: data.clientId,
|
|
502
|
+
user_id: session.userId,
|
|
503
|
+
redirect_uri: data.redirectUri,
|
|
504
|
+
scope: data.scopes.join(" ") || null,
|
|
505
|
+
code_challenge: data.codeChallenge ?? null,
|
|
506
|
+
code_challenge_method: data.codeChallengeMethod ?? null,
|
|
507
|
+
expires_at: expiresAt
|
|
508
|
+
});
|
|
509
|
+
void requestBody.trust;
|
|
510
|
+
return {
|
|
511
|
+
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
512
|
+
code,
|
|
513
|
+
state: data.state
|
|
514
|
+
})
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/server/services/OAuthClientsService.ts
|
|
520
|
+
var OAuthClientsService = class {
|
|
521
|
+
constructor(clientsRepo) {
|
|
522
|
+
this.clientsRepo = clientsRepo;
|
|
523
|
+
}
|
|
524
|
+
clientsRepo;
|
|
525
|
+
/**
|
|
526
|
+
* List all OAuth clients owned by a user
|
|
527
|
+
* @override
|
|
528
|
+
*/
|
|
529
|
+
async listForOwner(ownerUserId) {
|
|
530
|
+
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get detailed information about a specific OAuth client
|
|
534
|
+
* @override
|
|
535
|
+
*/
|
|
536
|
+
async getByClientId(ownerUserId, clientId) {
|
|
537
|
+
const client = await this.clientsRepo.findClientById(clientId);
|
|
538
|
+
if (!client) {
|
|
539
|
+
throw new Error("Client not found");
|
|
540
|
+
}
|
|
541
|
+
if (client.owner_user_id !== ownerUserId) {
|
|
542
|
+
throw new Error("Access denied");
|
|
543
|
+
}
|
|
544
|
+
return this.mapToDetail(client);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Create a new OAuth client
|
|
548
|
+
* @override
|
|
549
|
+
*/
|
|
550
|
+
async create(ownerUserId, input) {
|
|
551
|
+
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
552
|
+
return {
|
|
553
|
+
client_id: result.client.client_id,
|
|
554
|
+
client_secret: result.clientSecret,
|
|
555
|
+
confidential: result.client.confidential,
|
|
556
|
+
client_name: result.client.client_name,
|
|
557
|
+
client_uri: result.client.client_uri,
|
|
558
|
+
redirect_uris: result.client.redirect_uris,
|
|
559
|
+
created_at: result.client.created_at
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Update an existing OAuth client
|
|
564
|
+
* @override
|
|
565
|
+
*/
|
|
566
|
+
async update(ownerUserId, clientId, input) {
|
|
567
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
568
|
+
if (!existing) {
|
|
569
|
+
throw new Error("Client not found");
|
|
570
|
+
}
|
|
571
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
572
|
+
throw new Error("Access denied");
|
|
573
|
+
}
|
|
574
|
+
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Rotate the client secret
|
|
578
|
+
* @override
|
|
579
|
+
*/
|
|
580
|
+
async rotateSecret(ownerUserId, clientId) {
|
|
581
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
582
|
+
if (!existing) {
|
|
583
|
+
throw new Error("Client not found");
|
|
584
|
+
}
|
|
585
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
586
|
+
throw new Error("Access denied");
|
|
587
|
+
}
|
|
588
|
+
if (!existing.confidential) {
|
|
589
|
+
throw new Error("Public clients do not have a client_secret");
|
|
590
|
+
}
|
|
591
|
+
const result = await this.clientsRepo.rotateClientSecret(
|
|
592
|
+
ownerUserId,
|
|
593
|
+
clientId
|
|
594
|
+
);
|
|
595
|
+
return {
|
|
596
|
+
client_id: clientId,
|
|
597
|
+
client_secret: result.clientSecret
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Delete an OAuth client
|
|
602
|
+
* @override
|
|
603
|
+
*/
|
|
604
|
+
async delete(ownerUserId, clientId) {
|
|
605
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
606
|
+
if (!existing) {
|
|
607
|
+
throw new Error("Client not found");
|
|
608
|
+
}
|
|
609
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
610
|
+
throw new Error("Access denied");
|
|
611
|
+
}
|
|
612
|
+
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
613
|
+
}
|
|
614
|
+
mapToDetail(row) {
|
|
615
|
+
return {
|
|
616
|
+
client_id: row.client_id,
|
|
617
|
+
client_name: row.client_name,
|
|
618
|
+
client_uri: row.client_uri,
|
|
619
|
+
logo_uri: row.logo_uri,
|
|
620
|
+
redirect_uris: row.redirect_uris,
|
|
621
|
+
grant_types: row.grant_types,
|
|
622
|
+
scopes: row.scopes,
|
|
623
|
+
confidential: row.confidential,
|
|
624
|
+
created_at: row.created_at,
|
|
625
|
+
updated_at: row.updated_at
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/server/services/OAuthTokenService.ts
|
|
631
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
632
|
+
|
|
633
|
+
// src/server/utils/OAuthWrapperError.ts
|
|
634
|
+
import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
|
|
635
|
+
var OAuthWrapperError = class extends ExecutorError2 {
|
|
636
|
+
constructor(id, status, cause) {
|
|
637
|
+
super(id, cause);
|
|
638
|
+
this.status = status;
|
|
639
|
+
}
|
|
640
|
+
status;
|
|
641
|
+
name = "OAuthWrapperError";
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// src/server/services/OAuthTokenService.ts
|
|
645
|
+
function hashOpaqueToken(token) {
|
|
646
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
647
|
+
}
|
|
648
|
+
var OAuthTokenService = class _OAuthTokenService {
|
|
649
|
+
constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
|
|
650
|
+
this.tokenEncryption = tokenEncryption;
|
|
651
|
+
this.exchangeProviderAccessToken = exchangeProviderAccessToken;
|
|
652
|
+
this.oauthRepo = oauthRepo;
|
|
653
|
+
}
|
|
654
|
+
tokenEncryption;
|
|
655
|
+
exchangeProviderAccessToken;
|
|
656
|
+
oauthRepo;
|
|
657
|
+
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
658
|
+
/**
|
|
659
|
+
* @override
|
|
660
|
+
*/
|
|
661
|
+
async exchangeToken(rawFields) {
|
|
662
|
+
let request;
|
|
663
|
+
try {
|
|
664
|
+
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
665
|
+
} catch {
|
|
666
|
+
throw new OAuthWrapperError(
|
|
667
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
668
|
+
400,
|
|
669
|
+
"Malformed token request"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
let client;
|
|
673
|
+
try {
|
|
674
|
+
client = await this.oauthRepo.verifyClientCredentials(
|
|
675
|
+
request.client_id,
|
|
676
|
+
request.client_secret
|
|
677
|
+
);
|
|
678
|
+
} catch {
|
|
679
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
680
|
+
}
|
|
681
|
+
if (!client.grant_types.includes(request.grant_type)) {
|
|
682
|
+
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
683
|
+
}
|
|
684
|
+
if (request.grant_type === "authorization_code") {
|
|
685
|
+
return await this.exchangeAuthorizationCode(request, client.client_id);
|
|
686
|
+
}
|
|
687
|
+
return await this.exchangeRefreshToken(request, client.client_id);
|
|
688
|
+
}
|
|
689
|
+
async exchangeAuthorizationCode(request, verifiedClientId) {
|
|
690
|
+
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
691
|
+
if (!authCode) {
|
|
692
|
+
throw new OAuthWrapperError(
|
|
693
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
694
|
+
400,
|
|
695
|
+
"Authorization code is invalid, expired, or already used"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
if (authCode.client_id !== verifiedClientId) {
|
|
699
|
+
throw new OAuthWrapperError(
|
|
700
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
701
|
+
400,
|
|
702
|
+
"client_id mismatch"
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
706
|
+
throw new OAuthWrapperError(
|
|
707
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
708
|
+
400,
|
|
709
|
+
"redirect_uri mismatch"
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
this.assertPkceForAuthorizationCode(request, authCode);
|
|
713
|
+
const providerTokens = await this.fetchProviderAccessToken(
|
|
714
|
+
authCode.user_id
|
|
715
|
+
);
|
|
716
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
717
|
+
authCode.client_id,
|
|
718
|
+
authCode.user_id
|
|
719
|
+
);
|
|
720
|
+
return {
|
|
721
|
+
access_token: providerTokens.access_token,
|
|
722
|
+
token_type: "Bearer",
|
|
723
|
+
expires_in: providerTokens.expires_in,
|
|
724
|
+
refresh_token: middlewareRefresh,
|
|
725
|
+
scope: authCode.scope ?? void 0
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
assertPkceForAuthorizationCode(request, authCode) {
|
|
729
|
+
const storedChallenge = authCode.code_challenge?.trim();
|
|
730
|
+
if (storedChallenge) {
|
|
465
731
|
const verifier = request.code_verifier?.trim();
|
|
466
732
|
if (!verifier) {
|
|
467
733
|
throw new OAuthWrapperError(
|
|
@@ -528,7 +794,7 @@ var OAuthTokenService = class _OAuthTokenService {
|
|
|
528
794
|
);
|
|
529
795
|
}
|
|
530
796
|
try {
|
|
531
|
-
const access = await this.
|
|
797
|
+
const access = await this.exchangeProviderAccessToken({
|
|
532
798
|
token: sessionToken
|
|
533
799
|
});
|
|
534
800
|
if (access.refresh_token) {
|
|
@@ -551,7 +817,7 @@ var OAuthTokenService = class _OAuthTokenService {
|
|
|
551
817
|
}
|
|
552
818
|
}
|
|
553
819
|
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
554
|
-
const plain =
|
|
820
|
+
const plain = randomBytes2(32).toString("base64url");
|
|
555
821
|
const expiresAt = new Date(
|
|
556
822
|
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
557
823
|
).toISOString();
|
|
@@ -599,111 +865,41 @@ var OAuthTokenService = class _OAuthTokenService {
|
|
|
599
865
|
};
|
|
600
866
|
|
|
601
867
|
// src/server/services/OAuthWrapperService.ts
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// src/server/utils/authorizeUtil.ts
|
|
606
|
-
function validatePkceParams(parsed, confidential) {
|
|
607
|
-
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
608
|
-
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
609
|
-
if (!confidential) {
|
|
610
|
-
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
611
|
-
return {
|
|
612
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
613
|
-
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
617
|
-
return {
|
|
618
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
619
|
-
message: "Invalid code_challenge."
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
if (hasChallenge !== hasMethod) {
|
|
625
|
-
return {
|
|
626
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
627
|
-
message: "code_challenge and code_challenge_method must be sent together."
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
if (hasChallenge) {
|
|
631
|
-
if (parsed.code_challenge_method !== "S256") {
|
|
632
|
-
return {
|
|
633
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
634
|
-
message: "Only code_challenge_method=S256 is supported."
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
638
|
-
return {
|
|
639
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
640
|
-
message: "Invalid code_challenge."
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
function normalizeQuery(raw) {
|
|
647
|
-
const result = {};
|
|
648
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
649
|
-
if (Array.isArray(value)) {
|
|
650
|
-
result[key] = value[0];
|
|
651
|
-
} else {
|
|
652
|
-
result[key] = value;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
if (!result.response_type) {
|
|
656
|
-
result.response_type = "code";
|
|
657
|
-
}
|
|
658
|
-
return result;
|
|
659
|
-
}
|
|
660
|
-
function isRedirectUriAllowed(redirectUri, client) {
|
|
661
|
-
return client.redirect_uris.includes(redirectUri);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// src/server/utils/oauthRedirectUtils.ts
|
|
665
|
-
function buildOAuthRedirectUrl(redirectUri, params) {
|
|
666
|
-
const url = new URL(redirectUri);
|
|
667
|
-
for (const [key, value] of Object.entries(params)) {
|
|
668
|
-
if (value != null && value !== "") {
|
|
669
|
-
url.searchParams.set(key, value);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
return url.toString();
|
|
673
|
-
}
|
|
674
|
-
function parseScopeList(scope) {
|
|
675
|
-
if (!scope?.trim()) {
|
|
676
|
-
return ["openid", "profile", "email"];
|
|
677
|
-
}
|
|
678
|
-
return scope.trim().split(/\s+/).filter(Boolean);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// src/server/services/OAuthWrapperService.ts
|
|
682
|
-
var AUTH_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
683
|
-
var OAuthWrapperService = class {
|
|
684
|
-
constructor(oauthSession, userAdapter, tokenService, oauthRepo) {
|
|
868
|
+
var OAuthWrapperService = class extends OAuthAbstractProvider {
|
|
869
|
+
constructor(oauthSession, tokenEncryption, oauthRepo) {
|
|
870
|
+
super();
|
|
685
871
|
this.oauthSession = oauthSession;
|
|
686
|
-
this.userAdapter = userAdapter;
|
|
687
|
-
this.tokenService = tokenService;
|
|
688
872
|
this.oauthRepo = oauthRepo;
|
|
873
|
+
this.tokenService = new OAuthTokenService(
|
|
874
|
+
tokenEncryption,
|
|
875
|
+
(params) => this.providerExchangeAccessToken(params),
|
|
876
|
+
oauthRepo
|
|
877
|
+
);
|
|
689
878
|
}
|
|
879
|
+
oauthSession;
|
|
880
|
+
oauthRepo;
|
|
881
|
+
tokenService;
|
|
690
882
|
/**
|
|
691
883
|
* @override
|
|
692
884
|
*/
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
885
|
+
async login(params) {
|
|
886
|
+
const credentials = await this.providerLogin(params);
|
|
887
|
+
const sessionToken = credentials.token;
|
|
888
|
+
if (!sessionToken) {
|
|
889
|
+
throw new Error("User provider login did not return a session token");
|
|
890
|
+
}
|
|
891
|
+
const userInfo = await this.providerGetUserInfo(sessionToken);
|
|
892
|
+
const sessionPayload = this.generageSessionPayload({
|
|
893
|
+
email: params.email,
|
|
894
|
+
...userInfo,
|
|
895
|
+
sessionToken
|
|
896
|
+
});
|
|
897
|
+
this.oauthSession.setSession(sessionPayload);
|
|
898
|
+
const oauthrepo = this.getOAuthRepo();
|
|
899
|
+
await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
|
|
900
|
+
provider_session_token: sessionPayload.providerSessionToken
|
|
901
|
+
});
|
|
902
|
+
return sessionPayload;
|
|
707
903
|
}
|
|
708
904
|
/**
|
|
709
905
|
* @override
|
|
@@ -711,155 +907,23 @@ var OAuthWrapperService = class {
|
|
|
711
907
|
getOAuthRepo() {
|
|
712
908
|
return this.oauthRepo;
|
|
713
909
|
}
|
|
714
|
-
isQuery(query) {
|
|
715
|
-
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
716
|
-
}
|
|
717
|
-
isValidateConsent(value) {
|
|
718
|
-
return OAuthConsentBodySchema.safeParse(value).success;
|
|
719
|
-
}
|
|
720
910
|
/**
|
|
721
911
|
* @override
|
|
722
912
|
*/
|
|
723
|
-
async
|
|
724
|
-
|
|
725
|
-
if (!this.isQuery(query)) {
|
|
726
|
-
return {
|
|
727
|
-
ok: false,
|
|
728
|
-
error: {
|
|
729
|
-
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
730
|
-
message: "Missing or invalid authorization request parameters."
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
if (query.response_type !== "code") {
|
|
735
|
-
return {
|
|
736
|
-
ok: false,
|
|
737
|
-
error: {
|
|
738
|
-
errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
|
|
739
|
-
message: "Only response_type=code is supported."
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
const client = await this.oauthRepo.findClientById(query.client_id);
|
|
744
|
-
if (!client) {
|
|
745
|
-
return {
|
|
746
|
-
ok: false,
|
|
747
|
-
error: {
|
|
748
|
-
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
749
|
-
message: "Unknown client_id."
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
if (!isRedirectUriAllowed(query.redirect_uri, client)) {
|
|
754
|
-
return {
|
|
755
|
-
ok: false,
|
|
756
|
-
error: {
|
|
757
|
-
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
758
|
-
message: "redirect_uri is not registered for this client."
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
const requestedScopes = parseScopeList(query.scope);
|
|
763
|
-
const invalidScope = requestedScopes.find(
|
|
764
|
-
(scope) => !client.scopes.includes(scope)
|
|
765
|
-
);
|
|
766
|
-
if (invalidScope) {
|
|
767
|
-
return {
|
|
768
|
-
ok: false,
|
|
769
|
-
error: {
|
|
770
|
-
errorKey: OAuthRfcCodes.INVALID_SCOPE,
|
|
771
|
-
message: `Scope "${invalidScope}" is not allowed for this client.`
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
const pkceError = validatePkceParams(query, client.confidential);
|
|
776
|
-
if (pkceError) {
|
|
777
|
-
return { ok: false, error: pkceError };
|
|
778
|
-
}
|
|
779
|
-
return {
|
|
780
|
-
ok: true,
|
|
781
|
-
data: {
|
|
782
|
-
clientId: client.client_id,
|
|
783
|
-
clientName: client.client_name,
|
|
784
|
-
clientUri: client.client_uri ?? null,
|
|
785
|
-
logoUri: client.logo_uri ?? null,
|
|
786
|
-
redirectUri: query.redirect_uri,
|
|
787
|
-
scopes: requestedScopes,
|
|
788
|
-
state: query.state,
|
|
789
|
-
responseType: "code",
|
|
790
|
-
codeChallenge: query.code_challenge,
|
|
791
|
-
codeChallengeMethod: query.code_challenge_method,
|
|
792
|
-
confidential: client.confidential
|
|
793
|
-
}
|
|
794
|
-
};
|
|
913
|
+
async exchangeToken(rawFields) {
|
|
914
|
+
return await this.tokenService.exchangeToken(rawFields);
|
|
795
915
|
}
|
|
796
916
|
/**
|
|
797
917
|
* @override
|
|
798
918
|
*/
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
throw new ExecutorError2(
|
|
802
|
-
OAuthRfcCodes.INVALID_REQUEST,
|
|
803
|
-
"Invalid consent request body"
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
const session = await this.oauthSession.getSession();
|
|
807
|
-
if (!session) {
|
|
808
|
-
throw new ExecutorError2(
|
|
809
|
-
OAuthRfcCodes.ACCESS_DENIED,
|
|
810
|
-
"User session expired. Please sign in again."
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
const pageResult = await this.resolveAuthorizePage({
|
|
814
|
-
response_type: "code",
|
|
815
|
-
client_id: requestBody.client_id,
|
|
816
|
-
redirect_uri: requestBody.redirect_uri,
|
|
817
|
-
scope: requestBody.scope,
|
|
818
|
-
state: requestBody.state,
|
|
819
|
-
code_challenge: requestBody.code_challenge,
|
|
820
|
-
code_challenge_method: requestBody.code_challenge_method
|
|
821
|
-
});
|
|
822
|
-
if (!pageResult.ok) {
|
|
823
|
-
throw new ExecutorError2(
|
|
824
|
-
pageResult.error.errorKey,
|
|
825
|
-
pageResult.error.message
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
const { data } = pageResult;
|
|
829
|
-
if (requestBody.action === "deny") {
|
|
830
|
-
return {
|
|
831
|
-
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
832
|
-
error: OAuthRfcCodes.ACCESS_DENIED,
|
|
833
|
-
error_description: "The resource owner denied the request",
|
|
834
|
-
state: data.state
|
|
835
|
-
})
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
const code = randomBytes2(32).toString("base64url");
|
|
839
|
-
const expiresAt = new Date(Date.now() + AUTH_CODE_TTL_MS).toISOString();
|
|
840
|
-
await this.oauthRepo.create({
|
|
841
|
-
code,
|
|
842
|
-
client_id: data.clientId,
|
|
843
|
-
user_id: session.userId,
|
|
844
|
-
redirect_uri: data.redirectUri,
|
|
845
|
-
scope: data.scopes.join(" ") || null,
|
|
846
|
-
code_challenge: data.codeChallenge ?? null,
|
|
847
|
-
code_challenge_method: data.codeChallengeMethod ?? null,
|
|
848
|
-
expires_at: expiresAt
|
|
849
|
-
});
|
|
850
|
-
void requestBody.trust;
|
|
851
|
-
return {
|
|
852
|
-
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
853
|
-
code,
|
|
854
|
-
state: data.state
|
|
855
|
-
})
|
|
856
|
-
};
|
|
919
|
+
getSession() {
|
|
920
|
+
return this.oauthSession.getSession();
|
|
857
921
|
}
|
|
858
922
|
/**
|
|
859
923
|
* @override
|
|
860
924
|
*/
|
|
861
|
-
|
|
862
|
-
return
|
|
925
|
+
clearSession() {
|
|
926
|
+
return this.oauthSession.clearSession();
|
|
863
927
|
}
|
|
864
928
|
/**
|
|
865
929
|
* @override
|
|
@@ -867,9 +931,6 @@ var OAuthWrapperService = class {
|
|
|
867
931
|
async revokeToken(rawFields) {
|
|
868
932
|
return await this.tokenService.revokeToken(rawFields);
|
|
869
933
|
}
|
|
870
|
-
/**
|
|
871
|
-
* @override
|
|
872
|
-
*/
|
|
873
934
|
async logoutUser(userId) {
|
|
874
935
|
await this.oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
875
936
|
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
@@ -881,9 +942,9 @@ var OAuthWrapperService = class {
|
|
|
881
942
|
/**
|
|
882
943
|
* @override
|
|
883
944
|
*/
|
|
884
|
-
async
|
|
945
|
+
async getUserInfoWithAccessToken(accessToken) {
|
|
885
946
|
try {
|
|
886
|
-
const profile = await this.
|
|
947
|
+
const profile = await this.providerGetUserInfoByAccessToken(accessToken);
|
|
887
948
|
return this.toUserInfoResponse(profile);
|
|
888
949
|
} catch {
|
|
889
950
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
@@ -898,8 +959,7 @@ var OAuthWrapperService = class {
|
|
|
898
959
|
if (!email) {
|
|
899
960
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
900
961
|
}
|
|
901
|
-
const
|
|
902
|
-
const name = profile.name?.trim() || nameFromParts || email;
|
|
962
|
+
const name = profile.name?.trim() || email;
|
|
903
963
|
return {
|
|
904
964
|
sub,
|
|
905
965
|
email,
|
|
@@ -934,6 +994,7 @@ async function verifyClientSecret(secret, storedHash) {
|
|
|
934
994
|
return timingSafeEqual2(derived, expected);
|
|
935
995
|
}
|
|
936
996
|
export {
|
|
997
|
+
OAuthAbstractProvider,
|
|
937
998
|
OAuthAuthorizationCodeRowSchema,
|
|
938
999
|
OAuthAuthorizeQuerySchema,
|
|
939
1000
|
OAuthClientCreateResponseSchema,
|
|
@@ -971,6 +1032,8 @@ export {
|
|
|
971
1032
|
normalizeQuery,
|
|
972
1033
|
parseScopeList,
|
|
973
1034
|
randomOAuthState,
|
|
1035
|
+
signWithEmailOtpSchema,
|
|
1036
|
+
signWithPhoneOtpSchema,
|
|
974
1037
|
validatePkceParams,
|
|
975
1038
|
verifyClientSecret,
|
|
976
1039
|
verifyPkceS256
|