@c9up/warden 0.1.18 → 0.1.19

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 (55) hide show
  1. package/dist/AuthManager.d.ts +46 -1
  2. package/dist/AuthManager.d.ts.map +1 -1
  3. package/dist/AuthManager.js +71 -10
  4. package/dist/AuthManager.js.map +1 -1
  5. package/dist/Authenticator.d.ts +13 -1
  6. package/dist/Authenticator.d.ts.map +1 -1
  7. package/dist/Authenticator.js +33 -15
  8. package/dist/Authenticator.js.map +1 -1
  9. package/dist/RememberMeToken.d.ts +70 -0
  10. package/dist/RememberMeToken.d.ts.map +1 -1
  11. package/dist/RememberMeToken.js +102 -0
  12. package/dist/RememberMeToken.js.map +1 -1
  13. package/dist/Secret.d.ts +11 -0
  14. package/dist/Secret.d.ts.map +1 -0
  15. package/dist/Secret.js +36 -0
  16. package/dist/Secret.js.map +1 -0
  17. package/dist/bouncer/Bouncer.d.ts +14 -0
  18. package/dist/bouncer/Bouncer.d.ts.map +1 -1
  19. package/dist/bouncer/Bouncer.js +23 -4
  20. package/dist/bouncer/Bouncer.js.map +1 -1
  21. package/dist/bouncer/PolicyAuthorizer.d.ts +9 -1
  22. package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
  23. package/dist/bouncer/PolicyAuthorizer.js +17 -1
  24. package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
  25. package/dist/bouncer/evaluate.d.ts +14 -2
  26. package/dist/bouncer/evaluate.d.ts.map +1 -1
  27. package/dist/bouncer/evaluate.js +16 -5
  28. package/dist/bouncer/evaluate.js.map +1 -1
  29. package/dist/bouncer/types.d.ts +4 -2
  30. package/dist/bouncer/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/sanitize.d.ts +10 -0
  36. package/dist/sanitize.d.ts.map +1 -0
  37. package/dist/sanitize.js +15 -0
  38. package/dist/sanitize.js.map +1 -0
  39. package/dist/strategies/SessionStrategy.d.ts +26 -0
  40. package/dist/strategies/SessionStrategy.d.ts.map +1 -1
  41. package/dist/strategies/SessionStrategy.js +49 -0
  42. package/dist/strategies/SessionStrategy.js.map +1 -1
  43. package/index.win32-x64-msvc.node +0 -0
  44. package/package.json +1 -1
  45. package/src/AuthManager.ts +86 -10
  46. package/src/Authenticator.ts +49 -20
  47. package/src/RememberMeToken.ts +134 -0
  48. package/src/Secret.ts +41 -0
  49. package/src/bouncer/Bouncer.ts +32 -3
  50. package/src/bouncer/PolicyAuthorizer.ts +20 -1
  51. package/src/bouncer/evaluate.ts +26 -6
  52. package/src/bouncer/types.ts +4 -2
  53. package/src/index.ts +7 -1
  54. package/src/sanitize.ts +17 -0
  55. package/src/strategies/SessionStrategy.ts +54 -0
@@ -1 +1 @@
1
- {"version":3,"file":"SessionStrategy.d.ts","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,WAAW,EACX,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAGN,KAAK,qBAAqB,EAE1B,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,YAAY;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,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;IAC/D;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAKD,qBAAa,eAAgB,YAAW,YAAY;;IACnD,IAAI,SAAa;gBAIL,MAAM,EAAE,qBAAqB;IAKzC,mDAAmD;IACnD,IAAI,oBAAoB,IAAI,MAAM,CAEjC;IAED,mDAAmD;IACnD,IAAI,oBAAoB,IAAI,OAAO,CAElC;IAMD;;;OAGG;IACG,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQrE;;;;;;;OAOG;IACG,8BAA8B,CACnC,WAAW,EAAE,OAAO,GAClB,OAAO,CAAC;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAiB7D;;;OAGG;IACG,qBAAqB,CAAC,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE,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;;;;;;OAMG;IACH,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,kBAAkB;IAI3D;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlD"}
1
+ {"version":3,"file":"SessionStrategy.d.ts","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,WAAW,EACX,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAGN,KAAK,qBAAqB,EAE1B,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,YAAY;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,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;IAC/D;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAKD,qBAAa,eAAgB,YAAW,YAAY;;IACnD,IAAI,SAAa;gBAOL,MAAM,EAAE,qBAAqB;IAKzC,mDAAmD;IACnD,IAAI,oBAAoB,IAAI,MAAM,CAEjC;IAED,mDAAmD;IACnD,IAAI,oBAAoB,IAAI,OAAO,CAElC;IAED;;;;;;;;OAQG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;OAGG;IACH,IAAI,oBAAoB,IAAI,OAAO,CAElC;IAED;;;;;OAKG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,+EAA+E;IAC/E,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,2FAA2F;IAC3F,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAMD;;;OAGG;IACG,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQrE;;;;;;;OAOG;IACG,8BAA8B,CACnC,WAAW,EAAE,OAAO,GAClB,OAAO,CAAC;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAmB7D;;;OAGG;IACG,qBAAqB,CAAC,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE,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;IAUpE;;;;;;OAMG;IACH,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,kBAAkB;IAI3D;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAMlD"}
@@ -14,6 +14,9 @@ export class SessionStrategy {
14
14
  name = "session";
15
15
  #config;
16
16
  #sessionKey;
17
+ #viaRemember = false;
18
+ #loggedOut = false;
19
+ #attemptedViaRemember = false;
17
20
  constructor(config) {
18
21
  this.#config = config;
19
22
  this.#sessionKey = config.sessionKey ?? "auth_user_id";
@@ -26,6 +29,42 @@ export class SessionStrategy {
26
29
  get usesRememberMeTokens() {
27
30
  return this.#config.rememberMeTokens !== undefined;
28
31
  }
32
+ /**
33
+ * Whether the current user was revived from a remember-me cookie rather
34
+ * than signing in (AdonisJS `viaRemember`).
35
+ *
36
+ * This is the distinction that lets an app demand the password again before
37
+ * something sensitive — changing an email, spending money, deleting an
38
+ * account. Nothing reported it, so a session restored from a cookie looked
39
+ * exactly like one where the user had just typed their password.
40
+ */
41
+ get viaRemember() {
42
+ return this.#viaRemember;
43
+ }
44
+ /**
45
+ * Whether a remember-me token was even tried on this request (AdonisJS
46
+ * `attemptedViaRemember`) — true whether or not it worked.
47
+ */
48
+ get attemptedViaRemember() {
49
+ return this.#attemptedViaRemember;
50
+ }
51
+ /**
52
+ * Whether `logout()` ran on this guard (AdonisJS `isLoggedOut`).
53
+ *
54
+ * A handler that logs out and then keeps working — clearing a cart, writing
55
+ * an audit line — could not tell that the session was already gone.
56
+ */
57
+ get isLoggedOut() {
58
+ return this.#loggedOut;
59
+ }
60
+ /** The session key the user id is stored under (AdonisJS `sessionKeyName`). */
61
+ get sessionKeyName() {
62
+ return this.#sessionKey;
63
+ }
64
+ /** The cookie key the remember-me token is stored under (AdonisJS `rememberMeKeyName`). */
65
+ get rememberMeKeyName() {
66
+ return this.rememberMeCookieName;
67
+ }
29
68
  #rememberMeAge() {
30
69
  return this.#config.rememberMeAge ?? DEFAULT_REMEMBER_AGE_SECONDS;
31
70
  }
@@ -53,12 +92,14 @@ export class SessionStrategy {
53
92
  const driver = this.#config.rememberMeTokens;
54
93
  if (!driver)
55
94
  return null;
95
+ this.#attemptedViaRemember = true;
56
96
  const recycled = await verifyAndRecycleRememberMeToken(driver, cookieValue, this.#rememberMeAge());
57
97
  if (!recycled)
58
98
  return null;
59
99
  const user = await this.#config.findUser(recycled.userId);
60
100
  if (!user)
61
101
  return null;
102
+ this.#viaRemember = true;
62
103
  return { user, cookieValue: recycled.value };
63
104
  }
64
105
  /**
@@ -120,6 +161,11 @@ export class SessionStrategy {
120
161
  async login(user, session) {
121
162
  session.regenerate();
122
163
  session.put(this.#sessionKey, user.id);
164
+ this.#loggedOut = false;
165
+ // A password was typed: this session is no longer "via remember", even
166
+ // if a cookie was tried earlier in the same request. Without the reset
167
+ // the flag would stay true and a re-auth prompt would never fire.
168
+ this.#viaRemember = false;
123
169
  }
124
170
  /**
125
171
  * The session entry a test client needs to be `user` (AdonisJS
@@ -139,6 +185,9 @@ export class SessionStrategy {
139
185
  */
140
186
  async logout(session) {
141
187
  session.forget(this.#sessionKey);
188
+ this.#viaRemember = false;
189
+ this.#attemptedViaRemember = false;
190
+ this.#loggedOut = true;
142
191
  }
143
192
  }
144
193
  //# sourceMappingURL=SessionStrategy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionStrategy.js","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SessionStrategy.js","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"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"}
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/warden",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Warden — Authentication & authorization for the Ream framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,7 +4,13 @@
4
4
  * @implements FR48, FR50, FR51
5
5
  */
6
6
 
7
+ import { Authenticator } from "./Authenticator.js";
7
8
  import { WardenError } from "./errors.js";
9
+ import type { WardenContext } from "./middleware.js";
10
+ import { sanitizePayload } from "./sanitize.js";
11
+
12
+ export { sanitizePayload };
13
+
8
14
  import { MemoryRightsStore } from "./rights/MemoryRightsStore.js";
9
15
  import { RightsResolver } from "./rights/RightsResolver.js";
10
16
  import type { EffectivePermissions, Scope } from "./rights/types.js";
@@ -47,6 +53,25 @@ export interface AuthClientResponse {
47
53
  cookies?: Record<string, string>;
48
54
  }
49
55
 
56
+ /**
57
+ * The event namespace for a guard, taken from its DRIVER.
58
+ *
59
+ * AdonisJS namespaces auth events per driver — `session_auth:*`,
60
+ * `access_tokens_auth:*`, `basic_auth:*` — so an app can audit session logins
61
+ * without also hearing about every bearer token. Warden emitted `session_auth:*`
62
+ * whatever authenticated, which told a session listener about JWT traffic and
63
+ * left no name to subscribe to for the others.
64
+ *
65
+ * Driven by the strategy's `name` rather than the guard's config key: a guard
66
+ * called `web` running a `SessionStrategy` is still the session driver, exactly
67
+ * as upstream. A driver already ending in `_auth` (`basic_auth`) keeps its name
68
+ * rather than growing a second suffix, which is how AdonisJS spells it.
69
+ */
70
+ export function authEventPrefix(driverName: string | undefined): string {
71
+ if (!driverName) return "session_auth";
72
+ return driverName.endsWith("_auth") ? driverName : `${driverName}_auth`;
73
+ }
74
+
50
75
  export interface AuthStrategy {
51
76
  name: string;
52
77
  authenticate(credentials: Record<string, unknown>): Promise<AuthResult>;
@@ -323,6 +348,63 @@ export class AuthManager {
323
348
  }
324
349
 
325
350
  /** Get a registered strategy by name. */
351
+ /**
352
+ * An {@link Authenticator} bound to one request (AdonisJS
353
+ * `createAuthenticator`).
354
+ *
355
+ * What the HTTP middleware builds per request. Exposed so anything outside
356
+ * that path — a console command acting as a user, a background job — can
357
+ * build one without reaching for the class.
358
+ */
359
+ createAuthenticator(ctx: WardenContext): Authenticator {
360
+ return new Authenticator(ctx, this);
361
+ }
362
+
363
+ /**
364
+ * What a test client needs to authenticate as a user (AdonisJS
365
+ * `createAuthenticatorClient`).
366
+ *
367
+ * Delegates to the guard's own `authenticateAsClient`, so a guard with no
368
+ * test seam says so instead of having one forged for it.
369
+ */
370
+ createAuthenticatorClient(): {
371
+ use(guard?: string): {
372
+ authenticateAsClient(
373
+ ...args: never[]
374
+ ): Promise<AuthClientResponse> | AuthClientResponse;
375
+ };
376
+ } {
377
+ return {
378
+ use: (guard?: string) => {
379
+ const strategy = this.getStrategy(guard);
380
+ const seam = strategy.authenticateAsClient?.bind(strategy);
381
+ if (!seam) {
382
+ throw new WardenError(
383
+ "STRATEGY_HAS_NO_CLIENT",
384
+ `Auth strategy '${guard ?? this.default}' has no authenticateAsClient() — it cannot forge a client request.`,
385
+ {
386
+ hint: "Implement authenticateAsClient() on the strategy, or authenticate through a real login in the test.",
387
+ },
388
+ );
389
+ }
390
+ return { authenticateAsClient: seam };
391
+ },
392
+ };
393
+ }
394
+
395
+ /**
396
+ * The event namespace a guard's events carry, resolved from its driver.
397
+ * Falls back to `session_auth` when the guard cannot be resolved, so a
398
+ * failure to look one up never becomes a second failure at emit time.
399
+ */
400
+ eventPrefixFor(guardName?: string): string {
401
+ try {
402
+ return authEventPrefix(this.getStrategy(guardName).name);
403
+ } catch {
404
+ return authEventPrefix(undefined);
405
+ }
406
+ }
407
+
326
408
  getStrategy(name?: string): AuthStrategy {
327
409
  const strategyName = name ?? this.default;
328
410
  const strategy = this.guards.get(strategyName);
@@ -382,12 +464,13 @@ export class AuthManager {
382
464
  },
383
465
  );
384
466
  }
385
- this.#emit("session_auth:login_attempted", {
467
+ const prefix = authEventPrefix(strategy.name);
468
+ this.#emit(`${prefix}:login_attempted`, {
386
469
  guardName: strategyName ?? this.default,
387
470
  user,
388
471
  });
389
472
  await strategy.login(user, session);
390
- this.#emit("session_auth:login_succeeded", {
473
+ this.#emit(`${prefix}:login_succeeded`, {
391
474
  guardName: strategyName ?? this.default,
392
475
  user,
393
476
  sessionId: undefined,
@@ -408,7 +491,7 @@ export class AuthManager {
408
491
  );
409
492
  }
410
493
  await strategy.logout(session);
411
- this.#emit("session_auth:logged_out", {
494
+ this.#emit(`${authEventPrefix(strategy.name)}:logged_out`, {
412
495
  guardName: strategyName ?? this.default,
413
496
  user: null,
414
497
  error: null,
@@ -446,10 +529,3 @@ function isLoginCapable(
446
529
  * directly (not `AuthManager.verify`), so without this it would skip
447
530
  * the guard JWT / api-key users get.
448
531
  */
449
- export function sanitizePayload(user: UserPayload): void {
450
- for (const key of ["__proto__", "constructor", "prototype"]) {
451
- if (key in user) {
452
- delete (user as Record<string, unknown>)[key];
453
- }
454
- }
455
- }
@@ -14,15 +14,15 @@
14
14
  * `@c9up/ream`.
15
15
  */
16
16
 
17
- import {
18
- type AuthManager,
19
- type AuthResult,
20
- type AuthStrategy,
21
- sanitizePayload,
22
- type UserPayload,
17
+ import type {
18
+ AuthManager,
19
+ AuthResult,
20
+ AuthStrategy,
21
+ UserPayload,
23
22
  } from "./AuthManager.js";
24
23
  import { E_UNAUTHORIZED_ACCESS, WardenError } from "./errors.js";
25
24
  import type { WardenContext } from "./middleware.js";
25
+ import { sanitizePayload } from "./sanitize.js";
26
26
  import type { SessionStore } from "./strategies/SessionStrategy.js";
27
27
 
28
28
  /**
@@ -308,6 +308,31 @@ export class Authenticator {
308
308
  }
309
309
  }
310
310
 
311
+ /**
312
+ * Try the given guards in order and report whether one succeeded, without
313
+ * throwing (AdonisJS `checkUsing`).
314
+ *
315
+ * The non-throwing sibling of {@link authenticateUsing}: `check()` is to
316
+ * `authenticate()` what this is to it. A handler that wants to branch on
317
+ * "is anyone signed in through either of these" had to wrap the throwing
318
+ * form in a try/catch itself.
319
+ */
320
+ async checkUsing(
321
+ guards?: string[],
322
+ options?: { loginRoute?: string },
323
+ ): Promise<boolean> {
324
+ try {
325
+ await this.authenticateUsing(guards, options);
326
+ return this.isAuthenticated;
327
+ } catch (err) {
328
+ // A credential rejection is an answer, not a failure. Anything else
329
+ // — a crashed strategy, a config error — is still a real problem and
330
+ // must not be reported as "not signed in".
331
+ if (err instanceof E_UNAUTHORIZED_ACCESS) return false;
332
+ throw err;
333
+ }
334
+ }
335
+
311
336
  /**
312
337
  * Authenticate by trying the given guards in order (default: the default
313
338
  * guard). Sets the user on the first success; throws `E_UNAUTHORIZED_ACCESS`
@@ -322,11 +347,13 @@ export class Authenticator {
322
347
  const names =
323
348
  guards && guards.length > 0 ? guards : [this.#auth.defaultStrategyName];
324
349
  const guardName = names[0] ?? this.#auth.defaultStrategyName;
325
- // The four authentication events AdonisJS' session guard emits. An app
326
- // auditing logins subscribes to exactly these names.
327
- this.#auth.emitAuthEvent("session_auth:authentication_attempted", {
328
- guardName,
329
- });
350
+ // Namespaced by the guard's driver, as AdonisJS does: `session_auth:*`,
351
+ // `access_tokens_auth:*`, `basic_auth:*`. An app auditing session logins
352
+ // must not be told about bearer-token traffic under the same name.
353
+ this.#auth.emitAuthEvent(
354
+ `${this.#auth.eventPrefixFor(guardName)}:authentication_attempted`,
355
+ { guardName },
356
+ );
330
357
  const creds = extractCredentials(this.#ctx, this.#auth);
331
358
  const hasSessionStrategy = names.includes("session");
332
359
  const { result, viaGuard, attemptCount, crashCount } =
@@ -338,11 +365,13 @@ export class Authenticator {
338
365
  if (result?.authenticated && result.user) {
339
366
  this.#user = result.user;
340
367
  this.#viaGuard = viaGuard;
341
- this.#auth.emitAuthEvent("session_auth:authentication_succeeded", {
342
- guardName: viaGuard ?? guardName,
343
- user: result.user,
344
- sessionId: undefined,
345
- });
368
+ // The guard that actually answered names the event, not the one asked
369
+ // for first.
370
+ const winner = viaGuard ?? guardName;
371
+ this.#auth.emitAuthEvent(
372
+ `${this.#auth.eventPrefixFor(winner)}:authentication_succeeded`,
373
+ { guardName: winner, user: result.user, sessionId: undefined },
374
+ );
346
375
  return;
347
376
  }
348
377
  // Every attempted guard crashed → a server-side incident, not a
@@ -361,10 +390,10 @@ export class Authenticator {
361
390
  redirectTo: hasSessionStrategy ? options?.loginRoute : undefined,
362
391
  },
363
392
  );
364
- this.#auth.emitAuthEvent("session_auth:authentication_failed", {
365
- guardName,
366
- error: failure,
367
- });
393
+ this.#auth.emitAuthEvent(
394
+ `${this.#auth.eventPrefixFor(guardName)}:authentication_failed`,
395
+ { guardName, error: failure },
396
+ );
368
397
  throw failure;
369
398
  }
370
399
 
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
24
+ import { Secret } from "./Secret.js";
24
25
 
25
26
  /** Bytes of entropy in a token secret. Adonis defaults to 40. */
26
27
  export const DEFAULT_SECRET_LENGTH = 40;
@@ -221,3 +222,136 @@ export class MemoryRememberMeTokenDriver implements RememberMeTokenDriver {
221
222
  }
222
223
  }
223
224
  }
225
+
226
+ /**
227
+ * A remember-me token as an object, matching `@adonisjs/auth`'s
228
+ * `RememberMeToken` (modules/session_guard/remember_me_token.ts).
229
+ *
230
+ * The functions above are what warden's own guard uses and stay the primary
231
+ * API. This class exists because an app moving over calls
232
+ * `RememberMeToken.decode(...)` / `token.verify(secret)` directly — upstream
233
+ * exports it — and finding nothing under that name is a migration stopping at
234
+ * an import line. Both sit on the same encoding and the same hashing, so they
235
+ * cannot disagree about what a token is.
236
+ *
237
+ * Secrets come back wrapped in {@link Secret} so one cannot end up in a log by
238
+ * being interpolated into a string, which is what upstream does too.
239
+ */
240
+ export class RememberMeToken {
241
+ /**
242
+ * Split a public token value into its identifier and secret. Returns `null`
243
+ * for anything malformed rather than throwing — a bad cookie is an ordinary
244
+ * event, not an exception.
245
+ */
246
+ static decode(
247
+ value: string,
248
+ ): { identifier: string; secret: Secret<string> } | null {
249
+ const decoded = decodeTokenValue(value);
250
+ if (!decoded) return null;
251
+ return {
252
+ identifier: decoded.identifier,
253
+ secret: new Secret(decoded.secret),
254
+ };
255
+ }
256
+
257
+ /** A fresh secret and the hash to store for it. */
258
+ static seed(size: number = DEFAULT_SECRET_LENGTH): {
259
+ secret: Secret<string>;
260
+ hash: string;
261
+ } {
262
+ const secret = randomBytes(size).toString("base64url");
263
+ return { secret: new Secret(secret), hash: hashSecret(secret) };
264
+ }
265
+
266
+ /**
267
+ * Everything the persistence layer needs for a new token, without deciding
268
+ * how it is stored (upstream `createTransientToken`).
269
+ *
270
+ * `expiresIn` is in SECONDS, as everywhere else in this module. AdonisJS
271
+ * also accepts a duration string there; warden takes the number, so the
272
+ * unit is never ambiguous at a call site.
273
+ */
274
+ static createTransientToken(
275
+ userId: string | number,
276
+ size: number,
277
+ expiresIn: number,
278
+ ): {
279
+ secret: Secret<string>;
280
+ hash: string;
281
+ userId: string | number;
282
+ expiresAt: Date;
283
+ } {
284
+ const { secret, hash } = RememberMeToken.seed(size);
285
+ return {
286
+ secret,
287
+ hash,
288
+ userId,
289
+ expiresAt: new Date(Date.now() + expiresIn * 1000),
290
+ };
291
+ }
292
+
293
+ identifier: string;
294
+ tokenableId: string | number;
295
+ hash: string;
296
+ createdAt: Date;
297
+ updatedAt: Date;
298
+ expiresAt: Date;
299
+ /** The public `identifier.secret` value — only on a token just minted. */
300
+ value?: Secret<string>;
301
+
302
+ constructor(attributes: {
303
+ identifier: string;
304
+ tokenableId: string | number;
305
+ hash: string;
306
+ createdAt: Date;
307
+ updatedAt: Date;
308
+ expiresAt: Date;
309
+ secret?: Secret<string>;
310
+ }) {
311
+ this.identifier = attributes.identifier;
312
+ this.tokenableId = attributes.tokenableId;
313
+ this.hash = attributes.hash;
314
+ this.createdAt = attributes.createdAt;
315
+ this.updatedAt = attributes.updatedAt;
316
+ this.expiresAt = attributes.expiresAt;
317
+ if (attributes.secret) {
318
+ this.value = new Secret(
319
+ encodeTokenValue(attributes.identifier, attributes.secret.release()),
320
+ );
321
+ }
322
+ }
323
+
324
+ /** Build one from a stored row, whose timestamps are epoch milliseconds. */
325
+ static fromStored(stored: StoredRememberMeToken): RememberMeToken {
326
+ return new RememberMeToken({
327
+ identifier: stored.identifier,
328
+ tokenableId: stored.tokenableId,
329
+ hash: stored.hash,
330
+ createdAt: new Date(stored.createdAt),
331
+ updatedAt: new Date(stored.updatedAt),
332
+ expiresAt: new Date(stored.expiresAt),
333
+ });
334
+ }
335
+
336
+ /** The row to persist, in the shape {@link RememberMeTokenDriver} takes. */
337
+ toStored(): StoredRememberMeToken {
338
+ return {
339
+ identifier: this.identifier,
340
+ tokenableId: this.tokenableId,
341
+ hash: this.hash,
342
+ createdAt: this.createdAt.getTime(),
343
+ updatedAt: this.updatedAt.getTime(),
344
+ expiresAt: this.expiresAt.getTime(),
345
+ };
346
+ }
347
+
348
+ /** Whether the token is past its expiry. */
349
+ isExpired(): boolean {
350
+ return this.expiresAt.getTime() <= Date.now();
351
+ }
352
+
353
+ /** Constant-time check of a presented secret against the stored hash. */
354
+ verify(secret: Secret<string>): boolean {
355
+ return safeCompareHashes(this.hash, hashSecret(secret.release()));
356
+ }
357
+ }
package/src/Secret.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Wraps a sensitive value so it never leaks through a log, `JSON.stringify`, or
3
+ * `util.inspect`. Read it back deliberately with {@link Secret.release}.
4
+ *
5
+ * AdonisJS takes this from `@adonisjs/core/helpers`, and its `RememberMeToken`
6
+ * hands secrets out wrapped in one. Warden carries its own copy rather than
7
+ * importing `@c9up/ream`: it has no runtime dependencies and ships a standalone
8
+ * entry point, so reaching for a peer here would break the framework-free mode.
9
+ * Same reason every native package carries its own platform map.
10
+ */
11
+ const REDACTED = "[redacted]";
12
+
13
+ export class Secret<T> {
14
+ readonly #value: T;
15
+ readonly #keyword: string;
16
+
17
+ constructor(value: T, redactedKeyword: string = REDACTED) {
18
+ this.#value = value;
19
+ this.#keyword = redactedKeyword;
20
+ }
21
+
22
+ /** Redacted placeholder, so `JSON.stringify` never emits the secret. */
23
+ toJSON(): string {
24
+ return this.#keyword;
25
+ }
26
+
27
+ /** Redacted placeholder for string coercion and template literals. */
28
+ toString(): string {
29
+ return this.#keyword;
30
+ }
31
+
32
+ /** Redacted placeholder for `console.log` / `util.inspect`. */
33
+ [Symbol.for("nodejs.util.inspect.custom")](): string {
34
+ return this.#keyword;
35
+ }
36
+
37
+ /** Release the underlying value — the one deliberate way to read it. */
38
+ release(): T {
39
+ return this.#value;
40
+ }
41
+ }
@@ -18,7 +18,13 @@ import type { EffectivePermissions, Scope } from "../rights/types.js";
18
18
  import { AbilitiesBuilder } from "./AbilitiesBuilder.js";
19
19
  import { AuthorizationResponse } from "./AuthorizationResponse.js";
20
20
  import type { BasePolicy } from "./BasePolicy.js";
21
- import { evaluate, isAction, throwAuthorizationFailure } from "./evaluate.js";
21
+ import {
22
+ defaultResponseBuilder,
23
+ evaluate,
24
+ isAction,
25
+ type ResponseBuilder,
26
+ throwAuthorizationFailure,
27
+ } from "./evaluate.js";
22
28
  import { PolicyAuthorizer } from "./PolicyAuthorizer.js";
23
29
  import { emptyPermissions } from "./policyContext.js";
24
30
  import type {
@@ -31,6 +37,20 @@ import type {
31
37
  } from "./types.js";
32
38
 
33
39
  export class Bouncer {
40
+ /**
41
+ * How a bare boolean from an ability or a policy becomes an
42
+ * {@link AuthorizationResponse} (AdonisJS `Bouncer.responseBuilder`).
43
+ *
44
+ * Replace it once at boot to give every `return false` a house message and
45
+ * status instead of a naked 403:
46
+ *
47
+ * ```ts
48
+ * Bouncer.responseBuilder = (value) =>
49
+ * value === false ? AuthorizationResponse.deny('Nope', 404) : normalizeResponse(value)
50
+ * ```
51
+ */
52
+ static responseBuilder: ResponseBuilder = defaultResponseBuilder;
53
+
34
54
  readonly #userOrResolver: UserPayload | (() => UserPayload | null) | null;
35
55
  /** Lazily-resolved user cache (`undefined` until `#getUser` runs). */
36
56
  #user: UserPayload | null | undefined;
@@ -231,6 +251,7 @@ export class Bouncer {
231
251
  this.#scope,
232
252
  () => this.#resolvePermissions(),
233
253
  this.#emitter,
254
+ Bouncer.responseBuilder,
234
255
  );
235
256
  }
236
257
 
@@ -245,10 +266,17 @@ export class Bouncer {
245
266
  }
246
267
 
247
268
  /** Emit `authorization:finished` when an emitter is wired (no-op otherwise). */
248
- #emit(action: string, response: AuthorizationResponse): void {
269
+ #emit(
270
+ action: string,
271
+ response: AuthorizationResponse,
272
+ parameters: unknown[],
273
+ ): void {
249
274
  this.#emitter?.emit("authorization:finished", {
250
275
  user: this.#getUser(),
251
276
  action,
277
+ // What the check was ABOUT. AdonisJS carries it, and without it an
278
+ // audit log can say "Ada was denied editPost" but never which post.
279
+ parameters,
252
280
  response,
253
281
  });
254
282
  }
@@ -321,8 +349,9 @@ export class Bouncer {
321
349
  allowGuest: resolved.allowGuest,
322
350
  run: (user) => resolved.execute(user, ...args),
323
351
  args,
352
+ responseBuilder: Bouncer.responseBuilder,
324
353
  });
325
- this.#emit(action, response);
354
+ this.#emit(action, response, args);
326
355
  return response;
327
356
  }
328
357