@vunexa/lixa 0.1.6-alpha.13 → 0.1.6-alpha.15
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/README.md +392 -674
- package/dist/credentials/credentials-manager.d.ts +14 -0
- package/dist/credentials/credentials-manager.d.ts.map +1 -1
- package/dist/credentials/types.d.ts +29 -6
- package/dist/credentials/types.d.ts.map +1 -1
- package/dist/dao/session-cache.d.ts.map +1 -1
- package/dist/dao/types.d.ts +24 -0
- package/dist/dao/types.d.ts.map +1 -1
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/export-types/index.d.ts +899 -88
- package/dist/identity/composite.d.ts +65 -0
- package/dist/identity/composite.d.ts.map +1 -0
- package/dist/identity/index.d.ts +4 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/self-managed.d.ts +66 -0
- package/dist/identity/self-managed.d.ts.map +1 -0
- package/dist/identity/types.d.ts +232 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/index.cjs +1543 -341
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +868 -80
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1461 -267
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +190 -35
- package/dist/lixa.d.ts.map +1 -1
- package/dist/models/session.d.ts +12 -6
- package/dist/models/session.d.ts.map +1 -1
- package/dist/providers/IProvider.d.ts +4 -0
- package/dist/providers/IProvider.d.ts.map +1 -1
- package/dist/types.d.ts +140 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/user-info.d.ts.map +1 -1
- package/package.json +3 -11
package/dist/index.cjs
CHANGED
|
@@ -30,8 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
AccountLinkingChallengeRequiredError: () => AccountLinkingChallengeRequiredError,
|
|
33
34
|
AccountLinkingStrategy: () => AccountLinkingStrategy,
|
|
35
|
+
AccountNotVerifiedError: () => AccountNotVerifiedError,
|
|
34
36
|
AccountUnlinkError: () => AccountUnlinkError,
|
|
37
|
+
CompositeIdentityProvider: () => CompositeIdentityProvider,
|
|
35
38
|
CredentialsManager: () => CredentialsManager,
|
|
36
39
|
CredentialsNotConfiguredError: () => CredentialsNotConfiguredError,
|
|
37
40
|
DEFAULT_SESSION_COOKIE_NAME: () => DEFAULT_SESSION_COOKIE_NAME,
|
|
@@ -39,6 +42,8 @@ __export(src_exports, {
|
|
|
39
42
|
DEFAULT_STATE_COOKIE_NAME: () => DEFAULT_STATE_COOKIE_NAME,
|
|
40
43
|
DEFAULT_STATE_MAX_AGE_SECONDS: () => DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
41
44
|
EmailNotVerifiedError: () => EmailNotVerifiedError,
|
|
45
|
+
IdentityNotConfiguredError: () => IdentityNotConfiguredError,
|
|
46
|
+
IdentityProviderError: () => IdentityProviderError,
|
|
42
47
|
InvalidCredentialsError: () => InvalidCredentialsError,
|
|
43
48
|
InvalidOAuthCallbackError: () => InvalidOAuthCallbackError,
|
|
44
49
|
InvalidProviderConfigError: () => InvalidProviderConfigError,
|
|
@@ -49,7 +54,9 @@ __export(src_exports, {
|
|
|
49
54
|
Pbkdf2PasswordHasher: () => Pbkdf2PasswordHasher,
|
|
50
55
|
ProviderNotConfiguredError: () => ProviderNotConfiguredError,
|
|
51
56
|
RefreshTokenError: () => RefreshTokenError,
|
|
57
|
+
SSOAccountAlreadyExistsError: () => SSOAccountAlreadyExistsError,
|
|
52
58
|
ScryptPasswordHasher: () => ScryptPasswordHasher,
|
|
59
|
+
SelfManagedIdentityProvider: () => SelfManagedIdentityProvider,
|
|
53
60
|
SessionNotFoundError: () => SessionNotFoundError,
|
|
54
61
|
TokenExchangeError: () => TokenExchangeError,
|
|
55
62
|
UserAlreadyExistsError: () => UserAlreadyExistsError,
|
|
@@ -64,6 +71,7 @@ __export(src_exports, {
|
|
|
64
71
|
determineProviderFromIssuer: () => determineProviderFromIssuer,
|
|
65
72
|
extractUserInfo: () => extractUserInfo,
|
|
66
73
|
fetchUserInfo: () => fetchUserInfo,
|
|
74
|
+
identity: () => identity_exports,
|
|
67
75
|
isProductionEnvironment: () => isProductionEnvironment,
|
|
68
76
|
serializeCookie: () => serializeCookie,
|
|
69
77
|
validatePasswordPolicy: () => validatePasswordPolicy
|
|
@@ -71,7 +79,7 @@ __export(src_exports, {
|
|
|
71
79
|
module.exports = __toCommonJS(src_exports);
|
|
72
80
|
|
|
73
81
|
// src/lixa.ts
|
|
74
|
-
var
|
|
82
|
+
var import_crypto5 = require("crypto");
|
|
75
83
|
|
|
76
84
|
// src/credentials/hasher.ts
|
|
77
85
|
var import_crypto = __toESM(require("crypto"), 1);
|
|
@@ -453,10 +461,65 @@ var WeakPasswordError = class extends LixaError {
|
|
|
453
461
|
}
|
|
454
462
|
};
|
|
455
463
|
var CredentialsNotConfiguredError = class extends LixaError {
|
|
456
|
-
constructor(message = "Credentials authentication is not configured in this Lixa instance. Pass credentials: {} in LixaConfig to enable it.", details) {
|
|
464
|
+
constructor(message = "Credentials authentication is not configured in this Lixa instance. Pass credentials: {} or identity: ... in LixaConfig to enable it.", details) {
|
|
457
465
|
super(message, "CREDENTIALS_NOT_CONFIGURED", details);
|
|
458
466
|
}
|
|
459
467
|
};
|
|
468
|
+
var IdentityProviderError = class extends LixaError {
|
|
469
|
+
provider;
|
|
470
|
+
statusCode;
|
|
471
|
+
constructor(message, provider, statusCode, details) {
|
|
472
|
+
super(message, "IDENTITY_PROVIDER_ERROR", { provider, statusCode, ...details });
|
|
473
|
+
this.provider = provider;
|
|
474
|
+
this.statusCode = statusCode;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
var IdentityNotConfiguredError = class extends CredentialsNotConfiguredError {
|
|
478
|
+
constructor(message = "Identity provider is not configured in this Lixa instance. Pass identity: ... or credentials: {} in LixaConfig to enable it.", details) {
|
|
479
|
+
super(message, details);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var AccountLinkingChallengeRequiredError = class extends LixaError {
|
|
483
|
+
challengeToken;
|
|
484
|
+
email;
|
|
485
|
+
incomingProvider;
|
|
486
|
+
incomingProviderUserId;
|
|
487
|
+
existingUserId;
|
|
488
|
+
existingProviders;
|
|
489
|
+
hasPassword;
|
|
490
|
+
constructor(message, details) {
|
|
491
|
+
super(message, "ACCOUNT_LINKING_CHALLENGE_REQUIRED", details);
|
|
492
|
+
this.challengeToken = details.challengeToken;
|
|
493
|
+
this.email = details.email;
|
|
494
|
+
this.incomingProvider = details.incomingProvider;
|
|
495
|
+
this.incomingProviderUserId = details.incomingProviderUserId;
|
|
496
|
+
this.existingUserId = details.existingUserId;
|
|
497
|
+
this.existingProviders = details.existingProviders;
|
|
498
|
+
this.hasPassword = details.hasPassword;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var SSOAccountAlreadyExistsError = class extends LixaError {
|
|
502
|
+
existingProviders;
|
|
503
|
+
email;
|
|
504
|
+
constructor(email, existingProviders, details) {
|
|
505
|
+
super(
|
|
506
|
+
`An account with email '${email}' already exists via ${existingProviders.join(", ")}. Please sign in with your original provider and set a password in Settings.`,
|
|
507
|
+
"SSO_ACCOUNT_EXISTS",
|
|
508
|
+
{ email, existingProviders, ...details }
|
|
509
|
+
);
|
|
510
|
+
this.email = email;
|
|
511
|
+
this.existingProviders = existingProviders;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var AccountNotVerifiedError = class extends LixaError {
|
|
515
|
+
constructor(identifier, details) {
|
|
516
|
+
super(
|
|
517
|
+
`Account${identifier ? ` '${identifier}'` : ""} has not been verified. Please confirm your account with the verification code sent to your email.`,
|
|
518
|
+
"ACCOUNT_NOT_VERIFIED",
|
|
519
|
+
{ identifier, ...details }
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
460
523
|
|
|
461
524
|
// src/credentials/credentials-manager.ts
|
|
462
525
|
var CredentialsManager = class {
|
|
@@ -620,6 +683,41 @@ var CredentialsManager = class {
|
|
|
620
683
|
await this.storage.updatePassword(user.id, newHash);
|
|
621
684
|
return true;
|
|
622
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Sets a password for a user who doesn't have one yet (e.g. SSO-first users),
|
|
688
|
+
* or updates an existing password without requiring the old one.
|
|
689
|
+
* This should only be called from an authenticated context (user already verified their identity via SSO).
|
|
690
|
+
*
|
|
691
|
+
* @param params - userId and newPassword
|
|
692
|
+
* @returns True if password was successfully set
|
|
693
|
+
*/
|
|
694
|
+
async setPassword(params) {
|
|
695
|
+
const policyResult = await validatePasswordPolicy(params.newPassword, this.policy);
|
|
696
|
+
if (!policyResult.valid) {
|
|
697
|
+
throw new WeakPasswordError(policyResult.errors);
|
|
698
|
+
}
|
|
699
|
+
const newHash = await this.hasher.hash(params.newPassword);
|
|
700
|
+
const existingUser = await this.storage.findUserById(params.userId);
|
|
701
|
+
if (existingUser) {
|
|
702
|
+
await this.storage.updatePassword(params.userId, newHash);
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
const now = Date.now();
|
|
706
|
+
const identifier = params.identifier || params.email || params.userId;
|
|
707
|
+
const normalized = this.normalizeIdentifier(identifier);
|
|
708
|
+
const isEmail = normalized.includes("@");
|
|
709
|
+
const user = {
|
|
710
|
+
id: params.userId,
|
|
711
|
+
identifier: normalized,
|
|
712
|
+
email: isEmail ? normalized : params.email?.trim().toLowerCase(),
|
|
713
|
+
username: !isEmail ? normalized : void 0,
|
|
714
|
+
passwordHash: newHash,
|
|
715
|
+
createdAt: now,
|
|
716
|
+
updatedAt: now
|
|
717
|
+
};
|
|
718
|
+
await this.storage.saveUser(user);
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
623
721
|
/**
|
|
624
722
|
* Finds a user by ID and returns safe user data.
|
|
625
723
|
*/
|
|
@@ -652,16 +750,394 @@ var CredentialsManager = class {
|
|
|
652
750
|
}
|
|
653
751
|
};
|
|
654
752
|
|
|
753
|
+
// src/identity/self-managed.ts
|
|
754
|
+
var import_crypto3 = __toESM(require("crypto"), 1);
|
|
755
|
+
var SelfManagedIdentityProvider = class {
|
|
756
|
+
name = "self-managed";
|
|
757
|
+
manager;
|
|
758
|
+
pendingSignups = /* @__PURE__ */ new Map();
|
|
759
|
+
requireVerification;
|
|
760
|
+
config;
|
|
761
|
+
constructor(configOrManager) {
|
|
762
|
+
if (configOrManager instanceof CredentialsManager) {
|
|
763
|
+
this.manager = configOrManager;
|
|
764
|
+
this.config = {};
|
|
765
|
+
this.requireVerification = false;
|
|
766
|
+
} else {
|
|
767
|
+
this.config = configOrManager || {};
|
|
768
|
+
this.manager = new CredentialsManager(this.config);
|
|
769
|
+
this.requireVerification = this.config.requireVerification ?? false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
mapUser(user) {
|
|
773
|
+
return {
|
|
774
|
+
id: user.id,
|
|
775
|
+
identifier: user.identifier,
|
|
776
|
+
username: user.username,
|
|
777
|
+
email: user.email,
|
|
778
|
+
emailVerified: true,
|
|
779
|
+
createdAt: user.createdAt,
|
|
780
|
+
updatedAt: user.updatedAt,
|
|
781
|
+
metadata: user.metadata,
|
|
782
|
+
provider: this.name
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
generateConfirmationCode() {
|
|
786
|
+
return String(import_crypto3.default.randomInt(1e5, 999999));
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Registers a new user with optional pending verification.
|
|
790
|
+
* When requireVerification is true, credentials are NOT persisted to the database until confirmSignUp() is called.
|
|
791
|
+
*/
|
|
792
|
+
async signUp(params) {
|
|
793
|
+
if (!this.requireVerification) {
|
|
794
|
+
const user = await this.manager.signUp(params);
|
|
795
|
+
return this.mapUser(user);
|
|
796
|
+
}
|
|
797
|
+
const rawIdentifier = params.username || params.identifier || params.email || "";
|
|
798
|
+
const trimmed = (rawIdentifier || "").trim();
|
|
799
|
+
if (!trimmed) {
|
|
800
|
+
throw new InvalidCredentialsError("Identifier cannot be empty");
|
|
801
|
+
}
|
|
802
|
+
const identifierType = this.config.identifierType || "both";
|
|
803
|
+
if (identifierType === "email") {
|
|
804
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
805
|
+
if (!emailRegex.test(trimmed)) {
|
|
806
|
+
throw new InvalidCredentialsError("Identifier must be a valid email address");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const normalized = trimmed.toLowerCase();
|
|
810
|
+
const isEmail = normalized.includes("@");
|
|
811
|
+
const email = params.email ? params.email.trim().toLowerCase() : isEmail ? normalized : void 0;
|
|
812
|
+
const username = params.username ? params.username.trim() : !isEmail ? normalized : void 0;
|
|
813
|
+
if (this.config.requireUsername && (!username || username.trim() === "")) {
|
|
814
|
+
throw new InvalidCredentialsError("Username is required");
|
|
815
|
+
}
|
|
816
|
+
if (this.config.requireEmail && (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) {
|
|
817
|
+
throw new InvalidCredentialsError("A valid email address is required");
|
|
818
|
+
}
|
|
819
|
+
const existing = await this.manager.getUserByIdentifier(normalized);
|
|
820
|
+
if (existing) {
|
|
821
|
+
throw new UserAlreadyExistsError(normalized);
|
|
822
|
+
}
|
|
823
|
+
const existingPending = this.pendingSignups.get(normalized);
|
|
824
|
+
if (existingPending && existingPending.expiresAt > Date.now()) {
|
|
825
|
+
throw new UserAlreadyExistsError(normalized);
|
|
826
|
+
}
|
|
827
|
+
const policy = { minLength: 8, maxLength: 128, ...this.config.policy };
|
|
828
|
+
const policyResult = await validatePasswordPolicy(params.password, policy);
|
|
829
|
+
if (!policyResult.valid) {
|
|
830
|
+
throw new WeakPasswordError(policyResult.errors);
|
|
831
|
+
}
|
|
832
|
+
const passwordHash = await this.manager.getHasher().hash(params.password);
|
|
833
|
+
const userId = typeof import_crypto3.default.randomUUID === "function" ? import_crypto3.default.randomUUID() : import_crypto3.default.randomBytes(16).toString("hex");
|
|
834
|
+
const confirmationCode = this.generateConfirmationCode();
|
|
835
|
+
const now = Date.now();
|
|
836
|
+
const pending = {
|
|
837
|
+
identifier: normalized,
|
|
838
|
+
email,
|
|
839
|
+
username,
|
|
840
|
+
passwordHash,
|
|
841
|
+
confirmationCode,
|
|
842
|
+
userId,
|
|
843
|
+
metadata: params.metadata,
|
|
844
|
+
createdAt: now,
|
|
845
|
+
expiresAt: now + 15 * 60 * 1e3
|
|
846
|
+
// 15 minutes TTL
|
|
847
|
+
};
|
|
848
|
+
this.pendingSignups.set(normalized, pending);
|
|
849
|
+
console.log(`[Lixa Self-Managed] Verification code for '${normalized}': ${confirmationCode}`);
|
|
850
|
+
return {
|
|
851
|
+
id: userId,
|
|
852
|
+
identifier: normalized,
|
|
853
|
+
username,
|
|
854
|
+
email,
|
|
855
|
+
emailVerified: false,
|
|
856
|
+
createdAt: now,
|
|
857
|
+
updatedAt: now,
|
|
858
|
+
metadata: {
|
|
859
|
+
...params.metadata,
|
|
860
|
+
requiresVerification: true,
|
|
861
|
+
devConfirmationCode: confirmationCode
|
|
862
|
+
},
|
|
863
|
+
provider: this.name
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Confirms a pending signup registration using the verification code.
|
|
868
|
+
* Only after successful confirmation is the user persisted to the database.
|
|
869
|
+
*/
|
|
870
|
+
async confirmSignUp(params) {
|
|
871
|
+
const normalized = (params.identifier || "").trim().toLowerCase();
|
|
872
|
+
const pending = this.pendingSignups.get(normalized);
|
|
873
|
+
if (!pending) {
|
|
874
|
+
throw new InvalidCredentialsError("No pending registration found for this identifier. Please sign up first.");
|
|
875
|
+
}
|
|
876
|
+
if (pending.expiresAt < Date.now()) {
|
|
877
|
+
this.pendingSignups.delete(normalized);
|
|
878
|
+
throw new InvalidCredentialsError("Verification code has expired. Please sign up again.");
|
|
879
|
+
}
|
|
880
|
+
const code = (params.confirmationCode || "").trim();
|
|
881
|
+
if (code !== pending.confirmationCode) {
|
|
882
|
+
throw new InvalidCredentialsError("Invalid verification code.");
|
|
883
|
+
}
|
|
884
|
+
const storage = this.manager.getStorage();
|
|
885
|
+
const user = {
|
|
886
|
+
id: pending.userId,
|
|
887
|
+
identifier: pending.identifier,
|
|
888
|
+
email: pending.email,
|
|
889
|
+
username: pending.username,
|
|
890
|
+
passwordHash: pending.passwordHash,
|
|
891
|
+
createdAt: pending.createdAt,
|
|
892
|
+
updatedAt: Date.now(),
|
|
893
|
+
metadata: pending.metadata
|
|
894
|
+
};
|
|
895
|
+
await storage.saveUser(user);
|
|
896
|
+
this.pendingSignups.delete(normalized);
|
|
897
|
+
console.log(`[Lixa Self-Managed] User '${normalized}' verified and persisted to database.`);
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Resends the verification code for a pending signup.
|
|
902
|
+
*/
|
|
903
|
+
async resendConfirmationCode(params) {
|
|
904
|
+
const normalized = (params.identifier || "").trim().toLowerCase();
|
|
905
|
+
const pending = this.pendingSignups.get(normalized);
|
|
906
|
+
if (!pending) {
|
|
907
|
+
throw new InvalidCredentialsError("No pending registration found for this identifier.");
|
|
908
|
+
}
|
|
909
|
+
const newCode = this.generateConfirmationCode();
|
|
910
|
+
pending.confirmationCode = newCode;
|
|
911
|
+
pending.expiresAt = Date.now() + 15 * 60 * 1e3;
|
|
912
|
+
this.pendingSignups.set(normalized, pending);
|
|
913
|
+
console.log(`[Lixa Self-Managed] New verification code for '${normalized}': ${newCode}`);
|
|
914
|
+
return {
|
|
915
|
+
success: true,
|
|
916
|
+
deliveryMedium: "CONSOLE",
|
|
917
|
+
destination: pending.email || normalized
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Authenticates a user with password.
|
|
922
|
+
*/
|
|
923
|
+
async signIn(params) {
|
|
924
|
+
const user = await this.manager.verifyCredentials(params);
|
|
925
|
+
if (!user) {
|
|
926
|
+
throw new InvalidCredentialsError();
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
user: this.mapUser(user)
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Verifies credentials without throwing an error if invalid.
|
|
934
|
+
*/
|
|
935
|
+
async verifyCredentials(params) {
|
|
936
|
+
const user = await this.manager.verifyCredentials(params);
|
|
937
|
+
return user ? this.mapUser(user) : null;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Changes a user's password.
|
|
941
|
+
*/
|
|
942
|
+
async changePassword(params) {
|
|
943
|
+
return await this.manager.changePassword(params);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Retrieves a user by their unique ID.
|
|
947
|
+
*/
|
|
948
|
+
async getUserById(id) {
|
|
949
|
+
const user = await this.manager.getUserById(id);
|
|
950
|
+
return user ? this.mapUser(user) : null;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Retrieves a user by their identifier (email or username).
|
|
954
|
+
*/
|
|
955
|
+
async getUserByIdentifier(identifier) {
|
|
956
|
+
const user = await this.manager.getUserByIdentifier(identifier);
|
|
957
|
+
return user ? this.mapUser(user) : null;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Deletes a user from storage if supported by the storage adapter.
|
|
961
|
+
*/
|
|
962
|
+
async deleteUser(id) {
|
|
963
|
+
const storage = this.manager.getStorage();
|
|
964
|
+
if (typeof storage.deleteUser === "function") {
|
|
965
|
+
await storage.deleteUser(id);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Returns the underlying CredentialsManager instance.
|
|
970
|
+
*/
|
|
971
|
+
getCredentialsManager() {
|
|
972
|
+
return this.manager;
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// src/identity/composite.ts
|
|
977
|
+
var CompositeIdentityProvider = class {
|
|
978
|
+
name = "composite";
|
|
979
|
+
providers;
|
|
980
|
+
defaultProvider;
|
|
981
|
+
oauthProviders;
|
|
982
|
+
constructor(options) {
|
|
983
|
+
if (!options.providers || Object.keys(options.providers).length === 0) {
|
|
984
|
+
throw new IdentityProviderError(
|
|
985
|
+
"CompositeIdentityProvider requires at least one child provider in 'providers'",
|
|
986
|
+
this.name
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
this.providers = options.providers;
|
|
990
|
+
this.defaultProvider = options.defaultProvider;
|
|
991
|
+
this.oauthProviders = options.oauthProviders;
|
|
992
|
+
if (this.defaultProvider && !this.providers[this.defaultProvider]) {
|
|
993
|
+
throw new IdentityProviderError(
|
|
994
|
+
`Configured defaultProvider '${String(this.defaultProvider)}' does not exist in 'providers'. Available: ${Object.keys(this.providers).join(", ")}`,
|
|
995
|
+
this.name
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Resolves the target child provider based on explicit parameter or defaultProvider.
|
|
1001
|
+
* Throws an error immediately if the provider cannot be resolved or does not exist.
|
|
1002
|
+
*/
|
|
1003
|
+
selectProvider(providerName) {
|
|
1004
|
+
const targetKey = providerName || this.defaultProvider;
|
|
1005
|
+
if (!targetKey) {
|
|
1006
|
+
throw new IdentityProviderError(
|
|
1007
|
+
`No provider specified and no defaultProvider configured in CompositeIdentityProvider. Available providers: ${Object.keys(this.providers).join(", ")}`,
|
|
1008
|
+
this.name
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const provider = this.providers[targetKey];
|
|
1012
|
+
if (!provider) {
|
|
1013
|
+
throw new IdentityProviderError(
|
|
1014
|
+
`Provider '${targetKey}' is not configured in CompositeIdentityProvider. Available providers: ${Object.keys(this.providers).join(", ")}`,
|
|
1015
|
+
this.name
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return provider;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Returns all OAuth / OIDC provider configurations aggregated across this composite provider
|
|
1022
|
+
* and any child providers exposing `getOAuthProviders()` or `getProviders()`.
|
|
1023
|
+
*/
|
|
1024
|
+
getProviders() {
|
|
1025
|
+
const result = {};
|
|
1026
|
+
if (this.oauthProviders) {
|
|
1027
|
+
Object.assign(result, this.oauthProviders);
|
|
1028
|
+
}
|
|
1029
|
+
for (const child of Object.values(this.providers)) {
|
|
1030
|
+
if (typeof child.getOAuthProviders === "function") {
|
|
1031
|
+
Object.assign(result, child.getOAuthProviders());
|
|
1032
|
+
} else if (typeof child.getProviders === "function") {
|
|
1033
|
+
Object.assign(result, child.getProviders());
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return result;
|
|
1037
|
+
}
|
|
1038
|
+
async signUp(params) {
|
|
1039
|
+
const provider = this.selectProvider(params.provider);
|
|
1040
|
+
return provider.signUp(params);
|
|
1041
|
+
}
|
|
1042
|
+
async signIn(params) {
|
|
1043
|
+
const provider = this.selectProvider(params.provider);
|
|
1044
|
+
return provider.signIn(params);
|
|
1045
|
+
}
|
|
1046
|
+
async verifyCredentials(params) {
|
|
1047
|
+
const provider = this.selectProvider(params.provider);
|
|
1048
|
+
if (typeof provider.verifyCredentials === "function") {
|
|
1049
|
+
return provider.verifyCredentials(params);
|
|
1050
|
+
}
|
|
1051
|
+
const result = await provider.signIn(params);
|
|
1052
|
+
return result.user;
|
|
1053
|
+
}
|
|
1054
|
+
async changePassword(params) {
|
|
1055
|
+
const provider = this.selectProvider(params.provider);
|
|
1056
|
+
if (typeof provider.changePassword !== "function") {
|
|
1057
|
+
throw new IdentityProviderError(
|
|
1058
|
+
`Password change is not supported by provider '${provider.name}'`,
|
|
1059
|
+
this.name
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
return provider.changePassword(params);
|
|
1063
|
+
}
|
|
1064
|
+
async forgotPassword(params) {
|
|
1065
|
+
const provider = this.selectProvider(params.provider);
|
|
1066
|
+
if (typeof provider.forgotPassword !== "function") {
|
|
1067
|
+
throw new IdentityProviderError(
|
|
1068
|
+
`Password reset is not supported by provider '${provider.name}'`,
|
|
1069
|
+
this.name
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return provider.forgotPassword(params);
|
|
1073
|
+
}
|
|
1074
|
+
async confirmPasswordReset(params) {
|
|
1075
|
+
const provider = this.selectProvider(params.provider);
|
|
1076
|
+
if (typeof provider.confirmPasswordReset !== "function") {
|
|
1077
|
+
throw new IdentityProviderError(
|
|
1078
|
+
`Confirm password reset is not supported by provider '${provider.name}'`,
|
|
1079
|
+
this.name
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
return provider.confirmPasswordReset(params);
|
|
1083
|
+
}
|
|
1084
|
+
async confirmSignUp(params) {
|
|
1085
|
+
const provider = this.selectProvider(params.provider);
|
|
1086
|
+
if (typeof provider.confirmSignUp !== "function") {
|
|
1087
|
+
throw new IdentityProviderError(
|
|
1088
|
+
`Account confirmation is not supported by provider '${provider.name}'`,
|
|
1089
|
+
this.name
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
return provider.confirmSignUp(params);
|
|
1093
|
+
}
|
|
1094
|
+
async resendConfirmationCode(params) {
|
|
1095
|
+
const provider = this.selectProvider(params.provider);
|
|
1096
|
+
if (typeof provider.resendConfirmationCode !== "function") {
|
|
1097
|
+
throw new IdentityProviderError(
|
|
1098
|
+
`Resending confirmation code is not supported by provider '${provider.name}'`,
|
|
1099
|
+
this.name
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
return provider.resendConfirmationCode(params);
|
|
1103
|
+
}
|
|
1104
|
+
async getUserById(id, providerName) {
|
|
1105
|
+
const provider = this.selectProvider(providerName);
|
|
1106
|
+
if (typeof provider.getUserById !== "function") {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
return provider.getUserById(id);
|
|
1110
|
+
}
|
|
1111
|
+
async getUserByIdentifier(identifier, providerName) {
|
|
1112
|
+
const provider = this.selectProvider(providerName);
|
|
1113
|
+
if (typeof provider.getUserByIdentifier !== "function") {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return provider.getUserByIdentifier(identifier);
|
|
1117
|
+
}
|
|
1118
|
+
async deleteUser(id, providerName) {
|
|
1119
|
+
const provider = this.selectProvider(providerName);
|
|
1120
|
+
if (typeof provider.deleteUser !== "function") {
|
|
1121
|
+
throw new IdentityProviderError(
|
|
1122
|
+
`User deletion is not supported by provider '${provider.name}'`,
|
|
1123
|
+
this.name
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return provider.deleteUser(id);
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
|
|
655
1130
|
// src/types.ts
|
|
656
1131
|
var AccountLinkingStrategy = /* @__PURE__ */ ((AccountLinkingStrategy2) => {
|
|
657
1132
|
AccountLinkingStrategy2["AUTO_LINK_BY_VERIFIED_EMAIL"] = "AUTO_LINK_BY_VERIFIED_EMAIL";
|
|
658
1133
|
AccountLinkingStrategy2["ISOLATED"] = "ISOLATED";
|
|
1134
|
+
AccountLinkingStrategy2["STEP_UP_VERIFICATION"] = "STEP_UP_VERIFICATION";
|
|
659
1135
|
return AccountLinkingStrategy2;
|
|
660
1136
|
})(AccountLinkingStrategy || {});
|
|
661
1137
|
|
|
662
1138
|
// src/dao/state-cache.ts
|
|
663
1139
|
var import_node_cache = __toESM(require("node-cache"), 1);
|
|
664
|
-
var
|
|
1140
|
+
var import_crypto4 = require("crypto");
|
|
665
1141
|
var LocalStateHandler = class {
|
|
666
1142
|
cache;
|
|
667
1143
|
stateStorage;
|
|
@@ -681,8 +1157,8 @@ var LocalStateHandler = class {
|
|
|
681
1157
|
}
|
|
682
1158
|
// Default generateState implementation
|
|
683
1159
|
async generateState(provider) {
|
|
684
|
-
const state = (0,
|
|
685
|
-
const codeVerifier = (0,
|
|
1160
|
+
const state = (0, import_crypto4.randomBytes)(16).toString("hex");
|
|
1161
|
+
const codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
686
1162
|
return {
|
|
687
1163
|
state,
|
|
688
1164
|
data: {
|
|
@@ -699,7 +1175,7 @@ var LocalStateHandler = class {
|
|
|
699
1175
|
};
|
|
700
1176
|
|
|
701
1177
|
// src/lixa.ts
|
|
702
|
-
var
|
|
1178
|
+
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
703
1179
|
|
|
704
1180
|
// src/dao/session-cache.ts
|
|
705
1181
|
var import_node_cache2 = __toESM(require("node-cache"), 1);
|
|
@@ -757,10 +1233,7 @@ var LocalSessionHandler = class {
|
|
|
757
1233
|
if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
|
|
758
1234
|
throw new Error("No valid access token found in OAuth response");
|
|
759
1235
|
}
|
|
760
|
-
const session = {
|
|
761
|
-
token: tokenData.access_token,
|
|
762
|
-
raw: tokenData
|
|
763
|
-
};
|
|
1236
|
+
const session = {};
|
|
764
1237
|
return session;
|
|
765
1238
|
}
|
|
766
1239
|
// PascalCase alias for backward compatibility
|
|
@@ -821,7 +1294,7 @@ async function fetchUserInfo(accessToken, userInfoEndpoint) {
|
|
|
821
1294
|
family_name: "family_name" in data ? String(data.family_name) : void 0,
|
|
822
1295
|
name: "name" in data ? String(data.name) : void 0,
|
|
823
1296
|
picture: "picture" in data ? String(data.picture) : void 0,
|
|
824
|
-
email_verified: "email_verified" in data ?
|
|
1297
|
+
email_verified: "email_verified" in data ? data.email_verified === true || data.email_verified === "true" : void 0,
|
|
825
1298
|
iss: "iss" in data ? String(data.iss) : void 0
|
|
826
1299
|
};
|
|
827
1300
|
}
|
|
@@ -829,68 +1302,213 @@ async function extractUserInfo(tokenData, providerMetadata) {
|
|
|
829
1302
|
let userInfo;
|
|
830
1303
|
const userInfoEndpoint = providerMetadata.endpoints.userInfo;
|
|
831
1304
|
if (tokenData.id_token) {
|
|
832
|
-
|
|
833
|
-
|
|
1305
|
+
try {
|
|
1306
|
+
userInfo = decodeIdToken(tokenData.id_token);
|
|
1307
|
+
if (userInfo && userInfo.email_verified !== void 0) {
|
|
1308
|
+
userInfo.email_verified = userInfo.email_verified === true || userInfo.email_verified === "true";
|
|
1309
|
+
}
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if ((!userInfo || !userInfo.email) && tokenData.access_token && userInfoEndpoint) {
|
|
1314
|
+
try {
|
|
1315
|
+
const fetched = await fetchUserInfo(tokenData.access_token, userInfoEndpoint);
|
|
1316
|
+
userInfo = { ...fetched, ...userInfo || {}, email: fetched.email || userInfo?.email || "" };
|
|
1317
|
+
if (fetched.email_verified !== void 0 && (userInfo.email_verified === void 0 || userInfo.email_verified === false)) {
|
|
1318
|
+
userInfo.email_verified = fetched.email_verified === true || fetched.email_verified === "true";
|
|
1319
|
+
}
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
} else if (!userInfo && tokenData.access_token && userInfoEndpoint) {
|
|
834
1323
|
userInfo = await fetchUserInfo(tokenData.access_token, userInfoEndpoint);
|
|
835
|
-
}
|
|
1324
|
+
}
|
|
1325
|
+
if (!userInfo) {
|
|
836
1326
|
throw new Error("No ID token or access token available to fetch user info");
|
|
837
1327
|
}
|
|
838
1328
|
return { userInfo };
|
|
839
1329
|
}
|
|
840
1330
|
|
|
841
|
-
// src/
|
|
842
|
-
var
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1331
|
+
// src/utils/cookies.ts
|
|
1332
|
+
var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
|
|
1333
|
+
var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
|
|
1334
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
1335
|
+
var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
|
|
1336
|
+
function isProductionEnvironment() {
|
|
1337
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
1338
|
+
}
|
|
1339
|
+
function serializeCookie(name, value, options) {
|
|
1340
|
+
const isProd = isProductionEnvironment();
|
|
1341
|
+
const path = options?.path ?? "/";
|
|
1342
|
+
const httpOnly = options?.httpOnly ?? true;
|
|
1343
|
+
const secure = options?.secure ?? isProd;
|
|
1344
|
+
const sameSite = options?.sameSite ?? "lax";
|
|
1345
|
+
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
1346
|
+
if (path) {
|
|
1347
|
+
parts.push(`Path=${path}`);
|
|
1348
|
+
}
|
|
1349
|
+
if (typeof options?.maxAge === "number") {
|
|
1350
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
1351
|
+
const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
|
|
1352
|
+
parts.push(`Expires=${expires}`);
|
|
1353
|
+
}
|
|
1354
|
+
if (options?.domain) {
|
|
1355
|
+
parts.push(`Domain=${options.domain}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (httpOnly) {
|
|
1358
|
+
parts.push("HttpOnly");
|
|
1359
|
+
}
|
|
1360
|
+
if (secure) {
|
|
1361
|
+
parts.push("Secure");
|
|
1362
|
+
}
|
|
1363
|
+
if (sameSite) {
|
|
1364
|
+
const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
|
|
1365
|
+
parts.push(`SameSite=${capitalized}`);
|
|
1366
|
+
}
|
|
1367
|
+
return parts.join("; ");
|
|
1368
|
+
}
|
|
1369
|
+
function createSessionCookie(sessionId, options) {
|
|
1370
|
+
const isProd = isProductionEnvironment();
|
|
1371
|
+
const resolvedOptions = {
|
|
1372
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1373
|
+
path: options?.path ?? "/",
|
|
1374
|
+
maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1375
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1376
|
+
secure: options?.secure ?? isProd,
|
|
1377
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1378
|
+
domain: options?.domain
|
|
1379
|
+
};
|
|
1380
|
+
const name = resolvedOptions.name;
|
|
1381
|
+
const header = serializeCookie(name, sessionId, resolvedOptions);
|
|
1382
|
+
return {
|
|
1383
|
+
name,
|
|
1384
|
+
value: sessionId,
|
|
1385
|
+
options: resolvedOptions,
|
|
1386
|
+
header
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function clearSessionCookie(options) {
|
|
1390
|
+
const isProd = isProductionEnvironment();
|
|
1391
|
+
const resolvedOptions = {
|
|
1392
|
+
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
1393
|
+
path: options?.path ?? "/",
|
|
1394
|
+
maxAge: 0,
|
|
1395
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1396
|
+
secure: options?.secure ?? isProd,
|
|
1397
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1398
|
+
domain: options?.domain
|
|
1399
|
+
};
|
|
1400
|
+
const name = resolvedOptions.name;
|
|
1401
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1402
|
+
return {
|
|
1403
|
+
name,
|
|
1404
|
+
value: "",
|
|
1405
|
+
options: resolvedOptions,
|
|
1406
|
+
header
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function createStateCookie(state, options) {
|
|
1410
|
+
const isProd = isProductionEnvironment();
|
|
1411
|
+
const resolvedOptions = {
|
|
1412
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1413
|
+
path: options?.path ?? "/",
|
|
1414
|
+
maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1415
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1416
|
+
secure: options?.secure ?? isProd,
|
|
1417
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1418
|
+
domain: options?.domain
|
|
1419
|
+
};
|
|
1420
|
+
const name = resolvedOptions.name;
|
|
1421
|
+
const header = serializeCookie(name, state, resolvedOptions);
|
|
1422
|
+
return {
|
|
1423
|
+
name,
|
|
1424
|
+
value: state,
|
|
1425
|
+
options: resolvedOptions,
|
|
1426
|
+
header
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function clearStateCookie(options) {
|
|
1430
|
+
const isProd = isProductionEnvironment();
|
|
1431
|
+
const resolvedOptions = {
|
|
1432
|
+
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
1433
|
+
path: options?.path ?? "/",
|
|
1434
|
+
maxAge: 0,
|
|
1435
|
+
httpOnly: options?.httpOnly ?? true,
|
|
1436
|
+
secure: options?.secure ?? isProd,
|
|
1437
|
+
sameSite: options?.sameSite ?? "lax",
|
|
1438
|
+
domain: options?.domain
|
|
1439
|
+
};
|
|
1440
|
+
const name = resolvedOptions.name;
|
|
1441
|
+
const header = serializeCookie(name, "", resolvedOptions);
|
|
1442
|
+
return {
|
|
1443
|
+
name,
|
|
1444
|
+
value: "",
|
|
1445
|
+
options: resolvedOptions,
|
|
1446
|
+
header
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/lixa.ts
|
|
1451
|
+
var Lixa = class _Lixa {
|
|
1452
|
+
static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
|
|
1453
|
+
static CONFIGURED_PROVIDERS = /* @__PURE__ */ new Map();
|
|
1454
|
+
// Legacy registry for backward compatibility
|
|
1455
|
+
// Instance-scoped fallback handlers to ensure no cross-instance state pollution
|
|
1456
|
+
localStateHandler;
|
|
1457
|
+
localSessionHandler;
|
|
1458
|
+
localResourceHandler;
|
|
1459
|
+
localAccountHandler;
|
|
1460
|
+
userResourceStore = /* @__PURE__ */ new Map();
|
|
1461
|
+
userAccountStore = /* @__PURE__ */ new Map();
|
|
1462
|
+
refreshMutexes = /* @__PURE__ */ new Map();
|
|
1463
|
+
identityProvider;
|
|
1464
|
+
credentialsManager;
|
|
1465
|
+
config;
|
|
1466
|
+
stateHandler;
|
|
1467
|
+
sessionHandler;
|
|
1468
|
+
resourceHandler;
|
|
1469
|
+
accountHandler;
|
|
1470
|
+
debug;
|
|
1471
|
+
sessionCookieResolver;
|
|
1472
|
+
/**
|
|
1473
|
+
* Creates a new Lixa instance with the provided configuration.
|
|
1474
|
+
*
|
|
1475
|
+
* @remarks
|
|
1476
|
+
* Providers can be passed inline in the configuration using the `provider` field.
|
|
1477
|
+
* Provider resolution priority: inline custom provider \> default providers \> legacy registry.
|
|
1478
|
+
*
|
|
1479
|
+
* @param config - The configuration object containing provider settings and optional session strategy
|
|
1480
|
+
*
|
|
1481
|
+
* @throws Error when provider configuration is missing required fields
|
|
1482
|
+
* @throws Error when provider implementation is missing required properties
|
|
1483
|
+
* @throws Error when provider is not available and no inline implementation is provided
|
|
1484
|
+
*/
|
|
1485
|
+
constructor(config) {
|
|
1486
|
+
this.sessionCookieResolver = config.sessionCookieName;
|
|
1487
|
+
this.config = {
|
|
1488
|
+
...config,
|
|
1489
|
+
...config.providers ? { providers: config.providers } : {}
|
|
1490
|
+
};
|
|
1491
|
+
this.debug = config.debug || false;
|
|
1492
|
+
this.localStateHandler = new LocalStateHandler();
|
|
1493
|
+
this.localSessionHandler = new LocalSessionHandler();
|
|
1494
|
+
this.localResourceHandler = {
|
|
1495
|
+
resourceStorage: {
|
|
1496
|
+
saveResource: async (userId, provider, resource) => {
|
|
1497
|
+
let userMap = this.userResourceStore.get(userId);
|
|
1498
|
+
if (!userMap) {
|
|
1499
|
+
userMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
this.userResourceStore.set(userId, userMap);
|
|
1501
|
+
}
|
|
1502
|
+
userMap.set(provider.toLowerCase(), resource);
|
|
1503
|
+
},
|
|
1504
|
+
getResource: async (userId, provider) => {
|
|
1505
|
+
const userMap = this.userResourceStore.get(userId);
|
|
1506
|
+
return userMap?.get(provider.toLowerCase()) || null;
|
|
1507
|
+
},
|
|
1508
|
+
getUserResources: async (userId) => {
|
|
1509
|
+
const userMap = this.userResourceStore.get(userId);
|
|
1510
|
+
const result = {};
|
|
1511
|
+
if (userMap) {
|
|
894
1512
|
for (const [p, r] of userMap.entries()) {
|
|
895
1513
|
result[p] = r;
|
|
896
1514
|
}
|
|
@@ -905,19 +1523,84 @@ var Lixa = class _Lixa {
|
|
|
905
1523
|
}
|
|
906
1524
|
}
|
|
907
1525
|
};
|
|
908
|
-
this.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1526
|
+
this.localAccountHandler = {
|
|
1527
|
+
accountStorage: {
|
|
1528
|
+
saveAccount: async (userId, provider, account) => {
|
|
1529
|
+
let userMap = this.userAccountStore.get(userId);
|
|
1530
|
+
if (!userMap) {
|
|
1531
|
+
userMap = /* @__PURE__ */ new Map();
|
|
1532
|
+
this.userAccountStore.set(userId, userMap);
|
|
1533
|
+
}
|
|
1534
|
+
userMap.set(provider.toLowerCase(), account);
|
|
1535
|
+
},
|
|
1536
|
+
getAccount: async (userId, provider) => {
|
|
1537
|
+
const userMap = this.userAccountStore.get(userId);
|
|
1538
|
+
return userMap?.get(provider.toLowerCase()) || null;
|
|
1539
|
+
},
|
|
1540
|
+
getUserAccounts: async (userId) => {
|
|
1541
|
+
const userMap = this.userAccountStore.get(userId);
|
|
1542
|
+
const result = {};
|
|
1543
|
+
if (userMap) {
|
|
1544
|
+
for (const [p, a] of userMap.entries()) {
|
|
1545
|
+
result[p] = a;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return result;
|
|
1549
|
+
},
|
|
1550
|
+
deleteAccount: async (userId, provider) => {
|
|
1551
|
+
const userMap = this.userAccountStore.get(userId);
|
|
1552
|
+
if (userMap) {
|
|
1553
|
+
userMap.delete(provider.toLowerCase());
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
const storage = config.storage;
|
|
1559
|
+
this.stateHandler = config.stateHandler || (storage?.stateStorage ? { stateStorage: storage.stateStorage } : this.localStateHandler);
|
|
1560
|
+
this.sessionHandler = config.sessionHandler || (storage?.sessionStorage ? { sessionStorage: storage.sessionStorage } : this.localSessionHandler);
|
|
1561
|
+
this.resourceHandler = config.resourceHandler || (storage?.resourceStorage ? { resourceStorage: storage.resourceStorage } : this.localResourceHandler);
|
|
1562
|
+
this.accountHandler = storage?.accountStorage ? { accountStorage: storage.accountStorage } : this.localAccountHandler;
|
|
1563
|
+
const identityConfig = config.identityProvider || config.identity;
|
|
1564
|
+
if (identityConfig) {
|
|
1565
|
+
if (typeof identityConfig.signUp === "function") {
|
|
1566
|
+
this.identityProvider = identityConfig;
|
|
1567
|
+
if (this.identityProvider instanceof SelfManagedIdentityProvider) {
|
|
1568
|
+
this.credentialsManager = this.identityProvider.getCredentialsManager();
|
|
1569
|
+
} else if (this.identityProvider instanceof CompositeIdentityProvider) {
|
|
1570
|
+
for (const child of Object.values(this.identityProvider.providers)) {
|
|
1571
|
+
if (child instanceof SelfManagedIdentityProvider) {
|
|
1572
|
+
this.credentialsManager = child.getCredentialsManager();
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} else {
|
|
1578
|
+
const selfManaged = new SelfManagedIdentityProvider(identityConfig);
|
|
1579
|
+
this.identityProvider = selfManaged;
|
|
1580
|
+
this.credentialsManager = selfManaged.getCredentialsManager();
|
|
1581
|
+
}
|
|
1582
|
+
} else if (config.credentials !== void 0 && config.credentials.enabled !== false) {
|
|
1583
|
+
const selfManaged = new SelfManagedIdentityProvider(config.credentials);
|
|
1584
|
+
this.identityProvider = selfManaged;
|
|
1585
|
+
this.credentialsManager = selfManaged.getCredentialsManager();
|
|
1586
|
+
}
|
|
1587
|
+
if (this.identityProvider && typeof this.identityProvider.getProviders === "function") {
|
|
1588
|
+
const idpOAuthProviders = this.identityProvider.getProviders();
|
|
1589
|
+
this.config.providers = {
|
|
1590
|
+
...idpOAuthProviders,
|
|
1591
|
+
...this.config.providers || {}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
const oauthProviders = this.config.providers;
|
|
914
1595
|
this.log("INFO", "Init", "Initializing Lixa instance", {
|
|
915
|
-
providers:
|
|
916
|
-
|
|
1596
|
+
providers: oauthProviders ? Object.keys(oauthProviders) : [],
|
|
1597
|
+
identityProvider: this.identityProvider ? this.identityProvider.name : void 0,
|
|
1598
|
+
identityEnabled: this.identityProvider !== void 0,
|
|
1599
|
+
credentialsEnabled: this.identityProvider !== void 0,
|
|
917
1600
|
debug: this.debug
|
|
918
1601
|
});
|
|
919
|
-
if (
|
|
920
|
-
for (const [providerName, providerConfig] of Object.entries(
|
|
1602
|
+
if (oauthProviders) {
|
|
1603
|
+
for (const [providerName, providerConfig] of Object.entries(oauthProviders)) {
|
|
921
1604
|
const name = providerName.toLowerCase();
|
|
922
1605
|
const typedConfig = providerConfig;
|
|
923
1606
|
this.validateProviderConfig(providerName, typedConfig);
|
|
@@ -946,7 +1629,22 @@ var Lixa = class _Lixa {
|
|
|
946
1629
|
* @throws InvalidProviderConfigError when required fields are missing or invalid
|
|
947
1630
|
*/
|
|
948
1631
|
validateProviderConfig(name, config) {
|
|
949
|
-
const
|
|
1632
|
+
const hasScopes = Array.isArray(config.scopes) && config.scopes.length > 0 || config.scopes && typeof config.scopes === "object" && !Array.isArray(config.scopes) && (config.scopes.auth && config.scopes.auth.length > 0 || config.scopes.resource && config.scopes.resource.length > 0) || config.authScopes && config.authScopes.length > 0 || config.resourceScopes && config.resourceScopes.length > 0;
|
|
1633
|
+
if (!hasScopes) {
|
|
1634
|
+
const p = config.provider;
|
|
1635
|
+
if (p?.defaultScopes && Array.isArray(p.defaultScopes) && p.defaultScopes.length > 0) {
|
|
1636
|
+
config.scopes = { auth: [...p.defaultScopes] };
|
|
1637
|
+
} else if (p?.authScopes && Array.isArray(p.authScopes) && p.authScopes.length > 0) {
|
|
1638
|
+
config.scopes = { auth: [...p.authScopes] };
|
|
1639
|
+
} else if (name === "google" || name.startsWith("google")) {
|
|
1640
|
+
config.scopes = { auth: ["openid", "profile", "email"] };
|
|
1641
|
+
} else if (name === "github" || name.startsWith("github")) {
|
|
1642
|
+
config.scopes = { auth: ["read:user", "user:email"] };
|
|
1643
|
+
} else if (name.startsWith("cognito")) {
|
|
1644
|
+
config.scopes = { auth: ["openid", "email", "profile"] };
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
const requiredFields = ["clientId", "clientSecret", "redirectUri"];
|
|
950
1648
|
const missingFields = requiredFields.filter((field) => {
|
|
951
1649
|
const value = config[field];
|
|
952
1650
|
return value === void 0 || value === null || typeof value === "string" && value.trim() === "";
|
|
@@ -957,16 +1655,33 @@ var Lixa = class _Lixa {
|
|
|
957
1655
|
{ provider: name, missingFields }
|
|
958
1656
|
);
|
|
959
1657
|
}
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1658
|
+
if (config.scopes) {
|
|
1659
|
+
if (Array.isArray(config.scopes)) {
|
|
1660
|
+
if (config.scopes.length === 0) {
|
|
1661
|
+
throw new InvalidProviderConfigError(
|
|
1662
|
+
`Provider '${name}' configuration error: 'scopes' array cannot be empty`,
|
|
1663
|
+
{ provider: name }
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
} else if (typeof config.scopes === "object") {
|
|
1667
|
+
const hasAuth = Array.isArray(config.scopes.auth) && config.scopes.auth.length > 0;
|
|
1668
|
+
const hasResource = Array.isArray(config.scopes.resource) && config.scopes.resource.length > 0;
|
|
1669
|
+
if (!hasAuth && !hasResource) {
|
|
1670
|
+
throw new InvalidProviderConfigError(
|
|
1671
|
+
`Provider '${name}' configuration error: 'scopes' object must specify non-empty 'auth' or 'resource' array`,
|
|
1672
|
+
{ provider: name }
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
} else {
|
|
1676
|
+
throw new InvalidProviderConfigError(
|
|
1677
|
+
`Provider '${name}' configuration error: 'scopes' must be an array of strings or { auth?: string[]; resource?: string[] }`,
|
|
1678
|
+
{ provider: name }
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
} else if (!config.authScopes && !config.resourceScopes) {
|
|
967
1682
|
throw new InvalidProviderConfigError(
|
|
968
|
-
`Provider '${name}' configuration
|
|
969
|
-
{ provider: name }
|
|
1683
|
+
`Provider '${name}' configuration is missing required fields: scopes`,
|
|
1684
|
+
{ provider: name, missingFields: ["scopes"] }
|
|
970
1685
|
);
|
|
971
1686
|
}
|
|
972
1687
|
}
|
|
@@ -1139,7 +1854,7 @@ var Lixa = class _Lixa {
|
|
|
1139
1854
|
* The state parameter is used to prevent CSRF attacks in OAuth flows.
|
|
1140
1855
|
*/
|
|
1141
1856
|
static generateRandomState() {
|
|
1142
|
-
return (0,
|
|
1857
|
+
return (0, import_crypto5.randomBytes)(16).toString("hex");
|
|
1143
1858
|
}
|
|
1144
1859
|
/**
|
|
1145
1860
|
* Generates a cryptographically secure code verifier for PKCE flows.
|
|
@@ -1176,7 +1891,7 @@ var Lixa = class _Lixa {
|
|
|
1176
1891
|
* @internal
|
|
1177
1892
|
*/
|
|
1178
1893
|
static generateCodeVerifier() {
|
|
1179
|
-
return (0,
|
|
1894
|
+
return (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
1180
1895
|
}
|
|
1181
1896
|
/**
|
|
1182
1897
|
* Generates a code challenge from a code verifier for PKCE flows.
|
|
@@ -1222,7 +1937,7 @@ var Lixa = class _Lixa {
|
|
|
1222
1937
|
* @internal
|
|
1223
1938
|
*/
|
|
1224
1939
|
static buildCodeChallenge(codeVerifier) {
|
|
1225
|
-
const hash =
|
|
1940
|
+
const hash = import_crypto6.default.createHash("sha256").update(codeVerifier).digest("base64");
|
|
1226
1941
|
return hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1227
1942
|
}
|
|
1228
1943
|
/**
|
|
@@ -1264,8 +1979,8 @@ var Lixa = class _Lixa {
|
|
|
1264
1979
|
await storage.saveState(stateValue, generated.data, 300);
|
|
1265
1980
|
} else {
|
|
1266
1981
|
this.log("INFO", "State", "Using default state generation");
|
|
1267
|
-
stateValue = state || (0,
|
|
1268
|
-
codeVerifier = (0,
|
|
1982
|
+
stateValue = state || (0, import_crypto5.randomBytes)(16).toString("hex");
|
|
1983
|
+
codeVerifier = (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
1269
1984
|
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
1270
1985
|
await storage.saveState(
|
|
1271
1986
|
stateValue,
|
|
@@ -1280,12 +1995,17 @@ var Lixa = class _Lixa {
|
|
|
1280
1995
|
}
|
|
1281
1996
|
const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
|
|
1282
1997
|
this.log("INFO", "State", `Saved state for provider: ${providerType}`, { state: stateValue });
|
|
1998
|
+
const rawAuthScopes = providerConfig.authScopes || (Array.isArray(providerConfig.scopes) ? providerConfig.scopes : providerConfig.scopes?.auth) || [];
|
|
1283
1999
|
const authNScopes = this.resolveAuthNScopes(
|
|
1284
2000
|
providerType,
|
|
1285
|
-
|
|
2001
|
+
rawAuthScopes,
|
|
1286
2002
|
providerImpl,
|
|
1287
2003
|
providerConfig.allowNonAuthScopes
|
|
1288
2004
|
);
|
|
2005
|
+
const authorizationParams = {
|
|
2006
|
+
...providerConfig.extraConfig || {},
|
|
2007
|
+
...providerConfig.authorizationParams || {}
|
|
2008
|
+
};
|
|
1289
2009
|
const params = new URLSearchParams({
|
|
1290
2010
|
client_id: providerConfig.clientId,
|
|
1291
2011
|
redirect_uri: providerConfig.redirectUri,
|
|
@@ -1294,7 +2014,7 @@ var Lixa = class _Lixa {
|
|
|
1294
2014
|
response_type: "code",
|
|
1295
2015
|
code_challenge: codeChallenge,
|
|
1296
2016
|
code_challenge_method: "S256",
|
|
1297
|
-
...
|
|
2017
|
+
...authorizationParams
|
|
1298
2018
|
});
|
|
1299
2019
|
const authUrl = `${providerImpl.authorizationEndpoint}?${params.toString()}`;
|
|
1300
2020
|
this.log("INFO", "Auth", `Authorization URL generated successfully`, {
|
|
@@ -1368,7 +2088,8 @@ var Lixa = class _Lixa {
|
|
|
1368
2088
|
async handleCallback({
|
|
1369
2089
|
provider,
|
|
1370
2090
|
code,
|
|
1371
|
-
state
|
|
2091
|
+
state,
|
|
2092
|
+
sessionId
|
|
1372
2093
|
}) {
|
|
1373
2094
|
const providerType = String(provider).toLowerCase();
|
|
1374
2095
|
this.log("INFO", "Auth", `Handling OAuth callback for provider: ${providerType}`);
|
|
@@ -1432,53 +2153,156 @@ var Lixa = class _Lixa {
|
|
|
1432
2153
|
}
|
|
1433
2154
|
const session = await generateSession(tokens, providerMetadata);
|
|
1434
2155
|
session.provider = session.provider || providerType;
|
|
2156
|
+
session.token = session.token || tokens.access_token;
|
|
2157
|
+
session.raw = session.raw || tokens;
|
|
2158
|
+
if (extractedUserInfo?.sub && !session.userId) {
|
|
2159
|
+
session.userId = extractedUserInfo.sub;
|
|
2160
|
+
} else if (extractedUserInfo?.id && !session.userId) {
|
|
2161
|
+
session.userId = String(extractedUserInfo.id);
|
|
2162
|
+
}
|
|
1435
2163
|
if (extractedUserInfo?.email && !session.email) {
|
|
1436
2164
|
session.email = extractedUserInfo.email;
|
|
1437
2165
|
}
|
|
1438
2166
|
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1439
2167
|
const linkingConfig = this.config.accountLinking;
|
|
1440
|
-
const mode = String(linkingConfig?.mode || "");
|
|
1441
|
-
const isLinkByEmail = mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
|
|
2168
|
+
const mode = typeof linkingConfig === "object" ? String(linkingConfig?.mode || "") : "";
|
|
2169
|
+
const isLinkByEmail = linkingConfig === true || linkingConfig === "linkByEmail" || linkingConfig === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || mode === "AUTO_LINK_BY_VERIFIED_EMAIL" || mode === "linkByEmail";
|
|
2170
|
+
const isStepUpMode = linkingConfig === "STEP_UP_VERIFICATION" || linkingConfig === "STEP_UP_VERIFICATION" /* STEP_UP_VERIFICATION */ || mode === "STEP_UP_VERIFICATION" || mode === "STEP_UP_VERIFICATION" /* STEP_UP_VERIFICATION */ || mode === "stepUp";
|
|
1442
2171
|
const email = extractedUserInfo?.email;
|
|
1443
|
-
const isVerified = extractedUserInfo?.email_verified
|
|
1444
|
-
const requireVerified = linkingConfig?.requireVerifiedEmail ?? true;
|
|
1445
|
-
const canLink = isLinkByEmail && email && (!requireVerified || isVerified);
|
|
2172
|
+
const isVerified = extractedUserInfo?.email_verified === true;
|
|
2173
|
+
const requireVerified = typeof linkingConfig === "object" ? linkingConfig?.requireVerifiedEmail ?? true : true;
|
|
2174
|
+
const canLink = (isLinkByEmail || isStepUpMode) && email && (!requireVerified || isVerified);
|
|
2175
|
+
if ((isLinkByEmail || isStepUpMode) && email && !isVerified && requireVerified) {
|
|
2176
|
+
this.log("WARN", "AccountLinking", `Rejected account linking for provider '${providerType}': email '${email}' is not cryptographically verified by the provider.`);
|
|
2177
|
+
}
|
|
2178
|
+
if (sessionId) {
|
|
2179
|
+
const activeSession = await sessionStorage.getSession(sessionId);
|
|
2180
|
+
if (activeSession) {
|
|
2181
|
+
this.log("INFO", "AccountLinking", `Linking provider '${providerType}' directly to active session '${sessionId}'`);
|
|
2182
|
+
const linkedAcc2 = {
|
|
2183
|
+
provider: providerType,
|
|
2184
|
+
providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
|
|
2185
|
+
email: extractedUserInfo?.email || activeSession.email,
|
|
2186
|
+
linkedAt: Date.now()
|
|
2187
|
+
};
|
|
2188
|
+
if (this.accountHandler?.accountStorage) {
|
|
2189
|
+
const userKey = this.getUserKeyFromSession(activeSession);
|
|
2190
|
+
await this.accountHandler.accountStorage.saveAccount(userKey, providerType, this.sanitizeIdentityForStorage(linkedAcc2));
|
|
2191
|
+
}
|
|
2192
|
+
return sessionId;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (canLink) {
|
|
2196
|
+
const existingAccounts = this.accountHandler?.accountStorage?.findAccountsByEmail ? await this.accountHandler.accountStorage.findAccountsByEmail(email) : [];
|
|
2197
|
+
const existingSessionRecord = sessionStorage.getSessionByEmail ? await sessionStorage.getSessionByEmail(email) : null;
|
|
2198
|
+
const existingProviders = /* @__PURE__ */ new Set();
|
|
2199
|
+
let existingUserId;
|
|
2200
|
+
if (existingSessionRecord) {
|
|
2201
|
+
existingUserId = existingSessionRecord.session.userId || existingSessionRecord.session.email;
|
|
2202
|
+
if (existingSessionRecord.session.provider) {
|
|
2203
|
+
existingProviders.add(existingSessionRecord.session.provider.toLowerCase());
|
|
2204
|
+
}
|
|
2205
|
+
if (existingSessionRecord.session.accounts) {
|
|
2206
|
+
Object.keys(existingSessionRecord.session.accounts).forEach(
|
|
2207
|
+
(p) => existingProviders.add(p.toLowerCase())
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
for (const acc of existingAccounts) {
|
|
2212
|
+
if (!existingUserId) existingUserId = acc.userId;
|
|
2213
|
+
existingProviders.add(acc.provider.toLowerCase());
|
|
2214
|
+
}
|
|
2215
|
+
if (existingProviders.size > 0 && !existingProviders.has(providerType.toLowerCase())) {
|
|
2216
|
+
if (isStepUpMode) {
|
|
2217
|
+
let hasPassword = existingProviders.has("credentials") || existingProviders.has("self-managed") || existingProviders.has("cognito");
|
|
2218
|
+
if (!hasPassword && this.credentialsManager) {
|
|
2219
|
+
try {
|
|
2220
|
+
const credUser = await this.credentialsManager.getUserByIdentifier(email);
|
|
2221
|
+
if (credUser) {
|
|
2222
|
+
hasPassword = true;
|
|
2223
|
+
}
|
|
2224
|
+
} catch {
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const challengeToken = (0, import_crypto5.randomBytes)(24).toString("hex");
|
|
2228
|
+
const stateStorage2 = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
2229
|
+
await stateStorage2.saveState(
|
|
2230
|
+
`linking_challenge:${challengeToken}`,
|
|
2231
|
+
{
|
|
2232
|
+
provider: providerType,
|
|
2233
|
+
codeVerifier: "",
|
|
2234
|
+
createdAt: Date.now(),
|
|
2235
|
+
challengeToken,
|
|
2236
|
+
email,
|
|
2237
|
+
incomingProvider: providerType,
|
|
2238
|
+
incomingProviderUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
|
|
2239
|
+
existingUserId: existingUserId || email,
|
|
2240
|
+
existingProviders: Array.from(existingProviders),
|
|
2241
|
+
hasPassword,
|
|
2242
|
+
tokens
|
|
2243
|
+
},
|
|
2244
|
+
600
|
|
2245
|
+
);
|
|
2246
|
+
this.log(
|
|
2247
|
+
"WARN",
|
|
2248
|
+
"AccountLinking",
|
|
2249
|
+
`Step-up challenge required for user '${email}'. Existing: [${Array.from(existingProviders).join(
|
|
2250
|
+
", "
|
|
2251
|
+
)}], Incoming: '${providerType}'`
|
|
2252
|
+
);
|
|
2253
|
+
throw new AccountLinkingChallengeRequiredError(
|
|
2254
|
+
`An account with email '${email}' already exists. Verification required before linking.`,
|
|
2255
|
+
{
|
|
2256
|
+
challengeToken,
|
|
2257
|
+
email,
|
|
2258
|
+
incomingProvider: providerType,
|
|
2259
|
+
incomingProviderUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
|
|
2260
|
+
existingUserId: existingUserId || email,
|
|
2261
|
+
existingProviders: Array.from(existingProviders),
|
|
2262
|
+
hasPassword
|
|
2263
|
+
}
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
1446
2268
|
if (canLink && sessionStorage.getSessionByEmail) {
|
|
1447
2269
|
const existingRecord = await sessionStorage.getSessionByEmail(email);
|
|
1448
2270
|
if (existingRecord) {
|
|
1449
2271
|
this.log("INFO", "AccountLinking", `Linking provider '${providerType}' to existing session for email '${email}'`);
|
|
1450
2272
|
const { sessionId: existingSessionId, session: existingSession } = existingRecord;
|
|
1451
|
-
|
|
1452
|
-
existingSession.accounts[providerType] = {
|
|
2273
|
+
const linkedAcc2 = {
|
|
1453
2274
|
provider: providerType,
|
|
1454
2275
|
providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
|
|
1455
2276
|
email,
|
|
1456
|
-
accessToken: tokens.access_token,
|
|
1457
|
-
raw: tokens,
|
|
1458
2277
|
linkedAt: Date.now()
|
|
1459
2278
|
};
|
|
1460
2279
|
existingSession.provider = providerType;
|
|
1461
2280
|
existingSession.token = tokens.access_token;
|
|
1462
2281
|
existingSession.raw = tokens;
|
|
1463
2282
|
await sessionStorage.saveSession(existingSessionId, existingSession, 86400);
|
|
2283
|
+
if (this.accountHandler?.accountStorage) {
|
|
2284
|
+
const userKey = this.getUserKeyFromSession(existingSession);
|
|
2285
|
+
await this.accountHandler.accountStorage.saveAccount(userKey, providerType, this.sanitizeIdentityForStorage(linkedAcc2));
|
|
2286
|
+
}
|
|
1464
2287
|
return existingSessionId;
|
|
1465
2288
|
}
|
|
1466
2289
|
}
|
|
1467
|
-
|
|
1468
|
-
session.accounts[providerType] = {
|
|
2290
|
+
const linkedAcc = {
|
|
1469
2291
|
provider: providerType,
|
|
1470
2292
|
providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
|
|
1471
2293
|
email: extractedUserInfo?.email,
|
|
1472
|
-
accessToken: tokens.access_token,
|
|
1473
|
-
raw: tokens,
|
|
1474
2294
|
linkedAt: Date.now()
|
|
1475
2295
|
};
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
2296
|
+
if (this.accountHandler?.accountStorage) {
|
|
2297
|
+
const userKey = this.getUserKeyFromSession(session);
|
|
2298
|
+
await this.accountHandler.accountStorage.saveAccount(userKey, providerType, this.sanitizeIdentityForStorage(linkedAcc));
|
|
2299
|
+
}
|
|
2300
|
+
const newSessionId = (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
2301
|
+
session.sessionId = newSessionId;
|
|
2302
|
+
this.log("INFO", "Session", "Storing session", { sessionId: newSessionId });
|
|
2303
|
+
await sessionStorage.saveSession(newSessionId, session, 86400);
|
|
2304
|
+
this.log("INFO", "Session", "Session created successfully", { sessionId: newSessionId });
|
|
2305
|
+
return newSessionId;
|
|
1482
2306
|
}
|
|
1483
2307
|
/**
|
|
1484
2308
|
* Explicitly link a new OAuth provider account to an active session.
|
|
@@ -1527,19 +2351,28 @@ var Lixa = class _Lixa {
|
|
|
1527
2351
|
extractedUserInfo = userInfo;
|
|
1528
2352
|
} catch {
|
|
1529
2353
|
}
|
|
1530
|
-
|
|
1531
|
-
existingSession.accounts[providerType] = {
|
|
2354
|
+
const linkedAcc = {
|
|
1532
2355
|
provider: providerType,
|
|
1533
2356
|
providerUserId: extractedUserInfo?.sub || extractedUserInfo?.id,
|
|
1534
2357
|
email: extractedUserInfo?.email,
|
|
1535
|
-
accessToken: tokens.access_token,
|
|
1536
|
-
raw: tokens,
|
|
1537
2358
|
linkedAt: Date.now()
|
|
1538
2359
|
};
|
|
1539
2360
|
if (extractedUserInfo?.email && !existingSession.email) {
|
|
1540
2361
|
existingSession.email = extractedUserInfo.email;
|
|
2362
|
+
await sessionStorage.saveSession(sessionId, existingSession, 86400);
|
|
2363
|
+
}
|
|
2364
|
+
if (this.accountHandler?.accountStorage) {
|
|
2365
|
+
const userKey = this.getUserKeyFromSession(existingSession);
|
|
2366
|
+
await this.accountHandler.accountStorage.saveAccount(
|
|
2367
|
+
userKey,
|
|
2368
|
+
providerType,
|
|
2369
|
+
this.sanitizeIdentityForStorage(linkedAcc)
|
|
2370
|
+
);
|
|
1541
2371
|
}
|
|
1542
|
-
|
|
2372
|
+
this.log("INFO", "AccountLinking", `Linked provider '${providerType}' directly to session '${sessionId}'`, {
|
|
2373
|
+
provider: providerType,
|
|
2374
|
+
sessionId
|
|
2375
|
+
});
|
|
1543
2376
|
return sessionId;
|
|
1544
2377
|
}
|
|
1545
2378
|
/**
|
|
@@ -1552,46 +2385,203 @@ var Lixa = class _Lixa {
|
|
|
1552
2385
|
async unlinkAccount(sessionId, providerToUnlink) {
|
|
1553
2386
|
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1554
2387
|
const session = await sessionStorage.getSession(sessionId);
|
|
1555
|
-
if (!session
|
|
1556
|
-
throw new AccountUnlinkError("Session not found
|
|
2388
|
+
if (!session) {
|
|
2389
|
+
throw new AccountUnlinkError("Session not found.", { sessionId });
|
|
2390
|
+
}
|
|
2391
|
+
const providerKey = providerToUnlink.toLowerCase();
|
|
2392
|
+
const userKey = this.getUserKeyFromSession(session);
|
|
2393
|
+
let allAccounts = {};
|
|
2394
|
+
if (this.accountHandler?.accountStorage) {
|
|
2395
|
+
allAccounts = await this.accountHandler.accountStorage.getUserAccounts(userKey) || {};
|
|
2396
|
+
}
|
|
2397
|
+
if (!allAccounts[providerKey]) {
|
|
2398
|
+
throw new AccountUnlinkError(`Provider '${providerToUnlink}' is not linked to this account.`, {
|
|
2399
|
+
sessionId,
|
|
2400
|
+
provider: providerToUnlink
|
|
2401
|
+
});
|
|
1557
2402
|
}
|
|
1558
|
-
const linkedProviders = Object.keys(
|
|
2403
|
+
const linkedProviders = Object.keys(allAccounts);
|
|
1559
2404
|
if (linkedProviders.length <= 1) {
|
|
1560
2405
|
throw new AccountUnlinkError("Cannot unlink the only authentication provider for this account.", {
|
|
1561
2406
|
sessionId,
|
|
1562
2407
|
provider: providerToUnlink
|
|
1563
2408
|
});
|
|
1564
2409
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
2410
|
+
if (this.accountHandler?.accountStorage) {
|
|
2411
|
+
await this.accountHandler.accountStorage.deleteAccount(userKey, providerKey);
|
|
2412
|
+
}
|
|
2413
|
+
if (session.provider?.toLowerCase() === providerKey) {
|
|
2414
|
+
const remaining = linkedProviders.filter((p) => p !== providerKey);
|
|
2415
|
+
session.provider = remaining.length > 0 ? remaining[0] : void 0;
|
|
2416
|
+
await sessionStorage.saveSession(sessionId, session, 86400);
|
|
2417
|
+
}
|
|
2418
|
+
this.log("INFO", "AccountLinking", `Unlinked provider '${providerKey}' for user '${session.userId || session.email}'`, {
|
|
2419
|
+
sessionId,
|
|
2420
|
+
provider: providerKey
|
|
2421
|
+
});
|
|
1567
2422
|
return true;
|
|
1568
2423
|
}
|
|
1569
2424
|
/**
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
* @remarks
|
|
1573
|
-
* Resource authorization is kept strictly separate from primary authentication (AuthN).
|
|
1574
|
-
* Call this method after a user is authenticated to request permissions for external API access
|
|
1575
|
-
* (e.g. GitHub repositories, Google Drive, Slack, etc.).
|
|
2425
|
+
* Confirms a pending account linking step-up challenge using the user's password.
|
|
2426
|
+
* On successful verification, links the pending provider to the existing account and creates a session.
|
|
1576
2427
|
*
|
|
1577
|
-
* @param params -
|
|
1578
|
-
* @returns
|
|
2428
|
+
* @param params - challengeToken and password
|
|
2429
|
+
* @returns Session ID, Session, and User identity
|
|
2430
|
+
* @throws InvalidStateError if challenge is invalid or expired
|
|
2431
|
+
* @throws InvalidCredentialsError if password verification fails
|
|
1579
2432
|
*/
|
|
1580
|
-
async
|
|
1581
|
-
const {
|
|
1582
|
-
const
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2433
|
+
async confirmLinkingWithPassword(params) {
|
|
2434
|
+
const { challengeToken, password } = params;
|
|
2435
|
+
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
2436
|
+
const stateKey = `linking_challenge:${challengeToken}`;
|
|
2437
|
+
const cached = await stateStorage.getState(stateKey);
|
|
2438
|
+
if (!cached || !cached.challengeToken) {
|
|
2439
|
+
throw new InvalidStateError("Invalid or expired account linking challenge.");
|
|
2440
|
+
}
|
|
2441
|
+
let verifiedUser = null;
|
|
2442
|
+
let passwordTokens = void 0;
|
|
2443
|
+
if (this.identityProvider) {
|
|
2444
|
+
try {
|
|
2445
|
+
const signInResult = await this.identityProvider.signIn({
|
|
2446
|
+
identifier: cached.email,
|
|
2447
|
+
password
|
|
2448
|
+
});
|
|
2449
|
+
verifiedUser = signInResult.user;
|
|
2450
|
+
passwordTokens = signInResult.tokens;
|
|
2451
|
+
} catch {
|
|
2452
|
+
verifiedUser = await this.verifyCredentials({
|
|
2453
|
+
identifier: cached.email,
|
|
2454
|
+
password
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
} else {
|
|
2458
|
+
verifiedUser = await this.verifyCredentials({
|
|
2459
|
+
identifier: cached.email,
|
|
2460
|
+
password
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
if (!verifiedUser) {
|
|
2464
|
+
throw new InvalidCredentialsError("Invalid password for account linking verification.");
|
|
2465
|
+
}
|
|
2466
|
+
await stateStorage.deleteState(stateKey);
|
|
2467
|
+
const providerType = String(cached.incomingProvider).toLowerCase();
|
|
2468
|
+
const incomingTokens = cached.tokens;
|
|
2469
|
+
const linkedAcc = {
|
|
2470
|
+
provider: providerType,
|
|
2471
|
+
providerUserId: cached.incomingProviderUserId,
|
|
2472
|
+
email: cached.email,
|
|
2473
|
+
linkedAt: Date.now()
|
|
2474
|
+
};
|
|
2475
|
+
if (this.accountHandler?.accountStorage) {
|
|
2476
|
+
await this.accountHandler.accountStorage.saveAccount(
|
|
2477
|
+
cached.existingUserId,
|
|
2478
|
+
providerType,
|
|
2479
|
+
this.sanitizeIdentityForStorage(linkedAcc)
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
const passwordProviderName = verifiedUser && verifiedUser.provider || (this.identityProvider instanceof CompositeIdentityProvider ? this.identityProvider.defaultProvider : this.identityProvider?.name) || "credentials";
|
|
2483
|
+
const passwordAccountKey = passwordProviderName === "self-managed" ? "credentials" : passwordProviderName;
|
|
2484
|
+
if (this.accountHandler?.accountStorage) {
|
|
2485
|
+
await this.accountHandler.accountStorage.saveAccount(
|
|
2486
|
+
cached.existingUserId,
|
|
2487
|
+
passwordAccountKey,
|
|
2488
|
+
this.sanitizeIdentityForStorage({
|
|
2489
|
+
provider: passwordAccountKey,
|
|
2490
|
+
providerUserId: verifiedUser.id,
|
|
2491
|
+
email: verifiedUser.email,
|
|
2492
|
+
linkedAt: Date.now()
|
|
2493
|
+
})
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
const sessionId = (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
2497
|
+
const session = {
|
|
2498
|
+
sessionId,
|
|
2499
|
+
userId: cached.existingUserId,
|
|
2500
|
+
email: cached.email,
|
|
2501
|
+
provider: providerType,
|
|
2502
|
+
token: incomingTokens?.access_token,
|
|
2503
|
+
raw: incomingTokens
|
|
2504
|
+
};
|
|
2505
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
2506
|
+
await sessionStorage.saveSession(sessionId, session, 86400);
|
|
2507
|
+
this.log(
|
|
2508
|
+
"INFO",
|
|
2509
|
+
"AccountLinking",
|
|
2510
|
+
`Successfully linked '${providerType}' to user '${cached.existingUserId}' via password step-up.`
|
|
2511
|
+
);
|
|
2512
|
+
return { sessionId, session, user: verifiedUser };
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* Sets a password for an SSO-authenticated user who doesn't have one yet.
|
|
2516
|
+
* Must be called from an authenticated context (user has active session proving identity).
|
|
2517
|
+
*
|
|
2518
|
+
* @param params - userId, newPassword, optional identifier and email
|
|
2519
|
+
* @returns True if password was set successfully
|
|
2520
|
+
* @throws IdentityNotConfiguredError if no identity provider configured
|
|
2521
|
+
* @throws WeakPasswordError if password doesn't meet policy
|
|
2522
|
+
*/
|
|
2523
|
+
async setPassword(params) {
|
|
2524
|
+
this.log("INFO", "Identity", "Processing setPassword request", { userId: params.userId });
|
|
2525
|
+
if (this.credentialsManager) {
|
|
2526
|
+
const result = await this.credentialsManager.setPassword(params);
|
|
2527
|
+
if (result && this.accountHandler?.accountStorage) {
|
|
2528
|
+
const providerName = this.identityProvider?.name === "self-managed" ? "credentials" : this.identityProvider?.name || "credentials";
|
|
2529
|
+
const credentialsAccount = {
|
|
2530
|
+
provider: providerName,
|
|
2531
|
+
providerUserId: params.userId,
|
|
2532
|
+
email: params.email,
|
|
2533
|
+
linkedAt: Date.now()
|
|
2534
|
+
};
|
|
2535
|
+
await this.accountHandler.accountStorage.saveAccount(
|
|
2536
|
+
params.userId,
|
|
2537
|
+
providerName,
|
|
2538
|
+
this.sanitizeIdentityForStorage(credentialsAccount)
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
this.log("INFO", "Identity", "Password set successfully", { userId: params.userId });
|
|
2542
|
+
return result;
|
|
2543
|
+
}
|
|
2544
|
+
if (this.identityProvider && typeof this.identityProvider.changePassword === "function") {
|
|
2545
|
+
throw new IdentityProviderError(
|
|
2546
|
+
"Setting a password for SSO users is only supported with self-managed credentials. Use the provider's admin console instead.",
|
|
2547
|
+
this.identityProvider.name
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
throw new CredentialsNotConfiguredError(
|
|
2551
|
+
"Cannot set password: No credentials manager is configured on this Lixa instance."
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Generates an authorization URL for connecting a resource provider (AuthZ) post-login.
|
|
2556
|
+
*
|
|
2557
|
+
* @remarks
|
|
2558
|
+
* Resource authorization is kept strictly separate from primary authentication (AuthN).
|
|
2559
|
+
* Call this method after a user is authenticated to request permissions for external API access
|
|
2560
|
+
* (e.g. GitHub repositories, Google Drive, Slack, etc.).
|
|
2561
|
+
*
|
|
2562
|
+
* @param params - Object containing sessionId, provider, requested resource scopes, and optional state
|
|
2563
|
+
* @returns The authorization URL for resource consent
|
|
2564
|
+
*/
|
|
2565
|
+
async getResourceAuthUrl(params) {
|
|
2566
|
+
const { sessionId, provider, state, prompt, extraConfig, authorizationParams } = params;
|
|
2567
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
2568
|
+
const activeSession = await sessionStorage.getSession(sessionId);
|
|
2569
|
+
if (!activeSession) {
|
|
2570
|
+
throw new SessionNotFoundError("Authentication required. Active session must exist to connect resource providers.", { sessionId });
|
|
2571
|
+
}
|
|
1587
2572
|
const providerType = String(provider).toLowerCase();
|
|
1588
2573
|
const providerConfig = this.findProviderByType(providerType);
|
|
1589
2574
|
if (!providerConfig) {
|
|
1590
2575
|
throw new ProviderNotConfiguredError(String(provider));
|
|
1591
2576
|
}
|
|
2577
|
+
const defaultResourceScopes = providerConfig.resourceScopes || (Array.isArray(providerConfig.scopes) ? void 0 : providerConfig.scopes?.resource) || [];
|
|
2578
|
+
const requestedScopes = params.scopes && params.scopes.length > 0 ? params.scopes : defaultResourceScopes;
|
|
2579
|
+
if (requestedScopes.length === 0) {
|
|
2580
|
+
throw new Error(`No scopes specified for resource '${provider}'. Provide scopes in getResourceAuthUrl() or configure 'scopes.resource' / 'resourceScopes' on the provider.`);
|
|
2581
|
+
}
|
|
1592
2582
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1593
|
-
const stateValue = state || (0,
|
|
1594
|
-
const codeVerifier = (0,
|
|
2583
|
+
const stateValue = state || (0, import_crypto5.randomBytes)(16).toString("hex");
|
|
2584
|
+
const codeVerifier = (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
1595
2585
|
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
1596
2586
|
await storage.saveState(
|
|
1597
2587
|
stateValue,
|
|
@@ -1603,22 +2593,35 @@ var Lixa = class _Lixa {
|
|
|
1603
2593
|
300
|
|
1604
2594
|
);
|
|
1605
2595
|
const codeChallenge = _Lixa.buildCodeChallenge(codeVerifier);
|
|
2596
|
+
const mergedAuthParams = {
|
|
2597
|
+
...providerConfig.extraConfig || {},
|
|
2598
|
+
...providerConfig.authorizationParams || {},
|
|
2599
|
+
...extraConfig || {},
|
|
2600
|
+
...authorizationParams || {}
|
|
2601
|
+
};
|
|
1606
2602
|
const searchParams = new URLSearchParams({
|
|
1607
2603
|
client_id: providerConfig.clientId,
|
|
1608
2604
|
redirect_uri: providerConfig.redirectUri,
|
|
1609
|
-
scope:
|
|
2605
|
+
scope: requestedScopes.join(" "),
|
|
1610
2606
|
state: stateValue,
|
|
1611
2607
|
response_type: "code",
|
|
1612
2608
|
code_challenge: codeChallenge,
|
|
1613
2609
|
code_challenge_method: "S256",
|
|
1614
2610
|
...prompt ? { prompt } : {},
|
|
1615
|
-
...
|
|
1616
|
-
...extraConfig
|
|
2611
|
+
...mergedAuthParams
|
|
1617
2612
|
});
|
|
1618
2613
|
return `${providerImpl.authorizationEndpoint}?${searchParams.toString()}`;
|
|
1619
2614
|
}
|
|
1620
2615
|
getUserKeyFromSession(session) {
|
|
1621
|
-
return session.userId || session.email || session.
|
|
2616
|
+
return session.userId || session.email || session.sessionId || "anonymous";
|
|
2617
|
+
}
|
|
2618
|
+
sanitizeIdentityForStorage(account) {
|
|
2619
|
+
return {
|
|
2620
|
+
provider: account.provider,
|
|
2621
|
+
providerUserId: account.providerUserId,
|
|
2622
|
+
email: account.email,
|
|
2623
|
+
linkedAt: account.linkedAt || Date.now()
|
|
2624
|
+
};
|
|
1622
2625
|
}
|
|
1623
2626
|
/**
|
|
1624
2627
|
* Handles the OAuth callback for a connected resource provider and stores resource tokens bound to user account.
|
|
@@ -1636,7 +2639,7 @@ var Lixa = class _Lixa {
|
|
|
1636
2639
|
throw new ProviderNotConfiguredError(String(provider));
|
|
1637
2640
|
}
|
|
1638
2641
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1639
|
-
let codeVerifier = (0,
|
|
2642
|
+
let codeVerifier = (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
1640
2643
|
if (state) {
|
|
1641
2644
|
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
1642
2645
|
const cachedState = await stateStorage.getState(state);
|
|
@@ -1648,12 +2651,14 @@ var Lixa = class _Lixa {
|
|
|
1648
2651
|
}
|
|
1649
2652
|
const tokens = await this.exchangeCodeForToken(code, providerConfig, providerImpl, codeVerifier);
|
|
1650
2653
|
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
2654
|
+
const defaultResourceScopes = providerConfig.resourceScopes || (Array.isArray(providerConfig.scopes) ? void 0 : providerConfig.scopes?.resource) || [];
|
|
2655
|
+
const effectiveScopes = scopes || (tokens.scope ? tokens.scope.split(" ") : defaultResourceScopes);
|
|
1651
2656
|
const connectedResource = {
|
|
1652
2657
|
provider: providerType,
|
|
1653
2658
|
accessToken: tokens.access_token,
|
|
1654
2659
|
refreshToken: tokens.refresh_token,
|
|
1655
2660
|
expiresAt,
|
|
1656
|
-
scopes:
|
|
2661
|
+
scopes: effectiveScopes,
|
|
1657
2662
|
raw: tokens,
|
|
1658
2663
|
connectedAt: Date.now()
|
|
1659
2664
|
};
|
|
@@ -1665,6 +2670,45 @@ var Lixa = class _Lixa {
|
|
|
1665
2670
|
await sessionStorage.saveSession(sessionId, activeSession, 86400);
|
|
1666
2671
|
return activeSession;
|
|
1667
2672
|
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Retrieves configured default resource scopes for a provider.
|
|
2675
|
+
*
|
|
2676
|
+
* @param provider - Provider name (e.g. 'github', 'google')
|
|
2677
|
+
* @returns Array of configured resource scopes, or undefined if not configured
|
|
2678
|
+
*/
|
|
2679
|
+
getResourceScopes(provider) {
|
|
2680
|
+
const providerConfig = this.findProviderByType(String(provider).toLowerCase());
|
|
2681
|
+
if (!providerConfig) return void 0;
|
|
2682
|
+
if (providerConfig.resourceScopes) return providerConfig.resourceScopes;
|
|
2683
|
+
if (providerConfig.scopes && !Array.isArray(providerConfig.scopes)) {
|
|
2684
|
+
return providerConfig.scopes.resource;
|
|
2685
|
+
}
|
|
2686
|
+
return void 0;
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Returns the resolved session cookie name.
|
|
2690
|
+
* Dynamically reflects any changes to the active identity provider.
|
|
2691
|
+
*
|
|
2692
|
+
* @param req - Optional request object for multi-tenant request-scoped cookie resolution
|
|
2693
|
+
* @returns Resolved session cookie name
|
|
2694
|
+
*/
|
|
2695
|
+
getSessionCookieName(req) {
|
|
2696
|
+
if (typeof this.sessionCookieResolver === "function") {
|
|
2697
|
+
return this.sessionCookieResolver(this, req);
|
|
2698
|
+
}
|
|
2699
|
+
if (typeof this.sessionCookieResolver === "string" && this.sessionCookieResolver.length > 0) {
|
|
2700
|
+
return this.sessionCookieResolver;
|
|
2701
|
+
}
|
|
2702
|
+
return DEFAULT_SESSION_COOKIE_NAME;
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Sets or updates the session cookie name or dynamic resolver on this instance.
|
|
2706
|
+
*
|
|
2707
|
+
* @param resolver - Static cookie name or dynamic resolver function
|
|
2708
|
+
*/
|
|
2709
|
+
setSessionCookieName(resolver) {
|
|
2710
|
+
this.sessionCookieResolver = resolver;
|
|
2711
|
+
}
|
|
1668
2712
|
/**
|
|
1669
2713
|
* Retrieves a connected resource for a specific User ID / Email directly (independent of session IDs).
|
|
1670
2714
|
* Automatically refreshes expired access tokens if a refresh token is present.
|
|
@@ -1845,7 +2889,32 @@ var Lixa = class _Lixa {
|
|
|
1845
2889
|
*/
|
|
1846
2890
|
async fetchSessionInfo(sessionId) {
|
|
1847
2891
|
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1848
|
-
|
|
2892
|
+
const session = await sessionStorage.getSession(sessionId);
|
|
2893
|
+
if (!session) return null;
|
|
2894
|
+
if (session.accounts) {
|
|
2895
|
+
delete session.accounts;
|
|
2896
|
+
}
|
|
2897
|
+
return session;
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Retrieves all linked accounts for a user from account storage.
|
|
2901
|
+
*
|
|
2902
|
+
* @param userIdOrSessionId - User ID, email, or active session ID
|
|
2903
|
+
* @returns Record mapping provider name to linked account metadata
|
|
2904
|
+
*/
|
|
2905
|
+
async getUserAccounts(userIdOrSessionId) {
|
|
2906
|
+
let userKey = userIdOrSessionId;
|
|
2907
|
+
if (this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage) {
|
|
2908
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
2909
|
+
const session = await sessionStorage.getSession(userIdOrSessionId);
|
|
2910
|
+
if (session) {
|
|
2911
|
+
userKey = this.getUserKeyFromSession(session);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
if (this.accountHandler?.accountStorage) {
|
|
2915
|
+
return await this.accountHandler.accountStorage.getUserAccounts(userKey) || {};
|
|
2916
|
+
}
|
|
2917
|
+
return {};
|
|
1849
2918
|
}
|
|
1850
2919
|
/**
|
|
1851
2920
|
* Deletes a session from session storage (e.g. on logout).
|
|
@@ -1896,117 +2965,178 @@ var Lixa = class _Lixa {
|
|
|
1896
2965
|
return response.json();
|
|
1897
2966
|
}
|
|
1898
2967
|
/**
|
|
1899
|
-
* Checks if
|
|
2968
|
+
* Checks if an identity provider (Self-Managed Database, AWS Cognito, Auth0, Okta, etc.) is configured.
|
|
2969
|
+
*
|
|
2970
|
+
* @returns True if identity provider is configured and available
|
|
2971
|
+
*/
|
|
2972
|
+
isIdentityEnabled() {
|
|
2973
|
+
return this.identityProvider !== void 0;
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Checks if credentials / identity authentication is configured and enabled.
|
|
1900
2977
|
*
|
|
1901
|
-
* @returns True if credentials authentication is available
|
|
2978
|
+
* @returns True if credentials / identity authentication is available
|
|
1902
2979
|
*/
|
|
1903
2980
|
isCredentialsEnabled() {
|
|
1904
|
-
return this.
|
|
2981
|
+
return this.identityProvider !== void 0;
|
|
1905
2982
|
}
|
|
1906
2983
|
/**
|
|
1907
|
-
* Returns the
|
|
2984
|
+
* Returns the configured IdentityProvider instance.
|
|
2985
|
+
*/
|
|
2986
|
+
getIdentityProvider() {
|
|
2987
|
+
return this.identityProvider;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Dynamically sets or updates the active identity provider (e.g. SelfManaged, Cognito, Auth0, Okta).
|
|
2991
|
+
*
|
|
2992
|
+
* @param provider - Identity provider instance or undefined to disable
|
|
2993
|
+
*/
|
|
2994
|
+
setIdentityProvider(provider) {
|
|
2995
|
+
this.identityProvider = provider;
|
|
2996
|
+
if (provider instanceof SelfManagedIdentityProvider) {
|
|
2997
|
+
this.credentialsManager = provider.getCredentialsManager();
|
|
2998
|
+
} else {
|
|
2999
|
+
this.credentialsManager = void 0;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Returns the underlying CredentialsManager instance if using Self-Managed identity.
|
|
1908
3004
|
*/
|
|
1909
3005
|
getCredentialsManager() {
|
|
1910
3006
|
return this.credentialsManager;
|
|
1911
3007
|
}
|
|
1912
3008
|
/**
|
|
1913
|
-
* Registers a new user with
|
|
1914
|
-
* Automatically
|
|
1915
|
-
* stores credentials, and creates a session (unless autoCreateSessionOnSignUp is false).
|
|
3009
|
+
* Registers a new user with the configured identity provider (Self-Managed DB, Cognito, Auth0, Okta).
|
|
3010
|
+
* Automatically creates an active session unless autoCreateSessionOnSignUp is explicitly set to false.
|
|
1916
3011
|
*
|
|
1917
3012
|
* @param params - Registration parameters (identifier, password, email, username, metadata)
|
|
1918
|
-
* @returns Created user
|
|
3013
|
+
* @returns Created user identity and optional active session
|
|
1919
3014
|
* @throws WeakPasswordError if password does not meet policy requirements
|
|
1920
3015
|
* @throws UserAlreadyExistsError if identifier is already registered
|
|
1921
|
-
* @throws
|
|
3016
|
+
* @throws IdentityNotConfiguredError if identity authentication is not configured
|
|
1922
3017
|
*/
|
|
1923
3018
|
async signUp(params) {
|
|
1924
|
-
if (!this.
|
|
1925
|
-
throw new
|
|
3019
|
+
if (!this.identityProvider) {
|
|
3020
|
+
throw new IdentityNotConfiguredError();
|
|
3021
|
+
}
|
|
3022
|
+
const rawProviderName = params.provider || this.identityProvider.name;
|
|
3023
|
+
const providerName = rawProviderName === "self-managed" ? "credentials" : rawProviderName;
|
|
3024
|
+
this.log("INFO", "Identity", `Processing signUp request via ${providerName}`, { identifier: params.identifier });
|
|
3025
|
+
const signUpEmail = params.email || (params.identifier?.includes("@") ? params.identifier : void 0);
|
|
3026
|
+
if (signUpEmail && this.accountHandler?.accountStorage?.findAccountsByEmail) {
|
|
3027
|
+
const normalizedEmail = signUpEmail.trim().toLowerCase();
|
|
3028
|
+
const existingAccounts = await this.accountHandler.accountStorage.findAccountsByEmail(normalizedEmail);
|
|
3029
|
+
if (existingAccounts.length > 0) {
|
|
3030
|
+
const existingProviderNames = existingAccounts.map((a) => a.provider);
|
|
3031
|
+
const hasCredentials = existingProviderNames.some(
|
|
3032
|
+
(p) => p === "credentials" || p === "self-managed" || p === "cognito"
|
|
3033
|
+
);
|
|
3034
|
+
if (!hasCredentials) {
|
|
3035
|
+
this.log("WARN", "Identity", `SignUp blocked: SSO account already exists for '${normalizedEmail}' via [${existingProviderNames.join(", ")}]`);
|
|
3036
|
+
throw new SSOAccountAlreadyExistsError(normalizedEmail, existingProviderNames);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
const user = await this.identityProvider.signUp(params);
|
|
3041
|
+
const isVerificationRequired = user.metadata?.requiresVerification === true || user.emailVerified === false && (user.metadata?.devConfirmationCode !== void 0 || this.identityProvider.name === "self-managed" || this.identityProvider.name === "cognito");
|
|
3042
|
+
if (isVerificationRequired) {
|
|
3043
|
+
this.log("INFO", "Identity", "SignUp requires verification \u2014 no session created", {
|
|
3044
|
+
identifier: user.identifier,
|
|
3045
|
+
email: user.email
|
|
3046
|
+
});
|
|
3047
|
+
return {
|
|
3048
|
+
user,
|
|
3049
|
+
requiresVerification: true,
|
|
3050
|
+
devConfirmationCode: user.metadata?.devConfirmationCode
|
|
3051
|
+
};
|
|
1926
3052
|
}
|
|
1927
|
-
this.log("INFO", "Credentials", "Processing signUp request", { identifier: params.identifier });
|
|
1928
|
-
const user = await this.credentialsManager.signUp(params);
|
|
1929
3053
|
const autoSession = this.config.credentials?.autoCreateSessionOnSignUp ?? true;
|
|
1930
3054
|
if (!autoSession) {
|
|
1931
3055
|
return { user };
|
|
1932
3056
|
}
|
|
1933
|
-
const sessionId = (0,
|
|
1934
|
-
const token = (0,
|
|
3057
|
+
const sessionId = (0, import_crypto5.randomBytes)(16).toString("hex");
|
|
3058
|
+
const token = (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
1935
3059
|
const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
|
|
3060
|
+
const activeProviderName = params.provider || user && user.provider || (this.identityProvider instanceof CompositeIdentityProvider ? this.identityProvider.defaultProvider : this.identityProvider.name);
|
|
3061
|
+
const accountKey = activeProviderName === "self-managed" ? "credentials" : activeProviderName || "credentials";
|
|
1936
3062
|
const credentialsAccount = {
|
|
1937
|
-
provider:
|
|
3063
|
+
provider: accountKey,
|
|
1938
3064
|
providerUserId: user.id,
|
|
1939
3065
|
email: user.email,
|
|
1940
|
-
accessToken: token,
|
|
1941
|
-
raw: {
|
|
1942
|
-
access_token: token,
|
|
1943
|
-
token_type: "Bearer",
|
|
1944
|
-
expires_in: ttl
|
|
1945
|
-
},
|
|
1946
3066
|
linkedAt: Date.now()
|
|
1947
3067
|
};
|
|
1948
3068
|
const session = {
|
|
1949
|
-
|
|
3069
|
+
sessionId,
|
|
1950
3070
|
userId: user.id,
|
|
1951
3071
|
email: user.email,
|
|
1952
|
-
|
|
1953
|
-
provider: "credentials",
|
|
1954
|
-
accounts: {
|
|
1955
|
-
credentials: credentialsAccount
|
|
1956
|
-
}
|
|
3072
|
+
provider: accountKey
|
|
1957
3073
|
};
|
|
3074
|
+
if (this.accountHandler?.accountStorage) {
|
|
3075
|
+
const userKey = this.getUserKeyFromSession(session);
|
|
3076
|
+
await this.accountHandler.accountStorage.saveAccount(userKey, accountKey, this.sanitizeIdentityForStorage(credentialsAccount));
|
|
3077
|
+
}
|
|
1958
3078
|
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1959
3079
|
await sessionStorage.saveSession(sessionId, session, ttl);
|
|
1960
|
-
this.log("INFO", "
|
|
3080
|
+
this.log("INFO", "Identity", "Created session on signUp", { userId: user.id, sessionId, provider: accountKey });
|
|
1961
3081
|
return { user, sessionId, session };
|
|
1962
3082
|
}
|
|
1963
3083
|
/**
|
|
1964
|
-
* Authenticates a user with
|
|
1965
|
-
*
|
|
1966
|
-
* If account linking is configured with AUTO_LINK_BY_VERIFIED_EMAIL, merges with existing session.
|
|
3084
|
+
* Authenticates a user with credentials via the configured identity provider (Self-Managed DB, Cognito, Auth0, Okta, or Composite).
|
|
3085
|
+
* Creates an active session and automatically links with existing sessions when AUTO_LINK_BY_VERIFIED_EMAIL is configured.
|
|
1967
3086
|
*
|
|
1968
|
-
* @param params - Sign-in parameters (identifier, password)
|
|
3087
|
+
* @param params - Sign-in parameters (identifier, password, optional provider)
|
|
1969
3088
|
* @returns Authenticated user, session ID, and session object
|
|
1970
3089
|
* @throws InvalidCredentialsError if authentication fails
|
|
1971
|
-
* @throws
|
|
3090
|
+
* @throws IdentityNotConfiguredError if identity authentication is not configured
|
|
1972
3091
|
*/
|
|
1973
3092
|
async signIn(params) {
|
|
1974
|
-
if (!this.
|
|
1975
|
-
throw new
|
|
3093
|
+
if (!this.identityProvider) {
|
|
3094
|
+
throw new IdentityNotConfiguredError();
|
|
1976
3095
|
}
|
|
1977
3096
|
const lookupKey = params.username || params.identifier;
|
|
1978
|
-
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
3097
|
+
const rawProviderName = params.provider || this.identityProvider.name;
|
|
3098
|
+
const providerName = rawProviderName === "self-managed" ? "credentials" : rawProviderName;
|
|
3099
|
+
this.log("INFO", "Identity", `Processing signIn request via ${providerName}`, { identifier: lookupKey });
|
|
3100
|
+
const authResult = await this.identityProvider.signIn(params);
|
|
3101
|
+
const user = authResult.user;
|
|
3102
|
+
const tokens = authResult.tokens;
|
|
1984
3103
|
const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
|
|
1985
|
-
const token = (0,
|
|
3104
|
+
const token = tokens?.accessToken || (0, import_crypto5.randomBytes)(32).toString("hex");
|
|
3105
|
+
const rawTokens = {
|
|
3106
|
+
access_token: token,
|
|
3107
|
+
token_type: tokens?.tokenType || "Bearer",
|
|
3108
|
+
expires_in: tokens?.expiresIn || ttl,
|
|
3109
|
+
id_token: tokens?.idToken,
|
|
3110
|
+
refresh_token: tokens?.refreshToken,
|
|
3111
|
+
...tokens?.raw
|
|
3112
|
+
};
|
|
3113
|
+
const activeProviderName = params.provider || user && user.provider || (this.identityProvider instanceof CompositeIdentityProvider ? this.identityProvider.defaultProvider : this.identityProvider.name);
|
|
3114
|
+
const accountKey = activeProviderName === "self-managed" ? "credentials" : activeProviderName || "credentials";
|
|
1986
3115
|
const credentialsAccount = {
|
|
1987
|
-
provider:
|
|
3116
|
+
provider: accountKey,
|
|
1988
3117
|
providerUserId: user.id,
|
|
1989
3118
|
email: user.email,
|
|
1990
|
-
accessToken: token,
|
|
1991
|
-
raw: {
|
|
1992
|
-
access_token: token,
|
|
1993
|
-
token_type: "Bearer",
|
|
1994
|
-
expires_in: ttl
|
|
1995
|
-
},
|
|
1996
3119
|
linkedAt: Date.now()
|
|
1997
3120
|
};
|
|
1998
3121
|
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1999
|
-
const
|
|
2000
|
-
const
|
|
3122
|
+
const linkingConfig = this.config.accountLinking;
|
|
3123
|
+
const linkingMode = typeof linkingConfig === "object" ? linkingConfig?.mode : void 0;
|
|
3124
|
+
const isAutoLink = linkingConfig === true || linkingConfig === "linkByEmail" || linkingConfig === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" || linkingMode === "linkByEmail";
|
|
2001
3125
|
if (isAutoLink && user.email && sessionStorage.getSessionByEmail) {
|
|
2002
3126
|
const existing = await sessionStorage.getSessionByEmail(user.email);
|
|
2003
3127
|
if (existing) {
|
|
2004
3128
|
const mergedSession = { ...existing.session };
|
|
2005
|
-
mergedSession.accounts = { ...mergedSession.accounts || {}, credentials: credentialsAccount };
|
|
2006
3129
|
if (!mergedSession.userId) mergedSession.userId = user.id;
|
|
2007
3130
|
if (!mergedSession.email) mergedSession.email = user.email;
|
|
3131
|
+
mergedSession.provider = accountKey;
|
|
3132
|
+
mergedSession.token = token;
|
|
3133
|
+
mergedSession.raw = rawTokens;
|
|
3134
|
+
if (this.accountHandler?.accountStorage) {
|
|
3135
|
+
const userKey = this.getUserKeyFromSession(mergedSession);
|
|
3136
|
+
await this.accountHandler.accountStorage.saveAccount(userKey, accountKey, this.sanitizeIdentityForStorage(credentialsAccount));
|
|
3137
|
+
}
|
|
2008
3138
|
await sessionStorage.saveSession(existing.sessionId, mergedSession, ttl);
|
|
2009
|
-
this.log("INFO", "AccountLinking",
|
|
3139
|
+
this.log("INFO", "AccountLinking", `Linked ${providerName} to existing session by email`, {
|
|
2010
3140
|
sessionId: existing.sessionId,
|
|
2011
3141
|
email: user.email
|
|
2012
3142
|
});
|
|
@@ -2017,53 +3147,229 @@ var Lixa = class _Lixa {
|
|
|
2017
3147
|
};
|
|
2018
3148
|
}
|
|
2019
3149
|
}
|
|
2020
|
-
const sessionId = (0,
|
|
3150
|
+
const sessionId = (0, import_crypto5.randomBytes)(16).toString("hex");
|
|
3151
|
+
let targetUserId = user.id;
|
|
3152
|
+
const allAccounts = {
|
|
3153
|
+
[accountKey]: credentialsAccount
|
|
3154
|
+
};
|
|
3155
|
+
if (this.accountHandler?.accountStorage) {
|
|
3156
|
+
if (user.email && this.accountHandler.accountStorage.findAccountsByEmail) {
|
|
3157
|
+
const matches = await this.accountHandler.accountStorage.findAccountsByEmail(user.email);
|
|
3158
|
+
if (matches.length > 0) {
|
|
3159
|
+
targetUserId = matches[0].userId;
|
|
3160
|
+
for (const match of matches) {
|
|
3161
|
+
const userAccs = await this.accountHandler.accountStorage.getUserAccounts(match.userId);
|
|
3162
|
+
if (userAccs) {
|
|
3163
|
+
for (const [k, v] of Object.entries(userAccs)) {
|
|
3164
|
+
if (k !== accountKey && !allAccounts[k]) {
|
|
3165
|
+
allAccounts[k] = v;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
const directAccounts = await this.accountHandler.accountStorage.getUserAccounts(user.id);
|
|
3173
|
+
if (directAccounts) {
|
|
3174
|
+
for (const [k, v] of Object.entries(directAccounts)) {
|
|
3175
|
+
if (k !== accountKey && !allAccounts[k]) {
|
|
3176
|
+
allAccounts[k] = v;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
await this.accountHandler.accountStorage.saveAccount(
|
|
3181
|
+
targetUserId,
|
|
3182
|
+
accountKey,
|
|
3183
|
+
this.sanitizeIdentityForStorage(credentialsAccount)
|
|
3184
|
+
);
|
|
3185
|
+
for (const [pKey, pAcc] of Object.entries(allAccounts)) {
|
|
3186
|
+
if (pKey !== accountKey) {
|
|
3187
|
+
await this.accountHandler.accountStorage.saveAccount(
|
|
3188
|
+
targetUserId,
|
|
3189
|
+
pKey,
|
|
3190
|
+
this.sanitizeIdentityForStorage(pAcc)
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
2021
3195
|
const session = {
|
|
2022
|
-
|
|
2023
|
-
userId:
|
|
3196
|
+
sessionId,
|
|
3197
|
+
userId: targetUserId,
|
|
2024
3198
|
email: user.email,
|
|
3199
|
+
provider: accountKey,
|
|
2025
3200
|
token,
|
|
2026
|
-
|
|
2027
|
-
accounts: {
|
|
2028
|
-
credentials: credentialsAccount
|
|
2029
|
-
}
|
|
3201
|
+
raw: rawTokens
|
|
2030
3202
|
};
|
|
2031
3203
|
await sessionStorage.saveSession(sessionId, session, ttl);
|
|
2032
|
-
this.log("INFO", "
|
|
3204
|
+
this.log("INFO", "Identity", `User signed in successfully via ${accountKey}`, { userId: targetUserId, sessionId });
|
|
2033
3205
|
return { user, sessionId, session };
|
|
2034
3206
|
}
|
|
2035
3207
|
/**
|
|
2036
3208
|
* Verifies username/email and password credentials without generating a session.
|
|
2037
3209
|
*
|
|
2038
|
-
* @param params - Verification parameters (identifier, password)
|
|
2039
|
-
* @returns User credentials
|
|
2040
|
-
* @throws
|
|
3210
|
+
* @param params - Verification parameters (identifier, password, optional provider)
|
|
3211
|
+
* @returns User credentials/identity or null if invalid
|
|
3212
|
+
* @throws IdentityNotConfiguredError if identity auth is not configured
|
|
2041
3213
|
*/
|
|
2042
3214
|
async verifyCredentials(params) {
|
|
2043
|
-
if (!this.
|
|
2044
|
-
throw new
|
|
3215
|
+
if (!this.identityProvider) {
|
|
3216
|
+
throw new IdentityNotConfiguredError();
|
|
3217
|
+
}
|
|
3218
|
+
if (typeof this.identityProvider.verifyCredentials === "function") {
|
|
3219
|
+
return this.identityProvider.verifyCredentials(params);
|
|
3220
|
+
}
|
|
3221
|
+
try {
|
|
3222
|
+
const result = await this.identityProvider.signIn(params);
|
|
3223
|
+
return result.user;
|
|
3224
|
+
} catch {
|
|
3225
|
+
return null;
|
|
2045
3226
|
}
|
|
2046
|
-
return this.credentialsManager.verifyCredentials(params);
|
|
2047
3227
|
}
|
|
2048
3228
|
/**
|
|
2049
3229
|
* Updates a user's password after verifying the current password and enforcing policy on the new password.
|
|
2050
3230
|
*
|
|
2051
|
-
* @param params - Change password parameters (userId/identifier, oldPassword, newPassword)
|
|
3231
|
+
* @param params - Change password parameters (userId/identifier, oldPassword, newPassword, optional provider)
|
|
2052
3232
|
* @returns True if password was updated successfully
|
|
2053
3233
|
* @throws UserNotFoundError if user is not found
|
|
2054
3234
|
* @throws InvalidCredentialsError if current password is incorrect
|
|
2055
3235
|
* @throws WeakPasswordError if new password does not meet policy requirements
|
|
2056
|
-
* @throws
|
|
3236
|
+
* @throws IdentityNotConfiguredError if identity auth is not configured
|
|
2057
3237
|
*/
|
|
2058
3238
|
async changePassword(params) {
|
|
2059
|
-
if (!this.
|
|
2060
|
-
throw new
|
|
3239
|
+
if (!this.identityProvider) {
|
|
3240
|
+
throw new IdentityNotConfiguredError();
|
|
2061
3241
|
}
|
|
2062
|
-
this.
|
|
3242
|
+
if (typeof this.identityProvider.changePassword !== "function") {
|
|
3243
|
+
throw new IdentityProviderError(
|
|
3244
|
+
`Password change is not supported by the '${this.identityProvider.name}' identity provider`,
|
|
3245
|
+
this.identityProvider.name
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
this.log("INFO", "Identity", "Processing changePassword request", {
|
|
2063
3249
|
userId: params.userId,
|
|
2064
3250
|
identifier: params.identifier
|
|
2065
3251
|
});
|
|
2066
|
-
return this.
|
|
3252
|
+
return this.identityProvider.changePassword(params);
|
|
3253
|
+
}
|
|
3254
|
+
/**
|
|
3255
|
+
* Initiates a password reset (forgot password flow) through the configured identity provider.
|
|
3256
|
+
*
|
|
3257
|
+
* @param params - Forgot password parameters (identifier, optional metadata, optional provider)
|
|
3258
|
+
* @returns ForgotPasswordResult indicating status and delivery medium (email/SMS)
|
|
3259
|
+
* @throws IdentityNotConfiguredError if identity auth is not configured
|
|
3260
|
+
*/
|
|
3261
|
+
async forgotPassword(params) {
|
|
3262
|
+
if (!this.identityProvider) {
|
|
3263
|
+
throw new IdentityNotConfiguredError();
|
|
3264
|
+
}
|
|
3265
|
+
if (typeof this.identityProvider.forgotPassword !== "function") {
|
|
3266
|
+
throw new IdentityProviderError(
|
|
3267
|
+
`Password reset request is not supported by the '${this.identityProvider.name}' identity provider`,
|
|
3268
|
+
this.identityProvider.name
|
|
3269
|
+
);
|
|
3270
|
+
}
|
|
3271
|
+
this.log("INFO", "Identity", "Processing forgotPassword request", { identifier: params.identifier });
|
|
3272
|
+
return this.identityProvider.forgotPassword(params);
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Confirms a password reset using a verification code / token and sets the new password.
|
|
3276
|
+
*
|
|
3277
|
+
* @param params - Verification code, identifier, new password, and optional provider
|
|
3278
|
+
* @returns True if password reset was successfully confirmed
|
|
3279
|
+
* @throws IdentityNotConfiguredError if identity auth is not configured
|
|
3280
|
+
*/
|
|
3281
|
+
async confirmPasswordReset(params) {
|
|
3282
|
+
if (!this.identityProvider) {
|
|
3283
|
+
throw new IdentityNotConfiguredError();
|
|
3284
|
+
}
|
|
3285
|
+
if (typeof this.identityProvider.confirmPasswordReset !== "function") {
|
|
3286
|
+
throw new IdentityProviderError(
|
|
3287
|
+
`Confirm password reset is not supported by the '${this.identityProvider.name}' identity provider`,
|
|
3288
|
+
this.identityProvider.name
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
3291
|
+
this.log("INFO", "Identity", "Processing confirmPasswordReset request", { identifier: params.identifier });
|
|
3292
|
+
return this.identityProvider.confirmPasswordReset(params);
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* Confirms user account registration using a verification code.
|
|
3296
|
+
*
|
|
3297
|
+
* @param params - Verification parameters including identifier, confirmationCode, and optional provider
|
|
3298
|
+
* @returns True if confirmation was successful
|
|
3299
|
+
* @throws IdentityNotConfiguredError if identity auth is not configured
|
|
3300
|
+
* @throws IdentityProviderError if identity provider does not support confirmation
|
|
3301
|
+
*/
|
|
3302
|
+
async confirmSignUp(params) {
|
|
3303
|
+
if (!this.identityProvider) {
|
|
3304
|
+
throw new IdentityNotConfiguredError();
|
|
3305
|
+
}
|
|
3306
|
+
if (typeof this.identityProvider.confirmSignUp !== "function") {
|
|
3307
|
+
throw new IdentityProviderError(
|
|
3308
|
+
`Account confirmation is not supported by the '${this.identityProvider.name}' identity provider`,
|
|
3309
|
+
this.identityProvider.name
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
this.log("INFO", "Identity", "Processing confirmSignUp request", { identifier: params.identifier });
|
|
3313
|
+
return this.identityProvider.confirmSignUp(params);
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Resends the sign up confirmation code to the user.
|
|
3317
|
+
*
|
|
3318
|
+
* @param params - Resend parameters including user identifier and optional provider
|
|
3319
|
+
* @returns Details about the code delivery (medium, destination)
|
|
3320
|
+
* @throws IdentityNotConfiguredError if identity auth is not configured
|
|
3321
|
+
* @throws IdentityProviderError if identity provider does not support resending codes
|
|
3322
|
+
*/
|
|
3323
|
+
async resendConfirmationCode(params) {
|
|
3324
|
+
if (!this.identityProvider) {
|
|
3325
|
+
throw new IdentityNotConfiguredError();
|
|
3326
|
+
}
|
|
3327
|
+
if (typeof this.identityProvider.resendConfirmationCode !== "function") {
|
|
3328
|
+
throw new IdentityProviderError(
|
|
3329
|
+
`Resending confirmation code is not supported by the '${this.identityProvider.name}' identity provider`,
|
|
3330
|
+
this.identityProvider.name
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
this.log("INFO", "Identity", "Processing resendConfirmationCode request", { identifier: params.identifier });
|
|
3334
|
+
return this.identityProvider.resendConfirmationCode(params);
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* Retrieves a user by their unique ID from the configured identity provider.
|
|
3338
|
+
*
|
|
3339
|
+
* @param id - Unique user ID
|
|
3340
|
+
* @param provider - Optional provider key when using CompositeIdentityProvider
|
|
3341
|
+
* @returns UserIdentity or null if not found
|
|
3342
|
+
*/
|
|
3343
|
+
async getUserById(id, provider) {
|
|
3344
|
+
if (!this.identityProvider || typeof this.identityProvider.getUserById !== "function") {
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
return this.identityProvider.getUserById(id, provider);
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Retrieves a user by their identifier (username or email) from the configured identity provider.
|
|
3351
|
+
*
|
|
3352
|
+
* @param identifier - Username or email string
|
|
3353
|
+
* @param provider - Optional provider key when using CompositeIdentityProvider
|
|
3354
|
+
* @returns UserIdentity or null if not found
|
|
3355
|
+
*/
|
|
3356
|
+
async getUserByIdentifier(identifier, provider) {
|
|
3357
|
+
if (!this.identityProvider || typeof this.identityProvider.getUserByIdentifier !== "function") {
|
|
3358
|
+
return null;
|
|
3359
|
+
}
|
|
3360
|
+
return this.identityProvider.getUserByIdentifier(identifier, provider);
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Deletes a user account from the configured identity provider.
|
|
3364
|
+
*
|
|
3365
|
+
* @param id - Unique user ID
|
|
3366
|
+
* @param provider - Optional provider key when using CompositeIdentityProvider
|
|
3367
|
+
*/
|
|
3368
|
+
async deleteUser(id, provider) {
|
|
3369
|
+
if (!this.identityProvider || typeof this.identityProvider.deleteUser !== "function") {
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
await this.identityProvider.deleteUser(id, provider);
|
|
2067
3373
|
}
|
|
2068
3374
|
findProviderByType(providerType) {
|
|
2069
3375
|
for (const [key, value] of Object.entries(this.config.providers || {})) {
|
|
@@ -2075,125 +3381,6 @@ var Lixa = class _Lixa {
|
|
|
2075
3381
|
}
|
|
2076
3382
|
};
|
|
2077
3383
|
|
|
2078
|
-
// src/utils/cookies.ts
|
|
2079
|
-
var DEFAULT_SESSION_COOKIE_NAME = "lixa_session";
|
|
2080
|
-
var DEFAULT_STATE_COOKIE_NAME = "lixa_oauth_state";
|
|
2081
|
-
var DEFAULT_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
2082
|
-
var DEFAULT_STATE_MAX_AGE_SECONDS = 5 * 60;
|
|
2083
|
-
function isProductionEnvironment() {
|
|
2084
|
-
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
2085
|
-
}
|
|
2086
|
-
function serializeCookie(name, value, options) {
|
|
2087
|
-
const isProd = isProductionEnvironment();
|
|
2088
|
-
const path = options?.path ?? "/";
|
|
2089
|
-
const httpOnly = options?.httpOnly ?? true;
|
|
2090
|
-
const secure = options?.secure ?? isProd;
|
|
2091
|
-
const sameSite = options?.sameSite ?? "lax";
|
|
2092
|
-
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
2093
|
-
if (path) {
|
|
2094
|
-
parts.push(`Path=${path}`);
|
|
2095
|
-
}
|
|
2096
|
-
if (typeof options?.maxAge === "number") {
|
|
2097
|
-
parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
2098
|
-
const expires = new Date(Date.now() + options.maxAge * 1e3).toUTCString();
|
|
2099
|
-
parts.push(`Expires=${expires}`);
|
|
2100
|
-
}
|
|
2101
|
-
if (options?.domain) {
|
|
2102
|
-
parts.push(`Domain=${options.domain}`);
|
|
2103
|
-
}
|
|
2104
|
-
if (httpOnly) {
|
|
2105
|
-
parts.push("HttpOnly");
|
|
2106
|
-
}
|
|
2107
|
-
if (secure) {
|
|
2108
|
-
parts.push("Secure");
|
|
2109
|
-
}
|
|
2110
|
-
if (sameSite) {
|
|
2111
|
-
const capitalized = sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase();
|
|
2112
|
-
parts.push(`SameSite=${capitalized}`);
|
|
2113
|
-
}
|
|
2114
|
-
return parts.join("; ");
|
|
2115
|
-
}
|
|
2116
|
-
function createSessionCookie(sessionId, options) {
|
|
2117
|
-
const isProd = isProductionEnvironment();
|
|
2118
|
-
const resolvedOptions = {
|
|
2119
|
-
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
2120
|
-
path: options?.path ?? "/",
|
|
2121
|
-
maxAge: options?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
2122
|
-
httpOnly: options?.httpOnly ?? true,
|
|
2123
|
-
secure: options?.secure ?? isProd,
|
|
2124
|
-
sameSite: options?.sameSite ?? "lax",
|
|
2125
|
-
domain: options?.domain
|
|
2126
|
-
};
|
|
2127
|
-
const name = resolvedOptions.name;
|
|
2128
|
-
const header = serializeCookie(name, sessionId, resolvedOptions);
|
|
2129
|
-
return {
|
|
2130
|
-
name,
|
|
2131
|
-
value: sessionId,
|
|
2132
|
-
options: resolvedOptions,
|
|
2133
|
-
header
|
|
2134
|
-
};
|
|
2135
|
-
}
|
|
2136
|
-
function clearSessionCookie(options) {
|
|
2137
|
-
const isProd = isProductionEnvironment();
|
|
2138
|
-
const resolvedOptions = {
|
|
2139
|
-
name: options?.name || DEFAULT_SESSION_COOKIE_NAME,
|
|
2140
|
-
path: options?.path ?? "/",
|
|
2141
|
-
maxAge: 0,
|
|
2142
|
-
httpOnly: options?.httpOnly ?? true,
|
|
2143
|
-
secure: options?.secure ?? isProd,
|
|
2144
|
-
sameSite: options?.sameSite ?? "lax",
|
|
2145
|
-
domain: options?.domain
|
|
2146
|
-
};
|
|
2147
|
-
const name = resolvedOptions.name;
|
|
2148
|
-
const header = serializeCookie(name, "", resolvedOptions);
|
|
2149
|
-
return {
|
|
2150
|
-
name,
|
|
2151
|
-
value: "",
|
|
2152
|
-
options: resolvedOptions,
|
|
2153
|
-
header
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
function createStateCookie(state, options) {
|
|
2157
|
-
const isProd = isProductionEnvironment();
|
|
2158
|
-
const resolvedOptions = {
|
|
2159
|
-
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
2160
|
-
path: options?.path ?? "/",
|
|
2161
|
-
maxAge: options?.maxAge ?? DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
2162
|
-
httpOnly: options?.httpOnly ?? true,
|
|
2163
|
-
secure: options?.secure ?? isProd,
|
|
2164
|
-
sameSite: options?.sameSite ?? "lax",
|
|
2165
|
-
domain: options?.domain
|
|
2166
|
-
};
|
|
2167
|
-
const name = resolvedOptions.name;
|
|
2168
|
-
const header = serializeCookie(name, state, resolvedOptions);
|
|
2169
|
-
return {
|
|
2170
|
-
name,
|
|
2171
|
-
value: state,
|
|
2172
|
-
options: resolvedOptions,
|
|
2173
|
-
header
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
function clearStateCookie(options) {
|
|
2177
|
-
const isProd = isProductionEnvironment();
|
|
2178
|
-
const resolvedOptions = {
|
|
2179
|
-
name: options?.name || DEFAULT_STATE_COOKIE_NAME,
|
|
2180
|
-
path: options?.path ?? "/",
|
|
2181
|
-
maxAge: 0,
|
|
2182
|
-
httpOnly: options?.httpOnly ?? true,
|
|
2183
|
-
secure: options?.secure ?? isProd,
|
|
2184
|
-
sameSite: options?.sameSite ?? "lax",
|
|
2185
|
-
domain: options?.domain
|
|
2186
|
-
};
|
|
2187
|
-
const name = resolvedOptions.name;
|
|
2188
|
-
const header = serializeCookie(name, "", resolvedOptions);
|
|
2189
|
-
return {
|
|
2190
|
-
name,
|
|
2191
|
-
value: "",
|
|
2192
|
-
options: resolvedOptions,
|
|
2193
|
-
header
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
3384
|
// src/credentials/index.ts
|
|
2198
3385
|
var credentials_exports = {};
|
|
2199
3386
|
__export(credentials_exports, {
|
|
@@ -2203,10 +3390,20 @@ __export(credentials_exports, {
|
|
|
2203
3390
|
ScryptPasswordHasher: () => ScryptPasswordHasher,
|
|
2204
3391
|
validatePasswordPolicy: () => validatePasswordPolicy
|
|
2205
3392
|
});
|
|
3393
|
+
|
|
3394
|
+
// src/identity/index.ts
|
|
3395
|
+
var identity_exports = {};
|
|
3396
|
+
__export(identity_exports, {
|
|
3397
|
+
CompositeIdentityProvider: () => CompositeIdentityProvider,
|
|
3398
|
+
SelfManagedIdentityProvider: () => SelfManagedIdentityProvider
|
|
3399
|
+
});
|
|
2206
3400
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2207
3401
|
0 && (module.exports = {
|
|
3402
|
+
AccountLinkingChallengeRequiredError,
|
|
2208
3403
|
AccountLinkingStrategy,
|
|
3404
|
+
AccountNotVerifiedError,
|
|
2209
3405
|
AccountUnlinkError,
|
|
3406
|
+
CompositeIdentityProvider,
|
|
2210
3407
|
CredentialsManager,
|
|
2211
3408
|
CredentialsNotConfiguredError,
|
|
2212
3409
|
DEFAULT_SESSION_COOKIE_NAME,
|
|
@@ -2214,6 +3411,8 @@ __export(credentials_exports, {
|
|
|
2214
3411
|
DEFAULT_STATE_COOKIE_NAME,
|
|
2215
3412
|
DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
2216
3413
|
EmailNotVerifiedError,
|
|
3414
|
+
IdentityNotConfiguredError,
|
|
3415
|
+
IdentityProviderError,
|
|
2217
3416
|
InvalidCredentialsError,
|
|
2218
3417
|
InvalidOAuthCallbackError,
|
|
2219
3418
|
InvalidProviderConfigError,
|
|
@@ -2224,7 +3423,9 @@ __export(credentials_exports, {
|
|
|
2224
3423
|
Pbkdf2PasswordHasher,
|
|
2225
3424
|
ProviderNotConfiguredError,
|
|
2226
3425
|
RefreshTokenError,
|
|
3426
|
+
SSOAccountAlreadyExistsError,
|
|
2227
3427
|
ScryptPasswordHasher,
|
|
3428
|
+
SelfManagedIdentityProvider,
|
|
2228
3429
|
SessionNotFoundError,
|
|
2229
3430
|
TokenExchangeError,
|
|
2230
3431
|
UserAlreadyExistsError,
|
|
@@ -2239,6 +3440,7 @@ __export(credentials_exports, {
|
|
|
2239
3440
|
determineProviderFromIssuer,
|
|
2240
3441
|
extractUserInfo,
|
|
2241
3442
|
fetchUserInfo,
|
|
3443
|
+
identity,
|
|
2242
3444
|
isProductionEnvironment,
|
|
2243
3445
|
serializeCookie,
|
|
2244
3446
|
validatePasswordPolicy
|