@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/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.string().email(),
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.literal("invalid_token"),
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/services/OAuthTokenService.ts
382
- function hashOpaqueToken(token) {
383
- return createHash2("sha256").update(token).digest("hex");
384
- }
385
- var OAuthTokenService = class _OAuthTokenService {
386
- constructor(tokenEncryption, userAdapter, oauthRepo) {
387
- this.tokenEncryption = tokenEncryption;
388
- this.userAdapter = userAdapter;
389
- this.oauthRepo = oauthRepo;
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
- let client;
407
- try {
408
- client = await this.oauthRepo.verifyClientCredentials(
409
- request.client_id,
410
- request.client_secret
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
- if (!client.grant_types.includes(request.grant_type)) {
416
- throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
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 (request.grant_type === "authorization_code") {
419
- return await this.exchangeAuthorizationCode(request, client.client_id);
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
- async exchangeAuthorizationCode(request, verifiedClientId) {
424
- const authCode = await this.oauthRepo.consumeCode(request.code);
425
- if (!authCode) {
426
- throw new OAuthWrapperError(
427
- OAuthRfcCodes.INVALID_GRANT,
428
- 400,
429
- "Authorization code is invalid, expired, or already used"
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
- if (authCode.redirect_uri !== request.redirect_uri) {
440
- throw new OAuthWrapperError(
441
- OAuthRfcCodes.INVALID_GRANT,
442
- 400,
443
- "redirect_uri mismatch"
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
- assertPkceForAuthorizationCode(request, authCode) {
463
- const storedChallenge = authCode.code_challenge?.trim();
464
- if (storedChallenge) {
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.userAdapter.exchangeAccessToken({
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 = randomBytes(32).toString("base64url");
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
- import { randomBytes as randomBytes2 } from "crypto";
603
- import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
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
- getOAuthSession() {
694
- return this.oauthSession;
695
- }
696
- /**
697
- * @override
698
- */
699
- getOAuthAdapter() {
700
- return this.userAdapter;
701
- }
702
- /**
703
- * @override
704
- */
705
- getOAuthTokenService() {
706
- return this.tokenService;
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 resolveAuthorizePage(rawQuery) {
724
- const query = normalizeQuery(rawQuery);
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
- async processConsent(requestBody) {
800
- if (!this.isValidateConsent(requestBody)) {
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
- async exchangeToken(rawFields) {
862
- return await this.tokenService.exchangeToken(rawFields);
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 getUserInfo(accessToken) {
945
+ async getUserInfoWithAccessToken(accessToken) {
885
946
  try {
886
- const profile = await this.userAdapter.getUserInfoByAccessToken(accessToken);
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 nameFromParts = [profile.first_name, profile.last_name].filter(Boolean).join(" ");
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