@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
|
@@ -5,10 +5,68 @@
|
|
|
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
|
+
// A NaN TTL (`Number(process.env.X)` with X unset) minted tokens whose
|
|
46
|
+
// `exp` serialised as `null`, which every verifier then rejected as
|
|
47
|
+
// "Invalid payload" — a misconfiguration that only surfaced as a mystery
|
|
48
|
+
// at first login. Zero and negative TTLs are deliberately still accepted:
|
|
49
|
+
// they mint already-expired tokens, which is a documented way to test
|
|
50
|
+
// expiry handling.
|
|
51
|
+
assertFiniteTtl(config.accessTtl, "accessTtl");
|
|
52
|
+
assertFiniteTtl(config.refreshTtl, "refreshTtl");
|
|
53
|
+
}
|
|
54
|
+
function assertFiniteTtl(value, field) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return;
|
|
57
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
58
|
+
throw new AuthConfigurationError(`TokenConfig.${field} must be a finite number of seconds; got ${String(value)}.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertSecret(secret, field) {
|
|
62
|
+
if (typeof secret !== "string" || secret.length === 0) {
|
|
63
|
+
throw new AuthConfigurationError(`TokenConfig.${field} is required and must be a non-empty string.`);
|
|
64
|
+
}
|
|
65
|
+
if (Buffer.byteLength(secret, "utf-8") < MIN_SECRET_BYTES) {
|
|
66
|
+
throw new AuthConfigurationError(`TokenConfig.${field} must be at least ${MIN_SECRET_BYTES} bytes; ` +
|
|
67
|
+
"a shorter HMAC key can be recovered offline from a single token.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
12
70
|
/**
|
|
13
71
|
* Sign a JWT payload with HMAC SHA-256.
|
|
14
72
|
*/
|
|
@@ -20,58 +78,95 @@ export function signToken(payload, secret) {
|
|
|
20
78
|
return `${signatureInput}.${signature}`;
|
|
21
79
|
}
|
|
22
80
|
/**
|
|
23
|
-
* Verify a JWT token's signature, algorithm, expiration, type,
|
|
81
|
+
* Verify a JWT token's signature, algorithm, expiration, not-before, type,
|
|
24
82
|
* and (when configured) issuer and audience.
|
|
83
|
+
*
|
|
84
|
+
* Never throws for untrusted input: every failure is reported as
|
|
85
|
+
* `{ valid: false, error }`. Oversized tokens are rejected before anything
|
|
86
|
+
* is decoded.
|
|
25
87
|
*/
|
|
26
88
|
export function verifyToken(token, secret, expectedType, config) {
|
|
27
|
-
const parts = token
|
|
28
|
-
if (parts
|
|
89
|
+
const parts = splitToken(token);
|
|
90
|
+
if (!parts) {
|
|
29
91
|
return { valid: false, error: "Invalid token format" };
|
|
30
92
|
}
|
|
31
93
|
const [headerB64, bodyB64, signature] = parts;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
header = JSON.parse(base64UrlDecode(headerB64));
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
94
|
+
const header = decodeJsonSegment(headerB64);
|
|
95
|
+
if (!header) {
|
|
37
96
|
return { valid: false, error: "Invalid header" };
|
|
38
97
|
}
|
|
39
|
-
if (header
|
|
98
|
+
if (header["alg"] !== ALGORITHM) {
|
|
40
99
|
return { valid: false, error: "Unsupported algorithm" };
|
|
41
100
|
}
|
|
42
101
|
const signatureInput = `${headerB64}.${bodyB64}`;
|
|
43
102
|
const expectedSignature = hmacSha256(signatureInput, secret);
|
|
44
|
-
const sigBuffer = Buffer.from(signature
|
|
103
|
+
const sigBuffer = Buffer.from(signature, "base64url");
|
|
45
104
|
const expectedBuffer = Buffer.from(expectedSignature, "base64url");
|
|
46
105
|
if (sigBuffer.length !== expectedBuffer.length ||
|
|
47
106
|
!timingSafeEqual(sigBuffer, expectedBuffer)) {
|
|
48
107
|
return { valid: false, error: "Invalid signature" };
|
|
49
108
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
payload
|
|
109
|
+
const decoded = decodeJsonSegment(bodyB64);
|
|
110
|
+
if (!decoded) {
|
|
111
|
+
return { valid: false, error: "Invalid payload" };
|
|
53
112
|
}
|
|
54
|
-
|
|
113
|
+
if (!isTokenPayload(decoded)) {
|
|
55
114
|
return { valid: false, error: "Invalid payload" };
|
|
56
115
|
}
|
|
116
|
+
const payload = decoded;
|
|
57
117
|
const now = Math.floor(Date.now() / 1000);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
if (payload.exp < now) {
|
|
118
|
+
const skew = config.clockToleranceSeconds ?? 0;
|
|
119
|
+
if (payload.exp + skew < now) {
|
|
62
120
|
return { valid: false, error: "Token expired" };
|
|
63
121
|
}
|
|
122
|
+
if (payload.iat - skew > now) {
|
|
123
|
+
return { valid: false, error: "Token issued in the future" };
|
|
124
|
+
}
|
|
125
|
+
const nbf = payload["nbf"];
|
|
126
|
+
if (typeof nbf === "number" && nbf - skew > now) {
|
|
127
|
+
return { valid: false, error: "Token not yet valid" };
|
|
128
|
+
}
|
|
64
129
|
if (payload.typ !== expectedType) {
|
|
65
130
|
return { valid: false, error: `Expected ${expectedType} token` };
|
|
66
131
|
}
|
|
67
|
-
if (config.issuer && payload
|
|
132
|
+
if (config.issuer && payload["iss"] !== config.issuer) {
|
|
68
133
|
return { valid: false, error: "Invalid issuer" };
|
|
69
134
|
}
|
|
70
|
-
if (config.audience && payload
|
|
135
|
+
if (config.audience && payload["aud"] !== config.audience) {
|
|
71
136
|
return { valid: false, error: "Invalid audience" };
|
|
72
137
|
}
|
|
73
138
|
return { valid: true, payload };
|
|
74
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Narrow a decoded JWT body to a {@link TokenPayload}.
|
|
142
|
+
*
|
|
143
|
+
* Replaces the `as TokenPayload` cast that previously let a `null`, an
|
|
144
|
+
* array, or a payload with a missing/mistyped `sub`, `jti`, `exp`, `iat` or
|
|
145
|
+
* `typ` flow into the caller.
|
|
146
|
+
*/
|
|
147
|
+
function isTokenPayload(value) {
|
|
148
|
+
if (typeof value["sub"] !== "string" || value["sub"].length === 0) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (typeof value["jti"] !== "string" || value["jti"].length === 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (value["typ"] !== "access" && value["typ"] !== "refresh")
|
|
155
|
+
return false;
|
|
156
|
+
if (!isFiniteNumber(value["exp"]))
|
|
157
|
+
return false;
|
|
158
|
+
if (!isFiniteNumber(value["iat"]))
|
|
159
|
+
return false;
|
|
160
|
+
const roles = value["roles"];
|
|
161
|
+
if (roles !== undefined &&
|
|
162
|
+
(!Array.isArray(roles) || roles.some((r) => typeof r !== "string"))) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
function isFiniteNumber(value) {
|
|
168
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
169
|
+
}
|
|
75
170
|
/**
|
|
76
171
|
* Generate a random token ID.
|
|
77
172
|
*/
|
|
@@ -82,10 +177,4 @@ export function generateTokenId() {
|
|
|
82
177
|
function hmacSha256(data, secret) {
|
|
83
178
|
return createHmac("sha256", secret).update(data).digest("base64url");
|
|
84
179
|
}
|
|
85
|
-
function base64UrlEncode(data) {
|
|
86
|
-
return Buffer.from(data, "utf-8").toString("base64url");
|
|
87
|
-
}
|
|
88
|
-
function base64UrlDecode(data) {
|
|
89
|
-
return Buffer.from(data, "base64url").toString("utf-8");
|
|
90
|
-
}
|
|
91
180
|
//# sourceMappingURL=authToken.signing.js.map
|
|
@@ -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,65 @@
|
|
|
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 — trimmed, NFKC-normalised and
|
|
21
|
+
* lower-cased by `createAuthService()` so that case and whitespace variants
|
|
22
|
+
* of one email share a budget — not a resolved user id, so unknown and
|
|
23
|
+
* known accounts are throttled identically and the endpoint stays free of
|
|
24
|
+
* an existence oracle. The in-memory implementation
|
|
25
|
+
* (`createMemoryLoginAttemptStore`) is per-process; back this with Redis to
|
|
26
|
+
* make limits hold across instances.
|
|
27
|
+
*/
|
|
28
|
+
export interface LoginAttemptStore {
|
|
29
|
+
/** Read the current state without recording anything. */
|
|
30
|
+
get(identifier: string): Promise<LoginAttemptRecord>;
|
|
31
|
+
/** Count an attempt (before credentials are checked). */
|
|
32
|
+
recordAttempt(identifier: string): Promise<LoginAttemptRecord>;
|
|
33
|
+
/** Count a failed authentication. */
|
|
34
|
+
recordFailure(identifier: string): Promise<LoginAttemptRecord>;
|
|
35
|
+
/** Lock an identifier until `until` (Unix milliseconds). */
|
|
36
|
+
lock(identifier: string, until: number): Promise<void>;
|
|
37
|
+
/** Clear all state for an identifier (successful login). */
|
|
38
|
+
reset(identifier: string): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Brute-force protection settings for `createAuthService()`.
|
|
42
|
+
*/
|
|
43
|
+
export interface LoginThrottleConfig {
|
|
44
|
+
/** Where attempt counters live. */
|
|
45
|
+
readonly store: LoginAttemptStore;
|
|
46
|
+
/**
|
|
47
|
+
* Consecutive failures that trigger a lockout (default: 5).
|
|
48
|
+
* `login()` then throws `AccountLockedError` until the lockout lapses.
|
|
49
|
+
*/
|
|
50
|
+
readonly maxFailedAttempts?: number;
|
|
51
|
+
/** Lockout duration in seconds (default: 900). */
|
|
52
|
+
readonly lockoutSeconds?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Attempts allowed per identifier inside the store's rate-limit window
|
|
55
|
+
* (default: 20). Exceeding it throws `AuthRateLimitError`. This bounds the
|
|
56
|
+
* scrypt work an attacker can force the server to perform.
|
|
57
|
+
*/
|
|
58
|
+
readonly maxAttemptsPerWindow?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Window length in seconds reported on `AuthRateLimitError.retryAfterSeconds`
|
|
61
|
+
* (default: 60). The store owns the actual window.
|
|
62
|
+
*/
|
|
63
|
+
readonly windowSeconds?: number;
|
|
64
|
+
}
|
|
65
|
+
//# 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
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module authToken/authToken
|
|
5
5
|
*/
|
|
6
6
|
import type { UserId } from "../authTypes/authUser.type.js";
|
|
7
|
-
import type { TokenId } from "@zudojs/constants";
|
|
7
|
+
import type { SessionId, TokenId } from "@zudojs/constants";
|
|
8
8
|
/** JWT token string. */
|
|
9
9
|
export type JwtToken = string;
|
|
10
10
|
/** Token identifier. Re-exported from @zudojs/constants for type safety. */
|
|
@@ -25,6 +25,14 @@ export interface TokenPayload {
|
|
|
25
25
|
readonly jti: TokenId;
|
|
26
26
|
/** User roles */
|
|
27
27
|
readonly roles?: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Session ID this token was issued against.
|
|
30
|
+
*
|
|
31
|
+
* Set by `createAuthService().login()`. `verifyToken()` and `refresh()`
|
|
32
|
+
* require the referenced session to still exist, which is what makes
|
|
33
|
+
* `logout()` actually invalidate outstanding tokens.
|
|
34
|
+
*/
|
|
35
|
+
readonly sid?: SessionId;
|
|
28
36
|
/** Custom claims */
|
|
29
37
|
readonly [key: string]: unknown;
|
|
30
38
|
}
|
|
@@ -57,6 +65,11 @@ export interface TokenConfig {
|
|
|
57
65
|
readonly issuer?: string;
|
|
58
66
|
/** JWT audience */
|
|
59
67
|
readonly audience?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Clock-skew tolerance in seconds applied to `exp`, `iat` and `nbf`
|
|
70
|
+
* (default: 0, maximum: 300).
|
|
71
|
+
*/
|
|
72
|
+
readonly clockToleranceSeconds?: number;
|
|
60
73
|
}
|
|
61
74
|
/**
|
|
62
75
|
* Store for revoked token IDs (`jti` claims).
|
|
@@ -76,6 +89,21 @@ export interface TokenRevocationStore {
|
|
|
76
89
|
revoke(tokenId: TokenId, expiresAt: number): Promise<void>;
|
|
77
90
|
/** Check whether a token ID has been revoked. */
|
|
78
91
|
isRevoked(tokenId: TokenId): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Atomically revoke a token ID **only if it was not already revoked**,
|
|
94
|
+
* returning whether this caller was the one that revoked it.
|
|
95
|
+
*
|
|
96
|
+
* This is the compare-and-set that makes refresh-token rotation safe: a
|
|
97
|
+
* separate `isRevoked()` then `revoke()` leaves two `await` points during
|
|
98
|
+
* which a concurrent replay of the same token also observes "not revoked"
|
|
99
|
+
* and also mints a valid pair. Implement it with a single synchronous
|
|
100
|
+
* `Map.has`/`set` in-process, or `SET NX` in Redis.
|
|
101
|
+
*
|
|
102
|
+
* Optional for backwards compatibility: `createAuthService().refresh()`
|
|
103
|
+
* falls back to `isRevoked()` + `revoke()` when it is absent, which is
|
|
104
|
+
* racy. Implement it in any store used in production.
|
|
105
|
+
*/
|
|
106
|
+
revokeIfNotRevoked?(tokenId: TokenId, expiresAt: number): Promise<boolean>;
|
|
79
107
|
}
|
|
80
108
|
/**
|
|
81
109
|
* Result of token verification.
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { UserId } from "@zudojs/constants";
|
|
7
7
|
export type { UserId } from "@zudojs/constants";
|
|
8
|
+
/**
|
|
9
|
+
* Brands a plain string as a {@link UserId}.
|
|
10
|
+
*
|
|
11
|
+
* `UserId` is a branded type with no public constructor, so callers holding an
|
|
12
|
+
* id from a database row, a decoded token or a request parameter have no way to
|
|
13
|
+
* produce one without a cast. This is that constructor.
|
|
14
|
+
*
|
|
15
|
+
* @param value - Non-empty user identifier.
|
|
16
|
+
* @returns The same string, typed as a `UserId`.
|
|
17
|
+
* @throws {TypeError} If `value` is not a non-empty string.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toUserId(value: string): UserId;
|
|
8
20
|
/**
|
|
9
21
|
* Authenticated user representation.
|
|
10
22
|
*/
|
|
@@ -3,5 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module authTypes/authUser
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Brands a plain string as a {@link UserId}.
|
|
8
|
+
*
|
|
9
|
+
* `UserId` is a branded type with no public constructor, so callers holding an
|
|
10
|
+
* id from a database row, a decoded token or a request parameter have no way to
|
|
11
|
+
* produce one without a cast. This is that constructor.
|
|
12
|
+
*
|
|
13
|
+
* @param value - Non-empty user identifier.
|
|
14
|
+
* @returns The same string, typed as a `UserId`.
|
|
15
|
+
* @throws {TypeError} If `value` is not a non-empty string.
|
|
16
|
+
*/
|
|
17
|
+
export function toUserId(value) {
|
|
18
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
+
throw new TypeError("toUserId: value must be a non-empty string.");
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
7
23
|
//# sourceMappingURL=authUser.type.js.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core auth types: users, tokens, sessions,
|
|
2
|
+
* Core auth types: users, tokens, sessions, credentials, and RBAC.
|
|
3
3
|
*
|
|
4
4
|
* @module authTypes
|
|
5
5
|
*/
|
|
6
|
-
export { type UserId, type AuthUser, type UserCredentials, type UserRegistration, } from "./authUser.type.js";
|
|
6
|
+
export { type UserId, toUserId, type AuthUser, type UserCredentials, type UserRegistration, } from "./authUser.type.js";
|
|
7
7
|
export { type JwtToken, type TokenId, type TokenPayload, type TokenPair, type TokenConfig, type TokenRevocationStore, type TokenVerificationResult, } from "./authToken.type.js";
|
|
8
|
-
export { type SessionId, type AuthSession, type CreateSessionOptions, type SessionStore, } from "./authSession.type.js";
|
|
9
|
-
export { type
|
|
8
|
+
export { type SessionId, toSessionId, type AuthSession, type CreateSessionOptions, type SessionStore, } from "./authSession.type.js";
|
|
9
|
+
export { type PasswordCredentials, type ApiKeyCredentials, } from "./authCredentials.type.js";
|
|
10
|
+
export { type LoginAttemptRecord, type LoginAttemptStore, type LoginThrottleConfig, } from "./authAttempt.type.js";
|
|
10
11
|
export { type Permission, type Role, type GuardResult, type GuardContext, } from "./authRbac.type.js";
|
|
11
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/authTypes/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core auth types: users, tokens, sessions,
|
|
2
|
+
* Core auth types: users, tokens, sessions, credentials, and RBAC.
|
|
3
3
|
*
|
|
4
4
|
* @module authTypes
|
|
5
5
|
*/
|
|
6
|
-
export {} from "./authUser.type.js";
|
|
6
|
+
export { toUserId, } from "./authUser.type.js";
|
|
7
7
|
export {} from "./authToken.type.js";
|
|
8
|
-
export {} from "./authSession.type.js";
|
|
9
|
-
export {} from "./
|
|
8
|
+
export { toSessionId, } from "./authSession.type.js";
|
|
9
|
+
export {} from "./authCredentials.type.js";
|
|
10
|
+
export {} from "./authAttempt.type.js";
|
|
10
11
|
export {} from "./authRbac.type.js";
|
|
11
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,31 +6,52 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Parse a Bearer token from an Authorization header.
|
|
8
8
|
*
|
|
9
|
+
* Accepts `unknown` on purpose: this sits on the HTTP trust boundary, where
|
|
10
|
+
* a duplicated header gives `string[]` and adapter layers routinely pass
|
|
11
|
+
* through values they have not narrowed. Anything that is not a plausible
|
|
12
|
+
* header value is `null`, never a thrown `TypeError`.
|
|
13
|
+
*
|
|
9
14
|
* @param authorization - Raw Authorization header value
|
|
10
15
|
* @returns The token string, or null if not a Bearer token
|
|
11
16
|
*/
|
|
12
|
-
export declare function parseBearerToken(authorization:
|
|
17
|
+
export declare function parseBearerToken(authorization: unknown): string | null;
|
|
13
18
|
/**
|
|
14
19
|
* Parse cookie string into a key-value map.
|
|
15
20
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* The returned object has a `null` prototype, so a cookie named
|
|
22
|
+
* `constructor`, `hasOwnProperty` or `__proto__` cannot shadow or confuse an
|
|
23
|
+
* inherited member for the caller. Oversized headers and cookie counts are
|
|
24
|
+
* capped rather than allocated.
|
|
25
|
+
*
|
|
26
|
+
* @param cookie - Raw Cookie header value (any type; see
|
|
27
|
+
* {@link parseBearerToken} for why)
|
|
28
|
+
* @returns Parsed cookies — an object with no prototype
|
|
18
29
|
*/
|
|
19
|
-
export declare function parseCookies(cookie:
|
|
30
|
+
export declare function parseCookies(cookie: unknown): Record<string, string>;
|
|
20
31
|
/**
|
|
21
|
-
* Check if a token is expired without verifying the signature
|
|
32
|
+
* Check if a token is expired **without verifying the signature**.
|
|
33
|
+
*
|
|
34
|
+
* The payload is attacker-controlled: treat the answer as a hint (e.g. "should
|
|
35
|
+
* I refresh before calling?"), never as an authorization decision.
|
|
22
36
|
*
|
|
23
37
|
* @param token - JWT token string
|
|
24
|
-
* @returns Whether the token appears expired
|
|
38
|
+
* @returns Whether the token appears expired. Unparseable, oversized, or
|
|
39
|
+
* malformed input is reported as expired.
|
|
25
40
|
*/
|
|
26
|
-
export declare function isTokenExpired(token:
|
|
41
|
+
export declare function isTokenExpired(token: unknown): boolean;
|
|
27
42
|
/**
|
|
28
|
-
* Extract the user ID from a JWT payload without verifying
|
|
43
|
+
* Extract the user ID from a JWT payload **without verifying the signature**.
|
|
44
|
+
*
|
|
45
|
+
* The returned value is attacker-controlled. It is safe to use as a
|
|
46
|
+
* diagnostic hint; it must never be used to decide who the caller is, or as
|
|
47
|
+
* a key in anything security-relevant, without verifying the token first.
|
|
29
48
|
*
|
|
30
49
|
* @param token - JWT token string
|
|
31
|
-
* @returns
|
|
50
|
+
* @returns The `sub` claim when it is a non-empty string, otherwise null.
|
|
51
|
+
* A `sub` that is a number, object, or array is reported as null rather
|
|
52
|
+
* than leaking a non-string through a `string | null` signature.
|
|
32
53
|
*/
|
|
33
|
-
export declare function extractUserId(token:
|
|
54
|
+
export declare function extractUserId(token: unknown): string | null;
|
|
34
55
|
/**
|
|
35
56
|
* Generate a CSRF token.
|
|
36
57
|
*
|