@c9up/warden 0.1.13 → 0.1.14
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/dist/AuthManager.d.ts +45 -9
- package/dist/AuthManager.d.ts.map +1 -1
- package/dist/AuthManager.js +75 -23
- package/dist/AuthManager.js.map +1 -1
- package/dist/Authenticator.d.ts +128 -0
- package/dist/Authenticator.d.ts.map +1 -0
- package/dist/Authenticator.js +296 -0
- package/dist/Authenticator.js.map +1 -0
- package/dist/WardenProvider.d.ts.map +1 -1
- package/dist/WardenProvider.js +51 -23
- package/dist/WardenProvider.js.map +1 -1
- package/dist/bouncer/AbilitiesBuilder.d.ts +20 -0
- package/dist/bouncer/AbilitiesBuilder.d.ts.map +1 -0
- package/dist/bouncer/AbilitiesBuilder.js +32 -0
- package/dist/bouncer/AbilitiesBuilder.js.map +1 -0
- package/dist/bouncer/AuthorizationResponse.d.ts +18 -8
- package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -1
- package/dist/bouncer/AuthorizationResponse.js +20 -8
- package/dist/bouncer/AuthorizationResponse.js.map +1 -1
- package/dist/bouncer/BasePolicy.d.ts +4 -3
- package/dist/bouncer/BasePolicy.d.ts.map +1 -1
- package/dist/bouncer/BasePolicy.js.map +1 -1
- package/dist/bouncer/Bouncer.d.ts +9 -2
- package/dist/bouncer/Bouncer.d.ts.map +1 -1
- package/dist/bouncer/Bouncer.js +59 -10
- package/dist/bouncer/Bouncer.js.map +1 -1
- package/dist/bouncer/PolicyAuthorizer.d.ts +2 -1
- package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
- package/dist/bouncer/PolicyAuthorizer.js +11 -3
- package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
- package/dist/bouncer/evaluate.d.ts +1 -1
- package/dist/bouncer/evaluate.d.ts.map +1 -1
- package/dist/bouncer/evaluate.js +3 -2
- package/dist/bouncer/evaluate.js.map +1 -1
- package/dist/bouncer/types.d.ts +22 -1
- package/dist/bouncer/types.d.ts.map +1 -1
- package/dist/config.d.ts +34 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +15 -13
- package/dist/configure.js.map +1 -1
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +37 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/mfa/OtpProvider.d.ts.map +1 -1
- package/dist/mfa/OtpProvider.js +21 -3
- package/dist/mfa/OtpProvider.js.map +1 -1
- package/dist/mfa/WebauthnProvider.d.ts +24 -2
- package/dist/mfa/WebauthnProvider.d.ts.map +1 -1
- package/dist/mfa/WebauthnProvider.js +28 -8
- package/dist/mfa/WebauthnProvider.js.map +1 -1
- package/dist/middleware.d.ts +29 -6
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +85 -166
- package/dist/middleware.js.map +1 -1
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +21 -13
- package/dist/standalone.js.map +1 -1
- package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -1
- package/dist/strategies/ApiKeyStrategy.js +4 -1
- package/dist/strategies/ApiKeyStrategy.js.map +1 -1
- package/dist/strategies/SessionStrategy.js +2 -2
- package/dist/strategies/SessionStrategy.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -1
- package/src/AuthManager.ts +125 -28
- package/src/Authenticator.ts +385 -0
- package/src/WardenProvider.ts +51 -25
- package/src/bouncer/AbilitiesBuilder.ts +41 -0
- package/src/bouncer/AuthorizationResponse.ts +24 -16
- package/src/bouncer/BasePolicy.ts +4 -2
- package/src/bouncer/Bouncer.ts +77 -14
- package/src/bouncer/PolicyAuthorizer.ts +14 -4
- package/src/bouncer/evaluate.ts +4 -2
- package/src/bouncer/types.ts +24 -1
- package/src/config.ts +47 -3
- package/src/configure.ts +15 -13
- package/src/errors.ts +43 -1
- package/src/index.ts +22 -2
- package/src/mfa/OtpProvider.ts +31 -3
- package/src/mfa/WebauthnProvider.ts +58 -12
- package/src/middleware.ts +112 -207
- package/src/standalone.ts +22 -14
- package/src/strategies/ApiKeyStrategy.ts +4 -1
- package/src/strategies/SessionStrategy.ts +2 -2
package/src/errors.ts
CHANGED
|
@@ -18,8 +18,50 @@ export class WardenError extends Error {
|
|
|
18
18
|
) {
|
|
19
19
|
super(message);
|
|
20
20
|
this.name = "WardenError";
|
|
21
|
-
|
|
21
|
+
// Warden's own codes carry the `WARDEN_` namespace. The AdonisJS auth
|
|
22
|
+
// identifiers (`E_UNAUTHORIZED_ACCESS`, `E_INVALID_CREDENTIALS`) are
|
|
23
|
+
// PRESERVED verbatim so the exposed `.code` matches Adonis exactly — a
|
|
24
|
+
// consumer branching on `error.code === 'E_UNAUTHORIZED_ACCESS'` (or a
|
|
25
|
+
// framework mapping the Adonis id) sees the same string it would under
|
|
26
|
+
// `@adonisjs/auth`. Documented deviation from the blanket prefix.
|
|
27
|
+
this.code = code.startsWith("E_") ? code : `WARDEN_${code}`;
|
|
22
28
|
this.hint = options?.hint;
|
|
23
29
|
this.status = options?.status;
|
|
24
30
|
}
|
|
25
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `E_UNAUTHORIZED_ACCESS` — raised when an incoming request cannot be
|
|
35
|
+
* authenticated (AdonisJS `@adonisjs/auth` parity). Carries `guardDriverName`
|
|
36
|
+
* (the guard that rejected) and, for session-guard flows, an optional
|
|
37
|
+
* `redirectTo` (the login route an HTML client is redirected to). Status 401.
|
|
38
|
+
*
|
|
39
|
+
* Content negotiation (redirect for `Accept: text/html` + session guard, else
|
|
40
|
+
* 401 JSON) is performed by Warden itself — see `renderAuthError` in
|
|
41
|
+
* `middleware.ts` — so the package stays agnostic of the host's exception
|
|
42
|
+
* handler.
|
|
43
|
+
*/
|
|
44
|
+
export class E_UNAUTHORIZED_ACCESS extends WardenError {
|
|
45
|
+
readonly redirectTo?: string;
|
|
46
|
+
readonly guardDriverName: string;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
message: string,
|
|
50
|
+
options: { guardDriverName: string; redirectTo?: string },
|
|
51
|
+
) {
|
|
52
|
+
super("E_UNAUTHORIZED_ACCESS", message, { status: 401 });
|
|
53
|
+
this.guardDriverName = options.guardDriverName;
|
|
54
|
+
this.redirectTo = options.redirectTo;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `E_INVALID_CREDENTIALS` — raised when a credential check (email/password)
|
|
60
|
+
* fails (AdonisJS parity). Status 400. Distinct from `E_UNAUTHORIZED_ACCESS`
|
|
61
|
+
* (a missing/invalid session or token on a protected route → 401).
|
|
62
|
+
*/
|
|
63
|
+
export class E_INVALID_CREDENTIALS extends WardenError {
|
|
64
|
+
constructor(message = "Invalid user credentials") {
|
|
65
|
+
super("E_INVALID_CREDENTIALS", message, { status: 400 });
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* @implements FR48, FR49, FR50, FR51, FR52, FR53
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export type { AuthAttempt, ExtractedCredentials } from "./Authenticator.js";
|
|
8
|
+
export {
|
|
9
|
+
API_KEY_GUARD_NAMES,
|
|
10
|
+
Authenticator,
|
|
11
|
+
extractCredentials,
|
|
12
|
+
GuardAccessor,
|
|
13
|
+
} from "./Authenticator.js";
|
|
7
14
|
export type {
|
|
8
15
|
AuthConfig,
|
|
9
16
|
AuthResult,
|
|
@@ -13,6 +20,7 @@ export type {
|
|
|
13
20
|
export { AuthManager } from "./AuthManager.js";
|
|
14
21
|
export type { AuthRateLimiterConfig } from "./AuthRateLimiter.js";
|
|
15
22
|
export { AuthRateLimiter } from "./AuthRateLimiter.js";
|
|
23
|
+
export { AbilitiesBuilder } from "./bouncer/AbilitiesBuilder.js";
|
|
16
24
|
export { AuthorizationResponse } from "./bouncer/AuthorizationResponse.js";
|
|
17
25
|
export { BasePolicy } from "./bouncer/BasePolicy.js";
|
|
18
26
|
export { Bouncer } from "./bouncer/Bouncer.js";
|
|
@@ -27,10 +35,22 @@ export type {
|
|
|
27
35
|
AbilityOptions,
|
|
28
36
|
AuthorizerResponse,
|
|
29
37
|
BouncerContext,
|
|
38
|
+
BouncerEmitter,
|
|
39
|
+
PolicyContainerResolver,
|
|
30
40
|
} from "./bouncer/types.js";
|
|
31
|
-
export {
|
|
41
|
+
export type { GuardFactory, WardenConfig } from "./config.js";
|
|
42
|
+
export {
|
|
43
|
+
apiKeyGuard,
|
|
44
|
+
defineConfig,
|
|
45
|
+
jwtGuard,
|
|
46
|
+
sessionGuard,
|
|
47
|
+
} from "./config.js";
|
|
32
48
|
export { configure } from "./configure.js";
|
|
33
|
-
export {
|
|
49
|
+
export {
|
|
50
|
+
E_INVALID_CREDENTIALS,
|
|
51
|
+
E_UNAUTHORIZED_ACCESS,
|
|
52
|
+
WardenError,
|
|
53
|
+
} from "./errors.js";
|
|
34
54
|
export { GitHubDriver } from "./firstcontact/drivers/GitHubDriver.js";
|
|
35
55
|
export { GoogleDriver } from "./firstcontact/drivers/GoogleDriver.js";
|
|
36
56
|
export { FirstContactManager } from "./firstcontact/FirstContactManager.js";
|
package/src/mfa/OtpProvider.ts
CHANGED
|
@@ -96,9 +96,37 @@ export class OtpProvider {
|
|
|
96
96
|
}
|
|
97
97
|
this.#channel = config.channel;
|
|
98
98
|
this.#store = config.store ?? new MemoryOtpChallengeStore();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
|
|
100
|
+
// Validate the numeric config up-front (mirrors TotpProvider's digit
|
|
101
|
+
// guard) so a misconfig fails loudly at construction instead of minting
|
|
102
|
+
// unusable codes: `digits` bounds the code length (also keeps `10**digits`
|
|
103
|
+
// within `randomInt`'s safe range); a non-positive `ttlSeconds` would make
|
|
104
|
+
// every code born-expired; a non-positive `maxAttempts` would burn the
|
|
105
|
+
// challenge on the first guess.
|
|
106
|
+
const digits = config.digits ?? 6;
|
|
107
|
+
if (!Number.isInteger(digits) || digits < 4 || digits > 10) {
|
|
108
|
+
throw new WardenError(
|
|
109
|
+
"INVALID_CONFIG",
|
|
110
|
+
`OTP digits must be an integer 4-10, got ${digits}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const ttlSeconds = config.ttlSeconds ?? 300;
|
|
114
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1) {
|
|
115
|
+
throw new WardenError(
|
|
116
|
+
"INVALID_CONFIG",
|
|
117
|
+
`OTP ttlSeconds must be a positive integer, got ${ttlSeconds}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const maxAttempts = config.maxAttempts ?? 5;
|
|
121
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
122
|
+
throw new WardenError(
|
|
123
|
+
"INVALID_CONFIG",
|
|
124
|
+
`OTP maxAttempts must be a positive integer, got ${maxAttempts}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
this.#digits = digits;
|
|
128
|
+
this.#ttlMs = ttlSeconds * 1000;
|
|
129
|
+
this.#maxAttempts = maxAttempts;
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
/** Mint a code, persist the challenge, and deliver it to `recipient`. */
|
|
@@ -81,10 +81,21 @@ export interface AuthenticationOptionsJSON {
|
|
|
81
81
|
}>;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Transient per-ceremony challenge storage.
|
|
86
|
+
*
|
|
87
|
+
* Implementations MUST be **time-bound**: a challenge that is never consumed has
|
|
88
|
+
* to expire server-side (single-use alone is not enough — an unconsumed
|
|
89
|
+
* challenge left live indefinitely widens the replay/relay window). A DB/Redis
|
|
90
|
+
* store should use a TTL column / `EXPIRE`; the in-memory default stamps an
|
|
91
|
+
* `expiresAt` and refuses an expired challenge in `take`.
|
|
92
|
+
*/
|
|
85
93
|
export interface WebauthnChallengeStore {
|
|
86
94
|
save(state: string, challenge: string): Promise<void>;
|
|
87
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Return the challenge for `state` and remove it (single-use). MUST return
|
|
97
|
+
* `null` if the stored challenge has passed its TTL.
|
|
98
|
+
*/
|
|
88
99
|
take(state: string): Promise<string | null>;
|
|
89
100
|
}
|
|
90
101
|
|
|
@@ -109,14 +120,24 @@ export interface WebauthnCredentialStore {
|
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
export class MemoryWebauthnChallengeStore implements WebauthnChallengeStore {
|
|
112
|
-
#store = new Map<string, string>();
|
|
123
|
+
#store = new Map<string, { challenge: string; expiresAt: number }>();
|
|
124
|
+
readonly #ttlMs: number;
|
|
125
|
+
|
|
126
|
+
/** @param ttlMs Server-side challenge lifetime. Default 5 min. */
|
|
127
|
+
constructor(ttlMs = 300_000) {
|
|
128
|
+
this.#ttlMs = ttlMs;
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
async save(state: string, challenge: string): Promise<void> {
|
|
114
|
-
this.#store.set(state, challenge);
|
|
132
|
+
this.#store.set(state, { challenge, expiresAt: Date.now() + this.#ttlMs });
|
|
115
133
|
}
|
|
116
134
|
async take(state: string): Promise<string | null> {
|
|
117
|
-
const
|
|
118
|
-
this.#store.delete(state);
|
|
119
|
-
|
|
135
|
+
const entry = this.#store.get(state);
|
|
136
|
+
this.#store.delete(state); // single-use regardless of freshness
|
|
137
|
+
if (!entry || entry.expiresAt < Date.now()) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return entry.challenge;
|
|
120
141
|
}
|
|
121
142
|
}
|
|
122
143
|
|
|
@@ -139,6 +160,12 @@ export class MemoryWebauthnCredentialStore implements WebauthnCredentialStore {
|
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
/** WebAuthn user-verification requirement (PIN / biometric). */
|
|
164
|
+
export type UserVerificationRequirement =
|
|
165
|
+
| "required"
|
|
166
|
+
| "preferred"
|
|
167
|
+
| "discouraged";
|
|
168
|
+
|
|
142
169
|
export interface WebauthnConfig {
|
|
143
170
|
/** Human-readable relying-party name (your app). */
|
|
144
171
|
rpName: string;
|
|
@@ -155,6 +182,13 @@ export interface WebauthnConfig {
|
|
|
155
182
|
* Default ES256, RS256, EdDSA.
|
|
156
183
|
*/
|
|
157
184
|
supportedAlgorithms?: number[];
|
|
185
|
+
/**
|
|
186
|
+
* User-verification requirement. Default `"preferred"`. Set `"required"`
|
|
187
|
+
* for strong MFA / sensitive passkeys — the ceremony then requests UV AND
|
|
188
|
+
* the server rejects an assertion whose authenticator-data UV flag is unset
|
|
189
|
+
* (a mere user-presence touch no longer satisfies the check).
|
|
190
|
+
*/
|
|
191
|
+
userVerification?: UserVerificationRequirement;
|
|
158
192
|
}
|
|
159
193
|
|
|
160
194
|
export interface WebauthnUser {
|
|
@@ -172,6 +206,7 @@ export class WebauthnProvider {
|
|
|
172
206
|
readonly #origins: string[];
|
|
173
207
|
readonly #timeout: number;
|
|
174
208
|
readonly #algorithms: number[];
|
|
209
|
+
readonly #userVerification: UserVerificationRequirement;
|
|
175
210
|
readonly #challenges: WebauthnChallengeStore;
|
|
176
211
|
readonly #credentials: WebauthnCredentialStore;
|
|
177
212
|
|
|
@@ -189,6 +224,7 @@ export class WebauthnProvider {
|
|
|
189
224
|
: [config.origin];
|
|
190
225
|
this.#timeout = config.timeout ?? 60_000;
|
|
191
226
|
this.#algorithms = config.supportedAlgorithms ?? DEFAULT_ALGS;
|
|
227
|
+
this.#userVerification = config.userVerification ?? "preferred";
|
|
192
228
|
this.#challenges =
|
|
193
229
|
config.challengeStore ?? new MemoryWebauthnChallengeStore();
|
|
194
230
|
this.#credentials =
|
|
@@ -226,7 +262,7 @@ export class WebauthnProvider {
|
|
|
226
262
|
})),
|
|
227
263
|
authenticatorSelection: {
|
|
228
264
|
residentKey: "preferred",
|
|
229
|
-
userVerification:
|
|
265
|
+
userVerification: this.#userVerification,
|
|
230
266
|
},
|
|
231
267
|
};
|
|
232
268
|
const state = randomBytes(16).toString("hex");
|
|
@@ -302,7 +338,7 @@ export class WebauthnProvider {
|
|
|
302
338
|
challenge,
|
|
303
339
|
timeout: this.#timeout,
|
|
304
340
|
rpId: this.#rpID,
|
|
305
|
-
userVerification:
|
|
341
|
+
userVerification: this.#userVerification,
|
|
306
342
|
allowCredentials: userId
|
|
307
343
|
? allow.map((c) => ({
|
|
308
344
|
id: c.id,
|
|
@@ -397,13 +433,23 @@ export class WebauthnProvider {
|
|
|
397
433
|
);
|
|
398
434
|
}
|
|
399
435
|
|
|
400
|
-
/**
|
|
436
|
+
/**
|
|
437
|
+
* Validate authenticator data: rpIdHash match, mandatory user-presence, and
|
|
438
|
+
* — when `userVerification: "required"` is configured — the user-verification
|
|
439
|
+
* flag (PIN / biometric actually performed, not just a presence touch).
|
|
440
|
+
*/
|
|
401
441
|
#validAuthenticator(authData: {
|
|
402
442
|
rpIdHash: Buffer;
|
|
403
|
-
flags: { up: boolean };
|
|
443
|
+
flags: { up: boolean; uv: boolean };
|
|
404
444
|
}): boolean {
|
|
405
445
|
const expectedRpIdHash = sha256(Buffer.from(this.#rpID, "utf8"));
|
|
406
|
-
|
|
446
|
+
if (!authData.flags.up || !expectedRpIdHash.equals(authData.rpIdHash)) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
if (this.#userVerification === "required" && !authData.flags.uv) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
407
453
|
}
|
|
408
454
|
}
|
|
409
455
|
|