@c9up/warden 0.1.18 → 0.1.20
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/augmentations.d.ts +23 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +17 -0
- package/dist/augmentations.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 +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +3 -2
- 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/augmentations.ts +27 -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 +9 -1
- package/src/sanitize.ts +17 -0
- package/src/strategies/SessionStrategy.ts +54 -0
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
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type Action,
|
|
17
17
|
evaluate,
|
|
18
18
|
isAction,
|
|
19
|
+
type ResponseBuilder,
|
|
19
20
|
throwAuthorizationFailure,
|
|
20
21
|
} from "./evaluate.js";
|
|
21
22
|
import { emptyPermissions, setPolicyContext } from "./policyContext.js";
|
|
@@ -69,7 +70,8 @@ export class PolicyAuthorizer {
|
|
|
69
70
|
readonly #factory: () => Promise<BasePolicy>;
|
|
70
71
|
readonly #scope: Scope;
|
|
71
72
|
readonly #resolvePermissions: () => Promise<EffectivePermissions>;
|
|
72
|
-
|
|
73
|
+
#emitter: BouncerEmitter | undefined;
|
|
74
|
+
readonly #responseBuilder: ResponseBuilder | undefined;
|
|
73
75
|
|
|
74
76
|
constructor(
|
|
75
77
|
user: UserPayload | null,
|
|
@@ -78,12 +80,14 @@ export class PolicyAuthorizer {
|
|
|
78
80
|
resolvePermissions: () => Promise<EffectivePermissions> = () =>
|
|
79
81
|
Promise.resolve(emptyPermissions(scope)),
|
|
80
82
|
emitter?: BouncerEmitter,
|
|
83
|
+
responseBuilder?: ResponseBuilder,
|
|
81
84
|
) {
|
|
82
85
|
this.#user = user;
|
|
83
86
|
this.#factory = factory;
|
|
84
87
|
this.#scope = scope;
|
|
85
88
|
this.#resolvePermissions = resolvePermissions;
|
|
86
89
|
this.#emitter = emitter;
|
|
90
|
+
this.#responseBuilder = responseBuilder;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
/** Run a check and resolve to the full response (D8 — fresh policy per check). */
|
|
@@ -106,15 +110,30 @@ export class PolicyAuthorizer {
|
|
|
106
110
|
args,
|
|
107
111
|
before: policy.before?.bind(policy),
|
|
108
112
|
after: policy.after?.bind(policy),
|
|
113
|
+
responseBuilder: this.#responseBuilder,
|
|
109
114
|
});
|
|
110
115
|
this.#emitter?.emit("authorization:finished", {
|
|
111
116
|
user: this.#user,
|
|
112
117
|
action,
|
|
118
|
+
// Same payload as an ability check: which resource the decision was
|
|
119
|
+
// about, not just that a decision happened.
|
|
120
|
+
parameters: args,
|
|
113
121
|
response,
|
|
114
122
|
});
|
|
115
123
|
return response;
|
|
116
124
|
}
|
|
117
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Swap the event sink after construction (AdonisJS `setEmitter`).
|
|
128
|
+
*
|
|
129
|
+
* The Bouncer passes its own down, but an authorizer built directly — a
|
|
130
|
+
* test, a console command — had no way to be given one.
|
|
131
|
+
*/
|
|
132
|
+
setEmitter(emitter?: BouncerEmitter): this {
|
|
133
|
+
this.#emitter = emitter;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
/** True iff the action is authorized. Never throws on denial. */
|
|
119
138
|
async allows(action: string, ...args: unknown[]): Promise<boolean> {
|
|
120
139
|
return (await this.execute(action, ...args)).authorized;
|
package/src/bouncer/evaluate.ts
CHANGED
|
@@ -19,14 +19,31 @@ export type Action = (
|
|
|
19
19
|
...args: unknown[]
|
|
20
20
|
) => AuthorizerResponse;
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
22
|
+
/** Turns whatever an ability returned into an {@link AuthorizationResponse}. */
|
|
23
|
+
export type ResponseBuilder = (
|
|
24
24
|
value: boolean | AuthorizationResponse,
|
|
25
|
-
)
|
|
25
|
+
) => AuthorizationResponse;
|
|
26
|
+
|
|
27
|
+
/** The default: boolean sugar → response; an explicit response passes through (D7). */
|
|
28
|
+
export const defaultResponseBuilder: ResponseBuilder = (value) => {
|
|
26
29
|
if (value instanceof AuthorizationResponse) {
|
|
27
30
|
return value;
|
|
28
31
|
}
|
|
29
32
|
return value ? AuthorizationResponse.allow() : AuthorizationResponse.deny();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Boolean sugar → response, through whatever builder is installed.
|
|
37
|
+
*
|
|
38
|
+
* `Bouncer.responseBuilder` replaces it app-wide, which is how you give every
|
|
39
|
+
* bare `return false` a house message and status instead of a naked 403
|
|
40
|
+
* (AdonisJS `Bouncer.responseBuilder`).
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeResponse(
|
|
43
|
+
value: boolean | AuthorizationResponse,
|
|
44
|
+
builder: ResponseBuilder = defaultResponseBuilder,
|
|
45
|
+
): AuthorizationResponse {
|
|
46
|
+
return builder(value);
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
/**
|
|
@@ -71,8 +88,11 @@ export async function evaluate(params: {
|
|
|
71
88
|
result: AuthorizationResponse,
|
|
72
89
|
...args: unknown[]
|
|
73
90
|
) => HookResponse;
|
|
91
|
+
/** Overrides how a bare boolean becomes a response. */
|
|
92
|
+
responseBuilder?: ResponseBuilder;
|
|
74
93
|
}): Promise<AuthorizationResponse> {
|
|
75
94
|
const { user, action, allowGuest, run, args, before, after } = params;
|
|
95
|
+
const builder = params.responseBuilder ?? defaultResponseBuilder;
|
|
76
96
|
|
|
77
97
|
let response: AuthorizationResponse | undefined;
|
|
78
98
|
|
|
@@ -80,7 +100,7 @@ export async function evaluate(params: {
|
|
|
80
100
|
if (before) {
|
|
81
101
|
const early = await before(user, action, ...args);
|
|
82
102
|
if (early !== undefined) {
|
|
83
|
-
response = normalizeResponse(early);
|
|
103
|
+
response = normalizeResponse(early, builder);
|
|
84
104
|
}
|
|
85
105
|
}
|
|
86
106
|
|
|
@@ -89,7 +109,7 @@ export async function evaluate(params: {
|
|
|
89
109
|
if (user === null && !allowGuest) {
|
|
90
110
|
response = AuthorizationResponse.deny();
|
|
91
111
|
} else {
|
|
92
|
-
response = normalizeResponse(await run(user));
|
|
112
|
+
response = normalizeResponse(await run(user), builder);
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
|
|
@@ -98,7 +118,7 @@ export async function evaluate(params: {
|
|
|
98
118
|
if (after) {
|
|
99
119
|
const override = await after(user, action, response, ...args);
|
|
100
120
|
if (override !== undefined) {
|
|
101
|
-
response = normalizeResponse(override);
|
|
121
|
+
response = normalizeResponse(override, builder);
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
124
|
|
package/src/bouncer/types.ts
CHANGED
|
@@ -23,8 +23,10 @@ export interface PolicyContainerResolver {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Agnostic event sink (Adonis `Bouncer.emitter`). When present, an
|
|
26
|
-
* `authorization:finished` event fires after every ability/policy evaluation
|
|
27
|
-
* `{ user, action, response }
|
|
26
|
+
* `authorization:finished` event fires after every ability/policy evaluation
|
|
27
|
+
* with `{ user, action, parameters, response }` (AdonisJS `BouncerEvents`).
|
|
28
|
+
* `parameters` are the arguments the check ran against — an audit log needs
|
|
29
|
+
* them to say WHICH resource was refused. No-op when absent.
|
|
28
30
|
*/
|
|
29
31
|
export interface BouncerEmitter {
|
|
30
32
|
emit(event: string, payload: unknown): void;
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @implements FR48, FR49, FR50, FR51, FR52, FR53
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import "./augmentations.js";
|
|
8
|
+
|
|
7
9
|
export type { AuthAttempt, ExtractedCredentials } from "./Authenticator.js";
|
|
8
10
|
export {
|
|
9
11
|
API_KEY_GUARD_NAMES,
|
|
@@ -17,7 +19,11 @@ export type {
|
|
|
17
19
|
AuthStrategy,
|
|
18
20
|
UserPayload,
|
|
19
21
|
} from "./AuthManager.js";
|
|
20
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
AuthManager,
|
|
24
|
+
authEventPrefix,
|
|
25
|
+
type WardenEmitter,
|
|
26
|
+
} from "./AuthManager.js";
|
|
21
27
|
export type { AuthRateLimiterConfig } from "./AuthRateLimiter.js";
|
|
22
28
|
export { AuthRateLimiter } from "./AuthRateLimiter.js";
|
|
23
29
|
export { AbilitiesBuilder } from "./bouncer/AbilitiesBuilder.js";
|
|
@@ -140,6 +146,7 @@ export {
|
|
|
140
146
|
MemoryRememberMeTokenDriver,
|
|
141
147
|
type MintedRememberMeToken,
|
|
142
148
|
mintRememberMeToken,
|
|
149
|
+
RememberMeToken,
|
|
143
150
|
type RememberMeTokenDriver,
|
|
144
151
|
type StoredRememberMeToken,
|
|
145
152
|
safeCompareHashes,
|
|
@@ -158,6 +165,7 @@ export type {
|
|
|
158
165
|
Scope,
|
|
159
166
|
} from "./rights/types.js";
|
|
160
167
|
export { scopeKey } from "./rights/types.js";
|
|
168
|
+
export { Secret } from "./Secret.js";
|
|
161
169
|
export type { ApiKeyConfig } from "./strategies/ApiKeyStrategy.js";
|
|
162
170
|
export { ApiKeyStrategy } from "./strategies/ApiKeyStrategy.js";
|
|
163
171
|
export type { BasicAuthConfig } from "./strategies/BasicAuthStrategy.js";
|
package/src/sanitize.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip the prototype-pollution keys from a user payload.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own module so `AuthManager` and `Authenticator` do not have to
|
|
5
|
+
* import each other's values: the manager builds an authenticator, and the
|
|
6
|
+
* authenticator sanitises — putting both in one file made a runtime cycle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { UserPayload } from "./AuthManager.js";
|
|
10
|
+
|
|
11
|
+
export function sanitizePayload(user: UserPayload): void {
|
|
12
|
+
for (const key of ["__proto__", "constructor", "prototype"]) {
|
|
13
|
+
if (key in user) {
|
|
14
|
+
delete (user as Record<string, unknown>)[key];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -69,6 +69,9 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
69
69
|
name = "session";
|
|
70
70
|
#config: SessionStrategyConfig;
|
|
71
71
|
#sessionKey: string;
|
|
72
|
+
#viaRemember = false;
|
|
73
|
+
#loggedOut = false;
|
|
74
|
+
#attemptedViaRemember = false;
|
|
72
75
|
|
|
73
76
|
constructor(config: SessionStrategyConfig) {
|
|
74
77
|
this.#config = config;
|
|
@@ -85,6 +88,47 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
85
88
|
return this.#config.rememberMeTokens !== undefined;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Whether the current user was revived from a remember-me cookie rather
|
|
93
|
+
* than signing in (AdonisJS `viaRemember`).
|
|
94
|
+
*
|
|
95
|
+
* This is the distinction that lets an app demand the password again before
|
|
96
|
+
* something sensitive — changing an email, spending money, deleting an
|
|
97
|
+
* account. Nothing reported it, so a session restored from a cookie looked
|
|
98
|
+
* exactly like one where the user had just typed their password.
|
|
99
|
+
*/
|
|
100
|
+
get viaRemember(): boolean {
|
|
101
|
+
return this.#viaRemember;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether a remember-me token was even tried on this request (AdonisJS
|
|
106
|
+
* `attemptedViaRemember`) — true whether or not it worked.
|
|
107
|
+
*/
|
|
108
|
+
get attemptedViaRemember(): boolean {
|
|
109
|
+
return this.#attemptedViaRemember;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether `logout()` ran on this guard (AdonisJS `isLoggedOut`).
|
|
114
|
+
*
|
|
115
|
+
* A handler that logs out and then keeps working — clearing a cart, writing
|
|
116
|
+
* an audit line — could not tell that the session was already gone.
|
|
117
|
+
*/
|
|
118
|
+
get isLoggedOut(): boolean {
|
|
119
|
+
return this.#loggedOut;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The session key the user id is stored under (AdonisJS `sessionKeyName`). */
|
|
123
|
+
get sessionKeyName(): string {
|
|
124
|
+
return this.#sessionKey;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The cookie key the remember-me token is stored under (AdonisJS `rememberMeKeyName`). */
|
|
128
|
+
get rememberMeKeyName(): string {
|
|
129
|
+
return this.rememberMeCookieName;
|
|
130
|
+
}
|
|
131
|
+
|
|
88
132
|
#rememberMeAge(): number {
|
|
89
133
|
return this.#config.rememberMeAge ?? DEFAULT_REMEMBER_AGE_SECONDS;
|
|
90
134
|
}
|
|
@@ -114,6 +158,7 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
114
158
|
): Promise<{ user: UserPayload; cookieValue: string } | null> {
|
|
115
159
|
const driver = this.#config.rememberMeTokens;
|
|
116
160
|
if (!driver) return null;
|
|
161
|
+
this.#attemptedViaRemember = true;
|
|
117
162
|
|
|
118
163
|
const recycled = await verifyAndRecycleRememberMeToken(
|
|
119
164
|
driver,
|
|
@@ -125,6 +170,7 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
125
170
|
const user = await this.#config.findUser(recycled.userId);
|
|
126
171
|
if (!user) return null;
|
|
127
172
|
|
|
173
|
+
this.#viaRemember = true;
|
|
128
174
|
return { user, cookieValue: recycled.value };
|
|
129
175
|
}
|
|
130
176
|
|
|
@@ -197,6 +243,11 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
197
243
|
async login(user: UserPayload, session: SessionStore): Promise<void> {
|
|
198
244
|
session.regenerate();
|
|
199
245
|
session.put(this.#sessionKey, user.id);
|
|
246
|
+
this.#loggedOut = false;
|
|
247
|
+
// A password was typed: this session is no longer "via remember", even
|
|
248
|
+
// if a cookie was tried earlier in the same request. Without the reset
|
|
249
|
+
// the flag would stay true and a re-auth prompt would never fire.
|
|
250
|
+
this.#viaRemember = false;
|
|
200
251
|
}
|
|
201
252
|
|
|
202
253
|
/**
|
|
@@ -218,5 +269,8 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
218
269
|
*/
|
|
219
270
|
async logout(session: SessionStore): Promise<void> {
|
|
220
271
|
session.forget(this.#sessionKey);
|
|
272
|
+
this.#viaRemember = false;
|
|
273
|
+
this.#attemptedViaRemember = false;
|
|
274
|
+
this.#loggedOut = true;
|
|
221
275
|
}
|
|
222
276
|
}
|