@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.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/server/index.ts
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
|
+
OAuthAbstractProvider: () => OAuthAbstractProvider,
|
|
23
24
|
OAuthAuthorizationCodeRowSchema: () => OAuthAuthorizationCodeRowSchema,
|
|
24
25
|
OAuthAuthorizeQuerySchema: () => OAuthAuthorizeQuerySchema,
|
|
25
26
|
OAuthClientCreateResponseSchema: () => OAuthClientCreateResponseSchema,
|
|
@@ -57,125 +58,14 @@ __export(server_exports, {
|
|
|
57
58
|
normalizeQuery: () => normalizeQuery,
|
|
58
59
|
parseScopeList: () => parseScopeList,
|
|
59
60
|
randomOAuthState: () => randomOAuthState,
|
|
61
|
+
signWithEmailOtpSchema: () => signWithEmailOtpSchema,
|
|
62
|
+
signWithPhoneOtpSchema: () => signWithPhoneOtpSchema,
|
|
60
63
|
validatePkceParams: () => validatePkceParams,
|
|
61
64
|
verifyClientSecret: () => verifyClientSecret,
|
|
62
65
|
verifyPkceS256: () => verifyPkceS256
|
|
63
66
|
});
|
|
64
67
|
module.exports = __toCommonJS(server_exports);
|
|
65
68
|
|
|
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
69
|
// src/core/schema/OAuthAuthorizeSchema.ts
|
|
180
70
|
var import_zod = require("zod");
|
|
181
71
|
function isOAuthRedirectUri(value) {
|
|
@@ -284,6 +174,18 @@ var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
|
|
|
284
174
|
used: import_zod.z.boolean(),
|
|
285
175
|
created_at: import_zod.z.string()
|
|
286
176
|
});
|
|
177
|
+
var signWithPhoneOtpSchema = import_zod.z.object({
|
|
178
|
+
/** The user's phone number. */
|
|
179
|
+
phone: import_zod.z.string(),
|
|
180
|
+
/** The otp sent to the user's phone number. */
|
|
181
|
+
token: import_zod.z.string().optional()
|
|
182
|
+
});
|
|
183
|
+
var signWithEmailOtpSchema = import_zod.z.object({
|
|
184
|
+
/** The user's email address. */
|
|
185
|
+
email: import_zod.z.email(),
|
|
186
|
+
/** The otp sent to the user's email address. */
|
|
187
|
+
token: import_zod.z.string().optional()
|
|
188
|
+
});
|
|
287
189
|
|
|
288
190
|
// src/core/schema/OAuthClientSchema.ts
|
|
289
191
|
var import_zod2 = require("zod");
|
|
@@ -314,12 +216,12 @@ var OAuthTokenResponseSchema = import_zod2.z.object({
|
|
|
314
216
|
var import_zod3 = require("zod");
|
|
315
217
|
var OAuthUserInfoResponseSchema = import_zod3.z.object({
|
|
316
218
|
sub: import_zod3.z.string(),
|
|
317
|
-
email: import_zod3.z.
|
|
219
|
+
email: import_zod3.z.email(),
|
|
318
220
|
name: import_zod3.z.string(),
|
|
319
221
|
roles: import_zod3.z.array(import_zod3.z.string()).optional()
|
|
320
222
|
});
|
|
321
223
|
var OAuthUserInfoErrorResponseSchema = import_zod3.z.object({
|
|
322
|
-
error: import_zod3.z.
|
|
224
|
+
error: import_zod3.z.string(),
|
|
323
225
|
error_id: import_zod3.z.string().optional()
|
|
324
226
|
});
|
|
325
227
|
|
|
@@ -400,16 +302,6 @@ function randomOAuthState() {
|
|
|
400
302
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
401
303
|
}
|
|
402
304
|
|
|
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
305
|
// src/server/utils/pkce.ts
|
|
414
306
|
var import_crypto = require("crypto");
|
|
415
307
|
var PKCE_VERIFIER_MIN = 43;
|
|
@@ -443,90 +335,467 @@ function verifyPkceS256(verifier, challenge) {
|
|
|
443
335
|
}
|
|
444
336
|
}
|
|
445
337
|
|
|
446
|
-
// src/server/
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
);
|
|
338
|
+
// src/server/utils/authorizeUtil.ts
|
|
339
|
+
function validatePkceParams(parsed, confidential) {
|
|
340
|
+
const hasChallenge = Boolean(parsed.code_challenge?.trim());
|
|
341
|
+
const hasMethod = Boolean(parsed.code_challenge_method);
|
|
342
|
+
if (!confidential) {
|
|
343
|
+
if (!hasChallenge || parsed.code_challenge_method !== "S256") {
|
|
344
|
+
return {
|
|
345
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
346
|
+
message: "Public clients must send code_challenge and code_challenge_method=S256."
|
|
347
|
+
};
|
|
470
348
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
);
|
|
477
|
-
} catch {
|
|
478
|
-
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
349
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
350
|
+
return {
|
|
351
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
352
|
+
message: "Invalid code_challenge."
|
|
353
|
+
};
|
|
479
354
|
}
|
|
480
|
-
|
|
481
|
-
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (hasChallenge !== hasMethod) {
|
|
358
|
+
return {
|
|
359
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
360
|
+
message: "code_challenge and code_challenge_method must be sent together."
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (hasChallenge) {
|
|
364
|
+
if (parsed.code_challenge_method !== "S256") {
|
|
365
|
+
return {
|
|
366
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
367
|
+
message: "Only code_challenge_method=S256 is supported."
|
|
368
|
+
};
|
|
482
369
|
}
|
|
483
|
-
if (
|
|
484
|
-
return
|
|
370
|
+
if (!isValidCodeChallenge(parsed.code_challenge)) {
|
|
371
|
+
return {
|
|
372
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
373
|
+
message: "Invalid code_challenge."
|
|
374
|
+
};
|
|
485
375
|
}
|
|
486
|
-
return await this.exchangeRefreshToken(request, client.client_id);
|
|
487
376
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (authCode.client_id !== verifiedClientId) {
|
|
498
|
-
throw new OAuthWrapperError(
|
|
499
|
-
OAuthRfcCodes.INVALID_GRANT,
|
|
500
|
-
400,
|
|
501
|
-
"client_id mismatch"
|
|
502
|
-
);
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
function normalizeQuery(raw) {
|
|
380
|
+
const result = {};
|
|
381
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
382
|
+
if (Array.isArray(value)) {
|
|
383
|
+
result[key] = value[0];
|
|
384
|
+
} else {
|
|
385
|
+
result[key] = value;
|
|
503
386
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
387
|
+
}
|
|
388
|
+
if (!result.response_type) {
|
|
389
|
+
result.response_type = "code";
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
function isRedirectUriAllowed(redirectUri, client) {
|
|
394
|
+
return client.redirect_uris.includes(redirectUri);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/server/utils/oauthRedirectUtils.ts
|
|
398
|
+
function buildOAuthRedirectUrl(redirectUri, params) {
|
|
399
|
+
const url = new URL(redirectUri);
|
|
400
|
+
for (const [key, value] of Object.entries(params)) {
|
|
401
|
+
if (value != null && value !== "") {
|
|
402
|
+
url.searchParams.set(key, value);
|
|
510
403
|
}
|
|
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
404
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
405
|
+
return url.toString();
|
|
406
|
+
}
|
|
407
|
+
function parseScopeList(scope) {
|
|
408
|
+
if (!scope?.trim()) {
|
|
409
|
+
return ["openid", "profile", "email"];
|
|
410
|
+
}
|
|
411
|
+
return scope.trim().split(/\s+/).filter(Boolean);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/server/services/OAuthAbstractProvider.ts
|
|
415
|
+
var import_fe_corekit = require("@qlover/fe-corekit");
|
|
416
|
+
var import_crypto2 = require("crypto");
|
|
417
|
+
var OAuthAbstractProvider = class {
|
|
418
|
+
authCodeTTLMs = 5 * 60 * 1e3;
|
|
419
|
+
isQuery(query) {
|
|
420
|
+
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
421
|
+
}
|
|
422
|
+
isValidateConsent(value) {
|
|
423
|
+
return OAuthConsentBodySchema.safeParse(value).success;
|
|
424
|
+
}
|
|
425
|
+
generageSessionPayload(userInfo) {
|
|
426
|
+
return {
|
|
427
|
+
userId: String(userInfo.id),
|
|
428
|
+
email: userInfo.email,
|
|
429
|
+
name: userInfo.name ?? userInfo.email,
|
|
430
|
+
providerSessionToken: userInfo.sessionToken
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* @override
|
|
435
|
+
*/
|
|
436
|
+
async logout(userId) {
|
|
437
|
+
const oauthRepo = this.getOAuthRepo();
|
|
438
|
+
await oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
439
|
+
await oauthRepo.upsertUserCredentials(userId, {
|
|
440
|
+
provider_refresh_token: null,
|
|
441
|
+
provider_session_token: null
|
|
442
|
+
});
|
|
443
|
+
await this.clearSession();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* @override
|
|
447
|
+
*/
|
|
448
|
+
async resolveAuthorizePage(rawQuery) {
|
|
449
|
+
const query = normalizeQuery(rawQuery);
|
|
450
|
+
if (!this.isQuery(query)) {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
error: {
|
|
454
|
+
errorKey: OAuthRfcCodes.INVALID_REQUEST,
|
|
455
|
+
message: "Missing or invalid authorization request parameters."
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (query.response_type !== "code") {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
error: {
|
|
463
|
+
errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
|
|
464
|
+
message: "Only response_type=code is supported."
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const oauthRepo = this.getOAuthRepo();
|
|
469
|
+
const client = await oauthRepo.findClientById(query.client_id);
|
|
470
|
+
if (!client) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
error: {
|
|
474
|
+
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
475
|
+
message: "Unknown client_id."
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (!isRedirectUriAllowed(query.redirect_uri, client)) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
error: {
|
|
483
|
+
errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
|
|
484
|
+
message: "redirect_uri is not registered for this client."
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const requestedScopes = parseScopeList(query.scope);
|
|
489
|
+
const invalidScope = requestedScopes.find(
|
|
490
|
+
(scope) => !client.scopes.includes(scope)
|
|
491
|
+
);
|
|
492
|
+
if (invalidScope) {
|
|
493
|
+
return {
|
|
494
|
+
ok: false,
|
|
495
|
+
error: {
|
|
496
|
+
errorKey: OAuthRfcCodes.INVALID_SCOPE,
|
|
497
|
+
message: `Scope "${invalidScope}" is not allowed for this client.`
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const pkceError = validatePkceParams(query, client.confidential);
|
|
502
|
+
if (pkceError) {
|
|
503
|
+
return { ok: false, error: pkceError };
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
data: {
|
|
508
|
+
clientId: client.client_id,
|
|
509
|
+
clientName: client.client_name,
|
|
510
|
+
clientUri: client.client_uri ?? null,
|
|
511
|
+
logoUri: client.logo_uri ?? null,
|
|
512
|
+
redirectUri: query.redirect_uri,
|
|
513
|
+
scopes: requestedScopes,
|
|
514
|
+
state: query.state,
|
|
515
|
+
responseType: "code",
|
|
516
|
+
codeChallenge: query.code_challenge,
|
|
517
|
+
codeChallengeMethod: query.code_challenge_method,
|
|
518
|
+
confidential: client.confidential
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* @override
|
|
524
|
+
*/
|
|
525
|
+
async processConsent(requestBody) {
|
|
526
|
+
if (!this.isValidateConsent(requestBody)) {
|
|
527
|
+
throw new import_fe_corekit.ExecutorError(
|
|
528
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
529
|
+
"Invalid consent request body"
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const session = await this.getSession();
|
|
533
|
+
if (!session) {
|
|
534
|
+
throw new import_fe_corekit.ExecutorError(
|
|
535
|
+
OAuthRfcCodes.ACCESS_DENIED,
|
|
536
|
+
"User session expired. Please sign in again."
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const pageResult = await this.resolveAuthorizePage({
|
|
540
|
+
response_type: "code",
|
|
541
|
+
client_id: requestBody.client_id,
|
|
542
|
+
redirect_uri: requestBody.redirect_uri,
|
|
543
|
+
scope: requestBody.scope,
|
|
544
|
+
state: requestBody.state,
|
|
545
|
+
code_challenge: requestBody.code_challenge,
|
|
546
|
+
code_challenge_method: requestBody.code_challenge_method
|
|
547
|
+
});
|
|
548
|
+
if (!pageResult.ok) {
|
|
549
|
+
throw new import_fe_corekit.ExecutorError(
|
|
550
|
+
pageResult.error.errorKey,
|
|
551
|
+
pageResult.error.message
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const { data } = pageResult;
|
|
555
|
+
if (requestBody.action === "deny") {
|
|
556
|
+
return {
|
|
557
|
+
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
558
|
+
error: OAuthRfcCodes.ACCESS_DENIED,
|
|
559
|
+
error_description: "The resource owner denied the request",
|
|
560
|
+
state: data.state
|
|
561
|
+
})
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const code = (0, import_crypto2.randomBytes)(32).toString("base64url");
|
|
565
|
+
const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
|
|
566
|
+
const oauthRepo = this.getOAuthRepo();
|
|
567
|
+
await oauthRepo.create({
|
|
568
|
+
code,
|
|
569
|
+
client_id: data.clientId,
|
|
570
|
+
user_id: session.userId,
|
|
571
|
+
redirect_uri: data.redirectUri,
|
|
572
|
+
scope: data.scopes.join(" ") || null,
|
|
573
|
+
code_challenge: data.codeChallenge ?? null,
|
|
574
|
+
code_challenge_method: data.codeChallengeMethod ?? null,
|
|
575
|
+
expires_at: expiresAt
|
|
576
|
+
});
|
|
577
|
+
void requestBody.trust;
|
|
578
|
+
return {
|
|
579
|
+
redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
|
|
580
|
+
code,
|
|
581
|
+
state: data.state
|
|
582
|
+
})
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/server/services/OAuthClientsService.ts
|
|
588
|
+
var OAuthClientsService = class {
|
|
589
|
+
constructor(clientsRepo) {
|
|
590
|
+
this.clientsRepo = clientsRepo;
|
|
591
|
+
}
|
|
592
|
+
clientsRepo;
|
|
593
|
+
/**
|
|
594
|
+
* List all OAuth clients owned by a user
|
|
595
|
+
* @override
|
|
596
|
+
*/
|
|
597
|
+
async listForOwner(ownerUserId) {
|
|
598
|
+
return this.clientsRepo.listClientByOwner(ownerUserId);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get detailed information about a specific OAuth client
|
|
602
|
+
* @override
|
|
603
|
+
*/
|
|
604
|
+
async getByClientId(ownerUserId, clientId) {
|
|
605
|
+
const client = await this.clientsRepo.findClientById(clientId);
|
|
606
|
+
if (!client) {
|
|
607
|
+
throw new Error("Client not found");
|
|
608
|
+
}
|
|
609
|
+
if (client.owner_user_id !== ownerUserId) {
|
|
610
|
+
throw new Error("Access denied");
|
|
611
|
+
}
|
|
612
|
+
return this.mapToDetail(client);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Create a new OAuth client
|
|
616
|
+
* @override
|
|
617
|
+
*/
|
|
618
|
+
async create(ownerUserId, input) {
|
|
619
|
+
const result = await this.clientsRepo.createClient(ownerUserId, input);
|
|
620
|
+
return {
|
|
621
|
+
client_id: result.client.client_id,
|
|
622
|
+
client_secret: result.clientSecret,
|
|
623
|
+
confidential: result.client.confidential,
|
|
624
|
+
client_name: result.client.client_name,
|
|
625
|
+
client_uri: result.client.client_uri,
|
|
626
|
+
redirect_uris: result.client.redirect_uris,
|
|
627
|
+
created_at: result.client.created_at
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Update an existing OAuth client
|
|
632
|
+
* @override
|
|
633
|
+
*/
|
|
634
|
+
async update(ownerUserId, clientId, input) {
|
|
635
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
636
|
+
if (!existing) {
|
|
637
|
+
throw new Error("Client not found");
|
|
638
|
+
}
|
|
639
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
640
|
+
throw new Error("Access denied");
|
|
641
|
+
}
|
|
642
|
+
return this.clientsRepo.updateClient(ownerUserId, clientId, input);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Rotate the client secret
|
|
646
|
+
* @override
|
|
647
|
+
*/
|
|
648
|
+
async rotateSecret(ownerUserId, clientId) {
|
|
649
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
650
|
+
if (!existing) {
|
|
651
|
+
throw new Error("Client not found");
|
|
652
|
+
}
|
|
653
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
654
|
+
throw new Error("Access denied");
|
|
655
|
+
}
|
|
656
|
+
if (!existing.confidential) {
|
|
657
|
+
throw new Error("Public clients do not have a client_secret");
|
|
658
|
+
}
|
|
659
|
+
const result = await this.clientsRepo.rotateClientSecret(
|
|
660
|
+
ownerUserId,
|
|
661
|
+
clientId
|
|
662
|
+
);
|
|
663
|
+
return {
|
|
664
|
+
client_id: clientId,
|
|
665
|
+
client_secret: result.clientSecret
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Delete an OAuth client
|
|
670
|
+
* @override
|
|
671
|
+
*/
|
|
672
|
+
async delete(ownerUserId, clientId) {
|
|
673
|
+
const existing = await this.clientsRepo.findClientById(clientId);
|
|
674
|
+
if (!existing) {
|
|
675
|
+
throw new Error("Client not found");
|
|
676
|
+
}
|
|
677
|
+
if (existing.owner_user_id !== ownerUserId) {
|
|
678
|
+
throw new Error("Access denied");
|
|
679
|
+
}
|
|
680
|
+
await this.clientsRepo.deleteClient(ownerUserId, clientId);
|
|
681
|
+
}
|
|
682
|
+
mapToDetail(row) {
|
|
683
|
+
return {
|
|
684
|
+
client_id: row.client_id,
|
|
685
|
+
client_name: row.client_name,
|
|
686
|
+
client_uri: row.client_uri,
|
|
687
|
+
logo_uri: row.logo_uri,
|
|
688
|
+
redirect_uris: row.redirect_uris,
|
|
689
|
+
grant_types: row.grant_types,
|
|
690
|
+
scopes: row.scopes,
|
|
691
|
+
confidential: row.confidential,
|
|
692
|
+
created_at: row.created_at,
|
|
693
|
+
updated_at: row.updated_at
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/server/services/OAuthTokenService.ts
|
|
699
|
+
var import_crypto3 = require("crypto");
|
|
700
|
+
|
|
701
|
+
// src/server/utils/OAuthWrapperError.ts
|
|
702
|
+
var import_fe_corekit2 = require("@qlover/fe-corekit");
|
|
703
|
+
var OAuthWrapperError = class extends import_fe_corekit2.ExecutorError {
|
|
704
|
+
constructor(id, status, cause) {
|
|
705
|
+
super(id, cause);
|
|
706
|
+
this.status = status;
|
|
707
|
+
}
|
|
708
|
+
status;
|
|
709
|
+
name = "OAuthWrapperError";
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// src/server/services/OAuthTokenService.ts
|
|
713
|
+
function hashOpaqueToken(token) {
|
|
714
|
+
return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
|
|
715
|
+
}
|
|
716
|
+
var OAuthTokenService = class _OAuthTokenService {
|
|
717
|
+
constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
|
|
718
|
+
this.tokenEncryption = tokenEncryption;
|
|
719
|
+
this.exchangeProviderAccessToken = exchangeProviderAccessToken;
|
|
720
|
+
this.oauthRepo = oauthRepo;
|
|
721
|
+
}
|
|
722
|
+
tokenEncryption;
|
|
723
|
+
exchangeProviderAccessToken;
|
|
724
|
+
oauthRepo;
|
|
725
|
+
static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
726
|
+
/**
|
|
727
|
+
* @override
|
|
728
|
+
*/
|
|
729
|
+
async exchangeToken(rawFields) {
|
|
730
|
+
let request;
|
|
731
|
+
try {
|
|
732
|
+
request = OAuthTokenRequestSchema.parse(rawFields);
|
|
733
|
+
} catch {
|
|
734
|
+
throw new OAuthWrapperError(
|
|
735
|
+
OAuthRfcCodes.INVALID_REQUEST,
|
|
736
|
+
400,
|
|
737
|
+
"Malformed token request"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
let client;
|
|
741
|
+
try {
|
|
742
|
+
client = await this.oauthRepo.verifyClientCredentials(
|
|
743
|
+
request.client_id,
|
|
744
|
+
request.client_secret
|
|
745
|
+
);
|
|
746
|
+
} catch {
|
|
747
|
+
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
|
|
748
|
+
}
|
|
749
|
+
if (!client.grant_types.includes(request.grant_type)) {
|
|
750
|
+
throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
|
|
751
|
+
}
|
|
752
|
+
if (request.grant_type === "authorization_code") {
|
|
753
|
+
return await this.exchangeAuthorizationCode(request, client.client_id);
|
|
754
|
+
}
|
|
755
|
+
return await this.exchangeRefreshToken(request, client.client_id);
|
|
756
|
+
}
|
|
757
|
+
async exchangeAuthorizationCode(request, verifiedClientId) {
|
|
758
|
+
const authCode = await this.oauthRepo.consumeCode(request.code);
|
|
759
|
+
if (!authCode) {
|
|
760
|
+
throw new OAuthWrapperError(
|
|
761
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
762
|
+
400,
|
|
763
|
+
"Authorization code is invalid, expired, or already used"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (authCode.client_id !== verifiedClientId) {
|
|
767
|
+
throw new OAuthWrapperError(
|
|
768
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
769
|
+
400,
|
|
770
|
+
"client_id mismatch"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (authCode.redirect_uri !== request.redirect_uri) {
|
|
774
|
+
throw new OAuthWrapperError(
|
|
775
|
+
OAuthRfcCodes.INVALID_GRANT,
|
|
776
|
+
400,
|
|
777
|
+
"redirect_uri mismatch"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
this.assertPkceForAuthorizationCode(request, authCode);
|
|
781
|
+
const providerTokens = await this.fetchProviderAccessToken(
|
|
782
|
+
authCode.user_id
|
|
783
|
+
);
|
|
784
|
+
const middlewareRefresh = await this.issueMiddlewareRefreshToken(
|
|
785
|
+
authCode.client_id,
|
|
786
|
+
authCode.user_id
|
|
787
|
+
);
|
|
788
|
+
return {
|
|
789
|
+
access_token: providerTokens.access_token,
|
|
790
|
+
token_type: "Bearer",
|
|
791
|
+
expires_in: providerTokens.expires_in,
|
|
792
|
+
refresh_token: middlewareRefresh,
|
|
793
|
+
scope: authCode.scope ?? void 0
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
assertPkceForAuthorizationCode(request, authCode) {
|
|
797
|
+
const storedChallenge = authCode.code_challenge?.trim();
|
|
798
|
+
if (storedChallenge) {
|
|
530
799
|
const verifier = request.code_verifier?.trim();
|
|
531
800
|
if (!verifier) {
|
|
532
801
|
throw new OAuthWrapperError(
|
|
@@ -593,7 +862,7 @@ var OAuthTokenService = class _OAuthTokenService {
|
|
|
593
862
|
);
|
|
594
863
|
}
|
|
595
864
|
try {
|
|
596
|
-
const access = await this.
|
|
865
|
+
const access = await this.exchangeProviderAccessToken({
|
|
597
866
|
token: sessionToken
|
|
598
867
|
});
|
|
599
868
|
if (access.refresh_token) {
|
|
@@ -616,7 +885,7 @@ var OAuthTokenService = class _OAuthTokenService {
|
|
|
616
885
|
}
|
|
617
886
|
}
|
|
618
887
|
async issueMiddlewareRefreshToken(clientId, userId) {
|
|
619
|
-
const plain = (0,
|
|
888
|
+
const plain = (0, import_crypto3.randomBytes)(32).toString("base64url");
|
|
620
889
|
const expiresAt = new Date(
|
|
621
890
|
Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
|
|
622
891
|
).toISOString();
|
|
@@ -664,111 +933,41 @@ var OAuthTokenService = class _OAuthTokenService {
|
|
|
664
933
|
};
|
|
665
934
|
|
|
666
935
|
// src/server/services/OAuthWrapperService.ts
|
|
667
|
-
var
|
|
668
|
-
|
|
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) {
|
|
936
|
+
var OAuthWrapperService = class extends OAuthAbstractProvider {
|
|
937
|
+
constructor(oauthSession, tokenEncryption, oauthRepo) {
|
|
938
|
+
super();
|
|
750
939
|
this.oauthSession = oauthSession;
|
|
751
|
-
this.userAdapter = userAdapter;
|
|
752
|
-
this.tokenService = tokenService;
|
|
753
940
|
this.oauthRepo = oauthRepo;
|
|
941
|
+
this.tokenService = new OAuthTokenService(
|
|
942
|
+
tokenEncryption,
|
|
943
|
+
(params) => this.providerExchangeAccessToken(params),
|
|
944
|
+
oauthRepo
|
|
945
|
+
);
|
|
754
946
|
}
|
|
947
|
+
oauthSession;
|
|
948
|
+
oauthRepo;
|
|
949
|
+
tokenService;
|
|
755
950
|
/**
|
|
756
951
|
* @override
|
|
757
952
|
*/
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
953
|
+
async login(params) {
|
|
954
|
+
const credentials = await this.providerLogin(params);
|
|
955
|
+
const sessionToken = credentials.token;
|
|
956
|
+
if (!sessionToken) {
|
|
957
|
+
throw new Error("User provider login did not return a session token");
|
|
958
|
+
}
|
|
959
|
+
const userInfo = await this.providerGetUserInfo(sessionToken);
|
|
960
|
+
const sessionPayload = this.generageSessionPayload({
|
|
961
|
+
email: params.email,
|
|
962
|
+
...userInfo,
|
|
963
|
+
sessionToken
|
|
964
|
+
});
|
|
965
|
+
this.oauthSession.setSession(sessionPayload);
|
|
966
|
+
const oauthrepo = this.getOAuthRepo();
|
|
967
|
+
await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
|
|
968
|
+
provider_session_token: sessionPayload.providerSessionToken
|
|
969
|
+
});
|
|
970
|
+
return sessionPayload;
|
|
772
971
|
}
|
|
773
972
|
/**
|
|
774
973
|
* @override
|
|
@@ -776,155 +975,23 @@ var OAuthWrapperService = class {
|
|
|
776
975
|
getOAuthRepo() {
|
|
777
976
|
return this.oauthRepo;
|
|
778
977
|
}
|
|
779
|
-
isQuery(query) {
|
|
780
|
-
return OAuthAuthorizeQuerySchema.safeParse(query).success;
|
|
781
|
-
}
|
|
782
|
-
isValidateConsent(value) {
|
|
783
|
-
return OAuthConsentBodySchema.safeParse(value).success;
|
|
784
|
-
}
|
|
785
978
|
/**
|
|
786
979
|
* @override
|
|
787
980
|
*/
|
|
788
|
-
async
|
|
789
|
-
|
|
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
|
-
};
|
|
981
|
+
async exchangeToken(rawFields) {
|
|
982
|
+
return await this.tokenService.exchangeToken(rawFields);
|
|
860
983
|
}
|
|
861
984
|
/**
|
|
862
985
|
* @override
|
|
863
986
|
*/
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
};
|
|
987
|
+
getSession() {
|
|
988
|
+
return this.oauthSession.getSession();
|
|
922
989
|
}
|
|
923
990
|
/**
|
|
924
991
|
* @override
|
|
925
992
|
*/
|
|
926
|
-
|
|
927
|
-
return
|
|
993
|
+
clearSession() {
|
|
994
|
+
return this.oauthSession.clearSession();
|
|
928
995
|
}
|
|
929
996
|
/**
|
|
930
997
|
* @override
|
|
@@ -932,9 +999,6 @@ var OAuthWrapperService = class {
|
|
|
932
999
|
async revokeToken(rawFields) {
|
|
933
1000
|
return await this.tokenService.revokeToken(rawFields);
|
|
934
1001
|
}
|
|
935
|
-
/**
|
|
936
|
-
* @override
|
|
937
|
-
*/
|
|
938
1002
|
async logoutUser(userId) {
|
|
939
1003
|
await this.oauthRepo.revokeRefreshTokensByUserId(userId);
|
|
940
1004
|
await this.oauthRepo.upsertUserCredentials(userId, {
|
|
@@ -946,9 +1010,9 @@ var OAuthWrapperService = class {
|
|
|
946
1010
|
/**
|
|
947
1011
|
* @override
|
|
948
1012
|
*/
|
|
949
|
-
async
|
|
1013
|
+
async getUserInfoWithAccessToken(accessToken) {
|
|
950
1014
|
try {
|
|
951
|
-
const profile = await this.
|
|
1015
|
+
const profile = await this.providerGetUserInfoByAccessToken(accessToken);
|
|
952
1016
|
return this.toUserInfoResponse(profile);
|
|
953
1017
|
} catch {
|
|
954
1018
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
@@ -963,8 +1027,7 @@ var OAuthWrapperService = class {
|
|
|
963
1027
|
if (!email) {
|
|
964
1028
|
throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
|
|
965
1029
|
}
|
|
966
|
-
const
|
|
967
|
-
const name = profile.name?.trim() || nameFromParts || email;
|
|
1030
|
+
const name = profile.name?.trim() || email;
|
|
968
1031
|
return {
|
|
969
1032
|
sub,
|
|
970
1033
|
email,
|
|
@@ -1000,6 +1063,7 @@ async function verifyClientSecret(secret, storedHash) {
|
|
|
1000
1063
|
}
|
|
1001
1064
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1002
1065
|
0 && (module.exports = {
|
|
1066
|
+
OAuthAbstractProvider,
|
|
1003
1067
|
OAuthAuthorizationCodeRowSchema,
|
|
1004
1068
|
OAuthAuthorizeQuerySchema,
|
|
1005
1069
|
OAuthClientCreateResponseSchema,
|
|
@@ -1037,6 +1101,8 @@ async function verifyClientSecret(secret, storedHash) {
|
|
|
1037
1101
|
normalizeQuery,
|
|
1038
1102
|
parseScopeList,
|
|
1039
1103
|
randomOAuthState,
|
|
1104
|
+
signWithEmailOtpSchema,
|
|
1105
|
+
signWithPhoneOtpSchema,
|
|
1040
1106
|
validatePkceParams,
|
|
1041
1107
|
verifyClientSecret,
|
|
1042
1108
|
verifyPkceS256
|