@zudojs/auth 0.1.0 → 1.0.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 +228 -17
- 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 +31 -4
- package/dist/authPassword/authPassword.core.js +119 -20
- 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 +56 -8
- package/dist/authProvider/authProvider.core.js +211 -37
- package/dist/authProvider/index.d.ts +2 -1
- package/dist/authProvider/index.js +1 -0
- package/dist/authSession/authSession.core.d.ts +13 -3
- package/dist/authSession/authSession.core.js +62 -13
- package/dist/authToken/authToken.core.d.ts +27 -1
- package/dist/authToken/authToken.core.js +35 -4
- package/dist/authToken/authToken.encoding.d.ts +43 -0
- package/dist/authToken/authToken.encoding.js +74 -0
- package/dist/authToken/authToken.revocation.d.ts +25 -0
- package/dist/authToken/authToken.revocation.js +71 -0
- package/dist/authToken/authToken.signing.d.ts +25 -1
- package/dist/authToken/authToken.signing.js +111 -33
- package/dist/authToken/index.d.ts +1 -0
- package/dist/authToken/index.js +1 -0
- package/dist/authToken/jwt.namespace.d.ts +18 -8
- package/dist/authToken/jwt.namespace.js +11 -1
- package/dist/authTypes/authAttempt.type.d.ts +63 -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 +48 -1
- package/dist/authTypes/authUser.type.d.ts +12 -0
- package/dist/authTypes/authUser.type.js +17 -1
- package/dist/authTypes/index.d.ts +6 -5
- 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 +23 -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.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
|
@@ -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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory token revocation store.
|
|
3
|
+
*
|
|
4
|
+
* @module authToken/authToken.revocation
|
|
5
|
+
*
|
|
6
|
+
* Good for development and single-process deployments. For production,
|
|
7
|
+
* implement TokenRevocationStore with Redis or a database so revocations
|
|
8
|
+
* are shared across instances.
|
|
9
|
+
*/
|
|
10
|
+
import type { TokenRevocationStore } from "../authTypes/authToken.type.js";
|
|
11
|
+
/**
|
|
12
|
+
* Create an in-memory token revocation store.
|
|
13
|
+
*
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createMemoryTokenRevocationStore(options?: {
|
|
23
|
+
readonly purgeIntervalMs?: number;
|
|
24
|
+
}): TokenRevocationStore;
|
|
25
|
+
//# sourceMappingURL=authToken.revocation.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory token revocation store.
|
|
3
|
+
*
|
|
4
|
+
* @module authToken/authToken.revocation
|
|
5
|
+
*
|
|
6
|
+
* Good for development and single-process deployments. For production,
|
|
7
|
+
* implement TokenRevocationStore with Redis or a database so revocations
|
|
8
|
+
* are shared across instances.
|
|
9
|
+
*/
|
|
10
|
+
/** Minimum interval between full sweeps of the revocation map. */
|
|
11
|
+
const DEFAULT_PURGE_INTERVAL_MS = 60_000;
|
|
12
|
+
/**
|
|
13
|
+
* Create an in-memory token revocation store.
|
|
14
|
+
*
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
export function createMemoryTokenRevocationStore(options) {
|
|
24
|
+
const revoked = new Map();
|
|
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);
|
|
34
|
+
for (const [id, expiresAt] of revoked) {
|
|
35
|
+
if (expiresAt < now) {
|
|
36
|
+
revoked.delete(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
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
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
async revoke(tokenId, expiresAt) {
|
|
53
|
+
maybePurgeExpired();
|
|
54
|
+
revoked.set(tokenId, expiresAt);
|
|
55
|
+
},
|
|
56
|
+
async isRevoked(tokenId) {
|
|
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;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=authToken.revocation.js.map
|
|
@@ -6,12 +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, expiration,
|
|
33
|
+
* Verify a JWT token's signature, algorithm, expiration, not-before, type,
|
|
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.
|
|
15
39
|
*/
|
|
16
40
|
export declare function verifyToken(token: JwtToken, secret: string, expectedType: "access" | "refresh", config: TokenConfig): TokenVerificationResult;
|
|
17
41
|
/**
|
|
@@ -5,10 +5,53 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Not exported from the package barrel — used internally by authToken.core.ts.
|
|
7
7
|
*/
|
|
8
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
9
|
-
import {
|
|
8
|
+
import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";
|
|
9
|
+
import { AuthConfigurationError } from "../authErrors/authError.base.js";
|
|
10
|
+
import { base64UrlEncode, decodeJsonSegment, splitToken, } from "./authToken.encoding.js";
|
|
10
11
|
/** HMAC signing algorithm. */
|
|
11
12
|
const ALGORITHM = "HS256";
|
|
13
|
+
/**
|
|
14
|
+
* Minimum accepted signing-secret length in bytes.
|
|
15
|
+
*
|
|
16
|
+
* HS256 keys should be at least as long as the digest (32 bytes); shorter
|
|
17
|
+
* keys are brute-forceable offline from a single captured token.
|
|
18
|
+
*/
|
|
19
|
+
export const MIN_SECRET_BYTES = 32;
|
|
20
|
+
/**
|
|
21
|
+
* Validate a {@link TokenConfig}'s signing secrets.
|
|
22
|
+
*
|
|
23
|
+
* Called on every mint and every verification. An empty or short secret
|
|
24
|
+
* yields tokens anyone can forge, and Node accepts a zero-length HMAC key
|
|
25
|
+
* without complaint, so this has to be checked explicitly rather than
|
|
26
|
+
* assumed.
|
|
27
|
+
*
|
|
28
|
+
* @throws {AuthConfigurationError} when either secret is missing, shorter
|
|
29
|
+
* than {@link MIN_SECRET_BYTES}, or when both secrets are identical.
|
|
30
|
+
*/
|
|
31
|
+
export function assertTokenSecrets(config) {
|
|
32
|
+
assertSecret(config?.accessSecret, "accessSecret");
|
|
33
|
+
assertSecret(config?.refreshSecret, "refreshSecret");
|
|
34
|
+
if (config.accessSecret === config.refreshSecret) {
|
|
35
|
+
throw new AuthConfigurationError("TokenConfig.accessSecret and TokenConfig.refreshSecret must differ; " +
|
|
36
|
+
"sharing one secret collapses the separation between access and " +
|
|
37
|
+
"refresh tokens.");
|
|
38
|
+
}
|
|
39
|
+
if (config.clockToleranceSeconds !== undefined &&
|
|
40
|
+
(!Number.isFinite(config.clockToleranceSeconds) ||
|
|
41
|
+
config.clockToleranceSeconds < 0 ||
|
|
42
|
+
config.clockToleranceSeconds > 300)) {
|
|
43
|
+
throw new AuthConfigurationError("TokenConfig.clockToleranceSeconds must be between 0 and 300 seconds.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertSecret(secret, field) {
|
|
47
|
+
if (typeof secret !== "string" || secret.length === 0) {
|
|
48
|
+
throw new AuthConfigurationError(`TokenConfig.${field} is required and must be a non-empty string.`);
|
|
49
|
+
}
|
|
50
|
+
if (Buffer.byteLength(secret, "utf-8") < MIN_SECRET_BYTES) {
|
|
51
|
+
throw new AuthConfigurationError(`TokenConfig.${field} must be at least ${MIN_SECRET_BYTES} bytes; ` +
|
|
52
|
+
"a shorter HMAC key can be recovered offline from a single token.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
12
55
|
/**
|
|
13
56
|
* Sign a JWT payload with HMAC SHA-256.
|
|
14
57
|
*/
|
|
@@ -20,41 +63,95 @@ export function signToken(payload, secret) {
|
|
|
20
63
|
return `${signatureInput}.${signature}`;
|
|
21
64
|
}
|
|
22
65
|
/**
|
|
23
|
-
* Verify a JWT token's signature, expiration,
|
|
66
|
+
* Verify a JWT token's signature, algorithm, expiration, not-before, type,
|
|
67
|
+
* and (when configured) issuer and audience.
|
|
68
|
+
*
|
|
69
|
+
* Never throws for untrusted input: every failure is reported as
|
|
70
|
+
* `{ valid: false, error }`. Oversized tokens are rejected before anything
|
|
71
|
+
* is decoded.
|
|
24
72
|
*/
|
|
25
73
|
export function verifyToken(token, secret, expectedType, config) {
|
|
26
|
-
const parts = token
|
|
27
|
-
if (parts
|
|
74
|
+
const parts = splitToken(token);
|
|
75
|
+
if (!parts) {
|
|
28
76
|
return { valid: false, error: "Invalid token format" };
|
|
29
77
|
}
|
|
30
78
|
const [headerB64, bodyB64, signature] = parts;
|
|
79
|
+
const header = decodeJsonSegment(headerB64);
|
|
80
|
+
if (!header) {
|
|
81
|
+
return { valid: false, error: "Invalid header" };
|
|
82
|
+
}
|
|
83
|
+
if (header["alg"] !== ALGORITHM) {
|
|
84
|
+
return { valid: false, error: "Unsupported algorithm" };
|
|
85
|
+
}
|
|
31
86
|
const signatureInput = `${headerB64}.${bodyB64}`;
|
|
32
87
|
const expectedSignature = hmacSha256(signatureInput, secret);
|
|
33
|
-
const sigBuffer = Buffer.from(signature
|
|
34
|
-
const expectedBuffer = Buffer.from(expectedSignature, "
|
|
88
|
+
const sigBuffer = Buffer.from(signature, "base64url");
|
|
89
|
+
const expectedBuffer = Buffer.from(expectedSignature, "base64url");
|
|
35
90
|
if (sigBuffer.length !== expectedBuffer.length ||
|
|
36
91
|
!timingSafeEqual(sigBuffer, expectedBuffer)) {
|
|
37
92
|
return { valid: false, error: "Invalid signature" };
|
|
38
93
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
payload
|
|
94
|
+
const decoded = decodeJsonSegment(bodyB64);
|
|
95
|
+
if (!decoded) {
|
|
96
|
+
return { valid: false, error: "Invalid payload" };
|
|
42
97
|
}
|
|
43
|
-
|
|
98
|
+
if (!isTokenPayload(decoded)) {
|
|
44
99
|
return { valid: false, error: "Invalid payload" };
|
|
45
100
|
}
|
|
101
|
+
const payload = decoded;
|
|
46
102
|
const now = Math.floor(Date.now() / 1000);
|
|
47
|
-
|
|
103
|
+
const skew = config.clockToleranceSeconds ?? 0;
|
|
104
|
+
if (payload.exp + skew < now) {
|
|
48
105
|
return { valid: false, error: "Token expired" };
|
|
49
106
|
}
|
|
107
|
+
if (payload.iat - skew > now) {
|
|
108
|
+
return { valid: false, error: "Token issued in the future" };
|
|
109
|
+
}
|
|
110
|
+
const nbf = payload["nbf"];
|
|
111
|
+
if (typeof nbf === "number" && nbf - skew > now) {
|
|
112
|
+
return { valid: false, error: "Token not yet valid" };
|
|
113
|
+
}
|
|
50
114
|
if (payload.typ !== expectedType) {
|
|
51
115
|
return { valid: false, error: `Expected ${expectedType} token` };
|
|
52
116
|
}
|
|
53
|
-
if (config.issuer && payload
|
|
117
|
+
if (config.issuer && payload["iss"] !== config.issuer) {
|
|
54
118
|
return { valid: false, error: "Invalid issuer" };
|
|
55
119
|
}
|
|
120
|
+
if (config.audience && payload["aud"] !== config.audience) {
|
|
121
|
+
return { valid: false, error: "Invalid audience" };
|
|
122
|
+
}
|
|
56
123
|
return { valid: true, payload };
|
|
57
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Narrow a decoded JWT body to a {@link TokenPayload}.
|
|
127
|
+
*
|
|
128
|
+
* Replaces the `as TokenPayload` cast that previously let a `null`, an
|
|
129
|
+
* array, or a payload with a missing/mistyped `sub`, `jti`, `exp`, `iat` or
|
|
130
|
+
* `typ` flow into the caller.
|
|
131
|
+
*/
|
|
132
|
+
function isTokenPayload(value) {
|
|
133
|
+
if (typeof value["sub"] !== "string" || value["sub"].length === 0) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (typeof value["jti"] !== "string" || value["jti"].length === 0) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (value["typ"] !== "access" && value["typ"] !== "refresh")
|
|
140
|
+
return false;
|
|
141
|
+
if (!isFiniteNumber(value["exp"]))
|
|
142
|
+
return false;
|
|
143
|
+
if (!isFiniteNumber(value["iat"]))
|
|
144
|
+
return false;
|
|
145
|
+
const roles = value["roles"];
|
|
146
|
+
if (roles !== undefined &&
|
|
147
|
+
(!Array.isArray(roles) || roles.some((r) => typeof r !== "string"))) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
function isFiniteNumber(value) {
|
|
153
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
154
|
+
}
|
|
58
155
|
/**
|
|
59
156
|
* Generate a random token ID.
|
|
60
157
|
*/
|
|
@@ -63,25 +160,6 @@ export function generateTokenId() {
|
|
|
63
160
|
}
|
|
64
161
|
// ─── Internal helpers ─────────────────────────────────────────────────────
|
|
65
162
|
function hmacSha256(data, secret) {
|
|
66
|
-
return createHmac("sha256", secret).update(data).digest("
|
|
67
|
-
}
|
|
68
|
-
function base64UrlEncode(data) {
|
|
69
|
-
const encoded = Buffer.from(data).toString("base64");
|
|
70
|
-
let result = "";
|
|
71
|
-
for (let i = 0; i < encoded.length; i++) {
|
|
72
|
-
const char = encoded[i];
|
|
73
|
-
if (char === "+")
|
|
74
|
-
result += "-";
|
|
75
|
-
else if (char === "/")
|
|
76
|
-
result += "_";
|
|
77
|
-
else if (char !== "=")
|
|
78
|
-
result += char;
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
function base64UrlDecode(data) {
|
|
83
|
-
const padded = data.replace(/-/g, "+").replace(/_/g, "/");
|
|
84
|
-
const padLength = (4 - (padded.length % 4)) % 4;
|
|
85
|
-
return Buffer.from(padded + "=".repeat(padLength), "base64").toString("utf-8");
|
|
163
|
+
return createHmac("sha256", secret).update(data).digest("base64url");
|
|
86
164
|
}
|
|
87
165
|
//# sourceMappingURL=authToken.signing.js.map
|
package/dist/authToken/index.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/auth — JWT Namespace
|
|
3
3
|
*
|
|
4
|
-
* Convenience namespace for
|
|
4
|
+
* Convenience namespace for the JWT primitives: minting, verification, the
|
|
5
|
+
* non-rotating refresh helper, the in-memory revocation store, and the
|
|
6
|
+
* unverified header/claim parsers.
|
|
7
|
+
*
|
|
8
|
+
* Note what is *not* here: `createAuthService()`. `jwt.refreshAccessToken`
|
|
9
|
+
* performs no rotation and consults no revocation store — pair it with
|
|
10
|
+
* `jwt.createMemoryTokenRevocationStore` yourself, or use
|
|
11
|
+
* `createAuthService().refresh()`, which rotates, revokes, re-loads the user
|
|
12
|
+
* and validates the session.
|
|
5
13
|
*/
|
|
6
14
|
import { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken } from "./authToken.core.js";
|
|
15
|
+
import { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
|
|
7
16
|
import { parseBearerToken, isTokenExpired, extractUserId } from "../authUtils/authUtils.helper.js";
|
|
8
17
|
export type { JwtToken, TokenPair, TokenConfig, TokenVerificationResult, } from "../authTypes/authToken.type.js";
|
|
9
18
|
export declare const jwt: {
|
|
10
|
-
createTokenPair: typeof createTokenPair;
|
|
11
|
-
verifyAccessToken: typeof verifyAccessToken;
|
|
12
|
-
verifyRefreshToken: typeof verifyRefreshToken;
|
|
13
|
-
refreshAccessToken: typeof refreshAccessToken;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
readonly createTokenPair: typeof createTokenPair;
|
|
20
|
+
readonly verifyAccessToken: typeof verifyAccessToken;
|
|
21
|
+
readonly verifyRefreshToken: typeof verifyRefreshToken;
|
|
22
|
+
readonly refreshAccessToken: typeof refreshAccessToken;
|
|
23
|
+
readonly createMemoryTokenRevocationStore: typeof createMemoryTokenRevocationStore;
|
|
24
|
+
readonly parseBearerToken: typeof parseBearerToken;
|
|
25
|
+
readonly isTokenExpired: typeof isTokenExpired;
|
|
26
|
+
readonly extractUserId: typeof extractUserId;
|
|
17
27
|
};
|
|
18
28
|
//# sourceMappingURL=jwt.namespace.d.ts.map
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/auth — JWT Namespace
|
|
3
3
|
*
|
|
4
|
-
* Convenience namespace for
|
|
4
|
+
* Convenience namespace for the JWT primitives: minting, verification, the
|
|
5
|
+
* non-rotating refresh helper, the in-memory revocation store, and the
|
|
6
|
+
* unverified header/claim parsers.
|
|
7
|
+
*
|
|
8
|
+
* Note what is *not* here: `createAuthService()`. `jwt.refreshAccessToken`
|
|
9
|
+
* performs no rotation and consults no revocation store — pair it with
|
|
10
|
+
* `jwt.createMemoryTokenRevocationStore` yourself, or use
|
|
11
|
+
* `createAuthService().refresh()`, which rotates, revokes, re-loads the user
|
|
12
|
+
* and validates the session.
|
|
5
13
|
*/
|
|
6
14
|
import { createTokenPair, verifyAccessToken, verifyRefreshToken, refreshAccessToken, } from "./authToken.core.js";
|
|
15
|
+
import { createMemoryTokenRevocationStore } from "./authToken.revocation.js";
|
|
7
16
|
import { parseBearerToken, isTokenExpired, extractUserId, } from "../authUtils/authUtils.helper.js";
|
|
8
17
|
export const jwt = {
|
|
9
18
|
createTokenPair,
|
|
10
19
|
verifyAccessToken,
|
|
11
20
|
verifyRefreshToken,
|
|
12
21
|
refreshAccessToken,
|
|
22
|
+
createMemoryTokenRevocationStore,
|
|
13
23
|
parseBearerToken,
|
|
14
24
|
isTokenExpired,
|
|
15
25
|
extractUserId,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login attempt tracking types — brute-force lockout and rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* @module authTypes/authAttempt
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Current attempt state for one login identifier.
|
|
8
|
+
*/
|
|
9
|
+
export interface LoginAttemptRecord {
|
|
10
|
+
/** Consecutive failed authentications since the last successful login. */
|
|
11
|
+
readonly failures: number;
|
|
12
|
+
/** Attempts counted inside the current rate-limit window. */
|
|
13
|
+
readonly attempts: number;
|
|
14
|
+
/** Unix milliseconds until which the identifier is locked, if locked. */
|
|
15
|
+
readonly lockedUntil?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Store backing failed-attempt lockout and login rate limiting.
|
|
19
|
+
*
|
|
20
|
+
* Keys are the *submitted* identifier, not a resolved user id, so unknown
|
|
21
|
+
* and known accounts are throttled identically and the endpoint stays free
|
|
22
|
+
* of an existence oracle. The in-memory implementation
|
|
23
|
+
* (`createMemoryLoginAttemptStore`) is per-process; back this with Redis to
|
|
24
|
+
* make limits hold across instances.
|
|
25
|
+
*/
|
|
26
|
+
export interface LoginAttemptStore {
|
|
27
|
+
/** Read the current state without recording anything. */
|
|
28
|
+
get(identifier: string): Promise<LoginAttemptRecord>;
|
|
29
|
+
/** Count an attempt (before credentials are checked). */
|
|
30
|
+
recordAttempt(identifier: string): Promise<LoginAttemptRecord>;
|
|
31
|
+
/** Count a failed authentication. */
|
|
32
|
+
recordFailure(identifier: string): Promise<LoginAttemptRecord>;
|
|
33
|
+
/** Lock an identifier until `until` (Unix milliseconds). */
|
|
34
|
+
lock(identifier: string, until: number): Promise<void>;
|
|
35
|
+
/** Clear all state for an identifier (successful login). */
|
|
36
|
+
reset(identifier: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Brute-force protection settings for `createAuthService()`.
|
|
40
|
+
*/
|
|
41
|
+
export interface LoginThrottleConfig {
|
|
42
|
+
/** Where attempt counters live. */
|
|
43
|
+
readonly store: LoginAttemptStore;
|
|
44
|
+
/**
|
|
45
|
+
* Consecutive failures that trigger a lockout (default: 5).
|
|
46
|
+
* `login()` then throws `AccountLockedError` until the lockout lapses.
|
|
47
|
+
*/
|
|
48
|
+
readonly maxFailedAttempts?: number;
|
|
49
|
+
/** Lockout duration in seconds (default: 900). */
|
|
50
|
+
readonly lockoutSeconds?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Attempts allowed per identifier inside the store's rate-limit window
|
|
53
|
+
* (default: 20). Exceeding it throws `AuthRateLimitError`. This bounds the
|
|
54
|
+
* scrypt work an attacker can force the server to perform.
|
|
55
|
+
*/
|
|
56
|
+
readonly maxAttemptsPerWindow?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Window length in seconds reported on `AuthRateLimitError.retryAfterSeconds`
|
|
59
|
+
* (default: 60). The store owns the actual window.
|
|
60
|
+
*/
|
|
61
|
+
readonly windowSeconds?: number;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=authAttempt.type.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential shapes accepted by the authentication entry points.
|
|
3
|
+
*
|
|
4
|
+
* @module authTypes/authCredentials
|
|
5
|
+
*
|
|
6
|
+
* These are plain contracts for the credentials a caller collects from a
|
|
7
|
+
* request. They carry no behaviour; `createAuthService()` takes the
|
|
8
|
+
* identifier and password directly.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Password credentials: an identifier (username or email) and a password.
|
|
12
|
+
*/
|
|
13
|
+
export interface PasswordCredentials {
|
|
14
|
+
readonly identifier: string;
|
|
15
|
+
readonly password: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* API key credentials.
|
|
19
|
+
*
|
|
20
|
+
* The type exists for callers that model machine-to-machine auth; this
|
|
21
|
+
* package implements no API-key verification.
|
|
22
|
+
*/
|
|
23
|
+
export interface ApiKeyCredentials {
|
|
24
|
+
readonly apiKey: string;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=authCredentials.type.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential shapes accepted by the authentication entry points.
|
|
3
|
+
*
|
|
4
|
+
* @module authTypes/authCredentials
|
|
5
|
+
*
|
|
6
|
+
* These are plain contracts for the credentials a caller collects from a
|
|
7
|
+
* request. They carry no behaviour; `createAuthService()` takes the
|
|
8
|
+
* identifier and password directly.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=authCredentials.type.js.map
|
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
import type { UserId } from "../authTypes/authUser.type.js";
|
|
7
7
|
import type { SessionId } from "@zudojs/constants";
|
|
8
8
|
export type { SessionId } from "@zudojs/constants";
|
|
9
|
+
/**
|
|
10
|
+
* Brands a plain string as a {@link SessionId}.
|
|
11
|
+
*
|
|
12
|
+
* `SessionId` is a branded type with no public constructor, so callers holding
|
|
13
|
+
* an id from a session store, a cookie or a request parameter have no way to
|
|
14
|
+
* produce one without a cast. This is that constructor.
|
|
15
|
+
*
|
|
16
|
+
* @param value - Non-empty session identifier.
|
|
17
|
+
* @returns The same string, typed as a `SessionId`.
|
|
18
|
+
* @throws {TypeError} If `value` is not a non-empty string.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toSessionId(value: string): SessionId;
|
|
9
21
|
/**
|
|
10
22
|
* Server-side session representation.
|
|
11
23
|
*/
|
|
@@ -22,10 +34,19 @@ export interface AuthSession {
|
|
|
22
34
|
readonly createdAt: Date;
|
|
23
35
|
/** Last activity time */
|
|
24
36
|
readonly lastActivityAt: Date;
|
|
25
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Session expiration time.
|
|
39
|
+
*
|
|
40
|
+
* With sliding expiration this moves forward on every `touch()`, but never
|
|
41
|
+
* past `createdAt + absoluteTtlSeconds` when an absolute lifetime was
|
|
42
|
+
* requested.
|
|
43
|
+
*/
|
|
26
44
|
readonly expiresAt: Date;
|
|
27
|
-
/**
|
|
28
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Hard deadline for the session, when one was requested at creation.
|
|
47
|
+
* `touch()` never extends `expiresAt` beyond this.
|
|
48
|
+
*/
|
|
49
|
+
readonly absoluteExpiresAt?: Date;
|
|
29
50
|
/** Session metadata */
|
|
30
51
|
readonly metadata?: Record<string, unknown>;
|
|
31
52
|
}
|
|
@@ -36,7 +57,14 @@ export interface CreateSessionOptions {
|
|
|
36
57
|
readonly userId: UserId;
|
|
37
58
|
readonly userAgent?: string;
|
|
38
59
|
readonly ip?: string;
|
|
60
|
+
/** Idle timeout in seconds; refreshed by `touch()` (default: 86400). */
|
|
39
61
|
readonly ttlSeconds?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Absolute maximum session lifetime in seconds, measured from creation.
|
|
64
|
+
* Without it a session that is touched once per idle window never expires,
|
|
65
|
+
* so a stolen session id is valid indefinitely.
|
|
66
|
+
*/
|
|
67
|
+
readonly absoluteTtlSeconds?: number;
|
|
40
68
|
readonly metadata?: Record<string, unknown>;
|
|
41
69
|
}
|
|
42
70
|
/**
|
|
@@ -3,5 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module authSession/authSession
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Brands a plain string as a {@link SessionId}.
|
|
8
|
+
*
|
|
9
|
+
* `SessionId` is a branded type with no public constructor, so callers holding
|
|
10
|
+
* an id from a session store, a cookie or a request parameter have no way to
|
|
11
|
+
* produce one without a cast. This is that constructor.
|
|
12
|
+
*
|
|
13
|
+
* @param value - Non-empty session identifier.
|
|
14
|
+
* @returns The same string, typed as a `SessionId`.
|
|
15
|
+
* @throws {TypeError} If `value` is not a non-empty string.
|
|
16
|
+
*/
|
|
17
|
+
export function toSessionId(value) {
|
|
18
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
+
throw new TypeError("toSessionId: value must be a non-empty string.");
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
7
23
|
//# sourceMappingURL=authSession.type.js.map
|