@c9up/warden 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AuthManager.d.ts +45 -9
- package/dist/AuthManager.d.ts.map +1 -1
- package/dist/AuthManager.js +75 -23
- package/dist/AuthManager.js.map +1 -1
- package/dist/Authenticator.d.ts +128 -0
- package/dist/Authenticator.d.ts.map +1 -0
- package/dist/Authenticator.js +296 -0
- package/dist/Authenticator.js.map +1 -0
- package/dist/WardenProvider.d.ts.map +1 -1
- package/dist/WardenProvider.js +51 -23
- package/dist/WardenProvider.js.map +1 -1
- package/dist/bouncer/AbilitiesBuilder.d.ts +20 -0
- package/dist/bouncer/AbilitiesBuilder.d.ts.map +1 -0
- package/dist/bouncer/AbilitiesBuilder.js +32 -0
- package/dist/bouncer/AbilitiesBuilder.js.map +1 -0
- package/dist/bouncer/AuthorizationResponse.d.ts +18 -8
- package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -1
- package/dist/bouncer/AuthorizationResponse.js +20 -8
- package/dist/bouncer/AuthorizationResponse.js.map +1 -1
- package/dist/bouncer/BasePolicy.d.ts +4 -3
- package/dist/bouncer/BasePolicy.d.ts.map +1 -1
- package/dist/bouncer/BasePolicy.js.map +1 -1
- package/dist/bouncer/Bouncer.d.ts +9 -2
- package/dist/bouncer/Bouncer.d.ts.map +1 -1
- package/dist/bouncer/Bouncer.js +59 -10
- package/dist/bouncer/Bouncer.js.map +1 -1
- package/dist/bouncer/PolicyAuthorizer.d.ts +2 -1
- package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
- package/dist/bouncer/PolicyAuthorizer.js +11 -3
- package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
- package/dist/bouncer/evaluate.d.ts +1 -1
- package/dist/bouncer/evaluate.d.ts.map +1 -1
- package/dist/bouncer/evaluate.js +3 -2
- package/dist/bouncer/evaluate.js.map +1 -1
- package/dist/bouncer/types.d.ts +22 -1
- package/dist/bouncer/types.d.ts.map +1 -1
- package/dist/config.d.ts +34 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +15 -13
- package/dist/configure.js.map +1 -1
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +37 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/mfa/OtpProvider.d.ts.map +1 -1
- package/dist/mfa/OtpProvider.js +21 -3
- package/dist/mfa/OtpProvider.js.map +1 -1
- package/dist/mfa/WebauthnProvider.d.ts +24 -2
- package/dist/mfa/WebauthnProvider.d.ts.map +1 -1
- package/dist/mfa/WebauthnProvider.js +28 -8
- package/dist/mfa/WebauthnProvider.js.map +1 -1
- package/dist/middleware.d.ts +29 -6
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +85 -166
- package/dist/middleware.js.map +1 -1
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +21 -13
- package/dist/standalone.js.map +1 -1
- package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -1
- package/dist/strategies/ApiKeyStrategy.js +4 -1
- package/dist/strategies/ApiKeyStrategy.js.map +1 -1
- package/dist/strategies/SessionStrategy.js +2 -2
- package/dist/strategies/SessionStrategy.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -1
- package/src/AuthManager.ts +125 -28
- package/src/Authenticator.ts +385 -0
- package/src/WardenProvider.ts +51 -25
- package/src/bouncer/AbilitiesBuilder.ts +41 -0
- package/src/bouncer/AuthorizationResponse.ts +24 -16
- package/src/bouncer/BasePolicy.ts +4 -2
- package/src/bouncer/Bouncer.ts +77 -14
- package/src/bouncer/PolicyAuthorizer.ts +14 -4
- package/src/bouncer/evaluate.ts +4 -2
- package/src/bouncer/types.ts +24 -1
- package/src/config.ts +47 -3
- package/src/configure.ts +15 -13
- package/src/errors.ts +43 -1
- package/src/index.ts +22 -2
- package/src/mfa/OtpProvider.ts +31 -3
- package/src/mfa/WebauthnProvider.ts +58 -12
- package/src/middleware.ts +112 -207
- package/src/standalone.ts +22 -14
- package/src/strategies/ApiKeyStrategy.ts +4 -1
- package/src/strategies/SessionStrategy.ts +2 -2
package/src/middleware.ts
CHANGED
|
@@ -16,17 +16,13 @@
|
|
|
16
16
|
* }
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
type AuthResult,
|
|
22
|
-
type AuthStrategy,
|
|
23
|
-
sanitizePayload,
|
|
24
|
-
} from "./AuthManager.js";
|
|
19
|
+
import { Authenticator } from "./Authenticator.js";
|
|
20
|
+
import { AuthManager, type UserPayload } from "./AuthManager.js";
|
|
25
21
|
import type { BasePolicy } from "./bouncer/BasePolicy.js";
|
|
26
22
|
import { Bouncer } from "./bouncer/Bouncer.js";
|
|
27
23
|
import type { Ability } from "./bouncer/types.js";
|
|
28
24
|
import type { ScopeRequestContext } from "./config.js";
|
|
29
|
-
import { WardenError } from "./errors.js";
|
|
25
|
+
import { E_UNAUTHORIZED_ACCESS, WardenError } from "./errors.js";
|
|
30
26
|
import {
|
|
31
27
|
getGuardMetadata,
|
|
32
28
|
getPermissionMetadata,
|
|
@@ -37,18 +33,6 @@ import { RightsResolver } from "./rights/RightsResolver.js";
|
|
|
37
33
|
import type { Scope } from "./rights/types.js";
|
|
38
34
|
import type { SessionStore } from "./strategies/SessionStrategy.js";
|
|
39
35
|
|
|
40
|
-
interface StrategyWithContext extends AuthStrategy {
|
|
41
|
-
verifyWithContext(token: string, ctx: unknown): Promise<AuthResult>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function hasVerifyWithContext(
|
|
45
|
-
strategy: AuthStrategy,
|
|
46
|
-
): strategy is StrategyWithContext {
|
|
47
|
-
return (
|
|
48
|
-
typeof (strategy as StrategyWithContext).verifyWithContext === "function"
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
36
|
/**
|
|
53
37
|
* Tokens warden resolves from the container: a service class, or a string/symbol
|
|
54
38
|
* alias (`"bouncer:registry"`). Mirrors Ream's `ServiceToken` without importing
|
|
@@ -69,7 +53,7 @@ type ResolvableToken =
|
|
|
69
53
|
* misconfigured kernel) yields no resolution: guarded routes fail CLOSED.
|
|
70
54
|
*/
|
|
71
55
|
interface ContainerResolver {
|
|
72
|
-
make(token: ResolvableToken): unknown
|
|
56
|
+
make(token: ResolvableToken): Promise<unknown>;
|
|
73
57
|
}
|
|
74
58
|
|
|
75
59
|
/**
|
|
@@ -100,6 +84,13 @@ export interface WardenContext {
|
|
|
100
84
|
response: {
|
|
101
85
|
status: (code: number) => unknown;
|
|
102
86
|
json: (data: unknown) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Redirect the response (Ream's `ctx.response.redirect`). Optional so a
|
|
89
|
+
* minimal host without it still gets the 401 JSON fallback — Warden does
|
|
90
|
+
* its own content negotiation (see `renderAuthError`) rather than leaning
|
|
91
|
+
* on the host's exception handler.
|
|
92
|
+
*/
|
|
93
|
+
redirect?: (url: string) => unknown;
|
|
103
94
|
};
|
|
104
95
|
/**
|
|
105
96
|
* Matched-route info — Ream (like Adonis) exposes the controller class +
|
|
@@ -120,8 +111,12 @@ export interface WardenContext {
|
|
|
120
111
|
action?: string | symbol;
|
|
121
112
|
/** Session store — set by a session middleware upstream. */
|
|
122
113
|
session?: SessionStore;
|
|
123
|
-
/**
|
|
124
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The per-request {@link Authenticator} (Ream's `auth` slot). `silentAuth`
|
|
116
|
+
* (global) or `wardenMiddleware` (per guarded route) sets it; inline handlers
|
|
117
|
+
* then call `ctx.auth.authenticate()` / `ctx.auth.use('session').login(user)`.
|
|
118
|
+
*/
|
|
119
|
+
auth?: Authenticator;
|
|
125
120
|
/**
|
|
126
121
|
* Per-request authorization entry point — set by `initializeBouncer`. Typed
|
|
127
122
|
* as the agnostic {@link Authorizer} contract (Ream's `ctx.bouncer` slot),
|
|
@@ -137,9 +132,12 @@ export interface WardenContext {
|
|
|
137
132
|
* AuthManager gate in `wardenMiddleware`). No `@c9up/ream` import: warden reads
|
|
138
133
|
* only from the context Ream hands it.
|
|
139
134
|
*/
|
|
140
|
-
function resolveFromCtx(
|
|
135
|
+
async function resolveFromCtx(
|
|
136
|
+
ctx: WardenContext,
|
|
137
|
+
token: ResolvableToken,
|
|
138
|
+
): Promise<unknown> {
|
|
141
139
|
try {
|
|
142
|
-
return ctx.containerResolver?.make(token);
|
|
140
|
+
return await ctx.containerResolver?.make(token);
|
|
143
141
|
} catch {
|
|
144
142
|
return undefined;
|
|
145
143
|
}
|
|
@@ -162,7 +160,7 @@ type WardenNext = () => Promise<void> | void;
|
|
|
162
160
|
* and authenticates + authorizes the request.
|
|
163
161
|
*/
|
|
164
162
|
export async function wardenMiddleware(ctx: WardenContext, next: WardenNext) {
|
|
165
|
-
const resolvedAuth = resolveFromCtx(ctx, AuthManager);
|
|
163
|
+
const resolvedAuth = await resolveFromCtx(ctx, AuthManager);
|
|
166
164
|
if (!(resolvedAuth instanceof AuthManager)) {
|
|
167
165
|
// Fail CLOSED — a missing AuthManager (WardenProvider not registered, or
|
|
168
166
|
// no `ctx.containerResolver` on a non-Ream host) must never silently let
|
|
@@ -190,75 +188,33 @@ export async function wardenMiddleware(ctx: WardenContext, next: WardenNext) {
|
|
|
190
188
|
return;
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return (s as { headerName?: string }).headerName ?? "x-api-key";
|
|
207
|
-
} catch {
|
|
208
|
-
return "x-api-key";
|
|
191
|
+
// Delegate authentication to the per-request Authenticator (the SAME contract
|
|
192
|
+
// inline handlers use via `ctx.auth.authenticate()`), so the decorator path
|
|
193
|
+
// and the functional path share one code path. It sets `ctx.auth` and throws
|
|
194
|
+
// on failure — E_UNAUTHORIZED_ACCESS (401, credential rejection) or a
|
|
195
|
+
// WARDEN_AUTH_STRATEGY_ERROR (500, every attempted guard crashed).
|
|
196
|
+
const authenticator = ensureAuthenticator(ctx, auth);
|
|
197
|
+
const loginRoute = await resolveLoginRoute(ctx);
|
|
198
|
+
try {
|
|
199
|
+
await authenticator.authenticateUsing(strategies, { loginRoute });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err instanceof E_UNAUTHORIZED_ACCESS) {
|
|
202
|
+
renderAuthError(ctx, err);
|
|
203
|
+
return;
|
|
209
204
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// declared with `headerName: "X-Custom-Key"` would never match the
|
|
214
|
-
// incoming `x-custom-key` key without this normalisation — auth
|
|
215
|
-
// would silently fail even with the right header on the wire.
|
|
216
|
-
const apiKey = headers[apiKeyHeader.toLowerCase()] ?? "";
|
|
217
|
-
|
|
218
|
-
const hasSessionStrategy = strategies.some((s) => s === "session");
|
|
219
|
-
if (!bearerToken && !apiKey && !hasSessionStrategy) {
|
|
220
|
-
ctx.response.status(401);
|
|
221
|
-
ctx.response.json({
|
|
222
|
-
error: {
|
|
223
|
-
code: "UNAUTHORIZED",
|
|
224
|
-
message: "Missing authentication token (Bearer or x-api-key)",
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Try each declared strategy. If every attempted strategy crashed AND none
|
|
231
|
-
// succeeded, the caller returns 500 instead of 401 — a server-side incident
|
|
232
|
-
// must stay observably distinct from a credential rejection.
|
|
233
|
-
const { result, attemptCount, crashCount } = await tryAuthenticate(
|
|
234
|
-
auth,
|
|
235
|
-
strategies,
|
|
236
|
-
{ bearerToken, apiKey, session: ctx.session, hasSessionStrategy },
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
if (!result?.authenticated || !result.user) {
|
|
240
|
-
if (attemptCount > 0 && crashCount === attemptCount) {
|
|
205
|
+
// Every attempted guard crashed → server-side incident, mapped to 500 so
|
|
206
|
+
// it stays observably distinct from a credential rejection.
|
|
207
|
+
if (err instanceof WardenError && err.status === 500) {
|
|
241
208
|
ctx.response.status(500);
|
|
242
209
|
ctx.response.json({
|
|
243
|
-
error: {
|
|
244
|
-
code: "AUTH_STRATEGY_ERROR",
|
|
245
|
-
message:
|
|
246
|
-
"Authentication unavailable — one or more strategies failed. Check server logs.",
|
|
247
|
-
},
|
|
210
|
+
error: { code: "AUTH_STRATEGY_ERROR", message: err.message },
|
|
248
211
|
});
|
|
249
212
|
return;
|
|
250
213
|
}
|
|
251
|
-
|
|
252
|
-
ctx.response.json({
|
|
253
|
-
error: {
|
|
254
|
-
code: "UNAUTHORIZED",
|
|
255
|
-
message: result?.error ?? "Authentication failed",
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
return;
|
|
214
|
+
throw err;
|
|
259
215
|
}
|
|
260
216
|
|
|
261
|
-
const user =
|
|
217
|
+
const user = authenticator.getUserOrFail();
|
|
262
218
|
|
|
263
219
|
// Permission + role checks are independent AND gates: the user must satisfy
|
|
264
220
|
// ALL required permissions AND ALL required roles. Having a role does NOT
|
|
@@ -293,8 +249,7 @@ export async function wardenMiddleware(ctx: WardenContext, next: WardenNext) {
|
|
|
293
249
|
}
|
|
294
250
|
}
|
|
295
251
|
|
|
296
|
-
// Auth successful —
|
|
297
|
-
ctx.auth = result;
|
|
252
|
+
// Auth successful — `ctx.auth` (the Authenticator) already carries the user.
|
|
298
253
|
await next();
|
|
299
254
|
}
|
|
300
255
|
|
|
@@ -316,56 +271,80 @@ export async function wardenMiddleware(ctx: WardenContext, next: WardenNext) {
|
|
|
316
271
|
* middleware / route guard enforces).
|
|
317
272
|
*
|
|
318
273
|
* Uses the AuthManager's DEFAULT strategy (Adonis's "default guard"). A missing
|
|
319
|
-
* AuthManager
|
|
320
|
-
*
|
|
274
|
+
* AuthManager leaves the request a guest with `ctx.auth` unset; otherwise
|
|
275
|
+
* `ctx.auth` is ALWAYS set to an {@link Authenticator} (AdonisJS
|
|
276
|
+
* `initialize_auth_middleware` parity) — populated when credentials verify,
|
|
277
|
+
* left as a guest (`isAuthenticated === false`) when they're absent, invalid, or
|
|
278
|
+
* a guard crashes. Silent auth NEVER surfaces a 401/500.
|
|
321
279
|
*/
|
|
322
280
|
export async function silentAuth(ctx: WardenContext, next: WardenNext) {
|
|
323
|
-
const resolvedAuth = resolveFromCtx(ctx, AuthManager);
|
|
281
|
+
const resolvedAuth = await resolveFromCtx(ctx, AuthManager);
|
|
324
282
|
// Non-enforcing: a host without warden wired runs as guest. (Enforcement
|
|
325
283
|
// middlewares fail CLOSED on a missing AuthManager; silent auth does not.)
|
|
326
284
|
if (!(resolvedAuth instanceof AuthManager)) {
|
|
327
285
|
await next();
|
|
328
286
|
return;
|
|
329
287
|
}
|
|
330
|
-
const
|
|
288
|
+
const authenticator = ensureAuthenticator(ctx, resolvedAuth);
|
|
289
|
+
try {
|
|
290
|
+
// `check()` runs the default guard and returns a boolean instead of
|
|
291
|
+
// throwing on a credential rejection; the catch swallows the residual
|
|
292
|
+
// crash/config path so silent auth stays strictly non-enforcing.
|
|
293
|
+
await authenticator.check();
|
|
294
|
+
} catch {
|
|
295
|
+
// Guard crash / config error → stay a guest, never surface here.
|
|
296
|
+
}
|
|
297
|
+
await next();
|
|
298
|
+
}
|
|
331
299
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const apiKey = headers[apiKeyHeader.toLowerCase()] ?? "";
|
|
300
|
+
/**
|
|
301
|
+
* Reuse the request's Authenticator if one is already attached (e.g. `silentAuth`
|
|
302
|
+
* ran first), else build and attach a fresh one. Keeps `ctx.auth` a single
|
|
303
|
+
* per-request instance across the middleware chain.
|
|
304
|
+
*/
|
|
305
|
+
function ensureAuthenticator(
|
|
306
|
+
ctx: WardenContext,
|
|
307
|
+
auth: AuthManager,
|
|
308
|
+
): Authenticator {
|
|
309
|
+
if (ctx.auth instanceof Authenticator) return ctx.auth;
|
|
310
|
+
const authenticator = new Authenticator(ctx, auth);
|
|
311
|
+
ctx.auth = authenticator;
|
|
312
|
+
return authenticator;
|
|
313
|
+
}
|
|
347
314
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Optional login route (`ctx.response.redirect` target for session-guard HTML
|
|
317
|
+
* flows). Registered by `WardenProvider` from `config.auth.loginRoute` under the
|
|
318
|
+
* `"warden:loginRoute"` token; absent ⇒ no redirect (401 JSON).
|
|
319
|
+
*/
|
|
320
|
+
async function resolveLoginRoute(
|
|
321
|
+
ctx: WardenContext,
|
|
322
|
+
): Promise<string | undefined> {
|
|
323
|
+
const value = await resolveFromCtx(ctx, "warden:loginRoute");
|
|
324
|
+
return typeof value === "string" ? value : undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Render an `E_UNAUTHORIZED_ACCESS` to the response — Warden does its OWN content
|
|
329
|
+
* negotiation (AdonisJS session renderer parity) instead of leaning on the
|
|
330
|
+
* host's exception handler, so it stays agnostic. An `Accept: text/html` request
|
|
331
|
+
* against a session guard carrying a `redirectTo` is redirected to the login
|
|
332
|
+
* route; every other case gets a 401 JSON body. Always fail-closed.
|
|
333
|
+
*/
|
|
334
|
+
export function renderAuthError(
|
|
335
|
+
ctx: WardenContext,
|
|
336
|
+
error: E_UNAUTHORIZED_ACCESS,
|
|
337
|
+
): void {
|
|
338
|
+
const accept = ctx.request.headers().accept ?? "";
|
|
339
|
+
const wantsHtml = accept.includes("text/html");
|
|
340
|
+
if (wantsHtml && error.redirectTo && ctx.response.redirect) {
|
|
341
|
+
ctx.response.redirect(error.redirectTo);
|
|
353
342
|
return;
|
|
354
343
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// must NOT surface as 401/500 here — silent auth only ever populates or skips.
|
|
359
|
-
const { result } = await tryAuthenticate(auth, [strategy], {
|
|
360
|
-
bearerToken,
|
|
361
|
-
apiKey,
|
|
362
|
-
session: ctx.session,
|
|
363
|
-
hasSessionStrategy,
|
|
344
|
+
ctx.response.status(error.status ?? 401);
|
|
345
|
+
ctx.response.json({
|
|
346
|
+
error: { code: error.code, message: error.message },
|
|
364
347
|
});
|
|
365
|
-
if (result?.authenticated && result.user) {
|
|
366
|
-
ctx.auth = result;
|
|
367
|
-
}
|
|
368
|
-
await next();
|
|
369
348
|
}
|
|
370
349
|
|
|
371
350
|
/**
|
|
@@ -396,9 +375,9 @@ export async function initializeBouncer(ctx: WardenContext, next: WardenNext) {
|
|
|
396
375
|
// Both dependencies are registered by WardenProvider. Resolve defensively:
|
|
397
376
|
// a host that wired the middleware but not the provider gets an empty Bouncer
|
|
398
377
|
// (no abilities/resolver) rather than a crash on every request.
|
|
399
|
-
const resolved = tryResolve(ctx, RightsResolver);
|
|
378
|
+
const resolved = await tryResolve(ctx, RightsResolver);
|
|
400
379
|
const resolver = resolved instanceof RightsResolver ? resolved : undefined;
|
|
401
|
-
const candidate = tryResolve(ctx, "bouncer:registry");
|
|
380
|
+
const candidate = await tryResolve(ctx, "bouncer:registry");
|
|
402
381
|
const registry = isBouncerRegistry(candidate) ? candidate : undefined;
|
|
403
382
|
|
|
404
383
|
const user = ctx.auth?.user ?? null;
|
|
@@ -414,7 +393,10 @@ export async function initializeBouncer(ctx: WardenContext, next: WardenNext) {
|
|
|
414
393
|
}
|
|
415
394
|
|
|
416
395
|
/** Resolve a container token from the request resolver, returning undefined instead of throwing when absent. */
|
|
417
|
-
function tryResolve(
|
|
396
|
+
async function tryResolve(
|
|
397
|
+
ctx: WardenContext,
|
|
398
|
+
token: ResolvableToken,
|
|
399
|
+
): Promise<unknown> {
|
|
418
400
|
return resolveFromCtx(ctx, token);
|
|
419
401
|
}
|
|
420
402
|
|
|
@@ -435,90 +417,13 @@ function isBouncerRegistry(value: unknown): value is BouncerRegistry {
|
|
|
435
417
|
* hardcoded "global" and tenant-scoped roles/grants never satisfied @Role/@Permission.
|
|
436
418
|
*/
|
|
437
419
|
async function resolveRequestScope(ctx: WardenContext): Promise<Scope> {
|
|
438
|
-
const candidate = tryResolve(ctx, "bouncer:registry");
|
|
420
|
+
const candidate = await tryResolve(ctx, "bouncer:registry");
|
|
439
421
|
const registry = isBouncerRegistry(candidate) ? candidate : undefined;
|
|
440
422
|
return registry?.resolveScope
|
|
441
423
|
? await registry.resolveScope(toScopeContext(ctx))
|
|
442
424
|
: "global";
|
|
443
425
|
}
|
|
444
426
|
|
|
445
|
-
/**
|
|
446
|
-
* Try each declared strategy in order — session strategies via
|
|
447
|
-
* `verifyWithContext()`, others via `verify(token)` with native-first credential
|
|
448
|
-
* fallback. Distinguishes crashes (strategy threw / `strategyCrash`) from
|
|
449
|
-
* credential rejections so the caller can return 500 vs 401.
|
|
450
|
-
*/
|
|
451
|
-
async function tryAuthenticate(
|
|
452
|
-
auth: AuthManager,
|
|
453
|
-
strategies: string[],
|
|
454
|
-
creds: {
|
|
455
|
-
bearerToken: string;
|
|
456
|
-
apiKey: string;
|
|
457
|
-
session: SessionStore | undefined;
|
|
458
|
-
hasSessionStrategy: boolean;
|
|
459
|
-
},
|
|
460
|
-
): Promise<{
|
|
461
|
-
result: AuthResult | null;
|
|
462
|
-
attemptCount: number;
|
|
463
|
-
crashCount: number;
|
|
464
|
-
}> {
|
|
465
|
-
const { bearerToken, apiKey, session } = creds;
|
|
466
|
-
let result: AuthResult | null = null;
|
|
467
|
-
let attemptCount = 0;
|
|
468
|
-
let crashCount = 0;
|
|
469
|
-
for (const strategyName of strategies) {
|
|
470
|
-
try {
|
|
471
|
-
let r: AuthResult;
|
|
472
|
-
if (strategyName === "session") {
|
|
473
|
-
const strategy = auth.getStrategy(strategyName);
|
|
474
|
-
const verifyWithContext =
|
|
475
|
-
strategy && hasVerifyWithContext(strategy)
|
|
476
|
-
? strategy.verifyWithContext
|
|
477
|
-
: undefined;
|
|
478
|
-
if (verifyWithContext) {
|
|
479
|
-
attemptCount++;
|
|
480
|
-
r = await verifyWithContext.call(strategy, "", { session });
|
|
481
|
-
// The session path bypasses AuthManager.verify(), so apply the
|
|
482
|
-
// same prototype-pollution guard JWT / api-key users get there.
|
|
483
|
-
if (r.user) sanitizePayload(r.user);
|
|
484
|
-
} else {
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
} else {
|
|
488
|
-
// Native-first credential, other transport as fallback so a
|
|
489
|
-
// single-credential client still authenticates (and an invalid
|
|
490
|
-
// Bearer no longer masks a valid API key for the api-key strategy).
|
|
491
|
-
const credential =
|
|
492
|
-
strategyName === "api-key"
|
|
493
|
-
? apiKey || bearerToken
|
|
494
|
-
: bearerToken || apiKey;
|
|
495
|
-
if (!credential) continue;
|
|
496
|
-
attemptCount++;
|
|
497
|
-
r = await auth.verify(credential, strategyName);
|
|
498
|
-
}
|
|
499
|
-
if (r.authenticated) {
|
|
500
|
-
result = r;
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
if (r.strategyCrash === true) {
|
|
504
|
-
crashCount++;
|
|
505
|
-
console.error(
|
|
506
|
-
`[warden] strategy '${strategyName}' threw during verify(): ${r.error ?? "unknown error"}`,
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
} catch (err) {
|
|
510
|
-
// AuthManager rethrows structured WardenError sentinels — treat these
|
|
511
|
-
// as crashes too (config errors, SessionStrategy.USE_LOGIN, etc.).
|
|
512
|
-
crashCount++;
|
|
513
|
-
console.error(
|
|
514
|
-
`[warden] strategy '${strategyName}' threw during verify():`,
|
|
515
|
-
err,
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return { result, attemptCount, crashCount };
|
|
520
|
-
}
|
|
521
|
-
|
|
522
427
|
/**
|
|
523
428
|
* Gate the authenticated user against the route's @Permission / @Role decorators.
|
|
524
429
|
* Both are independent AND gates resolved ONCE against the same unified
|
|
@@ -527,7 +432,7 @@ async function tryAuthenticate(
|
|
|
527
432
|
*/
|
|
528
433
|
async function checkAuthorization(
|
|
529
434
|
auth: AuthManager,
|
|
530
|
-
user:
|
|
435
|
+
user: UserPayload,
|
|
531
436
|
controller: object,
|
|
532
437
|
action: string | symbol,
|
|
533
438
|
scope: Scope,
|
package/src/standalone.ts
CHANGED
|
@@ -52,25 +52,33 @@ export interface Warden {
|
|
|
52
52
|
* Create a standalone Warden instance — no Ream container needed.
|
|
53
53
|
*/
|
|
54
54
|
export function createWarden(config: WardenConfig): Warden {
|
|
55
|
-
// Mirror WardenProvider:
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
55
|
+
// Mirror WardenProvider: accept the AdonisJS `guards` form OR the legacy
|
|
56
|
+
// driver-keyed fields, building EVERY configured strategy — otherwise
|
|
57
|
+
// config.session / config.apiKey were silently dropped and a jwt-less config
|
|
58
|
+
// (e.g. { defaultStrategy: 'api-key', apiKey }) threw INVALID_CONFIG at boot
|
|
59
|
+
// (audit 2026-06-13). Keys match the @Guard names.
|
|
59
60
|
const strategies: Record<string, AuthStrategy> = {};
|
|
60
61
|
|
|
61
|
-
if (config.
|
|
62
|
-
strategies
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
if (config.guards && Object.keys(config.guards).length > 0) {
|
|
63
|
+
Object.assign(strategies, config.guards);
|
|
64
|
+
} else {
|
|
65
|
+
if (config.jwt) {
|
|
66
|
+
strategies.jwt = new JwtStrategy(config.jwt);
|
|
67
|
+
}
|
|
68
|
+
if (config.session) {
|
|
69
|
+
strategies.session = new SessionStrategy(config.session);
|
|
70
|
+
}
|
|
71
|
+
if (config.apiKey) {
|
|
72
|
+
const apiKey = new ApiKeyStrategy(config.apiKey);
|
|
73
|
+
strategies.access_tokens = apiKey;
|
|
74
|
+
strategies["api-key"] = apiKey;
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
const manager = new AuthManager({
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
default:
|
|
80
|
+
config.default ?? config.defaultStrategy ?? Object.keys(strategies)[0],
|
|
81
|
+
guards: strategies,
|
|
74
82
|
});
|
|
75
83
|
|
|
76
84
|
return {
|
|
@@ -16,7 +16,10 @@ export interface ApiKeyConfig {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export class ApiKeyStrategy implements AuthStrategy {
|
|
19
|
-
|
|
19
|
+
// AdonisJS names this guard driver "access_tokens". The legacy "api-key"
|
|
20
|
+
// spelling is still accepted as a guard name / credential trigger by the
|
|
21
|
+
// middleware and providers (see `API_KEY_GUARD_NAMES`) for back-compat.
|
|
22
|
+
name = "access_tokens";
|
|
20
23
|
#config: ApiKeyConfig;
|
|
21
24
|
#headerName: string;
|
|
22
25
|
|
|
@@ -58,9 +58,9 @@ export class SessionStrategy implements AuthStrategy {
|
|
|
58
58
|
// `strategy.authenticate()` directly.
|
|
59
59
|
throw new WardenError(
|
|
60
60
|
"USE_LOGIN",
|
|
61
|
-
"SessionStrategy
|
|
61
|
+
"SessionStrategy has no credential step. Verify the password yourself, then start the session via authManager.login(user, session) (or ctx.auth.use('session').login(user)).",
|
|
62
62
|
{
|
|
63
|
-
hint: "Verify the password yourself (e.g. via @c9up/sigil Hash.verify), then call SessionStrategy.login(user, session).",
|
|
63
|
+
hint: "Verify the password yourself (e.g. via @c9up/sigil Hash.verify), then call authManager.login(user, session) — SessionStrategy.login(user, session) also works directly.",
|
|
64
64
|
},
|
|
65
65
|
);
|
|
66
66
|
}
|