@zudojs/auth 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/dist/authErrors/authError.base.d.ts +0 -1
- package/dist/authErrors/authError.base.js +0 -1
- package/dist/authErrors/authError.throttle.d.ts +0 -1
- package/dist/authErrors/authError.throttle.js +0 -1
- package/dist/authErrors/index.d.ts +0 -1
- package/dist/authErrors/index.js +0 -1
- package/dist/authPassword/authPassword.core.d.ts +0 -1
- package/dist/authPassword/authPassword.core.js +0 -1
- package/dist/authPassword/authPassword.legacy.d.ts +0 -1
- package/dist/authPassword/authPassword.legacy.js +0 -1
- package/dist/authPassword/authPassword.policy.d.ts +0 -1
- package/dist/authPassword/authPassword.policy.js +0 -1
- package/dist/authPassword/authPassword.rehash.d.ts +0 -1
- package/dist/authPassword/authPassword.rehash.js +0 -1
- package/dist/authPassword/index.d.ts +0 -1
- package/dist/authPassword/index.js +0 -1
- package/dist/authProvider/authAttempt.eviction.d.ts +0 -1
- package/dist/authProvider/authAttempt.eviction.js +0 -1
- package/dist/authProvider/authAttempt.memory.d.ts +4 -1
- package/dist/authProvider/authAttempt.memory.js +9 -6
- package/dist/authProvider/authProvider.core.d.ts +18 -1
- package/dist/authProvider/authProvider.core.js +9 -3
- package/dist/authProvider/authProvider.external.d.ts +0 -1
- package/dist/authProvider/authProvider.external.js +1 -1
- package/dist/authProvider/authProvider.throttle.d.ts +4 -2
- package/dist/authProvider/authProvider.throttle.js +6 -4
- package/dist/authProvider/index.d.ts +0 -1
- package/dist/authProvider/index.js +0 -1
- package/dist/authSession/authSession.core.d.ts +4 -1
- package/dist/authSession/authSession.core.js +8 -5
- package/dist/authSession/index.d.ts +0 -1
- package/dist/authSession/index.js +0 -1
- package/dist/authToken/authToken.core.d.ts +20 -8
- package/dist/authToken/authToken.core.js +13 -22
- package/dist/authToken/authToken.encoding.d.ts +0 -1
- package/dist/authToken/authToken.encoding.js +0 -1
- package/dist/authToken/authToken.revocation.d.ts +19 -1
- package/dist/authToken/authToken.revocation.js +39 -6
- package/dist/authToken/authToken.signing.d.ts +0 -1
- package/dist/authToken/authToken.signing.js +2 -2
- package/dist/authToken/index.d.ts +2 -3
- package/dist/authToken/index.js +1 -2
- package/dist/authToken/jwt.namespace.d.ts +0 -1
- package/dist/authToken/jwt.namespace.js +0 -1
- package/dist/authTypes/authAttempt.type.d.ts +0 -1
- package/dist/authTypes/authAttempt.type.js +0 -1
- package/dist/authTypes/authCredentials.type.d.ts +0 -1
- package/dist/authTypes/authCredentials.type.js +0 -1
- package/dist/authTypes/authRbac.type.d.ts +0 -1
- package/dist/authTypes/authRbac.type.js +0 -1
- package/dist/authTypes/authSession.type.d.ts +0 -1
- package/dist/authTypes/authSession.type.js +0 -1
- package/dist/authTypes/authToken.type.d.ts +8 -1
- package/dist/authTypes/authToken.type.js +0 -1
- package/dist/authTypes/authUser.type.d.ts +7 -2
- package/dist/authTypes/authUser.type.js +0 -1
- package/dist/authTypes/index.d.ts +0 -1
- package/dist/authTypes/index.js +0 -1
- package/dist/authUtils/authUtils.claims.d.ts +21 -0
- package/dist/authUtils/authUtils.claims.js +44 -0
- package/dist/authUtils/authUtils.helper.d.ts +4 -2
- package/dist/authUtils/authUtils.helper.js +3 -3
- package/dist/authUtils/authUtils.identifier.d.ts +0 -1
- package/dist/authUtils/authUtils.identifier.js +0 -1
- package/dist/authUtils/index.d.ts +1 -1
- package/dist/authUtils/index.js +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ const tokenConfig: TokenConfig = {
|
|
|
55
55
|
issuer: "my-api",
|
|
56
56
|
audience: "my-app",
|
|
57
57
|
clockToleranceSeconds: 5, // optional skew allowance for exp/iat/nbf
|
|
58
|
+
// clock: myClock, // optional { now(): number } for deterministic expiry tests
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
const auth = createAuthService({
|
|
@@ -194,7 +195,38 @@ With a `revocationStore` configured, `refresh()` claims the presented token's
|
|
|
194
195
|
Replaying an already-used refresh token throws `TokenRevokedError` **and**
|
|
195
196
|
destroys every session for that user, on the assumption that the chain is
|
|
196
197
|
compromised (RFC 6819 §5.2.2.3). Implement `revokeIfNotRevoked` in any custom
|
|
197
|
-
store — the `isRevoked` + `revoke` fallback is racy.
|
|
198
|
+
store — the `isRevoked` + `revoke` fallback is racy. A store without it is
|
|
199
|
+
reported at construction: `createAuthService()` emits a `SecurityWarning`
|
|
200
|
+
(`process.emitWarning`, code `ZUDO_AUTH_RACY_REVOCATION`), and with
|
|
201
|
+
`requireAtomicRevocation: true` it throws `AuthConfigurationError` instead.
|
|
202
|
+
|
|
203
|
+
### Custom claims
|
|
204
|
+
|
|
205
|
+
`AuthUser.claims` (for example `{ plan: "pro", org: "acme" }`) is embedded in
|
|
206
|
+
every token the service mints — `login()`, `refresh()` and
|
|
207
|
+
`createSessionForUser()` — and comes back on `verifyToken()`'s payload. Reserved
|
|
208
|
+
names (`sub`, `iat`, `exp`, `nbf`, `typ`, `jti`, `sid`, `roles`, `iss`, `aud`)
|
|
209
|
+
are dropped, never overridden. The standalone `createTokenPair()` takes the same
|
|
210
|
+
`claims` option. Claims are readable by anyone holding the token, so keep them
|
|
211
|
+
small and non-sensitive.
|
|
212
|
+
|
|
213
|
+
### Deterministic time in tests
|
|
214
|
+
|
|
215
|
+
Pass a `{ now(): number }` clock instead of faking global timers:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const clock = { now: () => fixedMs };
|
|
219
|
+
const auth = createAuthService({
|
|
220
|
+
clock,
|
|
221
|
+
sessionStore: createMemorySessionStore({ clock }),
|
|
222
|
+
revocationStore: createMemoryTokenRevocationStore({ clock }),
|
|
223
|
+
loginThrottle: { store: createMemoryLoginAttemptStore({ clock }) },
|
|
224
|
+
// ...
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Token `iat`/`exp`, verification, session expiry, revocation expiry and
|
|
229
|
+
lockout deadlines all read that clock; advance it to test expiry.
|
|
198
230
|
|
|
199
231
|
### Access control
|
|
200
232
|
|
|
@@ -5,4 +5,3 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { AuthError, type AuthErrorOptions, AuthConfigurationError, InvalidCredentialsError, TokenExpiredError, TokenInvalidError, TokenRevokedError, AccountDeactivatedError, AccessDeniedError, SessionExpiredError, } from "./authError.base.js";
|
|
7
7
|
export { AccountLockedError, AuthRateLimitError, type ThrottleErrorOptions, } from "./authError.throttle.js";
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/authErrors/index.js
CHANGED
|
@@ -5,4 +5,3 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { AuthError, AuthConfigurationError, InvalidCredentialsError, TokenExpiredError, TokenInvalidError, TokenRevokedError, AccountDeactivatedError, AccessDeniedError, SessionExpiredError, } from "./authError.base.js";
|
|
7
7
|
export { AccountLockedError, AuthRateLimitError, } from "./authError.throttle.js";
|
|
8
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -18,4 +18,3 @@ export declare function isLegacyPasswordHash(hashedPassword: string): boolean;
|
|
|
18
18
|
* unparseable hash or parameters scrypt cannot satisfy; never throws.
|
|
19
19
|
*/
|
|
20
20
|
export declare function verifyLegacyPassword(password: string, hashedPassword: string): Promise<boolean>;
|
|
21
|
-
//# sourceMappingURL=authPassword.legacy.d.ts.map
|
|
@@ -6,4 +6,3 @@
|
|
|
6
6
|
export { hashPassword, verifyPassword, generateRandomToken, } from "./authPassword.core.js";
|
|
7
7
|
export { needsRehash } from "./authPassword.rehash.js";
|
|
8
8
|
export { MIN_SALT_LENGTH, MAX_SALT_LENGTH, MAX_PASSWORD_BYTES, } from "./authPassword.policy.js";
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -6,4 +6,3 @@
|
|
|
6
6
|
export { hashPassword, verifyPassword, generateRandomToken, } from "./authPassword.core.js";
|
|
7
7
|
export { needsRehash } from "./authPassword.rehash.js";
|
|
8
8
|
export { MIN_SALT_LENGTH, MAX_SALT_LENGTH, MAX_PASSWORD_BYTES, } from "./authPassword.policy.js";
|
|
9
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -23,4 +23,3 @@ export declare function isStale(entry: EvictableAttemptEntry, now: number, windo
|
|
|
23
23
|
* insertion order), or the oldest entry of all when every entry is locked.
|
|
24
24
|
*/
|
|
25
25
|
export declare function evictOne<E extends EvictableAttemptEntry>(entries: Map<string, E>, now: number): void;
|
|
26
|
-
//# sourceMappingURL=authAttempt.eviction.d.ts.map
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Single-process only: counters are not shared between instances. Back
|
|
7
7
|
* `LoginAttemptStore` with Redis for a real deployment.
|
|
8
8
|
*/
|
|
9
|
+
import type { Clock } from "@zudojs/types";
|
|
9
10
|
import type { LoginAttemptStore } from "../authTypes/authAttempt.type.js";
|
|
10
11
|
/**
|
|
11
12
|
* Create an in-memory {@link LoginAttemptStore}.
|
|
@@ -20,11 +21,13 @@ import type { LoginAttemptStore } from "../authTypes/authAttempt.type.js";
|
|
|
20
21
|
* @param options.maxEntries - Hard cap on tracked identifiers (default:
|
|
21
22
|
* 100000). At the cap the oldest unlocked entry is evicted; locked
|
|
22
23
|
* entries are only evicted when every entry is locked.
|
|
24
|
+
* @param options.clock - Time source for windows, lockouts and eviction
|
|
25
|
+
* (default: `Date.now`). Pass the same clock to `createAuthService`.
|
|
23
26
|
*/
|
|
24
27
|
export declare function createMemoryLoginAttemptStore(options?: {
|
|
25
28
|
readonly windowSeconds?: number;
|
|
26
29
|
readonly purgeIntervalMs?: number;
|
|
27
30
|
readonly failureTtlSeconds?: number;
|
|
28
31
|
readonly maxEntries?: number;
|
|
32
|
+
readonly clock?: Clock;
|
|
29
33
|
}): LoginAttemptStore;
|
|
30
|
-
//# sourceMappingURL=authAttempt.memory.d.ts.map
|
|
@@ -25,8 +25,12 @@ const EMPTY = { failures: 0, attempts: 0 };
|
|
|
25
25
|
* @param options.maxEntries - Hard cap on tracked identifiers (default:
|
|
26
26
|
* 100000). At the cap the oldest unlocked entry is evicted; locked
|
|
27
27
|
* entries are only evicted when every entry is locked.
|
|
28
|
+
* @param options.clock - Time source for windows, lockouts and eviction
|
|
29
|
+
* (default: `Date.now`). Pass the same clock to `createAuthService`.
|
|
28
30
|
*/
|
|
29
31
|
export function createMemoryLoginAttemptStore(options) {
|
|
32
|
+
const clock = options?.clock;
|
|
33
|
+
const currentMs = () => (clock ? clock.now() : Date.now());
|
|
30
34
|
const windowMs = (options?.windowSeconds ?? DEFAULT_WINDOW_SECONDS) * 1000;
|
|
31
35
|
const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
32
36
|
const failureTtlMs = (options?.failureTtlSeconds ?? DEFAULT_FAILURE_TTL_SECONDS) * 1000;
|
|
@@ -78,7 +82,7 @@ export function createMemoryLoginAttemptStore(options) {
|
|
|
78
82
|
}
|
|
79
83
|
return {
|
|
80
84
|
async get(identifier) {
|
|
81
|
-
const now =
|
|
85
|
+
const now = currentMs();
|
|
82
86
|
if (!entries.has(identifier)) {
|
|
83
87
|
maybePurge(now);
|
|
84
88
|
return EMPTY;
|
|
@@ -86,18 +90,18 @@ export function createMemoryLoginAttemptStore(options) {
|
|
|
86
90
|
return snapshot(load(identifier, now));
|
|
87
91
|
},
|
|
88
92
|
async recordAttempt(identifier) {
|
|
89
|
-
const entry = load(identifier,
|
|
93
|
+
const entry = load(identifier, currentMs());
|
|
90
94
|
entry.attempts++;
|
|
91
95
|
return snapshot(entry);
|
|
92
96
|
},
|
|
93
97
|
async recordFailure(identifier) {
|
|
94
|
-
const entry = load(identifier,
|
|
98
|
+
const entry = load(identifier, currentMs());
|
|
95
99
|
entry.failures++;
|
|
96
|
-
entry.lastFailureAt =
|
|
100
|
+
entry.lastFailureAt = currentMs();
|
|
97
101
|
return snapshot(entry);
|
|
98
102
|
},
|
|
99
103
|
async lock(identifier, until) {
|
|
100
|
-
const entry = load(identifier,
|
|
104
|
+
const entry = load(identifier, currentMs());
|
|
101
105
|
entry.lockedUntil = until;
|
|
102
106
|
},
|
|
103
107
|
async reset(identifier) {
|
|
@@ -105,4 +109,3 @@ export function createMemoryLoginAttemptStore(options) {
|
|
|
105
109
|
},
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
|
-
//# sourceMappingURL=authAttempt.memory.js.map
|
|
@@ -7,6 +7,7 @@ import type { AuthUser, UserCredentials, UserId } from "../authTypes/authUser.ty
|
|
|
7
7
|
import type { TokenPair, TokenPayload, TokenConfig, TokenRevocationStore } from "../authTypes/authToken.type.js";
|
|
8
8
|
import type { SessionStore, SessionId } from "../authTypes/authSession.type.js";
|
|
9
9
|
import type { LoginThrottleConfig } from "../authTypes/authAttempt.type.js";
|
|
10
|
+
import type { Clock } from "@zudojs/types";
|
|
10
11
|
import { type ExternalSessionOptions } from "./authProvider.external.js";
|
|
11
12
|
import type { GuardContext, GuardResult } from "../authTypes/authRbac.type.js";
|
|
12
13
|
import type { PermissionEngine } from "@zudojs/permissions";
|
|
@@ -100,6 +101,23 @@ export interface AuthServiceConfig {
|
|
|
100
101
|
* string, or your own function to match how you store identifiers.
|
|
101
102
|
*/
|
|
102
103
|
readonly normalizeIdentifier?: false | ((identifier: string) => string);
|
|
104
|
+
/**
|
|
105
|
+
* Time source for token `iat`/`exp`, token verification and lockout
|
|
106
|
+
* deadlines (default: `Date.now`). It is copied into `token.clock` unless
|
|
107
|
+
* that is already set. Pass the same clock to the memory stores
|
|
108
|
+
* (`createMemorySessionStore({ clock })`, and the revocation and attempt
|
|
109
|
+
* stores) so expiry tests need no fake timers.
|
|
110
|
+
*/
|
|
111
|
+
readonly clock?: Clock;
|
|
112
|
+
/**
|
|
113
|
+
* Refuse a `revocationStore` that lacks `revokeIfNotRevoked` (default:
|
|
114
|
+
* `false`). Without that method `refresh()` falls back to `isRevoked()` +
|
|
115
|
+
* `revoke()`, which is racy; by default construction emits a
|
|
116
|
+
* `SecurityWarning` (code `ZUDO_AUTH_RACY_REVOCATION`) through
|
|
117
|
+
* `process.emitWarning`, and with this flag it throws
|
|
118
|
+
* `AuthConfigurationError` instead.
|
|
119
|
+
*/
|
|
120
|
+
readonly requireAtomicRevocation?: boolean;
|
|
103
121
|
}
|
|
104
122
|
/**
|
|
105
123
|
* Auth service interface.
|
|
@@ -150,4 +168,3 @@ export interface LoginResult {
|
|
|
150
168
|
* Create an auth service.
|
|
151
169
|
*/
|
|
152
170
|
export declare function createAuthService(config: AuthServiceConfig): AuthService;
|
|
153
|
-
//# sourceMappingURL=authProvider.core.d.ts.map
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module authProvider/authProvider
|
|
5
5
|
*/
|
|
6
6
|
import { createLoginThrottleGate } from "./authProvider.throttle.js";
|
|
7
|
+
import { assertAtomicRevocationStore } from "../authToken/authToken.revocation.js";
|
|
7
8
|
import { createExternalSessionStarter, } from "./authProvider.external.js";
|
|
8
9
|
import { normalizeLoginIdentifier } from "../authUtils/authUtils.identifier.js";
|
|
9
10
|
import { hashPassword, verifyPassword, } from "../authPassword/authPassword.core.js";
|
|
@@ -24,7 +25,8 @@ export { throttleKey } from "./authProvider.throttle.js";
|
|
|
24
25
|
* Create an auth service.
|
|
25
26
|
*/
|
|
26
27
|
export function createAuthService(config) {
|
|
27
|
-
const {
|
|
28
|
+
const { sessionStore, findUser, findUserById, verifyPassword: verifyPwd, sessionTtlSeconds, absoluteSessionTtlSeconds, permissions, revocationStore, loginThrottle, allowInsecureFallbackGuard, fallbackAdminRole, allowSessionlessTokens, externalSessionMethods, normalizeIdentifier, clock, requireAtomicRevocation, } = config;
|
|
29
|
+
const tokenConfig = clock && !config.token.clock ? { ...config.token, clock } : config.token;
|
|
28
30
|
// Fail at construction, not at the first login: a bad secret or a NaN
|
|
29
31
|
// lifetime (`Number(process.env.X)` with X unset) otherwise surfaced as
|
|
30
32
|
// a runtime error on the request path — or, for the session TTL, not at
|
|
@@ -32,7 +34,10 @@ export function createAuthService(config) {
|
|
|
32
34
|
assertTokenSecrets(tokenConfig);
|
|
33
35
|
assertPositiveSeconds(sessionTtlSeconds, "sessionTtlSeconds");
|
|
34
36
|
assertPositiveSeconds(absoluteSessionTtlSeconds, "absoluteSessionTtlSeconds");
|
|
35
|
-
|
|
37
|
+
if (revocationStore) {
|
|
38
|
+
assertAtomicRevocationStore(revocationStore, requireAtomicRevocation === true);
|
|
39
|
+
}
|
|
40
|
+
const throttle = createLoginThrottleGate(loginThrottle, tokenConfig.clock);
|
|
36
41
|
const normalize = normalizeIdentifier === false
|
|
37
42
|
? (identifier) => identifier
|
|
38
43
|
: (normalizeIdentifier ?? normalizeLoginIdentifier);
|
|
@@ -124,6 +129,7 @@ export function createAuthService(config) {
|
|
|
124
129
|
const tokens = createTokenPair(user.id, tokenConfig, {
|
|
125
130
|
roles: user.roles,
|
|
126
131
|
sessionId: session.id,
|
|
132
|
+
claims: user.claims,
|
|
127
133
|
});
|
|
128
134
|
return { user, tokens, sessionId: session.id };
|
|
129
135
|
},
|
|
@@ -186,6 +192,7 @@ export function createAuthService(config) {
|
|
|
186
192
|
}
|
|
187
193
|
return createTokenPair(user.id, tokenConfig, {
|
|
188
194
|
roles: user.roles,
|
|
195
|
+
claims: user.claims,
|
|
189
196
|
...(sid ? { sessionId: sid } : {}),
|
|
190
197
|
});
|
|
191
198
|
},
|
|
@@ -291,4 +298,3 @@ function simpleGuard(userRoles, permission, userId, resourceOwnerId, adminRole)
|
|
|
291
298
|
userRoles: [...userRoles],
|
|
292
299
|
};
|
|
293
300
|
}
|
|
294
|
-
//# sourceMappingURL=authProvider.core.js.map
|
|
@@ -48,4 +48,3 @@ export interface ExternalSessionDependencies {
|
|
|
48
48
|
* users.
|
|
49
49
|
*/
|
|
50
50
|
export declare function createExternalSessionStarter(deps: ExternalSessionDependencies): (userId: UserId, options: ExternalSessionOptions) => Promise<ExternalSessionResult>;
|
|
51
|
-
//# sourceMappingURL=authProvider.external.d.ts.map
|
|
@@ -43,8 +43,8 @@ export function createExternalSessionStarter(deps) {
|
|
|
43
43
|
const tokens = createTokenPair(user.id, deps.tokenConfig, {
|
|
44
44
|
roles: user.roles,
|
|
45
45
|
sessionId: session.id,
|
|
46
|
+
claims: user.claims,
|
|
46
47
|
});
|
|
47
48
|
return { user, tokens, sessionId: session.id };
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
//# sourceMappingURL=authProvider.external.js.map
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module authProvider/authProvider.throttle
|
|
6
6
|
*/
|
|
7
|
+
import type { Clock } from "@zudojs/types";
|
|
7
8
|
import type { LoginThrottleConfig } from "../authTypes/authAttempt.type.js";
|
|
8
9
|
/**
|
|
9
10
|
* The key under which an identifier's login attempts are counted.
|
|
@@ -47,6 +48,7 @@ export interface LoginThrottleGate {
|
|
|
47
48
|
*
|
|
48
49
|
* An attempt that throws for another reason (e.g. the user lookup fails)
|
|
49
50
|
* keeps its reservation, so it counts as a failure.
|
|
51
|
+
*
|
|
52
|
+
* @param clock - Time source for lockout deadlines (default: `Date.now`).
|
|
50
53
|
*/
|
|
51
|
-
export declare function createLoginThrottleGate(config: LoginThrottleConfig | undefined): LoginThrottleGate;
|
|
52
|
-
//# sourceMappingURL=authProvider.throttle.d.ts.map
|
|
54
|
+
export declare function createLoginThrottleGate(config: LoginThrottleConfig | undefined, clock?: Clock): LoginThrottleGate;
|
|
@@ -36,8 +36,11 @@ const NO_SLOT = Object.freeze({ failures: 0 });
|
|
|
36
36
|
*
|
|
37
37
|
* An attempt that throws for another reason (e.g. the user lookup fails)
|
|
38
38
|
* keeps its reservation, so it counts as a failure.
|
|
39
|
+
*
|
|
40
|
+
* @param clock - Time source for lockout deadlines (default: `Date.now`).
|
|
39
41
|
*/
|
|
40
|
-
export function createLoginThrottleGate(config) {
|
|
42
|
+
export function createLoginThrottleGate(config, clock) {
|
|
43
|
+
const currentMs = () => (clock ? clock.now() : Date.now());
|
|
41
44
|
const maxFailedAttempts = config?.maxFailedAttempts ?? DEFAULT_MAX_FAILED_ATTEMPTS;
|
|
42
45
|
const lockoutMs = (config?.lockoutSeconds ?? DEFAULT_LOCKOUT_SECONDS) * 1000;
|
|
43
46
|
const maxAttemptsPerWindow = config?.maxAttemptsPerWindow ?? DEFAULT_MAX_ATTEMPTS_PER_WINDOW;
|
|
@@ -53,7 +56,7 @@ export function createLoginThrottleGate(config) {
|
|
|
53
56
|
if (!config)
|
|
54
57
|
return NO_SLOT;
|
|
55
58
|
const key = throttleKey(identifier);
|
|
56
|
-
const now =
|
|
59
|
+
const now = currentMs();
|
|
57
60
|
const current = await config.store.get(key);
|
|
58
61
|
if (current.lockedUntil !== undefined && current.lockedUntil > now) {
|
|
59
62
|
throw locked(current, now);
|
|
@@ -77,7 +80,7 @@ export function createLoginThrottleGate(config) {
|
|
|
77
80
|
if (!config)
|
|
78
81
|
return;
|
|
79
82
|
if (slot.failures >= maxFailedAttempts) {
|
|
80
|
-
await config.store.lock(throttleKey(identifier),
|
|
83
|
+
await config.store.lock(throttleKey(identifier), currentMs() + lockoutMs);
|
|
81
84
|
}
|
|
82
85
|
},
|
|
83
86
|
async succeed(identifier) {
|
|
@@ -85,4 +88,3 @@ export function createLoginThrottleGate(config) {
|
|
|
85
88
|
},
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
|
-
//# sourceMappingURL=authProvider.throttle.js.map
|
|
@@ -6,4 +6,3 @@
|
|
|
6
6
|
export { createAuthService, type AuthService, type AuthServiceConfig, type LoginResult, type UserLookup, type UserByIdLookup, type PasswordVerifier, } from "./authProvider.core.js";
|
|
7
7
|
export { createMemoryLoginAttemptStore } from "./authAttempt.memory.js";
|
|
8
8
|
export { type ExternalSessionOptions, type ExternalSessionResult, } from "./authProvider.external.js";
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* For production, implement SessionStore backed by Redis, database, etc.
|
|
7
7
|
*/
|
|
8
|
+
import type { Clock } from "@zudojs/types";
|
|
8
9
|
import type { SessionStore } from "../authTypes/authSession.type.js";
|
|
9
10
|
/**
|
|
10
11
|
* Create an in-memory session store.
|
|
@@ -19,9 +20,12 @@ import type { SessionStore } from "../authTypes/authSession.type.js";
|
|
|
19
20
|
*
|
|
20
21
|
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
21
22
|
* (default: 60000). Set to 0 to sweep on every access.
|
|
23
|
+
* @param options.clock - Time source for creation, activity and expiry
|
|
24
|
+
* (default: `Date.now`). Pass the same clock to `createAuthService`.
|
|
22
25
|
*/
|
|
23
26
|
export declare function createMemorySessionStore(storeOptions?: {
|
|
24
27
|
readonly purgeIntervalMs?: number;
|
|
28
|
+
readonly clock?: Clock;
|
|
25
29
|
}): SessionStore;
|
|
26
30
|
/**
|
|
27
31
|
* Reject a TTL that cannot produce a real expiry.
|
|
@@ -30,4 +34,3 @@ export declare function createMemorySessionStore(storeOptions?: {
|
|
|
30
34
|
* finite number greater than zero.
|
|
31
35
|
*/
|
|
32
36
|
export declare function assertPositiveSeconds(value: number | undefined, field: string): void;
|
|
33
|
-
//# sourceMappingURL=authSession.core.d.ts.map
|
|
@@ -23,14 +23,18 @@ const DEFAULT_PURGE_INTERVAL_MS = 60_000;
|
|
|
23
23
|
*
|
|
24
24
|
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
25
25
|
* (default: 60000). Set to 0 to sweep on every access.
|
|
26
|
+
* @param options.clock - Time source for creation, activity and expiry
|
|
27
|
+
* (default: `Date.now`). Pass the same clock to `createAuthService`.
|
|
26
28
|
*/
|
|
27
29
|
export function createMemorySessionStore(storeOptions) {
|
|
28
30
|
const sessions = new Map();
|
|
29
31
|
const ttls = new Map();
|
|
30
32
|
const purgeIntervalMs = storeOptions?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
33
|
+
const clock = storeOptions?.clock;
|
|
34
|
+
const currentMs = () => (clock ? clock.now() : Date.now());
|
|
31
35
|
let lastPurge = 0;
|
|
32
36
|
function maybePurgeExpired() {
|
|
33
|
-
const nowMs =
|
|
37
|
+
const nowMs = currentMs();
|
|
34
38
|
if (nowMs - lastPurge < purgeIntervalMs)
|
|
35
39
|
return;
|
|
36
40
|
lastPurge = nowMs;
|
|
@@ -50,7 +54,7 @@ export function createMemorySessionStore(storeOptions) {
|
|
|
50
54
|
assertPositiveSeconds(options.absoluteTtlSeconds, "absoluteTtlSeconds");
|
|
51
55
|
maybePurgeExpired();
|
|
52
56
|
const id = await generateSessionId();
|
|
53
|
-
const now = new Date();
|
|
57
|
+
const now = new Date(currentMs());
|
|
54
58
|
const ttlMs = (options.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
55
59
|
const absoluteExpiresAt = options.absoluteTtlSeconds !== undefined
|
|
56
60
|
? new Date(now.getTime() + options.absoluteTtlSeconds * 1000)
|
|
@@ -76,7 +80,7 @@ export function createMemorySessionStore(storeOptions) {
|
|
|
76
80
|
const session = sessions.get(sessionId);
|
|
77
81
|
if (!session)
|
|
78
82
|
return null;
|
|
79
|
-
if (
|
|
83
|
+
if (currentMs() > session.expiresAt.getTime()) {
|
|
80
84
|
sessions.delete(sessionId);
|
|
81
85
|
ttls.delete(sessionId);
|
|
82
86
|
return null;
|
|
@@ -88,7 +92,7 @@ export function createMemorySessionStore(storeOptions) {
|
|
|
88
92
|
const session = sessions.get(sessionId);
|
|
89
93
|
if (!session)
|
|
90
94
|
return;
|
|
91
|
-
const now = new Date();
|
|
95
|
+
const now = new Date(currentMs());
|
|
92
96
|
if (now.getTime() > session.expiresAt.getTime()) {
|
|
93
97
|
sessions.delete(sessionId);
|
|
94
98
|
ttls.delete(sessionId);
|
|
@@ -140,4 +144,3 @@ function clampToAbsolute(expiresAt, absolute) {
|
|
|
140
144
|
async function generateSessionId() {
|
|
141
145
|
return (await randomHex(64));
|
|
142
146
|
}
|
|
143
|
-
//# sourceMappingURL=authSession.core.js.map
|
|
@@ -8,19 +8,32 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { SessionId, UserId } from "@zudojs/constants";
|
|
10
10
|
import type { JwtToken, TokenPair, TokenConfig, TokenVerificationResult } from "../authTypes/authToken.type.js";
|
|
11
|
+
/** Options for {@link createTokenPair}. */
|
|
12
|
+
export interface CreateTokenPairOptions {
|
|
13
|
+
/** Roles to embed in both tokens. */
|
|
14
|
+
readonly roles?: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* Session to bind the pair to (`sid` claim). `createAuthService()` sets
|
|
17
|
+
* this so that `logout()` invalidates the pair.
|
|
18
|
+
*/
|
|
19
|
+
readonly sessionId?: SessionId;
|
|
20
|
+
/**
|
|
21
|
+
* Custom claims embedded in both tokens, e.g. `{ plan: "pro" }`.
|
|
22
|
+
* Reserved names (`sub`, `iat`, `exp`, `nbf`, `typ`, `jti`, `sid`,
|
|
23
|
+
* `roles`, `iss`, `aud`) are dropped, never overridden. Keep them small:
|
|
24
|
+
* a token over 8 KB is rejected on verification.
|
|
25
|
+
*/
|
|
26
|
+
readonly claims?: Readonly<Record<string, unknown>>;
|
|
27
|
+
}
|
|
11
28
|
/**
|
|
12
29
|
* Create a new token pair (access + refresh).
|
|
13
30
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `createAuthService()` sets this so that `logout()` invalidates the pair.
|
|
31
|
+
* `iat`/`exp` come from `config.clock` when set (default: `Date.now`).
|
|
32
|
+
*
|
|
17
33
|
* @throws {AuthConfigurationError} when the signing secrets are missing,
|
|
18
34
|
* shorter than 32 bytes, or identical to each other.
|
|
19
35
|
*/
|
|
20
|
-
export declare function createTokenPair(userId: UserId, config: TokenConfig, options?:
|
|
21
|
-
readonly roles?: readonly string[];
|
|
22
|
-
readonly sessionId?: SessionId;
|
|
23
|
-
}): TokenPair;
|
|
36
|
+
export declare function createTokenPair(userId: UserId, config: TokenConfig, options?: CreateTokenPairOptions): TokenPair;
|
|
24
37
|
/**
|
|
25
38
|
* Verify and decode an access token.
|
|
26
39
|
*
|
|
@@ -60,4 +73,3 @@ export declare function verifyRefreshToken(token: JwtToken, config: TokenConfig)
|
|
|
60
73
|
export declare function refreshAccessToken(refreshToken: JwtToken, config: TokenConfig, options?: {
|
|
61
74
|
readonly roles?: readonly string[];
|
|
62
75
|
}): TokenPair | null;
|
|
63
|
-
//# sourceMappingURL=authToken.core.d.ts.map
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Pure Node.js implementation (no jsonwebtoken dependency).
|
|
7
7
|
* Uses HMAC SHA-256 for signing.
|
|
8
8
|
*/
|
|
9
|
+
import { sanitizeCustomClaims } from "../authUtils/authUtils.claims.js";
|
|
9
10
|
import { signToken, verifyToken, generateTokenId, assertTokenSecrets, } from "./authToken.signing.js";
|
|
10
11
|
// TTLs are in seconds — they are added to Unix-second `iat`/`exp` claims.
|
|
11
12
|
const DEFAULT_ACCESS_TTL = 900; // 15 minutes
|
|
@@ -13,9 +14,8 @@ const DEFAULT_REFRESH_TTL = 604_800; // 7 days
|
|
|
13
14
|
/**
|
|
14
15
|
* Create a new token pair (access + refresh).
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `createAuthService()` sets this so that `logout()` invalidates the pair.
|
|
17
|
+
* `iat`/`exp` come from `config.clock` when set (default: `Date.now`).
|
|
18
|
+
*
|
|
19
19
|
* @throws {AuthConfigurationError} when the signing secrets are missing,
|
|
20
20
|
* shorter than 32 bytes, or identical to each other.
|
|
21
21
|
*/
|
|
@@ -23,32 +23,24 @@ export function createTokenPair(userId, config, options) {
|
|
|
23
23
|
assertTokenSecrets(config);
|
|
24
24
|
const accessTtl = config.accessTtl ?? DEFAULT_ACCESS_TTL;
|
|
25
25
|
const refreshTtl = config.refreshTtl ?? DEFAULT_REFRESH_TTL;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
26
|
+
const nowMs = config.clock ? config.clock.now() : Date.now();
|
|
27
|
+
const now = Math.floor(nowMs / 1000);
|
|
28
|
+
const custom = sanitizeCustomClaims(options?.claims);
|
|
29
|
+
const payload = (typ, ttl) => ({
|
|
30
|
+
...custom,
|
|
28
31
|
sub: userId,
|
|
29
32
|
iat: now,
|
|
30
|
-
exp: now +
|
|
31
|
-
typ
|
|
33
|
+
exp: now + ttl,
|
|
34
|
+
typ,
|
|
32
35
|
jti: generateTokenId(),
|
|
33
36
|
roles: options?.roles,
|
|
34
37
|
...(options?.sessionId ? { sid: options.sessionId } : {}),
|
|
35
38
|
...(config.issuer ? { iss: config.issuer } : {}),
|
|
36
39
|
...(config.audience ? { aud: config.audience } : {}),
|
|
37
|
-
}
|
|
38
|
-
const refreshToken = signToken({
|
|
39
|
-
sub: userId,
|
|
40
|
-
iat: now,
|
|
41
|
-
exp: now + refreshTtl,
|
|
42
|
-
typ: "refresh",
|
|
43
|
-
jti: generateTokenId(),
|
|
44
|
-
roles: options?.roles,
|
|
45
|
-
...(options?.sessionId ? { sid: options.sessionId } : {}),
|
|
46
|
-
...(config.issuer ? { iss: config.issuer } : {}),
|
|
47
|
-
...(config.audience ? { aud: config.audience } : {}),
|
|
48
|
-
}, config.refreshSecret);
|
|
40
|
+
});
|
|
49
41
|
return {
|
|
50
|
-
accessToken,
|
|
51
|
-
refreshToken,
|
|
42
|
+
accessToken: signToken(payload("access", accessTtl), config.accessSecret),
|
|
43
|
+
refreshToken: signToken(payload("refresh", refreshTtl), config.refreshSecret),
|
|
52
44
|
expiresIn: accessTtl,
|
|
53
45
|
tokenType: "Bearer",
|
|
54
46
|
};
|
|
@@ -105,4 +97,3 @@ export function refreshAccessToken(refreshToken, config, options) {
|
|
|
105
97
|
...(sid ? { sessionId: sid } : {}),
|
|
106
98
|
});
|
|
107
99
|
}
|
|
108
|
-
//# sourceMappingURL=authToken.core.js.map
|
|
@@ -40,4 +40,3 @@ export declare function splitToken(token: unknown): readonly [string, string, st
|
|
|
40
40
|
* or does not decode to a non-null, non-array object.
|
|
41
41
|
*/
|
|
42
42
|
export declare function decodeJsonSegment(segment: string): Record<string, unknown> | null;
|
|
43
|
-
//# sourceMappingURL=authToken.encoding.d.ts.map
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* implement TokenRevocationStore with Redis or a database so revocations
|
|
8
8
|
* are shared across instances.
|
|
9
9
|
*/
|
|
10
|
+
import type { Clock } from "@zudojs/types";
|
|
10
11
|
import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
|
|
12
|
+
/** `code` of the process warning emitted for a non-atomic revocation store. */
|
|
13
|
+
export declare const RACY_REVOCATION_WARNING_CODE = "ZUDO_AUTH_RACY_REVOCATION";
|
|
11
14
|
/**
|
|
12
15
|
* Create an in-memory token revocation store.
|
|
13
16
|
*
|
|
@@ -18,8 +21,23 @@ import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
|
|
|
18
21
|
*
|
|
19
22
|
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
20
23
|
* (default: 60000). Set to 0 to sweep on every access.
|
|
24
|
+
* @param options.clock - Time source for expiry (default: `Date.now`).
|
|
21
25
|
*/
|
|
22
26
|
export declare function createMemoryTokenRevocationStore(options?: {
|
|
23
27
|
readonly purgeIntervalMs?: number;
|
|
28
|
+
readonly clock?: Clock;
|
|
24
29
|
}): TokenRevocationStore;
|
|
25
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Check that a revocation store can claim a refresh token atomically.
|
|
32
|
+
*
|
|
33
|
+
* Without `revokeIfNotRevoked`, `createAuthService().refresh()` falls back
|
|
34
|
+
* to `isRevoked()` then `revoke()`, which leaves a window in which two
|
|
35
|
+
* concurrent replays of one refresh token both mint a valid pair. That
|
|
36
|
+
* used to happen silently. Now a `SecurityWarning` with code
|
|
37
|
+
* {@link RACY_REVOCATION_WARNING_CODE} is emitted through
|
|
38
|
+
* `process.emitWarning` at construction, or, with `required`, the service
|
|
39
|
+
* refuses to start.
|
|
40
|
+
*
|
|
41
|
+
* @throws {AuthConfigurationError} when `required` and the method is absent.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertAtomicRevocationStore(store: TokenRevocationStore, required: boolean): void;
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* implement TokenRevocationStore with Redis or a database so revocations
|
|
8
8
|
* are shared across instances.
|
|
9
9
|
*/
|
|
10
|
+
import { AuthConfigurationError } from "../authErrors/authError.base.js";
|
|
10
11
|
/** Minimum interval between full sweeps of the revocation map. */
|
|
11
12
|
const DEFAULT_PURGE_INTERVAL_MS = 60_000;
|
|
13
|
+
/** `code` of the process warning emitted for a non-atomic revocation store. */
|
|
14
|
+
export const RACY_REVOCATION_WARNING_CODE = "ZUDO_AUTH_RACY_REVOCATION";
|
|
12
15
|
/**
|
|
13
16
|
* Create an in-memory token revocation store.
|
|
14
17
|
*
|
|
@@ -19,18 +22,21 @@ const DEFAULT_PURGE_INTERVAL_MS = 60_000;
|
|
|
19
22
|
*
|
|
20
23
|
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
21
24
|
* (default: 60000). Set to 0 to sweep on every access.
|
|
25
|
+
* @param options.clock - Time source for expiry (default: `Date.now`).
|
|
22
26
|
*/
|
|
23
27
|
export function createMemoryTokenRevocationStore(options) {
|
|
24
28
|
const revoked = new Map();
|
|
25
29
|
const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
30
|
+
const clock = options?.clock;
|
|
31
|
+
const nowMs = () => (clock ? clock.now() : Date.now());
|
|
26
32
|
let lastPurge = 0;
|
|
27
33
|
/** Full sweep, rate-limited so a large map cannot be walked per request. */
|
|
28
34
|
function maybePurgeExpired() {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
35
|
+
const current = nowMs();
|
|
36
|
+
if (current - lastPurge < purgeIntervalMs)
|
|
31
37
|
return;
|
|
32
|
-
lastPurge =
|
|
33
|
-
const now = Math.floor(
|
|
38
|
+
lastPurge = current;
|
|
39
|
+
const now = Math.floor(current / 1000);
|
|
34
40
|
for (const [id, expiresAt] of revoked) {
|
|
35
41
|
if (expiresAt < now) {
|
|
36
42
|
revoked.delete(id);
|
|
@@ -42,7 +48,7 @@ export function createMemoryTokenRevocationStore(options) {
|
|
|
42
48
|
const expiresAt = revoked.get(tokenId);
|
|
43
49
|
if (expiresAt === undefined)
|
|
44
50
|
return false;
|
|
45
|
-
if (expiresAt < Math.floor(
|
|
51
|
+
if (expiresAt < Math.floor(nowMs() / 1000)) {
|
|
46
52
|
revoked.delete(tokenId);
|
|
47
53
|
return false;
|
|
48
54
|
}
|
|
@@ -68,4 +74,31 @@ export function createMemoryTokenRevocationStore(options) {
|
|
|
68
74
|
},
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Check that a revocation store can claim a refresh token atomically.
|
|
79
|
+
*
|
|
80
|
+
* Without `revokeIfNotRevoked`, `createAuthService().refresh()` falls back
|
|
81
|
+
* to `isRevoked()` then `revoke()`, which leaves a window in which two
|
|
82
|
+
* concurrent replays of one refresh token both mint a valid pair. That
|
|
83
|
+
* used to happen silently. Now a `SecurityWarning` with code
|
|
84
|
+
* {@link RACY_REVOCATION_WARNING_CODE} is emitted through
|
|
85
|
+
* `process.emitWarning` at construction, or, with `required`, the service
|
|
86
|
+
* refuses to start.
|
|
87
|
+
*
|
|
88
|
+
* @throws {AuthConfigurationError} when `required` and the method is absent.
|
|
89
|
+
*/
|
|
90
|
+
export function assertAtomicRevocationStore(store, required) {
|
|
91
|
+
if (typeof store.revokeIfNotRevoked === "function")
|
|
92
|
+
return;
|
|
93
|
+
const message = "TokenRevocationStore does not implement revokeIfNotRevoked(); " +
|
|
94
|
+
"refresh() falls back to isRevoked() + revoke(), which is racy: two " +
|
|
95
|
+
"concurrent replays of one refresh token can both succeed. Implement " +
|
|
96
|
+
"revokeIfNotRevoked (SET NX in Redis) in any store used in production.";
|
|
97
|
+
if (required) {
|
|
98
|
+
throw new AuthConfigurationError(`${message} Set requireAtomicRevocation: false to accept the fallback.`);
|
|
99
|
+
}
|
|
100
|
+
process.emitWarning(message, {
|
|
101
|
+
type: "SecurityWarning",
|
|
102
|
+
code: RACY_REVOCATION_WARNING_CODE,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -119,7 +119,8 @@ export function verifyToken(token, secret, expectedType, config) {
|
|
|
119
119
|
return { valid: false, error: "Invalid payload" };
|
|
120
120
|
}
|
|
121
121
|
const payload = decoded;
|
|
122
|
-
const
|
|
122
|
+
const nowMs = config.clock ? config.clock.now() : Date.now();
|
|
123
|
+
const now = Math.floor(nowMs / 1000);
|
|
123
124
|
const skew = config.clockToleranceSeconds ?? 0;
|
|
124
125
|
if (payload.exp + skew < now) {
|
|
125
126
|
return { valid: false, error: "Token expired" };
|
|
@@ -182,4 +183,3 @@ export function generateTokenId() {
|
|
|
182
183
|
function hmacSha256(data, secret) {
|
|
183
184
|
return createHmac("sha256", secret).update(data).digest("base64url");
|
|
184
185
|
}
|
|
185
|
-
//# sourceMappingURL=authToken.signing.js.map
|
|
@@ -3,6 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module authToken
|
|
5
5
|
*/
|
|
6
|
-
export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
|
|
7
|
-
export { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
6
|
+
export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, type CreateTokenPairOptions, } from "./authToken.core.js";
|
|
7
|
+
export { createMemoryTokenRevocationStore, assertAtomicRevocationStore, RACY_REVOCATION_WARNING_CODE, } from "./authToken.revocation.js";
|
package/dist/authToken/index.js
CHANGED
|
@@ -4,5 +4,4 @@
|
|
|
4
4
|
* @module authToken
|
|
5
5
|
*/
|
|
6
6
|
export { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
|
|
7
|
-
export { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
|
|
8
|
-
//# sourceMappingURL=index.js.map
|
|
7
|
+
export { createMemoryTokenRevocationStore, assertAtomicRevocationStore, RACY_REVOCATION_WARNING_CODE, } from "./authToken.revocation.js";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { UserId } from "../authTypes/authUser.type.js";
|
|
7
7
|
import type { SessionId, TokenId } from "@zudojs/constants";
|
|
8
|
+
import type { Clock } from "@zudojs/types";
|
|
8
9
|
/** JWT token string. */
|
|
9
10
|
export type JwtToken = string;
|
|
10
11
|
/** Token identifier. Re-exported from @zudojs/constants for type safety. */
|
|
@@ -70,6 +71,13 @@ export interface TokenConfig {
|
|
|
70
71
|
* (default: 0, maximum: 300).
|
|
71
72
|
*/
|
|
72
73
|
readonly clockToleranceSeconds?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Time source for `iat`/`exp` when minting and for expiry when verifying
|
|
76
|
+
* (default: `Date.now`). Inject a fixed or advanceable clock in tests
|
|
77
|
+
* instead of faking global timers. `createAuthService({ clock })` sets it
|
|
78
|
+
* for you.
|
|
79
|
+
*/
|
|
80
|
+
readonly clock?: Clock;
|
|
73
81
|
}
|
|
74
82
|
/**
|
|
75
83
|
* Store for revoked token IDs (`jti` claims).
|
|
@@ -116,4 +124,3 @@ export interface TokenVerificationResult {
|
|
|
116
124
|
/** Error message (if invalid) */
|
|
117
125
|
readonly error?: string;
|
|
118
126
|
}
|
|
119
|
-
//# sourceMappingURL=authToken.type.d.ts.map
|
|
@@ -29,7 +29,13 @@ export interface AuthUser {
|
|
|
29
29
|
readonly name?: string;
|
|
30
30
|
/** Assigned roles */
|
|
31
31
|
readonly roles: readonly string[];
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Custom claims embedded in every token `createAuthService()` mints for
|
|
34
|
+
* this user (`login()`, `refresh()`, `createSessionForUser()`), e.g.
|
|
35
|
+
* `{ plan: "pro" }`. Reserved JWT names (`sub`, `exp`, `roles`, `sid`, ...)
|
|
36
|
+
* are dropped, never overridden; see `RESERVED_JWT_CLAIMS`. Keep them
|
|
37
|
+
* small and non-sensitive: they are readable by anyone holding the token.
|
|
38
|
+
*/
|
|
33
39
|
readonly claims?: Record<string, unknown>;
|
|
34
40
|
/** Whether the user is active */
|
|
35
41
|
readonly active: boolean;
|
|
@@ -56,4 +62,3 @@ export interface UserRegistration {
|
|
|
56
62
|
readonly name?: string;
|
|
57
63
|
readonly roles?: readonly string[];
|
|
58
64
|
}
|
|
59
|
-
//# sourceMappingURL=authUser.type.d.ts.map
|
|
@@ -9,4 +9,3 @@ export { type SessionId, toSessionId, type AuthSession, type CreateSessionOption
|
|
|
9
9
|
export { type PasswordCredentials, type ApiKeyCredentials, } from "./authCredentials.type.js";
|
|
10
10
|
export { type LoginAttemptRecord, type LoginAttemptStore, type LoginThrottleConfig, } from "./authAttempt.type.js";
|
|
11
11
|
export { type Permission, type Role, type GuardResult, type GuardContext, } from "./authRbac.type.js";
|
|
12
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/authTypes/index.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom JWT claim handling.
|
|
3
|
+
*
|
|
4
|
+
* @module authUtils/authUtils.claims
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Claim names the token minter owns. A custom claim with one of these names
|
|
8
|
+
* is dropped rather than embedded, so `user.claims` can never forge the
|
|
9
|
+
* subject, lifetime, type, id, session binding, roles, issuer or audience of
|
|
10
|
+
* a token.
|
|
11
|
+
*/
|
|
12
|
+
export declare const RESERVED_JWT_CLAIMS: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Return the custom claims that may be embedded in a token: every own,
|
|
15
|
+
* enumerable entry of `claims` whose name is not reserved and whose value
|
|
16
|
+
* is not `undefined`.
|
|
17
|
+
*
|
|
18
|
+
* @param claims - Custom claims, typically `AuthUser.claims`.
|
|
19
|
+
* @returns A fresh plain object, or `undefined` when nothing survives.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sanitizeCustomClaims(claims: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom JWT claim handling.
|
|
3
|
+
*
|
|
4
|
+
* @module authUtils/authUtils.claims
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Claim names the token minter owns. A custom claim with one of these names
|
|
8
|
+
* is dropped rather than embedded, so `user.claims` can never forge the
|
|
9
|
+
* subject, lifetime, type, id, session binding, roles, issuer or audience of
|
|
10
|
+
* a token.
|
|
11
|
+
*/
|
|
12
|
+
export const RESERVED_JWT_CLAIMS = new Set([
|
|
13
|
+
"sub",
|
|
14
|
+
"iat",
|
|
15
|
+
"exp",
|
|
16
|
+
"nbf",
|
|
17
|
+
"typ",
|
|
18
|
+
"jti",
|
|
19
|
+
"sid",
|
|
20
|
+
"roles",
|
|
21
|
+
"iss",
|
|
22
|
+
"aud",
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* Return the custom claims that may be embedded in a token: every own,
|
|
26
|
+
* enumerable entry of `claims` whose name is not reserved and whose value
|
|
27
|
+
* is not `undefined`.
|
|
28
|
+
*
|
|
29
|
+
* @param claims - Custom claims, typically `AuthUser.claims`.
|
|
30
|
+
* @returns A fresh plain object, or `undefined` when nothing survives.
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeCustomClaims(claims) {
|
|
33
|
+
if (!claims)
|
|
34
|
+
return undefined;
|
|
35
|
+
const result = {};
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const [name, value] of Object.entries(claims)) {
|
|
38
|
+
if (RESERVED_JWT_CLAIMS.has(name) || value === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
result[name] = value;
|
|
41
|
+
count++;
|
|
42
|
+
}
|
|
43
|
+
return count > 0 ? result : undefined;
|
|
44
|
+
}
|
|
@@ -35,10 +35,13 @@ export declare function parseCookies(cookie: unknown): Record<string, string>;
|
|
|
35
35
|
* I refresh before calling?"), never as an authorization decision.
|
|
36
36
|
*
|
|
37
37
|
* @param token - JWT token string
|
|
38
|
+
* @param clock - Time source (default: `Date.now`).
|
|
38
39
|
* @returns Whether the token appears expired. Unparseable, oversized, or
|
|
39
40
|
* malformed input is reported as expired.
|
|
40
41
|
*/
|
|
41
|
-
export declare function isTokenExpired(token: unknown
|
|
42
|
+
export declare function isTokenExpired(token: unknown, clock?: {
|
|
43
|
+
now(): number;
|
|
44
|
+
}): boolean;
|
|
42
45
|
/**
|
|
43
46
|
* Extract the user ID from a JWT payload **without verifying the signature**.
|
|
44
47
|
*
|
|
@@ -62,4 +65,3 @@ export declare function extractUserId(token: unknown): string | null;
|
|
|
62
65
|
* @returns Random hex string for CSRF protection
|
|
63
66
|
*/
|
|
64
67
|
export declare function generateCsrfToken(): string;
|
|
65
|
-
//# sourceMappingURL=authUtils.helper.d.ts.map
|
|
@@ -76,10 +76,11 @@ export function parseCookies(cookie) {
|
|
|
76
76
|
* I refresh before calling?"), never as an authorization decision.
|
|
77
77
|
*
|
|
78
78
|
* @param token - JWT token string
|
|
79
|
+
* @param clock - Time source (default: `Date.now`).
|
|
79
80
|
* @returns Whether the token appears expired. Unparseable, oversized, or
|
|
80
81
|
* malformed input is reported as expired.
|
|
81
82
|
*/
|
|
82
|
-
export function isTokenExpired(token) {
|
|
83
|
+
export function isTokenExpired(token, clock) {
|
|
83
84
|
const parts = splitToken(token);
|
|
84
85
|
if (!parts)
|
|
85
86
|
return true;
|
|
@@ -87,7 +88,7 @@ export function isTokenExpired(token) {
|
|
|
87
88
|
if (!payload)
|
|
88
89
|
return true;
|
|
89
90
|
const exp = payload["exp"];
|
|
90
|
-
const now = Math.floor(Date.now() / 1000);
|
|
91
|
+
const now = Math.floor((clock ? clock.now() : Date.now()) / 1000);
|
|
91
92
|
return typeof exp !== "number" || !Number.isFinite(exp) || exp < now;
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
@@ -124,4 +125,3 @@ export function extractUserId(token) {
|
|
|
124
125
|
export function generateCsrfToken() {
|
|
125
126
|
return randomBytes(32).toString("hex");
|
|
126
127
|
}
|
|
127
|
-
//# sourceMappingURL=authUtils.helper.js.map
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { parseBearerToken, parseCookies, isTokenExpired, extractUserId, generateCsrfToken, } from "./authUtils.helper.js";
|
|
8
8
|
export { normalizeLoginIdentifier } from "./authUtils.identifier.js";
|
|
9
|
-
|
|
9
|
+
export { RESERVED_JWT_CLAIMS, sanitizeCustomClaims, } from "./authUtils.claims.js";
|
package/dist/authUtils/index.js
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { parseBearerToken, parseCookies, isTokenExpired, extractUserId, generateCsrfToken, } from "./authUtils.helper.js";
|
|
8
8
|
export { normalizeLoginIdentifier } from "./authUtils.identifier.js";
|
|
9
|
-
|
|
9
|
+
export { RESERVED_JWT_CLAIMS, sanitizeCustomClaims, } from "./authUtils.claims.js";
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Authentication and authorization services for the Zudojs framework — JWT, sessions, RBAC, and password management.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/constants": "1.
|
|
29
|
-
"@zudojs/crypto": "1.
|
|
30
|
-
"@zudojs/errors": "1.
|
|
31
|
-
"@zudojs/permissions": "1.
|
|
32
|
-
"@zudojs/types": "1.
|
|
28
|
+
"@zudojs/constants": "1.2.0",
|
|
29
|
+
"@zudojs/crypto": "1.4.0",
|
|
30
|
+
"@zudojs/errors": "1.4.0",
|
|
31
|
+
"@zudojs/permissions": "1.5.0",
|
|
32
|
+
"@zudojs/types": "1.3.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=24.0.0"
|