@qlover/oauth-wrapper 0.4.0 → 0.6.2

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,3 +1,117 @@
1
+ // src/server/services/OAuthClientsService.ts
2
+ var OAuthClientsService = class {
3
+ constructor(clientsRepo) {
4
+ this.clientsRepo = clientsRepo;
5
+ }
6
+ clientsRepo;
7
+ /**
8
+ * List all OAuth clients owned by a user
9
+ * @override
10
+ */
11
+ async listForOwner(ownerUserId) {
12
+ return this.clientsRepo.listClientByOwner(ownerUserId);
13
+ }
14
+ /**
15
+ * Get detailed information about a specific OAuth client
16
+ * @override
17
+ */
18
+ async getByClientId(ownerUserId, clientId) {
19
+ const client = await this.clientsRepo.findClientById(clientId);
20
+ if (!client) {
21
+ throw new Error("Client not found");
22
+ }
23
+ if (client.owner_user_id !== ownerUserId) {
24
+ throw new Error("Access denied");
25
+ }
26
+ return this.mapToDetail(client);
27
+ }
28
+ /**
29
+ * Create a new OAuth client
30
+ * @override
31
+ */
32
+ async create(ownerUserId, input) {
33
+ const result = await this.clientsRepo.createClient(ownerUserId, input);
34
+ return {
35
+ client_id: result.client.client_id,
36
+ client_secret: result.clientSecret,
37
+ confidential: result.client.confidential,
38
+ client_name: result.client.client_name,
39
+ client_uri: result.client.client_uri,
40
+ redirect_uris: result.client.redirect_uris,
41
+ created_at: result.client.created_at
42
+ };
43
+ }
44
+ /**
45
+ * Update an existing OAuth client
46
+ * @override
47
+ */
48
+ async update(ownerUserId, clientId, input) {
49
+ const existing = await this.clientsRepo.findClientById(clientId);
50
+ if (!existing) {
51
+ throw new Error("Client not found");
52
+ }
53
+ if (existing.owner_user_id !== ownerUserId) {
54
+ throw new Error("Access denied");
55
+ }
56
+ return this.clientsRepo.updateClient(ownerUserId, clientId, input);
57
+ }
58
+ /**
59
+ * Rotate the client secret
60
+ * @override
61
+ */
62
+ async rotateSecret(ownerUserId, clientId) {
63
+ const existing = await this.clientsRepo.findClientById(clientId);
64
+ if (!existing) {
65
+ throw new Error("Client not found");
66
+ }
67
+ if (existing.owner_user_id !== ownerUserId) {
68
+ throw new Error("Access denied");
69
+ }
70
+ if (!existing.confidential) {
71
+ throw new Error("Public clients do not have a client_secret");
72
+ }
73
+ const result = await this.clientsRepo.rotateClientSecret(
74
+ ownerUserId,
75
+ clientId
76
+ );
77
+ return {
78
+ client_id: clientId,
79
+ client_secret: result.clientSecret
80
+ };
81
+ }
82
+ /**
83
+ * Delete an OAuth client
84
+ * @override
85
+ */
86
+ async delete(ownerUserId, clientId) {
87
+ const existing = await this.clientsRepo.findClientById(clientId);
88
+ if (!existing) {
89
+ throw new Error("Client not found");
90
+ }
91
+ if (existing.owner_user_id !== ownerUserId) {
92
+ throw new Error("Access denied");
93
+ }
94
+ await this.clientsRepo.deleteClient(ownerUserId, clientId);
95
+ }
96
+ mapToDetail(row) {
97
+ return {
98
+ client_id: row.client_id,
99
+ client_name: row.client_name,
100
+ client_uri: row.client_uri,
101
+ logo_uri: row.logo_uri,
102
+ redirect_uris: row.redirect_uris,
103
+ grant_types: row.grant_types,
104
+ scopes: row.scopes,
105
+ confidential: row.confidential,
106
+ created_at: row.created_at,
107
+ updated_at: row.updated_at
108
+ };
109
+ }
110
+ };
111
+
112
+ // src/server/services/OAuthTokenService.ts
113
+ import { createHash as createHash2, randomBytes } from "crypto";
114
+
1
115
  // src/core/schema/OAuthAuthorizeSchema.ts
2
116
  import { z } from "zod";
3
117
  function isOAuthRedirectUri(value) {
@@ -138,7 +252,7 @@ var OAuthRefreshTokenSchema = z2.object({
138
252
  });
139
253
  var OAuthTokenResponseSchema = z2.object({
140
254
  access_token: z2.string(),
141
- token_type: z2.literal("Bearer"),
255
+ token_type: z2.string(),
142
256
  expires_in: z2.number(),
143
257
  refresh_token: z2.string().optional(),
144
258
  scope: z2.string().optional()
@@ -234,6 +348,17 @@ function randomOAuthState() {
234
348
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
235
349
  }
236
350
 
351
+ // src/server/utils/OAuthWrapperError.ts
352
+ import { ExecutorError } from "@qlover/fe-corekit";
353
+ var OAuthWrapperError = class extends ExecutorError {
354
+ constructor(id, status, cause) {
355
+ super(id, cause);
356
+ this.status = status;
357
+ }
358
+ status;
359
+ name = "OAuthWrapperError";
360
+ };
361
+
237
362
  // src/server/utils/pkce.ts
238
363
  import { createHash, timingSafeEqual } from "crypto";
239
364
  var PKCE_VERIFIER_MIN = 43;
@@ -255,391 +380,17 @@ function isValidCodeChallenge(challenge) {
255
380
  function computeS256CodeChallenge(verifier) {
256
381
  return createHash("sha256").update(verifier).digest("base64url");
257
382
  }
258
- function verifyPkceS256(verifier, challenge) {
259
- if (!isValidCodeVerifier(verifier) || !isValidCodeChallenge(challenge)) {
260
- return false;
261
- }
262
- const computed = computeS256CodeChallenge(verifier);
263
- try {
264
- return timingSafeEqual(Buffer.from(computed), Buffer.from(challenge));
265
- } catch {
266
- return false;
267
- }
268
- }
269
-
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
- };
280
- }
281
- if (!isValidCodeChallenge(parsed.code_challenge)) {
282
- return {
283
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
284
- message: "Invalid code_challenge."
285
- };
286
- }
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
- };
301
- }
302
- if (!isValidCodeChallenge(parsed.code_challenge)) {
303
- return {
304
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
305
- message: "Invalid code_challenge."
306
- };
307
- }
308
- }
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;
318
- }
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);
335
- }
336
- }
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
- };
383
+ function verifyPkceS256(verifier, challenge) {
384
+ if (!isValidCodeVerifier(verifier) || !isValidCodeChallenge(challenge)) {
385
+ return false;
627
386
  }
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;
387
+ const computed = computeS256CodeChallenge(verifier);
388
+ try {
389
+ return timingSafeEqual(Buffer.from(computed), Buffer.from(challenge));
390
+ } catch {
391
+ return false;
639
392
  }
640
- status;
641
- name = "OAuthWrapperError";
642
- };
393
+ }
643
394
 
644
395
  // src/server/services/OAuthTokenService.ts
645
396
  function hashOpaqueToken(token) {
@@ -795,7 +546,9 @@ var OAuthTokenService = class _OAuthTokenService {
795
546
  }
796
547
  try {
797
548
  const access = await this.exchangeProviderAccessToken({
798
- token: sessionToken
549
+ providerRefreshToken: sessionToken,
550
+ // TODO:
551
+ userId: ""
799
552
  });
800
553
  if (access.refresh_token) {
801
554
  await this.oauthRepo.upsertUserCredentials(userId, {
@@ -805,6 +558,7 @@ var OAuthTokenService = class _OAuthTokenService {
805
558
  });
806
559
  }
807
560
  return {
561
+ ...access,
808
562
  access_token: access.access_token,
809
563
  expires_in: access.expires_in
810
564
  };
@@ -817,7 +571,7 @@ var OAuthTokenService = class _OAuthTokenService {
817
571
  }
818
572
  }
819
573
  async issueMiddlewareRefreshToken(clientId, userId) {
820
- const plain = randomBytes2(32).toString("base64url");
574
+ const plain = randomBytes(32).toString("base64url");
821
575
  const expiresAt = new Date(
822
576
  Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
823
577
  ).toISOString();
@@ -865,9 +619,88 @@ var OAuthTokenService = class _OAuthTokenService {
865
619
  };
866
620
 
867
621
  // src/server/services/OAuthWrapperService.ts
868
- var OAuthWrapperService = class extends OAuthAbstractProvider {
622
+ import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
623
+
624
+ // src/server/utils/authorizeUtil.ts
625
+ function validatePkceParams(parsed, confidential) {
626
+ const hasChallenge = Boolean(parsed.code_challenge?.trim());
627
+ const hasMethod = Boolean(parsed.code_challenge_method);
628
+ if (!confidential) {
629
+ if (!hasChallenge || parsed.code_challenge_method !== "S256") {
630
+ return {
631
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
632
+ message: "Public clients must send code_challenge and code_challenge_method=S256."
633
+ };
634
+ }
635
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
636
+ return {
637
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
638
+ message: "Invalid code_challenge."
639
+ };
640
+ }
641
+ return null;
642
+ }
643
+ if (hasChallenge !== hasMethod) {
644
+ return {
645
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
646
+ message: "code_challenge and code_challenge_method must be sent together."
647
+ };
648
+ }
649
+ if (hasChallenge) {
650
+ if (parsed.code_challenge_method !== "S256") {
651
+ return {
652
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
653
+ message: "Only code_challenge_method=S256 is supported."
654
+ };
655
+ }
656
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
657
+ return {
658
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
659
+ message: "Invalid code_challenge."
660
+ };
661
+ }
662
+ }
663
+ return null;
664
+ }
665
+ function normalizeQuery(raw) {
666
+ const result = {};
667
+ for (const [key, value] of Object.entries(raw)) {
668
+ if (Array.isArray(value)) {
669
+ result[key] = value[0];
670
+ } else {
671
+ result[key] = value;
672
+ }
673
+ }
674
+ if (!result.response_type) {
675
+ result.response_type = "code";
676
+ }
677
+ return result;
678
+ }
679
+ function isRedirectUriAllowed(redirectUri, client) {
680
+ return client.redirect_uris.includes(redirectUri);
681
+ }
682
+
683
+ // src/server/utils/oauthRedirectUtils.ts
684
+ function buildOAuthRedirectUrl(redirectUri, params) {
685
+ const url = new URL(redirectUri);
686
+ for (const [key, value] of Object.entries(params)) {
687
+ if (value != null && value !== "") {
688
+ url.searchParams.set(key, value);
689
+ }
690
+ }
691
+ return url.toString();
692
+ }
693
+ function parseScopeList(scope) {
694
+ if (!scope?.trim()) {
695
+ return ["openid", "profile", "email"];
696
+ }
697
+ return scope.trim().split(/\s+/).filter(Boolean);
698
+ }
699
+
700
+ // src/server/services/OAuthWrapperService.ts
701
+ import { randomBytes as randomBytes2 } from "crypto";
702
+ var OAuthWrapperService = class {
869
703
  constructor(oauthSession, tokenEncryption, oauthRepo) {
870
- super();
871
704
  this.oauthSession = oauthSession;
872
705
  this.oauthRepo = oauthRepo;
873
706
  this.tokenService = new OAuthTokenService(
@@ -878,26 +711,29 @@ var OAuthWrapperService = class extends OAuthAbstractProvider {
878
711
  }
879
712
  oauthSession;
880
713
  oauthRepo;
714
+ authCodeTTLMs = 5 * 60 * 1e3;
881
715
  tokenService;
882
716
  /**
883
717
  * @override
884
718
  */
885
719
  async login(params) {
886
- const credentials = await this.providerLogin(params);
887
- const sessionToken = credentials.token;
720
+ const session = await this.providerLogin(params);
721
+ const sessionToken = session.providerRefreshToken;
888
722
  if (!sessionToken) {
889
723
  throw new Error("User provider login did not return a session token");
890
724
  }
891
725
  const userInfo = await this.providerGetUserInfo(sessionToken);
892
- const sessionPayload = this.generageSessionPayload({
893
- email: params.email,
894
- ...userInfo,
895
- sessionToken
896
- });
726
+ const sessionPayload = {
727
+ ...session,
728
+ user: {
729
+ ...session.user,
730
+ ...userInfo
731
+ }
732
+ };
897
733
  this.oauthSession.setSession(sessionPayload);
898
734
  const oauthrepo = this.getOAuthRepo();
899
735
  await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
900
- provider_session_token: sessionPayload.providerSessionToken
736
+ provider_session_token: sessionPayload.providerRefreshToken
901
737
  });
902
738
  return sessionPayload;
903
739
  }
@@ -945,26 +781,170 @@ var OAuthWrapperService = class extends OAuthAbstractProvider {
945
781
  async getUserInfoWithAccessToken(accessToken) {
946
782
  try {
947
783
  const profile = await this.providerGetUserInfoByAccessToken(accessToken);
948
- return this.toUserInfoResponse(profile);
784
+ return this.toUser(profile);
949
785
  } catch {
950
786
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
951
787
  }
952
788
  }
953
- toUserInfoResponse(profile) {
954
- const sub = String(profile.id);
955
- if (!sub || sub === "NaN") {
956
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
789
+ toUser(profile) {
790
+ return profile;
791
+ }
792
+ isQuery(query) {
793
+ return OAuthAuthorizeQuerySchema.safeParse(query).success;
794
+ }
795
+ isValidateConsent(value) {
796
+ return OAuthConsentBodySchema.safeParse(value).success;
797
+ }
798
+ /**
799
+ * @override
800
+ */
801
+ async logout(userId) {
802
+ const oauthRepo = this.getOAuthRepo();
803
+ await oauthRepo.revokeRefreshTokensByUserId(userId);
804
+ await oauthRepo.upsertUserCredentials(userId, {
805
+ provider_refresh_token: null,
806
+ provider_session_token: null
807
+ });
808
+ await this.clearSession();
809
+ }
810
+ /**
811
+ * @override
812
+ */
813
+ async resolveAuthorizePage(rawQuery) {
814
+ const query = normalizeQuery(rawQuery);
815
+ if (!this.isQuery(query)) {
816
+ return {
817
+ ok: false,
818
+ error: {
819
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
820
+ message: "Missing or invalid authorization request parameters."
821
+ }
822
+ };
957
823
  }
958
- const email = profile.email?.trim();
959
- if (!email) {
960
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
824
+ if (query.response_type !== "code") {
825
+ return {
826
+ ok: false,
827
+ error: {
828
+ errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
829
+ message: "Only response_type=code is supported."
830
+ }
831
+ };
832
+ }
833
+ const oauthRepo = this.getOAuthRepo();
834
+ const client = await oauthRepo.findClientById(query.client_id);
835
+ if (!client) {
836
+ return {
837
+ ok: false,
838
+ error: {
839
+ errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
840
+ message: "Unknown client_id."
841
+ }
842
+ };
843
+ }
844
+ if (!isRedirectUriAllowed(query.redirect_uri, client)) {
845
+ return {
846
+ ok: false,
847
+ error: {
848
+ errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
849
+ message: "redirect_uri is not registered for this client."
850
+ }
851
+ };
852
+ }
853
+ const requestedScopes = parseScopeList(query.scope);
854
+ const invalidScope = requestedScopes.find(
855
+ (scope) => !client.scopes.includes(scope)
856
+ );
857
+ if (invalidScope) {
858
+ return {
859
+ ok: false,
860
+ error: {
861
+ errorKey: OAuthRfcCodes.INVALID_SCOPE,
862
+ message: `Scope "${invalidScope}" is not allowed for this client.`
863
+ }
864
+ };
865
+ }
866
+ const pkceError = validatePkceParams(query, client.confidential);
867
+ if (pkceError) {
868
+ return { ok: false, error: pkceError };
869
+ }
870
+ return {
871
+ ok: true,
872
+ data: {
873
+ clientId: client.client_id,
874
+ clientName: client.client_name,
875
+ clientUri: client.client_uri ?? null,
876
+ logoUri: client.logo_uri ?? null,
877
+ redirectUri: query.redirect_uri,
878
+ scopes: requestedScopes,
879
+ state: query.state,
880
+ responseType: "code",
881
+ codeChallenge: query.code_challenge,
882
+ codeChallengeMethod: query.code_challenge_method,
883
+ confidential: client.confidential
884
+ }
885
+ };
886
+ }
887
+ /**
888
+ * @override
889
+ */
890
+ async processConsent(requestBody) {
891
+ if (!this.isValidateConsent(requestBody)) {
892
+ throw new ExecutorError2(
893
+ OAuthRfcCodes.INVALID_REQUEST,
894
+ "Invalid consent request body"
895
+ );
896
+ }
897
+ const session = await this.getSession();
898
+ if (!session) {
899
+ throw new ExecutorError2(
900
+ OAuthRfcCodes.ACCESS_DENIED,
901
+ "User session expired. Please sign in again."
902
+ );
903
+ }
904
+ const pageResult = await this.resolveAuthorizePage({
905
+ response_type: "code",
906
+ client_id: requestBody.client_id,
907
+ redirect_uri: requestBody.redirect_uri,
908
+ scope: requestBody.scope,
909
+ state: requestBody.state,
910
+ code_challenge: requestBody.code_challenge,
911
+ code_challenge_method: requestBody.code_challenge_method
912
+ });
913
+ if (!pageResult.ok) {
914
+ throw new ExecutorError2(
915
+ pageResult.error.errorKey,
916
+ pageResult.error.message
917
+ );
918
+ }
919
+ const { data } = pageResult;
920
+ if (requestBody.action === "deny") {
921
+ return {
922
+ redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
923
+ error: OAuthRfcCodes.ACCESS_DENIED,
924
+ error_description: "The resource owner denied the request",
925
+ state: data.state
926
+ })
927
+ };
961
928
  }
962
- const name = profile.name?.trim() || email;
929
+ const code = randomBytes2(32).toString("base64url");
930
+ const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
931
+ const oauthRepo = this.getOAuthRepo();
932
+ await oauthRepo.create({
933
+ code,
934
+ client_id: data.clientId,
935
+ user_id: session.userId,
936
+ redirect_uri: data.redirectUri,
937
+ scope: data.scopes.join(" ") || null,
938
+ code_challenge: data.codeChallenge ?? null,
939
+ code_challenge_method: data.codeChallengeMethod ?? null,
940
+ expires_at: expiresAt
941
+ });
942
+ void requestBody.trust;
963
943
  return {
964
- sub,
965
- email,
966
- name,
967
- ...profile.roles?.length ? { roles: profile.roles } : {}
944
+ redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
945
+ code,
946
+ state: data.state
947
+ })
968
948
  };
969
949
  }
970
950
  };
@@ -994,7 +974,6 @@ async function verifyClientSecret(secret, storedHash) {
994
974
  return timingSafeEqual2(derived, expected);
995
975
  }
996
976
  export {
997
- OAuthAbstractProvider,
998
977
  OAuthAuthorizationCodeRowSchema,
999
978
  OAuthAuthorizeQuerySchema,
1000
979
  OAuthClientCreateResponseSchema,