switched to hono/jwt to save some kb

This commit is contained in:
dswbx
2024-11-25 17:54:26 +01:00
parent 16a6a3315d
commit 1c94777317
7 changed files with 49 additions and 41 deletions

View File

@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { decodeJwt, jwtVerify } from "jose";
import { Authenticator, type User, type UserPool } from "../../src/auth"; import { Authenticator, type User, type UserPool } from "../../src/auth";
import { cookieConfig } from "../../src/auth/authenticate/Authenticator"; import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy"; import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";

View File

@@ -51,7 +51,6 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"fast-xml-parser": "^4.4.0", "fast-xml-parser": "^4.4.0",
"hono": "^4.6.12", "hono": "^4.6.12",
"jose": "^5.6.3",
"jotai": "^2.10.1", "jotai": "^2.10.1",
"kysely": "^0.27.4", "kysely": "^0.27.4",
"liquidjs": "^10.15.0", "liquidjs": "^10.15.0",
@@ -100,9 +99,6 @@
"splitting": false, "splitting": false,
"loader": { "loader": {
".svg": "dataurl" ".svg": "dataurl"
},
"esbuild": {
"drop": ["console", "debugger"]
} }
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -1,6 +1,6 @@
import { AuthApi } from "auth/api/AuthApi"; import { AuthApi } from "auth/api/AuthApi";
import { DataApi } from "data/api/DataApi"; import { DataApi } from "data/api/DataApi";
import { decodeJwt } from "jose"; import { decode } from "hono/jwt";
import { MediaApi } from "media/api/MediaApi"; import { MediaApi } from "media/api/MediaApi";
import { SystemApi } from "modules/SystemApi"; import { SystemApi } from "modules/SystemApi";
@@ -51,7 +51,7 @@ export class Api {
const token = localStorage.getItem(this.tokenKey); const token = localStorage.getItem(this.tokenKey);
if (token) { if (token) {
this.token = token; this.token = token;
this.user = decodeJwt(token) as any; this.user = decode(token).payload as any;
} }
} else { } else {
if (typeof window !== "undefined" && "__BKND__" in window) { if (typeof window !== "undefined" && "__BKND__" in window) {
@@ -63,7 +63,7 @@ export class Api {
updateToken(token?: string, rebuild?: boolean) { updateToken(token?: string, rebuild?: boolean) {
this.token = token; this.token = token;
this.user = token ? (decodeJwt(token) as any) : undefined; this.user = token ? (decode(token).payload as any) : undefined;
if (this.tokenStorage === "localStorage") { if (this.tokenStorage === "localStorage") {
const key = this.tokenKey; const key = this.tokenKey;

View File

@@ -11,10 +11,12 @@ import {
} from "core/utils"; } from "core/utils";
import type { Context, Hono } from "hono"; import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { decode, sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie"; import type { CookieOptions } from "hono/utils/cookie";
import { type JWTVerifyOptions, SignJWT, jwtVerify } from "jose"; import { omit } from "lodash-es";
type Input = any; // workaround type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0];
// @todo: add schema to interface to ensure proper inference // @todo: add schema to interface to ensure proper inference
export interface Strategy { export interface Strategy {
@@ -50,6 +52,7 @@ export interface UserPool<Fields = "id" | "email" | "username"> {
create: (user: CreateUser) => Promise<User | undefined>; create: (user: CreateUser) => Promise<User | undefined>;
} }
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
export const cookieConfig = Type.Partial( export const cookieConfig = Type.Partial(
Type.Object({ Type.Object({
renew: Type.Boolean({ default: true }), renew: Type.Boolean({ default: true }),
@@ -57,7 +60,7 @@ export const cookieConfig = Type.Partial(
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }), sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }),
secure: Type.Boolean({ default: true }), secure: Type.Boolean({ default: true }),
httpOnly: Type.Boolean({ default: true }), httpOnly: Type.Boolean({ default: true }),
expires: Type.Number({ default: 168 }) expires: Type.Number({ default: defaultCookieExpires }) // seconds
}), }),
{ default: {}, additionalProperties: false } { default: {}, additionalProperties: false }
); );
@@ -66,8 +69,8 @@ export const jwtConfig = Type.Object(
{ {
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
secret: Type.String({ default: "" }), secret: Type.String({ default: "" }),
alg: Type.Optional(Type.String({ enum: ["HS256"], default: "HS256" })), alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })),
expiresIn: Type.Optional(Type.String()), expires: Type.Optional(Type.Number()), // seconds
issuer: Type.Optional(Type.String()), issuer: Type.Optional(Type.String()),
fields: Type.Array(Type.String(), { default: ["id", "email", "role"] }) fields: Type.Array(Type.String(), { default: ["id", "email", "role"] })
}, },
@@ -157,56 +160,55 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
} }
} }
const jwt = new SignJWT(user) const payload: JWTPayload = {
.setProtectedHeader({ alg: this.config.jwt?.alg ?? "HS256" }) ...user,
.setIssuedAt(); iat: Math.floor(Date.now() / 1000)
};
// issuer
if (this.config.jwt?.issuer) { if (this.config.jwt?.issuer) {
jwt.setIssuer(this.config.jwt.issuer); payload.iss = this.config.jwt.issuer;
} }
if (this.config.jwt?.expiresIn) { // expires in seconds
jwt.setExpirationTime(this.config.jwt.expiresIn); if (this.config.jwt?.expires) {
payload.exp = Math.floor(Date.now() / 1000) + this.config.jwt.expires;
} }
return jwt.sign(new TextEncoder().encode(this.config.jwt?.secret ?? "")); return sign(payload, this.config.jwt?.secret ?? "", this.config.jwt?.alg ?? "HS256");
} }
async verify(jwt: string): Promise<boolean> { async verify(jwt: string): Promise<boolean> {
const options: JWTVerifyOptions = {
algorithms: [this.config.jwt?.alg ?? "HS256"]
};
if (this.config.jwt?.issuer) {
options.issuer = this.config.jwt.issuer;
}
if (this.config.jwt?.expiresIn) {
options.maxTokenAge = this.config.jwt.expiresIn;
}
try { try {
const { payload } = await jwtVerify<User>( const payload = await verify(
jwt, jwt,
new TextEncoder().encode(this.config.jwt?.secret ?? ""), this.config.jwt?.secret ?? "",
options this.config.jwt?.alg ?? "HS256"
); );
this._user = payload;
// manually verify issuer (hono doesn't support it)
if (this.config.jwt?.issuer) {
if (payload.iss !== this.config.jwt.issuer) {
throw new Exception("Invalid issuer", 403);
}
}
this._user = omit(payload, ["iat", "exp", "iss"]) as SafeUser;
return true; return true;
} catch (e) { } catch (e) {
this._user = undefined; this._user = undefined;
//console.error(e); console.error(e);
} }
return false; return false;
} }
private get cookieOptions(): CookieOptions { private get cookieOptions(): CookieOptions {
const { expires = 168, renew, ...cookieConfig } = this.config.cookie; const { expires = defaultCookieExpires, renew, ...cookieConfig } = this.config.cookie;
return { return {
...cookieConfig, ...cookieConfig,
expires: new Date(Date.now() + expires * 60 * 60 * 1000) expires: new Date(Date.now() + expires * 1000)
}; };
} }

View File

@@ -83,7 +83,15 @@ export const migrations: Migration[] = [
version: 7, version: 7,
up: async (config, { db }) => { up: async (config, { db }) => {
// automatically adds auth.cookie options // automatically adds auth.cookie options
return config; // remove "expiresIn" (string), it's now "expires" (number)
const { expiresIn, ...jwt } = config.auth.jwt;
return {
...config,
auth: {
...config.auth,
jwt
}
};
} }
} }
]; ];

BIN
bun.lockb

Binary file not shown.

View File

@@ -8,17 +8,20 @@ const result = await esbuild.build({
conditions: ["worker", "browser"], conditions: ["worker", "browser"],
entryPoints: ["./src/index.ts"], entryPoints: ["./src/index.ts"],
outdir: "dist", outdir: "dist",
external: [], external: ["__STATIC_CONTENT_MANIFEST", "cloudflare:workers"],
format: "esm", format: "esm",
target: "es2022", target: "es2022",
keepNames: true, keepNames: true,
bundle: true, bundle: true,
metafile: true, metafile: true,
minify: true, minify: true,
loader: {
".html": "copy"
},
define: { define: {
IS_CLOUDFLARE_WORKER: "true" IS_CLOUDFLARE_WORKER: "true"
} }
}); });
await Bun.write("dist/meta.json", JSON.stringify(result.metafile)); await Bun.write("dist/meta.json", JSON.stringify(result.metafile));
//console.log("result", result.metafile); await $`gzip dist/index.js -c > dist/index.js.gz`;