@korajs/auth 0.6.0 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/{chunk-7OXBRSJL.js → chunk-MPMXJ62R.js} +1 -1
- package/dist/chunk-MPMXJ62R.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/react.cjs +3 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +4 -3
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +67 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +24 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +67 -9
- package/dist/server.js.map +1 -1
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.js +1 -1
- package/dist/svelte.js.map +1 -1
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +1 -1
- package/dist/vue.js.map +1 -1
- package/package.json +6 -5
- package/src/svelte/AuthProvider.svelte +19 -19
- package/src/svelte/OrgProvider.svelte +12 -12
- package/dist/chunk-7OXBRSJL.js.map +0 -1
package/dist/server.d.cts
CHANGED
|
@@ -3602,6 +3602,13 @@ interface TotpSecret {
|
|
|
3602
3602
|
createdAt: number;
|
|
3603
3603
|
/** When MFA was verified (confirmed with first valid code) */
|
|
3604
3604
|
verifiedAt: number | null;
|
|
3605
|
+
/**
|
|
3606
|
+
* The most recent TOTP time-step counter that was successfully consumed
|
|
3607
|
+
* during authentication. Used to enforce single-use of codes (RFC 6238 §5.2):
|
|
3608
|
+
* a code at or before this time-step is rejected as a replay. `undefined`
|
|
3609
|
+
* until the first code is consumed.
|
|
3610
|
+
*/
|
|
3611
|
+
lastUsedTimeStep?: number;
|
|
3605
3612
|
}
|
|
3606
3613
|
/**
|
|
3607
3614
|
* Setup result returned when enabling TOTP MFA.
|
|
@@ -3702,6 +3709,11 @@ declare class TotpManager {
|
|
|
3702
3709
|
/**
|
|
3703
3710
|
* Verify a TOTP code during login.
|
|
3704
3711
|
* Returns true if the code is valid, false otherwise.
|
|
3712
|
+
*
|
|
3713
|
+
* Enforces single-use: a code is rejected if its time-step has already
|
|
3714
|
+
* been consumed (or predates a previously consumed time-step). This
|
|
3715
|
+
* prevents an attacker who observes one valid code from replaying it
|
|
3716
|
+
* within the acceptance window (RFC 6238 §5.2).
|
|
3705
3717
|
*/
|
|
3706
3718
|
verify(userId: string, code: string): Promise<boolean>;
|
|
3707
3719
|
/**
|
|
@@ -3728,6 +3740,18 @@ declare class TotpManager {
|
|
|
3728
3740
|
*/
|
|
3729
3741
|
remainingRecoveryCodes(userId: string): Promise<number>;
|
|
3730
3742
|
private validateCode;
|
|
3743
|
+
/**
|
|
3744
|
+
* Find the TOTP time-step counter (within the acceptance window) whose
|
|
3745
|
+
* generated code matches `code`, or null if none matches. Comparison is
|
|
3746
|
+
* timing-safe.
|
|
3747
|
+
*/
|
|
3748
|
+
private matchTimeStep;
|
|
3749
|
+
/**
|
|
3750
|
+
* Validate a code AND consume its time-step so it (and any earlier code in
|
|
3751
|
+
* the window) cannot be reused. Returns false for an invalid code or a
|
|
3752
|
+
* replay of an already-consumed time-step. Persists the consumed time-step.
|
|
3753
|
+
*/
|
|
3754
|
+
private consumeCode;
|
|
3731
3755
|
}
|
|
3732
3756
|
/**
|
|
3733
3757
|
* Encode bytes to base32 string (RFC 4648, no padding).
|
package/dist/server.d.ts
CHANGED
|
@@ -3602,6 +3602,13 @@ interface TotpSecret {
|
|
|
3602
3602
|
createdAt: number;
|
|
3603
3603
|
/** When MFA was verified (confirmed with first valid code) */
|
|
3604
3604
|
verifiedAt: number | null;
|
|
3605
|
+
/**
|
|
3606
|
+
* The most recent TOTP time-step counter that was successfully consumed
|
|
3607
|
+
* during authentication. Used to enforce single-use of codes (RFC 6238 §5.2):
|
|
3608
|
+
* a code at or before this time-step is rejected as a replay. `undefined`
|
|
3609
|
+
* until the first code is consumed.
|
|
3610
|
+
*/
|
|
3611
|
+
lastUsedTimeStep?: number;
|
|
3605
3612
|
}
|
|
3606
3613
|
/**
|
|
3607
3614
|
* Setup result returned when enabling TOTP MFA.
|
|
@@ -3702,6 +3709,11 @@ declare class TotpManager {
|
|
|
3702
3709
|
/**
|
|
3703
3710
|
* Verify a TOTP code during login.
|
|
3704
3711
|
* Returns true if the code is valid, false otherwise.
|
|
3712
|
+
*
|
|
3713
|
+
* Enforces single-use: a code is rejected if its time-step has already
|
|
3714
|
+
* been consumed (or predates a previously consumed time-step). This
|
|
3715
|
+
* prevents an attacker who observes one valid code from replaying it
|
|
3716
|
+
* within the acceptance window (RFC 6238 §5.2).
|
|
3705
3717
|
*/
|
|
3706
3718
|
verify(userId: string, code: string): Promise<boolean>;
|
|
3707
3719
|
/**
|
|
@@ -3728,6 +3740,18 @@ declare class TotpManager {
|
|
|
3728
3740
|
*/
|
|
3729
3741
|
remainingRecoveryCodes(userId: string): Promise<number>;
|
|
3730
3742
|
private validateCode;
|
|
3743
|
+
/**
|
|
3744
|
+
* Find the TOTP time-step counter (within the acceptance window) whose
|
|
3745
|
+
* generated code matches `code`, or null if none matches. Comparison is
|
|
3746
|
+
* timing-safe.
|
|
3747
|
+
*/
|
|
3748
|
+
private matchTimeStep;
|
|
3749
|
+
/**
|
|
3750
|
+
* Validate a code AND consume its time-step so it (and any earlier code in
|
|
3751
|
+
* the window) cannot be reused. Returns false for an invalid code or a
|
|
3752
|
+
* replay of an already-consumed time-step. Persists the consumed time-step.
|
|
3753
|
+
*/
|
|
3754
|
+
private consumeCode;
|
|
3731
3755
|
}
|
|
3732
3756
|
/**
|
|
3733
3757
|
* Encode bytes to base32 string (RFC 4648, no padding).
|
package/dist/server.js
CHANGED
|
@@ -77,7 +77,7 @@ var MAX_PASSWORD_LENGTH = 128;
|
|
|
77
77
|
var MAX_NAME_LENGTH = 200;
|
|
78
78
|
var CHALLENGE_TTL_MS = 6e4;
|
|
79
79
|
function isValidEmail(email) {
|
|
80
|
-
if (email.length === 0 || email.length > 254) {
|
|
80
|
+
if (typeof email !== "string" || email.length === 0 || email.length > 254) {
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
const atIndex = email.indexOf("@");
|
|
@@ -97,6 +97,9 @@ function isValidEmail(email) {
|
|
|
97
97
|
return true;
|
|
98
98
|
}
|
|
99
99
|
function sanitizeName(name) {
|
|
100
|
+
if (typeof name !== "string") {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
100
103
|
const cleaned = name.replace(/[\x00-\x1f\x7f]/g, "");
|
|
101
104
|
const trimmed = cleaned.trim();
|
|
102
105
|
if (trimmed.length > MAX_NAME_LENGTH) {
|
|
@@ -147,7 +150,7 @@ var BuiltInAuthRoutes = class {
|
|
|
147
150
|
}
|
|
148
151
|
};
|
|
149
152
|
}
|
|
150
|
-
if (body.password.length < MIN_PASSWORD_LENGTH) {
|
|
153
|
+
if (typeof body.password !== "string" || body.password.length < MIN_PASSWORD_LENGTH) {
|
|
151
154
|
return {
|
|
152
155
|
status: 400,
|
|
153
156
|
body: {
|
|
@@ -211,6 +214,12 @@ var BuiltInAuthRoutes = class {
|
|
|
211
214
|
* @returns Auth response with the user and tokens, or an error
|
|
212
215
|
*/
|
|
213
216
|
async handleSignIn(body, clientIp) {
|
|
217
|
+
if (typeof body.email !== "string" || typeof body.password !== "string") {
|
|
218
|
+
return {
|
|
219
|
+
status: 400,
|
|
220
|
+
body: { error: "Email and password are required." }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
214
223
|
const rateLimitKey = clientIp ? `signin:${body.email.toLowerCase()}:${clientIp}` : `signin:${body.email.toLowerCase()}`;
|
|
215
224
|
if (!await this.rateLimiter.isAllowed(rateLimitKey)) {
|
|
216
225
|
return {
|
|
@@ -714,6 +723,9 @@ function decodeJwt(token) {
|
|
|
714
723
|
}
|
|
715
724
|
}
|
|
716
725
|
function verifyJwt(token, secret) {
|
|
726
|
+
if (typeof token !== "string") {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
717
729
|
const parts = token.split(".");
|
|
718
730
|
if (parts.length !== 3) {
|
|
719
731
|
return null;
|
|
@@ -1716,6 +1728,11 @@ function createDefaultTokenManager(options) {
|
|
|
1716
1728
|
"createKoraAuthServer requires jwtSecret in production. Set KORA_AUTH_SECRET or pass jwtSecret."
|
|
1717
1729
|
);
|
|
1718
1730
|
}
|
|
1731
|
+
if (!secret) {
|
|
1732
|
+
console.warn(
|
|
1733
|
+
"[kora] No JWT secret configured; using an ephemeral random secret. Every token is invalidated when the process restarts. Set KORA_AUTH_SECRET or pass jwtSecret to createKoraAuthServer for stable sessions."
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1719
1736
|
return new TokenManager({
|
|
1720
1737
|
secret: secret ?? TokenManager.generateSecret(),
|
|
1721
1738
|
revocationStore: new InMemoryTokenRevocationStore(),
|
|
@@ -2018,7 +2035,11 @@ function notFound() {
|
|
|
2018
2035
|
return { status: 404, body: { error: "Not found" } };
|
|
2019
2036
|
}
|
|
2020
2037
|
function readEnvSecret() {
|
|
2021
|
-
|
|
2038
|
+
if (typeof process === "undefined") {
|
|
2039
|
+
return void 0;
|
|
2040
|
+
}
|
|
2041
|
+
const value = process.env.KORA_AUTH_SECRET;
|
|
2042
|
+
return value && value.trim().length > 0 ? value : void 0;
|
|
2022
2043
|
}
|
|
2023
2044
|
function isProduction() {
|
|
2024
2045
|
return typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
@@ -3515,7 +3536,7 @@ var MAX_ORG_NAME_LENGTH = 200;
|
|
|
3515
3536
|
var MAX_SLUG_LENGTH = 100;
|
|
3516
3537
|
var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,98}[a-z0-9]$/;
|
|
3517
3538
|
function isValidEmail2(email) {
|
|
3518
|
-
if (email.length === 0 || email.length > 254) return false;
|
|
3539
|
+
if (typeof email !== "string" || email.length === 0 || email.length > 254) return false;
|
|
3519
3540
|
const atIndex = email.indexOf("@");
|
|
3520
3541
|
if (atIndex < 1) return false;
|
|
3521
3542
|
const domain = email.slice(atIndex + 1);
|
|
@@ -3783,6 +3804,13 @@ var OrgRoutes = class {
|
|
|
3783
3804
|
body: { error: "Cannot assign owner role directly. Use ownership transfer." }
|
|
3784
3805
|
};
|
|
3785
3806
|
}
|
|
3807
|
+
const org = await this.store.getOrg(orgId);
|
|
3808
|
+
if (org && org.ownerId === params.targetUserId) {
|
|
3809
|
+
return {
|
|
3810
|
+
status: 403,
|
|
3811
|
+
body: { error: "The organization owner's role cannot be changed. Use ownership transfer." }
|
|
3812
|
+
};
|
|
3813
|
+
}
|
|
3786
3814
|
const callerMembership = await this.store.getMembership(orgId, userId);
|
|
3787
3815
|
if (callerMembership && callerMembership.role !== "owner" && params.role === "admin") {
|
|
3788
3816
|
return { status: 403, body: { error: "Only the owner can assign admin role." } };
|
|
@@ -5358,9 +5386,9 @@ var TotpManager = class {
|
|
|
5358
5386
|
throw new TotpNotEnabledError(userId);
|
|
5359
5387
|
}
|
|
5360
5388
|
if (stored.verified) {
|
|
5361
|
-
return this.
|
|
5389
|
+
return this.consumeCode(stored, code);
|
|
5362
5390
|
}
|
|
5363
|
-
const valid = this.
|
|
5391
|
+
const valid = await this.consumeCode(stored, code);
|
|
5364
5392
|
if (!valid) {
|
|
5365
5393
|
throw new TotpInvalidCodeError();
|
|
5366
5394
|
}
|
|
@@ -5372,6 +5400,11 @@ var TotpManager = class {
|
|
|
5372
5400
|
/**
|
|
5373
5401
|
* Verify a TOTP code during login.
|
|
5374
5402
|
* Returns true if the code is valid, false otherwise.
|
|
5403
|
+
*
|
|
5404
|
+
* Enforces single-use: a code is rejected if its time-step has already
|
|
5405
|
+
* been consumed (or predates a previously consumed time-step). This
|
|
5406
|
+
* prevents an attacker who observes one valid code from replaying it
|
|
5407
|
+
* within the acceptance window (RFC 6238 §5.2).
|
|
5375
5408
|
*/
|
|
5376
5409
|
async verify(userId, code) {
|
|
5377
5410
|
const stored = await this.store.getByUserId(userId);
|
|
@@ -5381,7 +5414,7 @@ var TotpManager = class {
|
|
|
5381
5414
|
if (!stored.verified) {
|
|
5382
5415
|
throw new TotpNotVerifiedError(userId);
|
|
5383
5416
|
}
|
|
5384
|
-
return this.
|
|
5417
|
+
return this.consumeCode(stored, code);
|
|
5385
5418
|
}
|
|
5386
5419
|
/**
|
|
5387
5420
|
* Verify a recovery code as an alternative to TOTP.
|
|
@@ -5465,16 +5498,41 @@ var TotpManager = class {
|
|
|
5465
5498
|
}
|
|
5466
5499
|
// --- Private ---
|
|
5467
5500
|
validateCode(base32Secret, code) {
|
|
5501
|
+
return this.matchTimeStep(base32Secret, code) !== null;
|
|
5502
|
+
}
|
|
5503
|
+
/**
|
|
5504
|
+
* Find the TOTP time-step counter (within the acceptance window) whose
|
|
5505
|
+
* generated code matches `code`, or null if none matches. Comparison is
|
|
5506
|
+
* timing-safe.
|
|
5507
|
+
*/
|
|
5508
|
+
matchTimeStep(base32Secret, code) {
|
|
5468
5509
|
const secretBytes = base32Decode(base32Secret);
|
|
5469
5510
|
const now = Math.floor(Date.now() / 1e3);
|
|
5470
5511
|
for (let offset = -this.window; offset <= this.window; offset++) {
|
|
5471
5512
|
const timeCounter = Math.floor((now + offset * this.period) / this.period);
|
|
5472
5513
|
const expected = generateTotpCode(secretBytes, timeCounter, this.digits, this.algorithm);
|
|
5473
5514
|
if (timingSafeEqual(code, expected)) {
|
|
5474
|
-
return
|
|
5515
|
+
return timeCounter;
|
|
5475
5516
|
}
|
|
5476
5517
|
}
|
|
5477
|
-
return
|
|
5518
|
+
return null;
|
|
5519
|
+
}
|
|
5520
|
+
/**
|
|
5521
|
+
* Validate a code AND consume its time-step so it (and any earlier code in
|
|
5522
|
+
* the window) cannot be reused. Returns false for an invalid code or a
|
|
5523
|
+
* replay of an already-consumed time-step. Persists the consumed time-step.
|
|
5524
|
+
*/
|
|
5525
|
+
async consumeCode(stored, code) {
|
|
5526
|
+
const matched = this.matchTimeStep(stored.secret, code);
|
|
5527
|
+
if (matched === null) {
|
|
5528
|
+
return false;
|
|
5529
|
+
}
|
|
5530
|
+
if (stored.lastUsedTimeStep !== void 0 && matched <= stored.lastUsedTimeStep) {
|
|
5531
|
+
return false;
|
|
5532
|
+
}
|
|
5533
|
+
stored.lastUsedTimeStep = matched;
|
|
5534
|
+
await this.store.save(stored);
|
|
5535
|
+
return true;
|
|
5478
5536
|
}
|
|
5479
5537
|
};
|
|
5480
5538
|
function generateTotpCode(secret, counter, digits, algorithm) {
|