@c9up/warden 0.1.3 → 0.1.5

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.
Files changed (185) hide show
  1. package/README.md +57 -0
  2. package/dist/AuthManager.d.ts +98 -0
  3. package/dist/AuthManager.d.ts.map +1 -0
  4. package/dist/AuthManager.js +147 -0
  5. package/dist/AuthManager.js.map +1 -0
  6. package/dist/AuthRateLimiter.d.ts +29 -0
  7. package/dist/AuthRateLimiter.d.ts.map +1 -0
  8. package/dist/AuthRateLimiter.js +88 -0
  9. package/dist/AuthRateLimiter.js.map +1 -0
  10. package/dist/Guard.d.ts +35 -0
  11. package/dist/Guard.d.ts.map +1 -0
  12. package/dist/Guard.js +68 -0
  13. package/dist/Guard.js.map +1 -0
  14. package/dist/RedisBlacklistDriver.d.ts +32 -0
  15. package/dist/RedisBlacklistDriver.d.ts.map +1 -0
  16. package/dist/RedisBlacklistDriver.js +37 -0
  17. package/dist/RedisBlacklistDriver.js.map +1 -0
  18. package/dist/RefreshTokenStore.d.ts +34 -0
  19. package/dist/RefreshTokenStore.d.ts.map +1 -0
  20. package/dist/RefreshTokenStore.js +48 -0
  21. package/dist/RefreshTokenStore.js.map +1 -0
  22. package/dist/ResilientBlacklistDriver.d.ts +35 -0
  23. package/dist/ResilientBlacklistDriver.d.ts.map +1 -0
  24. package/dist/ResilientBlacklistDriver.js +61 -0
  25. package/dist/ResilientBlacklistDriver.js.map +1 -0
  26. package/dist/TokenBlacklist.d.ts +30 -0
  27. package/dist/TokenBlacklist.d.ts.map +1 -0
  28. package/dist/TokenBlacklist.js +50 -0
  29. package/dist/TokenBlacklist.js.map +1 -0
  30. package/dist/WardenProvider.d.ts +19 -0
  31. package/dist/WardenProvider.d.ts.map +1 -0
  32. package/dist/WardenProvider.js +71 -0
  33. package/dist/WardenProvider.js.map +1 -0
  34. package/dist/bouncer/AuthorizationResponse.d.ts +25 -0
  35. package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -0
  36. package/dist/bouncer/AuthorizationResponse.js +31 -0
  37. package/dist/bouncer/AuthorizationResponse.js.map +1 -0
  38. package/dist/bouncer/BasePolicy.d.ts +47 -0
  39. package/dist/bouncer/BasePolicy.d.ts.map +1 -0
  40. package/dist/bouncer/BasePolicy.js +36 -0
  41. package/dist/bouncer/BasePolicy.js.map +1 -0
  42. package/dist/bouncer/Bouncer.d.ts +47 -0
  43. package/dist/bouncer/Bouncer.d.ts.map +1 -0
  44. package/dist/bouncer/Bouncer.js +134 -0
  45. package/dist/bouncer/Bouncer.js.map +1 -0
  46. package/dist/bouncer/PolicyAuthorizer.d.ts +24 -0
  47. package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -0
  48. package/dist/bouncer/PolicyAuthorizer.js +92 -0
  49. package/dist/bouncer/PolicyAuthorizer.js.map +1 -0
  50. package/dist/bouncer/decorators.d.ts +25 -0
  51. package/dist/bouncer/decorators.d.ts.map +1 -0
  52. package/dist/bouncer/decorators.js +36 -0
  53. package/dist/bouncer/decorators.js.map +1 -0
  54. package/dist/bouncer/evaluate.d.ts +36 -0
  55. package/dist/bouncer/evaluate.d.ts.map +1 -0
  56. package/dist/bouncer/evaluate.js +64 -0
  57. package/dist/bouncer/evaluate.js.map +1 -0
  58. package/dist/bouncer/policyContext.d.ts +29 -0
  59. package/dist/bouncer/policyContext.d.ts.map +1 -0
  60. package/dist/bouncer/policyContext.js +36 -0
  61. package/dist/bouncer/policyContext.js.map +1 -0
  62. package/dist/bouncer/types.d.ts +52 -0
  63. package/dist/bouncer/types.d.ts.map +1 -0
  64. package/dist/bouncer/types.js +9 -0
  65. package/dist/bouncer/types.js.map +1 -0
  66. package/dist/config.d.ts +51 -0
  67. package/dist/config.d.ts.map +1 -0
  68. package/dist/config.js +21 -0
  69. package/dist/config.js.map +1 -0
  70. package/dist/configure.d.ts +10 -0
  71. package/dist/configure.d.ts.map +1 -0
  72. package/dist/configure.js +44 -0
  73. package/dist/configure.js.map +1 -0
  74. package/dist/errors.d.ts +18 -0
  75. package/dist/errors.d.ts.map +1 -0
  76. package/dist/errors.js +21 -0
  77. package/dist/errors.js.map +1 -0
  78. package/dist/firstcontact/FirstContactManager.d.ts +25 -0
  79. package/dist/firstcontact/FirstContactManager.d.ts.map +1 -0
  80. package/dist/firstcontact/FirstContactManager.js +38 -0
  81. package/dist/firstcontact/FirstContactManager.js.map +1 -0
  82. package/dist/firstcontact/drivers/GitHubDriver.d.ts +14 -0
  83. package/dist/firstcontact/drivers/GitHubDriver.d.ts.map +1 -0
  84. package/dist/firstcontact/drivers/GitHubDriver.js +61 -0
  85. package/dist/firstcontact/drivers/GitHubDriver.js.map +1 -0
  86. package/dist/firstcontact/drivers/GoogleDriver.d.ts +14 -0
  87. package/dist/firstcontact/drivers/GoogleDriver.d.ts.map +1 -0
  88. package/dist/firstcontact/drivers/GoogleDriver.js +64 -0
  89. package/dist/firstcontact/drivers/GoogleDriver.js.map +1 -0
  90. package/dist/firstcontact/types.d.ts +34 -0
  91. package/dist/firstcontact/types.d.ts.map +1 -0
  92. package/dist/firstcontact/types.js +5 -0
  93. package/dist/firstcontact/types.js.map +1 -0
  94. package/dist/index.d.ts +52 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +34 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/mfa/BackupCodesProvider.d.ts +39 -0
  99. package/dist/mfa/BackupCodesProvider.d.ts.map +1 -0
  100. package/dist/mfa/BackupCodesProvider.js +87 -0
  101. package/dist/mfa/BackupCodesProvider.js.map +1 -0
  102. package/dist/mfa/MfaManager.d.ts +107 -0
  103. package/dist/mfa/MfaManager.d.ts.map +1 -0
  104. package/dist/mfa/MfaManager.js +219 -0
  105. package/dist/mfa/MfaManager.js.map +1 -0
  106. package/dist/mfa/OtpProvider.d.ts +67 -0
  107. package/dist/mfa/OtpProvider.d.ts.map +1 -0
  108. package/dist/mfa/OtpProvider.js +104 -0
  109. package/dist/mfa/OtpProvider.js.map +1 -0
  110. package/dist/mfa/TotpProvider.d.ts +52 -0
  111. package/dist/mfa/TotpProvider.d.ts.map +1 -0
  112. package/dist/mfa/TotpProvider.js +103 -0
  113. package/dist/mfa/TotpProvider.js.map +1 -0
  114. package/dist/mfa/WebauthnProvider.d.ts +175 -0
  115. package/dist/mfa/WebauthnProvider.d.ts.map +1 -0
  116. package/dist/mfa/WebauthnProvider.js +239 -0
  117. package/dist/mfa/WebauthnProvider.js.map +1 -0
  118. package/dist/mfa/base32.d.ts +13 -0
  119. package/dist/mfa/base32.d.ts.map +1 -0
  120. package/dist/mfa/base32.js +52 -0
  121. package/dist/mfa/base32.js.map +1 -0
  122. package/dist/mfa/webauthn-codec.d.ts +58 -0
  123. package/dist/mfa/webauthn-codec.d.ts.map +1 -0
  124. package/dist/mfa/webauthn-codec.js +221 -0
  125. package/dist/mfa/webauthn-codec.js.map +1 -0
  126. package/dist/middleware.d.ts +51 -0
  127. package/dist/middleware.d.ts.map +1 -0
  128. package/dist/middleware.js +213 -0
  129. package/dist/middleware.js.map +1 -0
  130. package/dist/native.d.ts +14 -0
  131. package/dist/native.d.ts.map +1 -0
  132. package/dist/native.js +39 -0
  133. package/dist/native.js.map +1 -0
  134. package/dist/rights/MemoryRightsStore.d.ts +29 -0
  135. package/dist/rights/MemoryRightsStore.d.ts.map +1 -0
  136. package/dist/rights/MemoryRightsStore.js +70 -0
  137. package/dist/rights/MemoryRightsStore.js.map +1 -0
  138. package/dist/rights/RightsResolver.d.ts +29 -0
  139. package/dist/rights/RightsResolver.d.ts.map +1 -0
  140. package/dist/rights/RightsResolver.js +73 -0
  141. package/dist/rights/RightsResolver.js.map +1 -0
  142. package/dist/rights/types.d.ts +55 -0
  143. package/dist/rights/types.d.ts.map +1 -0
  144. package/dist/rights/types.js +18 -0
  145. package/dist/rights/types.js.map +1 -0
  146. package/dist/services/main.d.ts +19 -0
  147. package/dist/services/main.d.ts.map +1 -0
  148. package/dist/services/main.js +32 -0
  149. package/dist/services/main.js.map +1 -0
  150. package/dist/standalone.d.ts +37 -0
  151. package/dist/standalone.d.ts.map +1 -0
  152. package/dist/standalone.js +92 -0
  153. package/dist/standalone.js.map +1 -0
  154. package/dist/strategies/ApiKeyStrategy.d.ts +28 -0
  155. package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -0
  156. package/dist/strategies/ApiKeyStrategy.js +36 -0
  157. package/dist/strategies/ApiKeyStrategy.js.map +1 -0
  158. package/dist/strategies/JwtStrategy.d.ts +51 -0
  159. package/dist/strategies/JwtStrategy.d.ts.map +1 -0
  160. package/dist/strategies/JwtStrategy.js +150 -0
  161. package/dist/strategies/JwtStrategy.js.map +1 -0
  162. package/dist/strategies/SessionStrategy.d.ts +58 -0
  163. package/dist/strategies/SessionStrategy.d.ts.map +1 -0
  164. package/dist/strategies/SessionStrategy.js +70 -0
  165. package/dist/strategies/SessionStrategy.js.map +1 -0
  166. package/index.darwin-arm64.node +0 -0
  167. package/index.darwin-x64.node +0 -0
  168. package/index.linux-arm64-gnu.node +0 -0
  169. package/index.linux-x64-gnu.node +0 -0
  170. package/index.win32-x64-msvc.node +0 -0
  171. package/package.json +1 -1
  172. package/src/Guard.ts +21 -0
  173. package/src/RedisBlacklistDriver.ts +60 -0
  174. package/src/ResilientBlacklistDriver.ts +84 -0
  175. package/src/WardenProvider.ts +11 -0
  176. package/src/config.ts +7 -0
  177. package/src/index.ts +58 -0
  178. package/src/mfa/BackupCodesProvider.ts +125 -0
  179. package/src/mfa/MfaManager.ts +307 -0
  180. package/src/mfa/OtpProvider.ts +177 -0
  181. package/src/mfa/TotpProvider.ts +140 -0
  182. package/src/mfa/WebauthnProvider.ts +416 -0
  183. package/src/mfa/base32.ts +54 -0
  184. package/src/mfa/webauthn-codec.ts +264 -0
  185. package/src/middleware.ts +14 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * JwtStrategy — HMAC-SHA256 JWT authentication strategy.
3
+ * All signing and verification runs through the Rust warden-engine via NAPI.
4
+ */
5
+ import type { AuthResult, AuthStrategy, UserPayload } from "../AuthManager.js";
6
+ import type { TokenBlacklist } from "../TokenBlacklist.js";
7
+ export interface JwtStrategyConfig {
8
+ /**
9
+ * Active signing secret. New tokens are always signed with this key.
10
+ * For zero-downtime key rotation, also pass `previousSecrets` — verify
11
+ * tries each entry until one matches, then `revoke()` the rotated key
12
+ * by removing it from the config on the next deploy.
13
+ */
14
+ secret: string;
15
+ /**
16
+ * Older secrets accepted at verify time but never used to sign new
17
+ * tokens. Empty / omitted by default. Order doesn't matter for
18
+ * correctness but ordering by recency keeps the common path fast.
19
+ */
20
+ previousSecrets?: readonly string[];
21
+ expiresInSeconds?: number;
22
+ findUser: (id: string) => Promise<UserPayload | null>;
23
+ verifyCredentials: (email: string, password: string) => Promise<UserPayload | null>;
24
+ /**
25
+ * Optional blacklist for revocation. When supplied, `verify` rejects
26
+ * tokens whose `jti` is present, and `revoke(token)` adds the token's
27
+ * `jti` to the blacklist for the remainder of its lifetime. Without a
28
+ * blacklist, `revoke()` throws — the call would be a no-op silently.
29
+ */
30
+ blacklist?: TokenBlacklist;
31
+ }
32
+ export declare class JwtStrategy implements AuthStrategy {
33
+ #private;
34
+ name: string;
35
+ constructor(config: JwtStrategyConfig);
36
+ authenticate(credentials: Record<string, unknown>): Promise<AuthResult>;
37
+ verify(token: string): Promise<AuthResult>;
38
+ signToken(user: UserPayload): string;
39
+ /**
40
+ * Revoke a token until its `exp` claim. Requires the strategy to be
41
+ * constructed with a `blacklist` driver — otherwise throws so that a
42
+ * caller assuming revocation does not silently succeed against
43
+ * a no-op implementation.
44
+ *
45
+ * Returns `true` when the token was added to the blacklist, `false` when
46
+ * the token is already expired (revocation is unnecessary) or unparseable.
47
+ */
48
+ revoke(token: string): Promise<boolean>;
49
+ }
50
+ export declare function generateJwtSecret(): string;
51
+ //# sourceMappingURL=JwtStrategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JwtStrategy.d.ts","sourceRoot":"","sources":["../../src/strategies/JwtStrategy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAwC3D,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACtD,iBAAiB,EAAE,CAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,KACZ,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,qBAAa,WAAY,YAAW,YAAY;;IAC/C,IAAI,SAAS;gBAaD,MAAM,EAAE,iBAAiB;IAgC/B,YAAY,CACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,UAAU,CAAC;IAkBhB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAgChD,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAepC;;;;;;;;OAQG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAa7C;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * JwtStrategy — HMAC-SHA256 JWT authentication strategy.
3
+ * All signing and verification runs through the Rust warden-engine via NAPI.
4
+ */
5
+ import { randomBytes, randomUUID } from "node:crypto";
6
+ import { nativeWarden } from "../native.js";
7
+ function sign(payload, secret) {
8
+ const rust = nativeWarden();
9
+ if (!rust) {
10
+ throw new Error("[WARDEN_NAPI_REQUIRED] The Rust warden-engine binary is required. Build it with `cd packages/warden && pnpm build:napi`.");
11
+ }
12
+ return rust.jwtSign(JSON.stringify(payload), secret);
13
+ }
14
+ function verify(token, secret) {
15
+ const rust = nativeWarden();
16
+ if (!rust) {
17
+ throw new Error("[WARDEN_NAPI_REQUIRED] The Rust warden-engine binary is required. Build it with `cd packages/warden && pnpm build:napi`.");
18
+ }
19
+ try {
20
+ const payloadJson = rust.jwtVerify(token, secret);
21
+ const payload = JSON.parse(payloadJson);
22
+ if (typeof payload.sub !== "string" || !payload.sub)
23
+ return null;
24
+ return payload;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ export class JwtStrategy {
31
+ name = "jwt";
32
+ #secret;
33
+ /**
34
+ * Verify-only secrets ordered active-first. Mirrors `config.secret` at
35
+ * index 0 plus any `previousSecrets`. Built once at construction so
36
+ * `verify()` doesn't re-allocate per call.
37
+ */
38
+ #verifySecrets;
39
+ #expiresIn;
40
+ #findUser;
41
+ #verifyCredentials;
42
+ #blacklist;
43
+ constructor(config) {
44
+ if (config.secret.length < 32) {
45
+ throw new Error("JWT secret must be at least 32 characters");
46
+ }
47
+ for (const prev of config.previousSecrets ?? []) {
48
+ if (prev.length < 32) {
49
+ throw new Error("JWT previousSecrets entries must be at least 32 characters");
50
+ }
51
+ }
52
+ this.#secret = config.secret;
53
+ this.#verifySecrets = [config.secret, ...(config.previousSecrets ?? [])];
54
+ this.#expiresIn = config.expiresInSeconds ?? 3600;
55
+ this.#findUser = config.findUser;
56
+ this.#verifyCredentials = config.verifyCredentials;
57
+ this.#blacklist = config.blacklist;
58
+ }
59
+ /**
60
+ * Try each accepted secret in order. First successful verify wins.
61
+ * Returns `null` when no secret accepts the token — same semantics as
62
+ * the single-secret path so callers see "invalid or expired token".
63
+ */
64
+ #verifyWithRotation(token) {
65
+ for (const secret of this.#verifySecrets) {
66
+ const payload = verify(token, secret);
67
+ if (payload)
68
+ return payload;
69
+ }
70
+ return null;
71
+ }
72
+ async authenticate(credentials) {
73
+ const email = typeof credentials.email === "string" ? credentials.email : null;
74
+ const password = typeof credentials.password === "string" ? credentials.password : null;
75
+ if (!email || !password) {
76
+ return { authenticated: false, error: "Email and password are required" };
77
+ }
78
+ const user = await this.#verifyCredentials(email, password);
79
+ if (!user) {
80
+ return { authenticated: false, error: "Invalid credentials" };
81
+ }
82
+ const token = this.signToken(user);
83
+ return { authenticated: true, user: { ...user, token } };
84
+ }
85
+ async verify(token) {
86
+ const payload = this.#verifyWithRotation(token);
87
+ if (!payload) {
88
+ return { authenticated: false, error: "Invalid or expired token" };
89
+ }
90
+ // Blacklist check runs BEFORE findUser so a revoked token never
91
+ // triggers a user lookup (cheaper, and avoids leaking user existence
92
+ // via timing differences between revoked and unknown-user paths).
93
+ if (this.#blacklist) {
94
+ // A token with no `jti` cannot be checked against the blacklist
95
+ // (`isRevoked(undefined)` always returns false), so it could
96
+ // never be revoked. When revocation is configured we must NOT
97
+ // trust such a token — reject it rather than let an
98
+ // unrevocable token through. (signToken always sets a jti;
99
+ // this guards legacy / externally-signed tokens.)
100
+ if (typeof payload.jti !== "string" || payload.jti.length === 0) {
101
+ return { authenticated: false, error: "Token missing jti claim" };
102
+ }
103
+ if (await this.#blacklist.isRevoked(payload.jti)) {
104
+ return { authenticated: false, error: "Token revoked" };
105
+ }
106
+ }
107
+ const user = await this.#findUser(payload.sub);
108
+ if (!user) {
109
+ return { authenticated: false, error: "User not found" };
110
+ }
111
+ return { authenticated: true, user };
112
+ }
113
+ signToken(user) {
114
+ const now = Math.floor(Date.now() / 1000);
115
+ return sign({
116
+ sub: user.id,
117
+ roles: user.roles,
118
+ permissions: user.permissions,
119
+ iat: now,
120
+ exp: now + this.#expiresIn,
121
+ jti: randomUUID(),
122
+ }, this.#secret);
123
+ }
124
+ /**
125
+ * Revoke a token until its `exp` claim. Requires the strategy to be
126
+ * constructed with a `blacklist` driver — otherwise throws so that a
127
+ * caller assuming revocation does not silently succeed against
128
+ * a no-op implementation.
129
+ *
130
+ * Returns `true` when the token was added to the blacklist, `false` when
131
+ * the token is already expired (revocation is unnecessary) or unparseable.
132
+ */
133
+ async revoke(token) {
134
+ if (!this.#blacklist) {
135
+ throw new Error("JwtStrategy.revoke() requires a `blacklist` driver. Pass `{ blacklist: new TokenBlacklist(driver) }` to the constructor.");
136
+ }
137
+ const payload = this.#verifyWithRotation(token);
138
+ if (!payload)
139
+ return false;
140
+ const expMs = payload.exp * 1000;
141
+ if (expMs <= Date.now())
142
+ return false;
143
+ await this.#blacklist.revoke(payload.jti, expMs);
144
+ return true;
145
+ }
146
+ }
147
+ export function generateJwtSecret() {
148
+ return randomBytes(48).toString("base64url");
149
+ }
150
+ //# sourceMappingURL=JwtStrategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JwtStrategy.js","sourceRoot":"","sources":["../../src/strategies/JwtStrategy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Session-based authentication strategy.
3
+ *
4
+ * AdonisJS session guard pattern:
5
+ * login stores userId in session, subsequent requests check session.
6
+ *
7
+ * @implements MISS-7
8
+ */
9
+ import type { AuthResult, AuthStrategy, UserPayload } from "../AuthManager.js";
10
+ export interface SessionStore {
11
+ get(key: string): unknown;
12
+ set(key: string, value: unknown): void;
13
+ forget(key: string): void;
14
+ /**
15
+ * Rotate the session id while preserving the data. REQUIRED to mitigate
16
+ * session fixation (CWE-384) at login. Ream's `Session.regenerate()`
17
+ * implements this; adapters around other session stores MUST too. The
18
+ * default ream → warden plumbing exposes the `Session` instance
19
+ * directly so this is wired automatically.
20
+ */
21
+ regenerate(): void;
22
+ }
23
+ export interface SessionStrategyConfig {
24
+ sessionKey?: string;
25
+ findUser: (id: string | number) => Promise<UserPayload | null>;
26
+ }
27
+ export declare class SessionStrategy implements AuthStrategy {
28
+ #private;
29
+ name: string;
30
+ constructor(config: SessionStrategyConfig);
31
+ /** Authenticate via email/password — stores user ID in session. */
32
+ authenticate(_credentials: {
33
+ email: string;
34
+ password: string;
35
+ }, _context?: {
36
+ session?: SessionStore;
37
+ }): Promise<AuthResult>;
38
+ /** Verify session — check if user ID is stored in session. */
39
+ verify(_token: string): Promise<AuthResult>;
40
+ /** Verify session with context — check if user ID is stored in session. */
41
+ verifyWithContext(_token: string, context?: {
42
+ session?: SessionStore;
43
+ }): Promise<AuthResult>;
44
+ /**
45
+ * Login a user — store their ID in the session.
46
+ *
47
+ * Rotates the session id BEFORE writing the authenticated user id, so an
48
+ * attacker who fed the victim a pre-login session cookie (classic
49
+ * session-fixation attack, CWE-384) ends up holding a now-discarded id
50
+ * while the victim continues under a fresh one. The driver-side cookie
51
+ * migration is handled by Ream's `SessionMiddleware` on the response
52
+ * path — see `wasRegenerated()` there.
53
+ */
54
+ login(user: UserPayload, session: SessionStore): Promise<void>;
55
+ /** Logout — remove user ID from session. */
56
+ logout(session: SessionStore): Promise<void>;
57
+ }
58
+ //# sourceMappingURL=SessionStrategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionStrategy.d.ts","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/E,MAAM,WAAW,YAAY;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;OAMG;IACH,UAAU,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC/D;AAED,qBAAa,eAAgB,YAAW,YAAY;;IACnD,IAAI,SAAa;gBAIL,MAAM,EAAE,qBAAqB;IAKzC,mEAAmE;IAC7D,YAAY,CACjB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EACjD,QAAQ,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,YAAY,CAAA;KAAE,GACnC,OAAO,CAAC,UAAU,CAAC;IAgBtB,8DAA8D;IACxD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAQjD,2EAA2E;IACrE,iBAAiB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,YAAY,CAAA;KAAE,GAClC,OAAO,CAAC,UAAU,CAAC;IAWtB;;;;;;;;;OASG;IACG,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpE,4CAA4C;IACtC,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlD"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Session-based authentication strategy.
3
+ *
4
+ * AdonisJS session guard pattern:
5
+ * login stores userId in session, subsequent requests check session.
6
+ *
7
+ * @implements MISS-7
8
+ */
9
+ import { WardenError } from "../errors.js";
10
+ export class SessionStrategy {
11
+ name = "session";
12
+ #config;
13
+ #sessionKey;
14
+ constructor(config) {
15
+ this.#config = config;
16
+ this.#sessionKey = config.sessionKey ?? "auth_user_id";
17
+ }
18
+ /** Authenticate via email/password — stores user ID in session. */
19
+ async authenticate(_credentials, _context) {
20
+ // Session strategy doesn't handle password verification — that's done
21
+ // by the caller. We throw a typed sentinel WardenError so AuthManager's
22
+ // try/catch (which swallows generic errors into a soft AuthResult)
23
+ // re-throws this one — the design boundary stays unmissable even when
24
+ // a caller routes through `authManager.authenticate()` instead of
25
+ // `strategy.authenticate()` directly.
26
+ throw new WardenError("USE_LOGIN", "SessionStrategy.authenticate() requires login() instead. Use authManager.login(user, session).", {
27
+ hint: "Verify the password yourself (e.g. via @c9up/sigil Hash.verify), then call SessionStrategy.login(user, session).",
28
+ });
29
+ }
30
+ /** Verify session — check if user ID is stored in session. */
31
+ async verify(_token) {
32
+ return {
33
+ authenticated: false,
34
+ error: "SessionStrategy.verify() requires context. Use verifyWithContext().",
35
+ };
36
+ }
37
+ /** Verify session with context — check if user ID is stored in session. */
38
+ async verifyWithContext(_token, context) {
39
+ if (!context?.session)
40
+ return { authenticated: false, error: "No session" };
41
+ const userId = context.session.get(this.#sessionKey);
42
+ if (!userId)
43
+ return { authenticated: false, error: "No session user" };
44
+ if (typeof userId !== "string" && typeof userId !== "number")
45
+ return { authenticated: false, error: "Invalid session data" };
46
+ const user = await this.#config.findUser(userId);
47
+ if (!user)
48
+ return { authenticated: false, error: "User not found" };
49
+ return { authenticated: true, user };
50
+ }
51
+ /**
52
+ * Login a user — store their ID in the session.
53
+ *
54
+ * Rotates the session id BEFORE writing the authenticated user id, so an
55
+ * attacker who fed the victim a pre-login session cookie (classic
56
+ * session-fixation attack, CWE-384) ends up holding a now-discarded id
57
+ * while the victim continues under a fresh one. The driver-side cookie
58
+ * migration is handled by Ream's `SessionMiddleware` on the response
59
+ * path — see `wasRegenerated()` there.
60
+ */
61
+ async login(user, session) {
62
+ session.regenerate();
63
+ session.set(this.#sessionKey, user.id);
64
+ }
65
+ /** Logout — remove user ID from session. */
66
+ async logout(session) {
67
+ session.forget(this.#sessionKey);
68
+ }
69
+ }
70
+ //# sourceMappingURL=SessionStrategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionStrategy.js","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAqB3C,MAAM,OAAO,eAAe;IAC3B,IAAI,GAAG,SAAS,CAAC;IACjB,OAAO,CAAwB;IAC/B,WAAW,CAAS;IAEpB,YAAY,MAA6B;QACxC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,UAAU,IAAI,cAAc,CAAC;IACxD,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,YAAY,CACjB,YAAiD,EACjD,QAAqC;QAErC,sEAAsE;QACtE,wEAAwE;QACxE,mEAAmE;QACnE,sEAAsE;QACtE,kEAAkE;QAClE,sCAAsC;QACtC,MAAM,IAAI,WAAW,CACpB,WAAW,EACX,gGAAgG,EAChG;YACC,IAAI,EAAE,kHAAkH;SACxH,CACD,CAAC;IACH,CAAC;IAED,8DAA8D;IAC9D,KAAK,CAAC,MAAM,CAAC,MAAc;QAC1B,OAAO;YACN,aAAa,EAAE,KAAK;YACpB,KAAK,EACJ,qEAAqE;SACtE,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,iBAAiB,CACtB,MAAc,EACd,OAAoC;QAEpC,IAAI,CAAC,OAAO,EAAE,OAAO;YAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;QACvE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ;YAC3D,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;QAChE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QACpE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACtC,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,KAAK,CAAC,IAAiB,EAAE,OAAqB;QACnD,OAAO,CAAC,UAAU,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,4CAA4C;IAC5C,KAAK,CAAC,MAAM,CAAC,OAAqB;QACjC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;CACD"}
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/warden",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Warden — Authentication for the Ream framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/Guard.ts CHANGED
@@ -13,6 +13,7 @@ import "reflect-metadata";
13
13
  const GUARD_KEY = Symbol.for("warden:guard");
14
14
  const PERMISSION_KEY = Symbol.for("warden:permission");
15
15
  const ROLE_KEY = Symbol.for("warden:role");
16
+ const MFA_KEY = Symbol.for("warden:mfa");
16
17
 
17
18
  /**
18
19
  * @Guard('jwt') — require authentication via the named strategy.
@@ -66,3 +67,23 @@ export function getRoleMetadata(
66
67
  ): string[] {
67
68
  return Reflect.getOwnMetadata(ROLE_KEY, target, propertyKey) ?? [];
68
69
  }
70
+
71
+ /**
72
+ * @RequireMfa() — require the authenticated user to have completed multi-factor
73
+ * authentication. The auth middleware rejects with 403 `MFA_REQUIRED` when the
74
+ * user payload lacks a truthy `mfa` claim (set by your MFA step-up flow once
75
+ * `MfaManager.verify()` succeeds).
76
+ */
77
+ export function RequireMfa(): MethodDecorator {
78
+ return (target, propertyKey) => {
79
+ Reflect.defineMetadata(MFA_KEY, true, target, propertyKey);
80
+ };
81
+ }
82
+
83
+ /** Whether the route requires completed MFA. */
84
+ export function getRequireMfaMetadata(
85
+ target: object,
86
+ propertyKey: string | symbol,
87
+ ): boolean {
88
+ return Reflect.getOwnMetadata(MFA_KEY, target, propertyKey) ?? false;
89
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * RedisBlacklistDriver — a persistent JWT blacklist backed by Redis / KeyDB,
3
+ * so logout revocation survives a process restart (unlike MemoryBlacklistDriver).
4
+ *
5
+ * Agnostic by construction: it depends on no Redis package. The caller injects
6
+ * any ioredis-compatible client (KeyDB, Redis, a mock…) that exposes `set` with
7
+ * an `"EX"` ttl and `exists`. Native key expiry does the housekeeping, so
8
+ * `cleanup()` is a no-op.
9
+ */
10
+
11
+ import type { BlacklistDriver } from "./TokenBlacklist.js";
12
+
13
+ /** The minimal ioredis-shaped surface this driver needs. */
14
+ export interface RedisLikeClient {
15
+ set(
16
+ key: string,
17
+ value: string,
18
+ mode: "EX",
19
+ ttlSeconds: number,
20
+ ): Promise<unknown>;
21
+ exists(key: string): Promise<number>;
22
+ }
23
+
24
+ export interface RedisBlacklistConfig {
25
+ /** Key prefix for blacklist entries. Default `warden:blacklist:`. */
26
+ prefix?: string;
27
+ }
28
+
29
+ export class RedisBlacklistDriver implements BlacklistDriver {
30
+ readonly #client: RedisLikeClient;
31
+ readonly #prefix: string;
32
+
33
+ constructor(client: RedisLikeClient, config: RedisBlacklistConfig = {}) {
34
+ this.#client = client;
35
+ this.#prefix = config.prefix ?? "warden:blacklist:";
36
+ }
37
+
38
+ /**
39
+ * Blacklist a JTI until its natural expiry. Tokens already past `expiresAt`
40
+ * are skipped — there is nothing left to revoke.
41
+ */
42
+ async add(jti: string, expiresAt: number): Promise<void> {
43
+ const ttlSeconds = Math.ceil((expiresAt - Date.now()) / 1000);
44
+ if (ttlSeconds <= 0) {
45
+ return;
46
+ }
47
+ await this.#client.set(this.#key(jti), "1", "EX", ttlSeconds);
48
+ }
49
+
50
+ async has(jti: string): Promise<boolean> {
51
+ return (await this.#client.exists(this.#key(jti))) > 0;
52
+ }
53
+
54
+ /** No-op: Redis/KeyDB evicts expired keys on its own. */
55
+ async cleanup(): Promise<void> {}
56
+
57
+ #key(jti: string): string {
58
+ return `${this.#prefix}${jti}`;
59
+ }
60
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ResilientBlacklistDriver — wraps a primary blacklist (e.g. KeyDB-backed
3
+ * `RedisBlacklistDriver`) with an in-memory fallback so a cache outage never
4
+ * takes the whole app down.
5
+ *
6
+ * Policy (fail-open on reads):
7
+ * - `add()` — if the primary throws, the revocation is recorded in the
8
+ * fallback so logout stays effective on this instance.
9
+ * - `has()` — if the primary throws, consult the fallback: a token revoked
10
+ * during the outage is still caught here; anything else is
11
+ * allowed through (fail-open). Access tokens are short-lived, so
12
+ * the window a missed revocation survives is bounded by the token
13
+ * TTL — the blacklist is defense-in-depth above expiry, not the
14
+ * primary boundary.
15
+ *
16
+ * Every degradation invokes `onDegrade` (defaults to a stderr warning) so the
17
+ * outage is observable.
18
+ */
19
+
20
+ import {
21
+ type BlacklistDriver,
22
+ MemoryBlacklistDriver,
23
+ } from "./TokenBlacklist.js";
24
+
25
+ export interface BlacklistDegradeEvent {
26
+ op: "add" | "has" | "cleanup";
27
+ error: unknown;
28
+ }
29
+
30
+ export interface ResilientBlacklistConfig {
31
+ /** Invoked whenever the primary driver fails and the fallback is used. */
32
+ onDegrade?: (event: BlacklistDegradeEvent) => void;
33
+ }
34
+
35
+ function defaultOnDegrade({ op, error }: BlacklistDegradeEvent): void {
36
+ console.warn(
37
+ `[warden] blacklist primary failed on ${op}; using in-memory fallback:`,
38
+ error,
39
+ );
40
+ }
41
+
42
+ export class ResilientBlacklistDriver implements BlacklistDriver {
43
+ readonly #primary: BlacklistDriver;
44
+ readonly #fallback: BlacklistDriver;
45
+ readonly #onDegrade: (event: BlacklistDegradeEvent) => void;
46
+
47
+ constructor(
48
+ primary: BlacklistDriver,
49
+ fallback: BlacklistDriver = new MemoryBlacklistDriver(),
50
+ config: ResilientBlacklistConfig = {},
51
+ ) {
52
+ this.#primary = primary;
53
+ this.#fallback = fallback;
54
+ this.#onDegrade = config.onDegrade ?? defaultOnDegrade;
55
+ }
56
+
57
+ async add(jti: string, expiresAt: number): Promise<void> {
58
+ try {
59
+ await this.#primary.add(jti, expiresAt);
60
+ } catch (error) {
61
+ this.#onDegrade({ op: "add", error });
62
+ await this.#fallback.add(jti, expiresAt);
63
+ }
64
+ }
65
+
66
+ async has(jti: string): Promise<boolean> {
67
+ try {
68
+ return await this.#primary.has(jti);
69
+ } catch (error) {
70
+ this.#onDegrade({ op: "has", error });
71
+ // Fail-open: honour in-outage revocations on this instance, allow rest.
72
+ return this.#fallback.has(jti);
73
+ }
74
+ }
75
+
76
+ async cleanup(): Promise<void> {
77
+ try {
78
+ await this.#primary.cleanup();
79
+ } catch (error) {
80
+ this.#onDegrade({ op: "cleanup", error });
81
+ }
82
+ await this.#fallback.cleanup();
83
+ }
84
+ }
@@ -1,6 +1,7 @@
1
1
  import { AuthManager } from "./AuthManager.js";
2
2
  import type { WardenConfig } from "./config.js";
3
3
  import { WardenError } from "./errors.js";
4
+ import { MfaManager } from "./mfa/MfaManager.js";
4
5
  import { MemoryRightsStore } from "./rights/MemoryRightsStore.js";
5
6
  import { RightsResolver } from "./rights/RightsResolver.js";
6
7
  import { setAuth } from "./services/main.js";
@@ -76,6 +77,16 @@ export default class WardenProvider {
76
77
  this.app.container.singleton("auth", () =>
77
78
  this.app.container.resolve(AuthManager),
78
79
  );
80
+
81
+ // MFA (optional): the app builds an MfaManager with its persistent stores
82
+ // + providers and passes it via config.mfa. Registered by class and under
83
+ // the "mfa" string alias (same convention as "auth") so controllers and
84
+ // agnostic consumers can resolve it.
85
+ const mfa = config.mfa?.manager;
86
+ if (mfa) {
87
+ this.app.container.singleton(MfaManager, () => mfa);
88
+ this.app.container.singleton("mfa", () => mfa);
89
+ }
79
90
  }
80
91
 
81
92
  async boot() {
package/src/config.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { UserPayload } from "./AuthManager.js";
19
+ import type { MfaManager } from "./mfa/MfaManager.js";
19
20
  import type { RightsStore } from "./rights/types.js";
20
21
 
21
22
  export interface JwtConfig {
@@ -40,6 +41,12 @@ export interface WardenConfig {
40
41
  * in-memory shipped).
41
42
  */
42
43
  rights?: { store?: RightsStore };
44
+ /**
45
+ * Multi-factor authentication. Supply a configured `MfaManager` (built with
46
+ * your persistent stores + providers) to register it in the container as
47
+ * `MfaManager` / `"mfa"`. Required to use `@RequireMfa` step-up flows.
48
+ */
49
+ mfa?: { manager: MfaManager };
43
50
  }
44
51
 
45
52
  /** Typed config helper — identity function for editor inference. */