moved auth ctx to request context, cleaned up system controller

This commit is contained in:
dswbx
2025-02-14 20:32:09 +01:00
parent fb76f8d789
commit 21a8f56c48
10 changed files with 131 additions and 166 deletions

View File

@@ -104,10 +104,9 @@ export class AuthController extends Controller {
}
hono.get("/me", auth(), async (c) => {
if (this.auth.authenticator.isUserLoggedIn()) {
const claims = this.auth.authenticator.getUser()!;
const claims = c.get("auth")?.user;
if (claims) {
const { data: user } = await this.userRepo.findId(claims.id);
return c.json({ user });
}

View File

@@ -106,17 +106,15 @@ export type AuthUserResolver = (
identifier: string,
profile: ProfileExchange
) => Promise<SafeUser | undefined>;
type AuthClaims = SafeUser & {
iat: number;
iss?: string;
exp?: number;
};
export class Authenticator<Strategies extends Record<string, Strategy> = Record<string, Strategy>> {
private readonly strategies: Strategies;
private readonly config: AuthConfig;
private _claims:
| undefined
| (SafeUser & {
iat: number;
iss?: string;
exp?: number;
});
private readonly userResolver: AuthUserResolver;
constructor(strategies: Strategies, userResolver?: AuthUserResolver, config?: AuthConfig) {
@@ -148,21 +146,6 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
return this.strategies;
}
isUserLoggedIn(): boolean {
return this._claims !== undefined;
}
getUser(): SafeUser | undefined {
if (!this._claims) return;
const { iat, exp, iss, ...user } = this._claims;
return user;
}
resetUser() {
this._claims = undefined;
}
strategy<
StrategyName extends keyof Strategies,
Strat extends Strategy = Strategies[StrategyName]
@@ -206,7 +189,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
return sign(payload, secret, this.config.jwt?.alg ?? "HS256");
}
async verify(jwt: string): Promise<boolean> {
async verify(jwt: string): Promise<AuthClaims | undefined> {
try {
const payload = await verify(
jwt,
@@ -221,14 +204,10 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
}
this._claims = payload as any;
return true;
} catch (e) {
this.resetUser();
//console.error(e);
}
return payload as any;
} catch (e) {}
return false;
return;
}
private get cookieOptions(): CookieOptions {
@@ -258,8 +237,8 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
}
async requestCookieRefresh(c: Context) {
if (this.config.cookie.renew && this.isUserLoggedIn()) {
async requestCookieRefresh(c: Context<ServerEnv>) {
if (this.config.cookie.renew && c.get("auth")?.user) {
const token = await this.getAuthCookie(c);
if (token) {
await this.setAuthCookie(c, token);
@@ -276,13 +255,14 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
await deleteCookie(c, "auth", this.cookieOptions);
}
async logout(c: Context) {
async logout(c: Context<ServerEnv>) {
c.set("auth", undefined);
const cookie = await this.getAuthCookie(c);
if (cookie) {
await this.deleteAuthCookie(c);
await addFlashMessage(c, "Signed out", "info");
}
this.resetUser();
}
// @todo: move this to a server helper
@@ -353,8 +333,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
if (token) {
await this.verify(token);
return this.getUser();
return await this.verify(token);
}
return undefined;

View File

@@ -1,21 +1,23 @@
import { Exception, Permission } from "core";
import { objectTransform } from "core/utils";
import type { Context } from "hono";
import type { ServerEnv } from "modules/Module";
import { Role } from "./Role";
export type GuardUserContext = {
role: string | null | undefined;
role?: string | null;
[key: string]: any;
};
export type GuardConfig = {
enabled?: boolean;
};
export type GuardContext = Context<ServerEnv> | GuardUserContext;
const debug = false;
export class Guard {
permissions: Permission[];
user?: GuardUserContext;
roles?: Role[];
config?: GuardConfig;
@@ -89,24 +91,19 @@ export class Guard {
return this;
}
setUserContext(user: GuardUserContext | undefined) {
this.user = user;
return this;
}
getUserRole(): Role | undefined {
if (this.user && typeof this.user.role === "string") {
const role = this.roles?.find((role) => role.name === this.user?.role);
getUserRole(user?: GuardUserContext): Role | undefined {
if (user && typeof user.role === "string") {
const role = this.roles?.find((role) => role.name === user?.role);
if (role) {
debug && console.log("guard: role found", [this.user.role]);
debug && console.log("guard: role found", [user.role]);
return role;
}
}
debug &&
console.log("guard: role not found", {
user: this.user,
role: this.user?.role
user: user,
role: user?.role
});
return this.getDefaultRole();
}
@@ -119,9 +116,9 @@ export class Guard {
return this.config?.enabled === true;
}
hasPermission(permission: Permission): boolean;
hasPermission(name: string): boolean;
hasPermission(permissionOrName: Permission | string): boolean {
hasPermission(permission: Permission, user?: GuardUserContext): boolean;
hasPermission(name: string, user?: GuardUserContext): boolean;
hasPermission(permissionOrName: Permission | string, user?: GuardUserContext): boolean {
if (!this.isEnabled()) {
//console.log("guard not enabled, allowing");
return true;
@@ -133,7 +130,7 @@ export class Guard {
throw new Error(`Permission ${name} does not exist`);
}
const role = this.getUserRole();
const role = this.getUserRole(user);
if (!role) {
debug && console.log("guard: role not found, denying");
@@ -156,12 +153,13 @@ export class Guard {
return !!rolePermission;
}
granted(permission: Permission | string): boolean {
return this.hasPermission(permission as any);
granted(permission: Permission | string, c?: GuardContext): boolean {
const user = c && "get" in c ? c.get("auth")?.user : c;
return this.hasPermission(permission as any, user);
}
throwUnlessGranted(permission: Permission | string) {
if (!this.granted(permission)) {
throwUnlessGranted(permission: Permission | string, c: GuardContext) {
if (!this.granted(permission, c)) {
throw new Exception(
`Permission "${typeof permission === "string" ? permission : permission.name}" not granted`,
403

View File

@@ -10,7 +10,12 @@ function getPath(reqOrCtx: Request | Context) {
}
export function shouldSkip(c: Context<ServerEnv>, skip?: (string | RegExp)[]) {
if (c.get("auth_skip")) return true;
const authCtx = c.get("auth");
if (!authCtx) {
throw new Error("auth ctx not found");
}
if (authCtx.skip) return true;
const req = c.req.raw;
if (!skip) return false;
@@ -18,7 +23,7 @@ export function shouldSkip(c: Context<ServerEnv>, skip?: (string | RegExp)[]) {
const path = getPath(req);
const result = skip.some((s) => patternMatch(path, s));
c.set("auth_skip", result);
authCtx.skip = result;
return result;
}
@@ -26,29 +31,31 @@ export const auth = (options?: {
skip?: (string | RegExp)[];
}) =>
createMiddleware<ServerEnv>(async (c, next) => {
if (!c.get("auth")) {
c.set("auth", {
registered: false,
resolved: false,
skip: false,
user: undefined
});
}
const app = c.get("app");
const guard = app?.modules.ctx().guard;
const authCtx = c.get("auth")!;
const authenticator = app?.module.auth.authenticator;
let skipped = shouldSkip(c, options?.skip) || !app?.module.auth.enabled;
// make sure to only register once
if (c.get("auth_registered")) {
if (authCtx.registered) {
skipped = true;
console.warn(`auth middleware already registered for ${getPath(c)}`);
} else {
c.set("auth_registered", true);
authCtx.registered = true;
if (!skipped) {
const resolved = c.get("auth_resolved");
if (!resolved) {
if (!app?.module.auth.enabled) {
guard?.setUserContext(undefined);
} else {
guard?.setUserContext(await authenticator?.resolveAuthFromRequest(c));
c.set("auth_resolved", true);
}
}
if (!skipped && !authCtx.resolved && app?.module.auth.enabled) {
authCtx.user = await authenticator?.resolveAuthFromRequest(c);
authCtx.resolved = true;
}
}
@@ -60,9 +67,9 @@ export const auth = (options?: {
}
// release
guard?.setUserContext(undefined);
authenticator?.resetUser();
c.set("auth_resolved", false);
authCtx.skip = false;
authCtx.resolved = false;
authCtx.user = undefined;
});
export const permission = (
@@ -75,23 +82,26 @@ export const permission = (
// @ts-ignore
createMiddleware<ServerEnv>(async (c, next) => {
const app = c.get("app");
//console.log("skip?", c.get("auth_skip"));
const authCtx = c.get("auth");
if (!authCtx) {
throw new Error("auth ctx not found");
}
// in tests, app is not defined
if (!c.get("auth_registered") || !app) {
if (!authCtx.registered || !app) {
const msg = `auth middleware not registered, cannot check permissions for ${getPath(c)}`;
if (app?.module.auth.enabled) {
throw new Error(msg);
} else {
console.warn(msg);
}
} else if (!c.get("auth_skip")) {
} else if (!authCtx.skip) {
const guard = app.modules.ctx().guard;
const permissions = Array.isArray(permission) ? permission : [permission];
if (options?.onGranted || options?.onDenied) {
let returned: undefined | void | Response;
if (permissions.every((p) => guard.granted(p))) {
if (permissions.every((p) => guard.granted(p, c))) {
returned = await options?.onGranted?.(c);
} else {
returned = await options?.onDenied?.(c);
@@ -100,7 +110,7 @@ export const permission = (
return returned;
}
} else {
permissions.some((p) => guard.throwUnlessGranted(p));
permissions.some((p) => guard.throwUnlessGranted(p, c));
}
}