@zudojs/auth 0.1.1 → 1.1.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/LICENSE +21 -0
- package/README.md +224 -19
- package/dist/authErrors/authError.base.d.ts +50 -16
- package/dist/authErrors/authError.base.js +94 -18
- package/dist/authErrors/index.d.ts +1 -1
- package/dist/authErrors/index.js +1 -1
- package/dist/authPassword/authPassword.core.d.ts +25 -2
- package/dist/authPassword/authPassword.core.js +50 -2
- package/dist/authPassword/index.d.ts +1 -1
- package/dist/authPassword/index.js +1 -1
- package/dist/authProvider/authAttempt.memory.d.ts +21 -0
- package/dist/authProvider/authAttempt.memory.js +91 -0
- package/dist/authProvider/authProvider.core.d.ts +63 -9
- package/dist/authProvider/authProvider.core.js +221 -37
- package/dist/authProvider/index.d.ts +2 -1
- package/dist/authProvider/index.js +1 -0
- package/dist/authSession/authSession.core.d.ts +20 -3
- package/dist/authSession/authSession.core.js +71 -21
- package/dist/authToken/authToken.core.d.ts +27 -1
- package/dist/authToken/authToken.core.js +31 -1
- package/dist/authToken/authToken.encoding.d.ts +43 -0
- package/dist/authToken/authToken.encoding.js +74 -0
- package/dist/authToken/authToken.revocation.d.ts +10 -2
- package/dist/authToken/authToken.revocation.js +42 -7
- package/dist/authToken/authToken.signing.d.ts +24 -1
- package/dist/authToken/authToken.signing.js +117 -28
- package/dist/authToken/jwt.namespace.d.ts +18 -8
- package/dist/authToken/jwt.namespace.js +11 -1
- package/dist/authTypes/authAttempt.type.d.ts +65 -0
- package/dist/authTypes/authAttempt.type.js +7 -0
- package/dist/authTypes/authCredentials.type.d.ts +26 -0
- package/dist/authTypes/authCredentials.type.js +11 -0
- package/dist/authTypes/authSession.type.d.ts +31 -3
- package/dist/authTypes/authSession.type.js +17 -1
- package/dist/authTypes/authToken.type.d.ts +29 -1
- package/dist/authTypes/authUser.type.d.ts +12 -0
- package/dist/authTypes/authUser.type.js +17 -1
- package/dist/authTypes/index.d.ts +5 -4
- package/dist/authTypes/index.js +5 -4
- package/dist/authUtils/authUtils.helper.d.ts +31 -10
- package/dist/authUtils/authUtils.helper.js +70 -31
- package/package.json +27 -16
- package/dist/.tsbuildinfo +0 -1
- package/dist/authErrors/authError.base.d.ts.map +0 -1
- package/dist/authErrors/authError.base.js.map +0 -1
- package/dist/authErrors/index.d.ts.map +0 -1
- package/dist/authErrors/index.js.map +0 -1
- package/dist/authPassword/authPassword.core.d.ts.map +0 -1
- package/dist/authPassword/authPassword.core.js.map +0 -1
- package/dist/authPassword/index.d.ts.map +0 -1
- package/dist/authPassword/index.js.map +0 -1
- package/dist/authProvider/authProvider.core.d.ts.map +0 -1
- package/dist/authProvider/authProvider.core.js.map +0 -1
- package/dist/authProvider/index.d.ts.map +0 -1
- package/dist/authProvider/index.js.map +0 -1
- package/dist/authSession/authSession.core.d.ts.map +0 -1
- package/dist/authSession/authSession.core.js.map +0 -1
- package/dist/authSession/index.d.ts.map +0 -1
- package/dist/authSession/index.js.map +0 -1
- package/dist/authToken/authToken.core.d.ts.map +0 -1
- package/dist/authToken/authToken.core.js.map +0 -1
- package/dist/authToken/authToken.revocation.d.ts.map +0 -1
- package/dist/authToken/authToken.revocation.js.map +0 -1
- package/dist/authToken/authToken.signing.d.ts.map +0 -1
- package/dist/authToken/authToken.signing.js.map +0 -1
- package/dist/authToken/index.d.ts.map +0 -1
- package/dist/authToken/index.js.map +0 -1
- package/dist/authToken/jwt.namespace.d.ts.map +0 -1
- package/dist/authToken/jwt.namespace.js.map +0 -1
- package/dist/authTypes/authRbac.type.d.ts.map +0 -1
- package/dist/authTypes/authRbac.type.js.map +0 -1
- package/dist/authTypes/authSession.type.d.ts.map +0 -1
- package/dist/authTypes/authSession.type.js.map +0 -1
- package/dist/authTypes/authStrategy.type.d.ts +0 -83
- package/dist/authTypes/authStrategy.type.d.ts.map +0 -1
- package/dist/authTypes/authStrategy.type.js +0 -7
- package/dist/authTypes/authStrategy.type.js.map +0 -1
- package/dist/authTypes/authToken.type.d.ts.map +0 -1
- package/dist/authTypes/authToken.type.js.map +0 -1
- package/dist/authTypes/authUser.type.d.ts.map +0 -1
- package/dist/authTypes/authUser.type.js.map +0 -1
- package/dist/authTypes/index.d.ts.map +0 -1
- package/dist/authTypes/index.js.map +0 -1
- package/dist/authUtils/authUtils.helper.d.ts.map +0 -1
- package/dist/authUtils/authUtils.helper.js.map +0 -1
- package/dist/authUtils/index.d.ts.map +0 -1
- package/dist/authUtils/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -9,8 +9,25 @@ import type { SessionStore } from "../authTypes/authSession.type.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Create an in-memory session store.
|
|
11
11
|
*
|
|
12
|
-
* Good for development and testing. For production,
|
|
13
|
-
*
|
|
12
|
+
* Good for development and testing. For production, implement SessionStore
|
|
13
|
+
* with Redis or a database.
|
|
14
|
+
*
|
|
15
|
+
* Expired sessions are reclaimed on any access — `create`, `get` and `touch`
|
|
16
|
+
* all run a rate-limited sweep — so a store that stops receiving logins
|
|
17
|
+
* still releases its memory. The sweep is rate-limited rather than run per
|
|
18
|
+
* call so that a large store does not turn an O(1) lookup into an O(n) walk.
|
|
19
|
+
*
|
|
20
|
+
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
21
|
+
* (default: 60000). Set to 0 to sweep on every access.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMemorySessionStore(storeOptions?: {
|
|
24
|
+
readonly purgeIntervalMs?: number;
|
|
25
|
+
}): SessionStore;
|
|
26
|
+
/**
|
|
27
|
+
* Reject a TTL that cannot produce a real expiry.
|
|
28
|
+
*
|
|
29
|
+
* @throws {AuthConfigurationError} when `value` is defined but is not a
|
|
30
|
+
* finite number greater than zero.
|
|
14
31
|
*/
|
|
15
|
-
export declare function
|
|
32
|
+
export declare function assertPositiveSeconds(value: number | undefined, field: string): void;
|
|
16
33
|
//# sourceMappingURL=authSession.core.d.ts.map
|
|
@@ -5,21 +5,37 @@
|
|
|
5
5
|
*
|
|
6
6
|
* For production, implement SessionStore backed by Redis, database, etc.
|
|
7
7
|
*/
|
|
8
|
+
import { AuthConfigurationError } from "../authErrors/authError.base.js";
|
|
8
9
|
import { randomBytes } from "node:crypto";
|
|
9
10
|
const DEFAULT_TTL_SECONDS = 86400; // 24 hours
|
|
11
|
+
/** Minimum interval between full sweeps of the session map. */
|
|
12
|
+
const DEFAULT_PURGE_INTERVAL_MS = 60_000;
|
|
10
13
|
/**
|
|
11
14
|
* Create an in-memory session store.
|
|
12
15
|
*
|
|
13
|
-
* Good for development and testing. For production,
|
|
14
|
-
*
|
|
16
|
+
* Good for development and testing. For production, implement SessionStore
|
|
17
|
+
* with Redis or a database.
|
|
18
|
+
*
|
|
19
|
+
* Expired sessions are reclaimed on any access — `create`, `get` and `touch`
|
|
20
|
+
* all run a rate-limited sweep — so a store that stops receiving logins
|
|
21
|
+
* still releases its memory. The sweep is rate-limited rather than run per
|
|
22
|
+
* call so that a large store does not turn an O(1) lookup into an O(n) walk.
|
|
23
|
+
*
|
|
24
|
+
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
25
|
+
* (default: 60000). Set to 0 to sweep on every access.
|
|
15
26
|
*/
|
|
16
|
-
export function createMemorySessionStore() {
|
|
27
|
+
export function createMemorySessionStore(storeOptions) {
|
|
17
28
|
const sessions = new Map();
|
|
18
29
|
const ttls = new Map();
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
const purgeIntervalMs = storeOptions?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
31
|
+
let lastPurge = 0;
|
|
32
|
+
function maybePurgeExpired() {
|
|
33
|
+
const nowMs = Date.now();
|
|
34
|
+
if (nowMs - lastPurge < purgeIntervalMs)
|
|
35
|
+
return;
|
|
36
|
+
lastPurge = nowMs;
|
|
21
37
|
for (const [id, session] of sessions) {
|
|
22
|
-
if (
|
|
38
|
+
if (nowMs > session.expiresAt.getTime()) {
|
|
23
39
|
sessions.delete(id);
|
|
24
40
|
ttls.delete(id);
|
|
25
41
|
}
|
|
@@ -27,10 +43,19 @@ export function createMemorySessionStore() {
|
|
|
27
43
|
}
|
|
28
44
|
return {
|
|
29
45
|
async create(options) {
|
|
30
|
-
|
|
46
|
+
// A non-finite TTL (`Number(undefinedEnvVar)` is the usual source)
|
|
47
|
+
// produced an `Invalid Date` expiry, and `now > NaN` is always false —
|
|
48
|
+
// so the session never expired, not even at its absolute deadline.
|
|
49
|
+
assertPositiveSeconds(options.ttlSeconds, "ttlSeconds");
|
|
50
|
+
assertPositiveSeconds(options.absoluteTtlSeconds, "absoluteTtlSeconds");
|
|
51
|
+
maybePurgeExpired();
|
|
31
52
|
const id = generateSessionId();
|
|
32
53
|
const now = new Date();
|
|
33
54
|
const ttlMs = (options.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
55
|
+
const absoluteExpiresAt = options.absoluteTtlSeconds !== undefined
|
|
56
|
+
? new Date(now.getTime() + options.absoluteTtlSeconds * 1000)
|
|
57
|
+
: undefined;
|
|
58
|
+
const expiresAt = clampToAbsolute(new Date(now.getTime() + ttlMs), absoluteExpiresAt);
|
|
34
59
|
const session = {
|
|
35
60
|
id,
|
|
36
61
|
userId: options.userId,
|
|
@@ -38,8 +63,8 @@ export function createMemorySessionStore() {
|
|
|
38
63
|
ip: options.ip,
|
|
39
64
|
createdAt: now,
|
|
40
65
|
lastActivityAt: now,
|
|
41
|
-
expiresAt
|
|
42
|
-
|
|
66
|
+
expiresAt,
|
|
67
|
+
absoluteExpiresAt,
|
|
43
68
|
metadata: options.metadata,
|
|
44
69
|
};
|
|
45
70
|
sessions.set(id, session);
|
|
@@ -47,12 +72,11 @@ export function createMemorySessionStore() {
|
|
|
47
72
|
return session;
|
|
48
73
|
},
|
|
49
74
|
async get(sessionId) {
|
|
75
|
+
maybePurgeExpired();
|
|
50
76
|
const session = sessions.get(sessionId);
|
|
51
77
|
if (!session)
|
|
52
78
|
return null;
|
|
53
|
-
if (
|
|
54
|
-
return null;
|
|
55
|
-
if (new Date() > session.expiresAt) {
|
|
79
|
+
if (Date.now() > session.expiresAt.getTime()) {
|
|
56
80
|
sessions.delete(sessionId);
|
|
57
81
|
ttls.delete(sessionId);
|
|
58
82
|
return null;
|
|
@@ -60,17 +84,24 @@ export function createMemorySessionStore() {
|
|
|
60
84
|
return session;
|
|
61
85
|
},
|
|
62
86
|
async touch(sessionId) {
|
|
87
|
+
maybePurgeExpired();
|
|
63
88
|
const session = sessions.get(sessionId);
|
|
64
|
-
if (session
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
expiresAt: new Date(now.getTime() + ttlMs),
|
|
72
|
-
});
|
|
89
|
+
if (!session)
|
|
90
|
+
return;
|
|
91
|
+
const now = new Date();
|
|
92
|
+
if (now.getTime() > session.expiresAt.getTime()) {
|
|
93
|
+
sessions.delete(sessionId);
|
|
94
|
+
ttls.delete(sessionId);
|
|
95
|
+
return;
|
|
73
96
|
}
|
|
97
|
+
const ttlMs = ttls.get(sessionId) ?? DEFAULT_TTL_SECONDS * 1000;
|
|
98
|
+
sessions.set(sessionId, {
|
|
99
|
+
...session,
|
|
100
|
+
lastActivityAt: now,
|
|
101
|
+
// Sliding expiration: activity extends the session by its TTL, but
|
|
102
|
+
// never past the absolute deadline.
|
|
103
|
+
expiresAt: clampToAbsolute(new Date(now.getTime() + ttlMs), session.absoluteExpiresAt),
|
|
104
|
+
});
|
|
74
105
|
},
|
|
75
106
|
async destroy(sessionId) {
|
|
76
107
|
sessions.delete(sessionId);
|
|
@@ -86,6 +117,25 @@ export function createMemorySessionStore() {
|
|
|
86
117
|
},
|
|
87
118
|
};
|
|
88
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Reject a TTL that cannot produce a real expiry.
|
|
122
|
+
*
|
|
123
|
+
* @throws {AuthConfigurationError} when `value` is defined but is not a
|
|
124
|
+
* finite number greater than zero.
|
|
125
|
+
*/
|
|
126
|
+
export function assertPositiveSeconds(value, field) {
|
|
127
|
+
if (value === undefined)
|
|
128
|
+
return;
|
|
129
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
130
|
+
throw new AuthConfigurationError(`${field} must be a finite number of seconds greater than zero; ` +
|
|
131
|
+
`got ${String(value)}. A NaN lifetime would create a session that never expires.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function clampToAbsolute(expiresAt, absolute) {
|
|
135
|
+
if (!absolute)
|
|
136
|
+
return expiresAt;
|
|
137
|
+
return expiresAt.getTime() > absolute.getTime() ? absolute : expiresAt;
|
|
138
|
+
}
|
|
89
139
|
function generateSessionId() {
|
|
90
140
|
return randomBytes(32).toString("hex");
|
|
91
141
|
}
|
|
@@ -6,24 +6,50 @@
|
|
|
6
6
|
* Pure Node.js implementation (no jsonwebtoken dependency).
|
|
7
7
|
* Uses HMAC SHA-256 for signing.
|
|
8
8
|
*/
|
|
9
|
-
import type { UserId } from "@zudojs/constants";
|
|
9
|
+
import type { SessionId, UserId } from "@zudojs/constants";
|
|
10
10
|
import type { JwtToken, TokenPair, TokenConfig, TokenVerificationResult } from "../authTypes/authToken.type.js";
|
|
11
11
|
/**
|
|
12
12
|
* Create a new token pair (access + refresh).
|
|
13
|
+
*
|
|
14
|
+
* @param options.roles - Roles to embed in both tokens.
|
|
15
|
+
* @param options.sessionId - Session to bind the pair to (`sid` claim).
|
|
16
|
+
* `createAuthService()` sets this so that `logout()` invalidates the pair.
|
|
17
|
+
* @throws {AuthConfigurationError} when the signing secrets are missing,
|
|
18
|
+
* shorter than 32 bytes, or identical to each other.
|
|
13
19
|
*/
|
|
14
20
|
export declare function createTokenPair(userId: UserId, config: TokenConfig, options?: {
|
|
15
21
|
readonly roles?: readonly string[];
|
|
22
|
+
readonly sessionId?: SessionId;
|
|
16
23
|
}): TokenPair;
|
|
17
24
|
/**
|
|
18
25
|
* Verify and decode an access token.
|
|
26
|
+
*
|
|
27
|
+
* @throws {AuthConfigurationError} when the signing secrets are invalid.
|
|
28
|
+
* Malformed or untrusted *tokens* never throw — they come back as
|
|
29
|
+
* `{ valid: false, error }`.
|
|
19
30
|
*/
|
|
20
31
|
export declare function verifyAccessToken(token: JwtToken, config: TokenConfig): TokenVerificationResult;
|
|
21
32
|
/**
|
|
22
33
|
* Verify and decode a refresh token.
|
|
34
|
+
*
|
|
35
|
+
* @throws {AuthConfigurationError} when the signing secrets are invalid.
|
|
23
36
|
*/
|
|
24
37
|
export declare function verifyRefreshToken(token: JwtToken, config: TokenConfig): TokenVerificationResult;
|
|
25
38
|
/**
|
|
26
39
|
* Refresh an access token using a valid refresh token.
|
|
40
|
+
*
|
|
41
|
+
* **This is the non-rotating, non-revoking variant.** It checks the refresh
|
|
42
|
+
* token's signature, expiry and type and nothing else: it consults no
|
|
43
|
+
* {@link TokenRevocationStore}, does not revoke the token it consumes, does
|
|
44
|
+
* not re-load the user, and does not check the session. A stolen refresh
|
|
45
|
+
* token therefore stays replayable for its full lifetime (7 days by
|
|
46
|
+
* default) — and so does the token it was already exchanged for.
|
|
47
|
+
*
|
|
48
|
+
* Use `createAuthService().refresh()` instead for anything user-facing: it
|
|
49
|
+
* rotates the refresh token atomically, revokes the used `jti`, re-loads the
|
|
50
|
+
* user so deactivation and role changes take effect, and validates the
|
|
51
|
+
* session. This function exists for callers who manage all of that
|
|
52
|
+
* themselves.
|
|
27
53
|
*/
|
|
28
54
|
export declare function refreshAccessToken(refreshToken: JwtToken, config: TokenConfig, options?: {
|
|
29
55
|
readonly roles?: readonly string[];
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
* Pure Node.js implementation (no jsonwebtoken dependency).
|
|
7
7
|
* Uses HMAC SHA-256 for signing.
|
|
8
8
|
*/
|
|
9
|
-
import { signToken, verifyToken, generateTokenId, } from "./authToken.signing.js";
|
|
9
|
+
import { signToken, verifyToken, generateTokenId, assertTokenSecrets, } from "./authToken.signing.js";
|
|
10
10
|
// TTLs are in seconds — they are added to Unix-second `iat`/`exp` claims.
|
|
11
11
|
const DEFAULT_ACCESS_TTL = 900; // 15 minutes
|
|
12
12
|
const DEFAULT_REFRESH_TTL = 604_800; // 7 days
|
|
13
13
|
/**
|
|
14
14
|
* Create a new token pair (access + refresh).
|
|
15
|
+
*
|
|
16
|
+
* @param options.roles - Roles to embed in both tokens.
|
|
17
|
+
* @param options.sessionId - Session to bind the pair to (`sid` claim).
|
|
18
|
+
* `createAuthService()` sets this so that `logout()` invalidates the pair.
|
|
19
|
+
* @throws {AuthConfigurationError} when the signing secrets are missing,
|
|
20
|
+
* shorter than 32 bytes, or identical to each other.
|
|
15
21
|
*/
|
|
16
22
|
export function createTokenPair(userId, config, options) {
|
|
23
|
+
assertTokenSecrets(config);
|
|
17
24
|
const accessTtl = config.accessTtl ?? DEFAULT_ACCESS_TTL;
|
|
18
25
|
const refreshTtl = config.refreshTtl ?? DEFAULT_REFRESH_TTL;
|
|
19
26
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -24,6 +31,7 @@ export function createTokenPair(userId, config, options) {
|
|
|
24
31
|
typ: "access",
|
|
25
32
|
jti: generateTokenId(),
|
|
26
33
|
roles: options?.roles,
|
|
34
|
+
...(options?.sessionId ? { sid: options.sessionId } : {}),
|
|
27
35
|
...(config.issuer ? { iss: config.issuer } : {}),
|
|
28
36
|
...(config.audience ? { aud: config.audience } : {}),
|
|
29
37
|
}, config.accessSecret);
|
|
@@ -34,6 +42,7 @@ export function createTokenPair(userId, config, options) {
|
|
|
34
42
|
typ: "refresh",
|
|
35
43
|
jti: generateTokenId(),
|
|
36
44
|
roles: options?.roles,
|
|
45
|
+
...(options?.sessionId ? { sid: options.sessionId } : {}),
|
|
37
46
|
...(config.issuer ? { iss: config.issuer } : {}),
|
|
38
47
|
...(config.audience ? { aud: config.audience } : {}),
|
|
39
48
|
}, config.refreshSecret);
|
|
@@ -46,18 +55,39 @@ export function createTokenPair(userId, config, options) {
|
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
57
|
* Verify and decode an access token.
|
|
58
|
+
*
|
|
59
|
+
* @throws {AuthConfigurationError} when the signing secrets are invalid.
|
|
60
|
+
* Malformed or untrusted *tokens* never throw — they come back as
|
|
61
|
+
* `{ valid: false, error }`.
|
|
49
62
|
*/
|
|
50
63
|
export function verifyAccessToken(token, config) {
|
|
64
|
+
assertTokenSecrets(config);
|
|
51
65
|
return verifyToken(token, config.accessSecret, "access", config);
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
54
68
|
* Verify and decode a refresh token.
|
|
69
|
+
*
|
|
70
|
+
* @throws {AuthConfigurationError} when the signing secrets are invalid.
|
|
55
71
|
*/
|
|
56
72
|
export function verifyRefreshToken(token, config) {
|
|
73
|
+
assertTokenSecrets(config);
|
|
57
74
|
return verifyToken(token, config.refreshSecret, "refresh", config);
|
|
58
75
|
}
|
|
59
76
|
/**
|
|
60
77
|
* Refresh an access token using a valid refresh token.
|
|
78
|
+
*
|
|
79
|
+
* **This is the non-rotating, non-revoking variant.** It checks the refresh
|
|
80
|
+
* token's signature, expiry and type and nothing else: it consults no
|
|
81
|
+
* {@link TokenRevocationStore}, does not revoke the token it consumes, does
|
|
82
|
+
* not re-load the user, and does not check the session. A stolen refresh
|
|
83
|
+
* token therefore stays replayable for its full lifetime (7 days by
|
|
84
|
+
* default) — and so does the token it was already exchanged for.
|
|
85
|
+
*
|
|
86
|
+
* Use `createAuthService().refresh()` instead for anything user-facing: it
|
|
87
|
+
* rotates the refresh token atomically, revokes the used `jti`, re-loads the
|
|
88
|
+
* user so deactivation and role changes take effect, and validates the
|
|
89
|
+
* session. This function exists for callers who manage all of that
|
|
90
|
+
* themselves.
|
|
61
91
|
*/
|
|
62
92
|
export function refreshAccessToken(refreshToken, config, options) {
|
|
63
93
|
const result = verifyRefreshToken(refreshToken, config);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal base64url + JWT segment helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module authToken/authToken.encoding
|
|
5
|
+
*
|
|
6
|
+
* Not exported from the package barrel. Every JWT segment in this package —
|
|
7
|
+
* verified or not — is decoded through here so that the bounds and the
|
|
8
|
+
* decoding rules stay in exactly one place.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Maximum accepted length, in characters, of a complete JWT.
|
|
12
|
+
*
|
|
13
|
+
* An HS256 JWT with a generous set of claims is well under 2 KB; 8 KB is the
|
|
14
|
+
* conventional HTTP header limit. Anything larger is rejected before it is
|
|
15
|
+
* decoded so that an unauthenticated request cannot make the process
|
|
16
|
+
* allocate and parse an arbitrarily large buffer.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_TOKEN_LENGTH = 8192;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum accepted length, in characters, of the JOSE header segment.
|
|
21
|
+
* A real header (`{"alg":"HS256","typ":"JWT"}`) is 36 characters encoded.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MAX_HEADER_SEGMENT_LENGTH = 1024;
|
|
24
|
+
/** Encode a UTF-8 string as base64url. */
|
|
25
|
+
export declare function base64UrlEncode(data: string): string;
|
|
26
|
+
/** Decode a base64url segment to a UTF-8 string. */
|
|
27
|
+
export declare function base64UrlDecode(data: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Split a JWT into its three segments, rejecting anything that is not a
|
|
30
|
+
* string, is empty, or exceeds {@link MAX_TOKEN_LENGTH}.
|
|
31
|
+
*
|
|
32
|
+
* @returns The three segments, or `null` when the input is not a
|
|
33
|
+
* plausibly-shaped, in-bounds JWT.
|
|
34
|
+
*/
|
|
35
|
+
export declare function splitToken(token: unknown): readonly [string, string, string] | null;
|
|
36
|
+
/**
|
|
37
|
+
* Decode a JWT body segment into a plain object.
|
|
38
|
+
*
|
|
39
|
+
* @returns The parsed object, or `null` when the segment is not valid JSON
|
|
40
|
+
* or does not decode to a non-null, non-array object.
|
|
41
|
+
*/
|
|
42
|
+
export declare function decodeJsonSegment(segment: string): Record<string, unknown> | null;
|
|
43
|
+
//# sourceMappingURL=authToken.encoding.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal base64url + JWT segment helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module authToken/authToken.encoding
|
|
5
|
+
*
|
|
6
|
+
* Not exported from the package barrel. Every JWT segment in this package —
|
|
7
|
+
* verified or not — is decoded through here so that the bounds and the
|
|
8
|
+
* decoding rules stay in exactly one place.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Maximum accepted length, in characters, of a complete JWT.
|
|
12
|
+
*
|
|
13
|
+
* An HS256 JWT with a generous set of claims is well under 2 KB; 8 KB is the
|
|
14
|
+
* conventional HTTP header limit. Anything larger is rejected before it is
|
|
15
|
+
* decoded so that an unauthenticated request cannot make the process
|
|
16
|
+
* allocate and parse an arbitrarily large buffer.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_TOKEN_LENGTH = 8192;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum accepted length, in characters, of the JOSE header segment.
|
|
21
|
+
* A real header (`{"alg":"HS256","typ":"JWT"}`) is 36 characters encoded.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_HEADER_SEGMENT_LENGTH = 1024;
|
|
24
|
+
/** Encode a UTF-8 string as base64url. */
|
|
25
|
+
export function base64UrlEncode(data) {
|
|
26
|
+
return Buffer.from(data, "utf-8").toString("base64url");
|
|
27
|
+
}
|
|
28
|
+
/** Decode a base64url segment to a UTF-8 string. */
|
|
29
|
+
export function base64UrlDecode(data) {
|
|
30
|
+
return Buffer.from(data, "base64url").toString("utf-8");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Split a JWT into its three segments, rejecting anything that is not a
|
|
34
|
+
* string, is empty, or exceeds {@link MAX_TOKEN_LENGTH}.
|
|
35
|
+
*
|
|
36
|
+
* @returns The three segments, or `null` when the input is not a
|
|
37
|
+
* plausibly-shaped, in-bounds JWT.
|
|
38
|
+
*/
|
|
39
|
+
export function splitToken(token) {
|
|
40
|
+
if (typeof token !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
if (token.length === 0 || token.length > MAX_TOKEN_LENGTH)
|
|
43
|
+
return null;
|
|
44
|
+
const parts = token.split(".");
|
|
45
|
+
if (parts.length !== 3)
|
|
46
|
+
return null;
|
|
47
|
+
const [header, body, signature] = parts;
|
|
48
|
+
if (header.length > MAX_HEADER_SEGMENT_LENGTH)
|
|
49
|
+
return null;
|
|
50
|
+
if (header.length === 0 || body.length === 0 || signature.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return [header, body, signature];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decode a JWT body segment into a plain object.
|
|
57
|
+
*
|
|
58
|
+
* @returns The parsed object, or `null` when the segment is not valid JSON
|
|
59
|
+
* or does not decode to a non-null, non-array object.
|
|
60
|
+
*/
|
|
61
|
+
export function decodeJsonSegment(segment) {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(base64UrlDecode(segment));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=authToken.encoding.js.map
|
|
@@ -11,7 +11,15 @@ import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
|
|
|
11
11
|
/**
|
|
12
12
|
* Create an in-memory token revocation store.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Expired entries are dropped lazily: the key being read is checked on every
|
|
15
|
+
* access (O(1)), and a full sweep runs at most once per `purgeIntervalMs`.
|
|
16
|
+
* The sweep is deliberately *not* driven by a timer — an interval would keep
|
|
17
|
+
* the process alive and leak without a `dispose()`.
|
|
18
|
+
*
|
|
19
|
+
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
20
|
+
* (default: 60000). Set to 0 to sweep on every access.
|
|
15
21
|
*/
|
|
16
|
-
export declare function createMemoryTokenRevocationStore(
|
|
22
|
+
export declare function createMemoryTokenRevocationStore(options?: {
|
|
23
|
+
readonly purgeIntervalMs?: number;
|
|
24
|
+
}): TokenRevocationStore;
|
|
17
25
|
//# sourceMappingURL=authToken.revocation.d.ts.map
|
|
@@ -7,29 +7,64 @@
|
|
|
7
7
|
* implement TokenRevocationStore with Redis or a database so revocations
|
|
8
8
|
* are shared across instances.
|
|
9
9
|
*/
|
|
10
|
+
/** Minimum interval between full sweeps of the revocation map. */
|
|
11
|
+
const DEFAULT_PURGE_INTERVAL_MS = 60_000;
|
|
10
12
|
/**
|
|
11
13
|
* Create an in-memory token revocation store.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
15
|
+
* Expired entries are dropped lazily: the key being read is checked on every
|
|
16
|
+
* access (O(1)), and a full sweep runs at most once per `purgeIntervalMs`.
|
|
17
|
+
* The sweep is deliberately *not* driven by a timer — an interval would keep
|
|
18
|
+
* the process alive and leak without a `dispose()`.
|
|
19
|
+
*
|
|
20
|
+
* @param options.purgeIntervalMs - Minimum gap between full sweeps
|
|
21
|
+
* (default: 60000). Set to 0 to sweep on every access.
|
|
14
22
|
*/
|
|
15
|
-
export function createMemoryTokenRevocationStore() {
|
|
23
|
+
export function createMemoryTokenRevocationStore(options) {
|
|
16
24
|
const revoked = new Map();
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
const purgeIntervalMs = options?.purgeIntervalMs ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
26
|
+
let lastPurge = 0;
|
|
27
|
+
/** Full sweep, rate-limited so a large map cannot be walked per request. */
|
|
28
|
+
function maybePurgeExpired() {
|
|
29
|
+
const nowMs = Date.now();
|
|
30
|
+
if (nowMs - lastPurge < purgeIntervalMs)
|
|
31
|
+
return;
|
|
32
|
+
lastPurge = nowMs;
|
|
33
|
+
const now = Math.floor(nowMs / 1000);
|
|
19
34
|
for (const [id, expiresAt] of revoked) {
|
|
20
35
|
if (expiresAt < now) {
|
|
21
36
|
revoked.delete(id);
|
|
22
37
|
}
|
|
23
38
|
}
|
|
24
39
|
}
|
|
40
|
+
/** O(1) expiry check for a single key. */
|
|
41
|
+
function isLive(tokenId) {
|
|
42
|
+
const expiresAt = revoked.get(tokenId);
|
|
43
|
+
if (expiresAt === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
if (expiresAt < Math.floor(Date.now() / 1000)) {
|
|
46
|
+
revoked.delete(tokenId);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
25
51
|
return {
|
|
26
52
|
async revoke(tokenId, expiresAt) {
|
|
27
|
-
|
|
53
|
+
maybePurgeExpired();
|
|
28
54
|
revoked.set(tokenId, expiresAt);
|
|
29
55
|
},
|
|
30
56
|
async isRevoked(tokenId) {
|
|
31
|
-
|
|
32
|
-
return
|
|
57
|
+
maybePurgeExpired();
|
|
58
|
+
return isLive(tokenId);
|
|
59
|
+
},
|
|
60
|
+
async revokeIfNotRevoked(tokenId, expiresAt) {
|
|
61
|
+
// The check and the write happen in one synchronous block with no
|
|
62
|
+
// intervening await, so two concurrent callers cannot both win.
|
|
63
|
+
maybePurgeExpired();
|
|
64
|
+
if (isLive(tokenId))
|
|
65
|
+
return false;
|
|
66
|
+
revoked.set(tokenId, expiresAt);
|
|
67
|
+
return true;
|
|
33
68
|
},
|
|
34
69
|
};
|
|
35
70
|
}
|
|
@@ -6,13 +6,36 @@
|
|
|
6
6
|
* Not exported from the package barrel — used internally by authToken.core.ts.
|
|
7
7
|
*/
|
|
8
8
|
import type { JwtToken, TokenId, TokenPayload, TokenConfig, TokenVerificationResult } from "../authTypes/authToken.type.js";
|
|
9
|
+
/**
|
|
10
|
+
* Minimum accepted signing-secret length in bytes.
|
|
11
|
+
*
|
|
12
|
+
* HS256 keys should be at least as long as the digest (32 bytes); shorter
|
|
13
|
+
* keys are brute-forceable offline from a single captured token.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MIN_SECRET_BYTES = 32;
|
|
16
|
+
/**
|
|
17
|
+
* Validate a {@link TokenConfig}'s signing secrets.
|
|
18
|
+
*
|
|
19
|
+
* Called on every mint and every verification. An empty or short secret
|
|
20
|
+
* yields tokens anyone can forge, and Node accepts a zero-length HMAC key
|
|
21
|
+
* without complaint, so this has to be checked explicitly rather than
|
|
22
|
+
* assumed.
|
|
23
|
+
*
|
|
24
|
+
* @throws {AuthConfigurationError} when either secret is missing, shorter
|
|
25
|
+
* than {@link MIN_SECRET_BYTES}, or when both secrets are identical.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertTokenSecrets(config: TokenConfig): void;
|
|
9
28
|
/**
|
|
10
29
|
* Sign a JWT payload with HMAC SHA-256.
|
|
11
30
|
*/
|
|
12
31
|
export declare function signToken(payload: TokenPayload, secret: string): JwtToken;
|
|
13
32
|
/**
|
|
14
|
-
* Verify a JWT token's signature, algorithm, expiration, type,
|
|
33
|
+
* Verify a JWT token's signature, algorithm, expiration, not-before, type,
|
|
15
34
|
* and (when configured) issuer and audience.
|
|
35
|
+
*
|
|
36
|
+
* Never throws for untrusted input: every failure is reported as
|
|
37
|
+
* `{ valid: false, error }`. Oversized tokens are rejected before anything
|
|
38
|
+
* is decoded.
|
|
16
39
|
*/
|
|
17
40
|
export declare function verifyToken(token: JwtToken, secret: string, expectedType: "access" | "refresh", config: TokenConfig): TokenVerificationResult;
|
|
18
41
|
/**
|