@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.
- package/dist/AuthManager.d.ts +46 -1
- package/dist/AuthManager.d.ts.map +1 -1
- package/dist/AuthManager.js +71 -10
- package/dist/AuthManager.js.map +1 -1
- package/dist/Authenticator.d.ts +13 -1
- package/dist/Authenticator.d.ts.map +1 -1
- package/dist/Authenticator.js +33 -15
- package/dist/Authenticator.js.map +1 -1
- package/dist/RememberMeToken.d.ts +70 -0
- package/dist/RememberMeToken.d.ts.map +1 -1
- package/dist/RememberMeToken.js +102 -0
- package/dist/RememberMeToken.js.map +1 -1
- package/dist/Secret.d.ts +11 -0
- package/dist/Secret.d.ts.map +1 -0
- package/dist/Secret.js +36 -0
- package/dist/Secret.js.map +1 -0
- package/dist/bouncer/Bouncer.d.ts +14 -0
- package/dist/bouncer/Bouncer.d.ts.map +1 -1
- package/dist/bouncer/Bouncer.js +23 -4
- package/dist/bouncer/Bouncer.js.map +1 -1
- package/dist/bouncer/PolicyAuthorizer.d.ts +9 -1
- package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
- package/dist/bouncer/PolicyAuthorizer.js +17 -1
- package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
- package/dist/bouncer/evaluate.d.ts +14 -2
- package/dist/bouncer/evaluate.d.ts.map +1 -1
- package/dist/bouncer/evaluate.js +16 -5
- package/dist/bouncer/evaluate.js.map +1 -1
- package/dist/bouncer/types.d.ts +4 -2
- package/dist/bouncer/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/sanitize.d.ts +10 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +15 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/strategies/SessionStrategy.d.ts +26 -0
- package/dist/strategies/SessionStrategy.d.ts.map +1 -1
- package/dist/strategies/SessionStrategy.js +49 -0
- package/dist/strategies/SessionStrategy.js.map +1 -1
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +86 -10
- package/src/Authenticator.ts +49 -20
- package/src/RememberMeToken.ts +134 -0
- package/src/Secret.ts +41 -0
- package/src/bouncer/Bouncer.ts +32 -3
- package/src/bouncer/PolicyAuthorizer.ts +20 -1
- package/src/bouncer/evaluate.ts +26 -6
- package/src/bouncer/types.ts +4 -2
- package/src/index.ts +7 -1
- package/src/sanitize.ts +17 -0
- 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;
|
|
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":"AAAA;;;;;;;GAOG;AAQH,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EACN,gBAAgB,EAChB,mBAAmB,EAEnB,+BAA+B,GAC/B,MAAM,uBAAuB,CAAC;AA2C/B,wDAAwD;AACxD,MAAM,4BAA4B,GAAG,UAAU,CAAC;AAEhD,MAAM,OAAO,eAAe;IAC3B,IAAI,GAAG,SAAS,CAAC;IACjB,OAAO,CAAwB;IAC/B,WAAW,CAAS;
|
|
1
|
+
{"version":3,"file":"SessionStrategy.js","sourceRoot":"","sources":["../../src/strategies/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EACN,gBAAgB,EAChB,mBAAmB,EAEnB,+BAA+B,GAC/B,MAAM,uBAAuB,CAAC;AA2C/B,wDAAwD;AACxD,MAAM,4BAA4B,GAAG,UAAU,CAAC;AAEhD,MAAM,OAAO,eAAe;IAC3B,IAAI,GAAG,SAAS,CAAC;IACjB,OAAO,CAAwB;IAC/B,WAAW,CAAS;IACpB,YAAY,GAAG,KAAK,CAAC;IACrB,UAAU,GAAG,KAAK,CAAC;IACnB,qBAAqB,GAAG,KAAK,CAAC;IAE9B,YAAY,MAA6B;QACxC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,UAAU,IAAI,cAAc,CAAC;IACxD,CAAC;IAED,mDAAmD;IACnD,IAAI,oBAAoB;QACvB,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,IAAI,cAAc,CAAC;IAC5D,CAAC;IAED,mDAAmD;IACnD,IAAI,oBAAoB;QACvB,OAAO,IAAI,CAAC,OAAO,CAAC,gBAAgB,KAAK,SAAS,CAAC;IACpD,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,WAAW;QACd,OAAO,IAAI,CAAC,YAAY,CAAC;IAC1B,CAAC;IAED;;;OAGG;IACH,IAAI,oBAAoB;QACvB,OAAO,IAAI,CAAC,qBAAqB,CAAC;IACnC,CAAC;IAED;;;;;OAKG;IACH,IAAI,WAAW;QACd,OAAO,IAAI,CAAC,UAAU,CAAC;IACxB,CAAC;IAED,+EAA+E;IAC/E,IAAI,cAAc;QACjB,OAAO,IAAI,CAAC,WAAW,CAAC;IACzB,CAAC;IAED,2FAA2F;IAC3F,IAAI,iBAAiB;QACpB,OAAO,IAAI,CAAC,oBAAoB,CAAC;IAClC,CAAC;IAED,cAAc;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,4BAA4B,CAAC;IACnE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,oBAAoB,CAAC,IAAiB;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,MAAM,MAAM,GAAG,mBAAmB,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;QACnE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,8BAA8B,CACnC,WAAoB;QAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC;QAElC,MAAM,QAAQ,GAAG,MAAM,+BAA+B,CACrD,MAAM,EACN,WAAW,EACX,IAAI,CAAC,cAAc,EAAE,CACrB,CAAC;QACF,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAE3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QAEvB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC9C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,qBAAqB,CAAC,WAAoB;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,OAAO,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACzC,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,6KAA6K,EAC7K;YACC,IAAI,EAAE,yKAAyK;SAC/K,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;QACvC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,uEAAuE;QACvE,uEAAuE;QACvE,kEAAkE;QAClE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACH,oBAAoB,CAAC,IAAiB;QACrC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC;IACrD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CAAC,OAAqB;QACjC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC,qBAAqB,GAAG,KAAK,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IACxB,CAAC;CACD"}
|
|
Binary file
|
package/package.json
CHANGED
package/src/AuthManager.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
}
|
package/src/Authenticator.ts
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
* `@c9up/ream`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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(
|
|
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
|
|
package/src/RememberMeToken.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/bouncer/Bouncer.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
|