mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 20:37:21 +00:00
reworked authentication and permission handling
This commit is contained in:
@@ -91,10 +91,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
return this._controller;
|
||||
}
|
||||
|
||||
override onServerInit(hono: Hono<any>) {
|
||||
hono.use(auth);
|
||||
}
|
||||
|
||||
getSchema() {
|
||||
return authConfigSchema;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export class AuthController extends Controller {
|
||||
}
|
||||
|
||||
override getController() {
|
||||
const { auth } = this.middlewares;
|
||||
const hono = this.create();
|
||||
const strategies = this.auth.authenticator.getStrategies();
|
||||
|
||||
@@ -19,7 +20,7 @@ export class AuthController extends Controller {
|
||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||
}
|
||||
|
||||
hono.get("/me", async (c) => {
|
||||
hono.get("/me", auth(), async (c) => {
|
||||
if (this.auth.authenticator.isUserLoggedIn()) {
|
||||
return c.json({ user: await this.auth.authenticator.getUser() });
|
||||
}
|
||||
@@ -27,7 +28,7 @@ export class AuthController extends Controller {
|
||||
return c.json({ user: null }, 403);
|
||||
});
|
||||
|
||||
hono.get("/logout", async (c) => {
|
||||
hono.get("/logout", auth(), async (c) => {
|
||||
await this.auth.authenticator.logout(c);
|
||||
if (this.auth.authenticator.isJsonRequest(c)) {
|
||||
return c.json({ ok: true });
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Context, Hono } from "hono";
|
||||
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||
import { sign, verify } from "hono/jwt";
|
||||
import type { CookieOptions } from "hono/utils/cookie";
|
||||
import { omit } from "lodash-es";
|
||||
import type { ServerEnv } from "modules/Module";
|
||||
|
||||
type Input = any; // workaround
|
||||
export type JWTPayload = Parameters<typeof sign>[0];
|
||||
@@ -101,7 +101,13 @@ export type AuthUserResolver = (
|
||||
export class Authenticator<Strategies extends Record<string, Strategy> = Record<string, Strategy>> {
|
||||
private readonly strategies: Strategies;
|
||||
private readonly config: AuthConfig;
|
||||
private _user: SafeUser | undefined;
|
||||
private _claims:
|
||||
| undefined
|
||||
| (SafeUser & {
|
||||
iat: number;
|
||||
iss?: string;
|
||||
exp?: number;
|
||||
});
|
||||
private readonly userResolver: AuthUserResolver;
|
||||
|
||||
constructor(strategies: Strategies, userResolver?: AuthUserResolver, config?: AuthConfig) {
|
||||
@@ -134,16 +140,18 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
}
|
||||
|
||||
isUserLoggedIn(): boolean {
|
||||
return this._user !== undefined;
|
||||
return this._claims !== undefined;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this._user;
|
||||
getUser(): SafeUser | undefined {
|
||||
if (!this._claims) return;
|
||||
|
||||
const { iat, exp, iss, ...user } = this._claims;
|
||||
return user;
|
||||
}
|
||||
|
||||
// @todo: determine what to do exactly
|
||||
resetUser() {
|
||||
this._user = undefined;
|
||||
this._claims = undefined;
|
||||
}
|
||||
|
||||
strategy<
|
||||
@@ -157,6 +165,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
}
|
||||
}
|
||||
|
||||
// @todo: add jwt tests
|
||||
async jwt(user: Omit<User, "password">): Promise<string> {
|
||||
const prohibited = ["password"];
|
||||
for (const prop of prohibited) {
|
||||
@@ -203,7 +212,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
}
|
||||
}
|
||||
|
||||
this._user = omit(payload, ["iat", "exp", "iss"]) as SafeUser;
|
||||
this._claims = payload as any;
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.resetUser();
|
||||
@@ -249,7 +258,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
}
|
||||
}
|
||||
|
||||
private async setAuthCookie(c: Context, token: string) {
|
||||
private async setAuthCookie(c: Context<ServerEnv>, token: string) {
|
||||
const secret = this.config.jwt.secret;
|
||||
await setSignedCookie(c, "auth", token, secret, this.cookieOptions);
|
||||
}
|
||||
@@ -281,10 +290,12 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
const successPath = this.config.cookie.pathSuccess ?? "/";
|
||||
const successUrl = new URL(c.req.url).origin + successPath.replace(/\/+$/, "/");
|
||||
const referer = new URL(redirect ?? c.req.header("Referer") ?? successUrl);
|
||||
console.log("auth respond", { redirect, successUrl, successPath });
|
||||
|
||||
if ("token" in data) {
|
||||
await this.setAuthCookie(c, data.token);
|
||||
// can't navigate to "/" – doesn't work on nextjs
|
||||
console.log("auth success, redirecting to", successUrl);
|
||||
return c.redirect(successUrl);
|
||||
}
|
||||
|
||||
@@ -294,6 +305,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
}
|
||||
|
||||
await addFlashMessage(c, message, "error");
|
||||
console.log("auth failed, redirecting to", referer);
|
||||
return c.redirect(referer);
|
||||
}
|
||||
|
||||
@@ -309,7 +321,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
|
||||
if (token) {
|
||||
await this.verify(token);
|
||||
return this._user;
|
||||
return this.getUser();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -98,12 +98,16 @@ export class Guard {
|
||||
if (this.user && typeof this.user.role === "string") {
|
||||
const role = this.roles?.find((role) => role.name === this.user?.role);
|
||||
if (role) {
|
||||
debug && console.log("guard: role found", this.user.role);
|
||||
debug && console.log("guard: role found", [this.user.role]);
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
debug && console.log("guard: role not found", this.user, this.user?.role);
|
||||
debug &&
|
||||
console.log("guard: role not found", {
|
||||
user: this.user,
|
||||
role: this.user?.role
|
||||
});
|
||||
return this.getDefaultRole();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type Permission, config } from "core";
|
||||
import type { Permission } from "core";
|
||||
import { patternMatch } from "core/utils";
|
||||
import type { Context } from "hono";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
import type { ServerEnv } from "modules/Module";
|
||||
@@ -8,51 +9,71 @@ function getPath(reqOrCtx: Request | Context) {
|
||||
return new URL(req.url).pathname;
|
||||
}
|
||||
|
||||
export function shouldSkipAuth(req: Request) {
|
||||
const skip = getPath(req).startsWith(config.server.assets_path);
|
||||
if (skip) {
|
||||
//console.log("skip auth for", req.url);
|
||||
}
|
||||
return skip;
|
||||
export function shouldSkip(c: Context<ServerEnv>, skip?: (string | RegExp)[]) {
|
||||
if (c.get("auth_skip")) return true;
|
||||
|
||||
const req = c.req.raw;
|
||||
if (!skip) return false;
|
||||
|
||||
const path = getPath(req);
|
||||
const result = skip.some((s) => patternMatch(path, s));
|
||||
|
||||
c.set("auth_skip", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export const auth = createMiddleware<ServerEnv>(async (c, next) => {
|
||||
// make sure to only register once
|
||||
if (c.get("auth_registered")) {
|
||||
throw new Error(`auth middleware already registered for ${getPath(c)}`);
|
||||
}
|
||||
c.set("auth_registered", true);
|
||||
export const auth = (options?: {
|
||||
skip?: (string | RegExp)[];
|
||||
}) =>
|
||||
createMiddleware<ServerEnv>(async (c, next) => {
|
||||
// make sure to only register once
|
||||
if (c.get("auth_registered")) {
|
||||
throw new Error(`auth middleware already registered for ${getPath(c)}`);
|
||||
}
|
||||
c.set("auth_registered", true);
|
||||
|
||||
const skipped = shouldSkipAuth(c.req.raw);
|
||||
const app = c.get("app");
|
||||
const guard = app.modules.ctx().guard;
|
||||
const authenticator = app.module.auth.authenticator;
|
||||
const app = c.get("app");
|
||||
const skipped = shouldSkip(c, options?.skip) || !app.module.auth.enabled;
|
||||
const guard = app.modules.ctx().guard;
|
||||
const authenticator = app.module.auth.authenticator;
|
||||
|
||||
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));
|
||||
|
||||
// renew cookie if applicable
|
||||
authenticator.requestCookieRefresh(c);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
if (!skipped) {
|
||||
// renew cookie if applicable
|
||||
authenticator.requestCookieRefresh(c);
|
||||
}
|
||||
|
||||
// release
|
||||
guard.setUserContext(undefined);
|
||||
authenticator?.resetUser();
|
||||
c.set("auth_resolved", false);
|
||||
});
|
||||
|
||||
export const permission = (
|
||||
permission: Permission | Permission[],
|
||||
options?: {
|
||||
onGranted?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
|
||||
onDenied?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
// release
|
||||
guard.setUserContext(undefined);
|
||||
authenticator.resetUser();
|
||||
c.set("auth_resolved", false);
|
||||
});
|
||||
|
||||
export const permission = (...permissions: Permission[]) =>
|
||||
) =>
|
||||
// @ts-ignore
|
||||
createMiddleware<ServerEnv>(async (c, next) => {
|
||||
const app = c.get("app");
|
||||
//console.log("skip?", c.get("auth_skip"));
|
||||
|
||||
// in tests, app is not defined
|
||||
if (!c.get("auth_registered")) {
|
||||
const msg = `auth middleware not registered, cannot check permissions for ${getPath(c)}`;
|
||||
@@ -61,11 +82,22 @@ export const permission = (...permissions: Permission[]) =>
|
||||
} else {
|
||||
console.warn(msg);
|
||||
}
|
||||
} else if (!shouldSkipAuth(c.req.raw)) {
|
||||
const p = Array.isArray(permissions) ? permissions : [permissions];
|
||||
} else if (!c.get("auth_skip")) {
|
||||
const guard = app.modules.ctx().guard;
|
||||
for (const permission of p) {
|
||||
guard.throwUnlessGranted(permission);
|
||||
const permissions = Array.isArray(permission) ? permission : [permission];
|
||||
|
||||
if (options?.onGranted || options?.onDenied) {
|
||||
let returned: undefined | void | Response;
|
||||
if (permissions.every((p) => guard.granted(p))) {
|
||||
returned = await options?.onGranted?.(c);
|
||||
} else {
|
||||
returned = await options?.onDenied?.(c);
|
||||
}
|
||||
if (returned instanceof Response) {
|
||||
return returned;
|
||||
}
|
||||
} else {
|
||||
permissions.some((p) => guard.throwUnlessGranted(p));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user