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 { decodeJwt, jwtVerify } from "jose";
import { Authenticator, type User, type UserPool } from "../../src/auth";
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";

View File

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

View File

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

View File

@@ -11,10 +11,12 @@ import {
} from "core/utils";
import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { decode, sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie";
import { type JWTVerifyOptions, SignJWT, jwtVerify } from "jose";
import { omit } from "lodash-es";
type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0];
// @todo: add schema to interface to ensure proper inference
export interface Strategy {
@@ -50,6 +52,7 @@ export interface UserPool<Fields = "id" | "email" | "username"> {
create: (user: CreateUser) => Promise<User | undefined>;
}
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
export const cookieConfig = Type.Partial(
Type.Object({
renew: Type.Boolean({ default: true }),
@@ -57,7 +60,7 @@ export const cookieConfig = Type.Partial(
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }),
secure: Type.Boolean({ default: true }),
httpOnly: Type.Boolean({ default: true }),
expires: Type.Number({ default: 168 })
expires: Type.Number({ default: defaultCookieExpires }) // seconds
}),
{ 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
secret: Type.String({ default: "" }),
alg: Type.Optional(Type.String({ enum: ["HS256"], default: "HS256" })),
expiresIn: Type.Optional(Type.String()),
alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })),
expires: Type.Optional(Type.Number()), // seconds
issuer: Type.Optional(Type.String()),
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)
.setProtectedHeader({ alg: this.config.jwt?.alg ?? "HS256" })
.setIssuedAt();
const payload: JWTPayload = {
...user,
iat: Math.floor(Date.now() / 1000)
};
// issuer
if (this.config.jwt?.issuer) {
jwt.setIssuer(this.config.jwt.issuer);
payload.iss = this.config.jwt.issuer;
}
if (this.config.jwt?.expiresIn) {
jwt.setExpirationTime(this.config.jwt.expiresIn);
// expires in seconds
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> {
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 {
const { payload } = await jwtVerify<User>(
const payload = await verify(
jwt,
new TextEncoder().encode(this.config.jwt?.secret ?? ""),
options
this.config.jwt?.secret ?? "",
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;
} catch (e) {
this._user = undefined;
//console.error(e);
console.error(e);
}
return false;
}
private get cookieOptions(): CookieOptions {
const { expires = 168, renew, ...cookieConfig } = this.config.cookie;
const { expires = defaultCookieExpires, renew, ...cookieConfig } = this.config.cookie;
return {
...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,
up: async (config, { db }) => {
// 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"],
entryPoints: ["./src/index.ts"],
outdir: "dist",
external: [],
external: ["__STATIC_CONTENT_MANIFEST", "cloudflare:workers"],
format: "esm",
target: "es2022",
keepNames: true,
bundle: true,
metafile: true,
minify: true,
loader: {
".html": "copy"
},
define: {
IS_CLOUDFLARE_WORKER: "true"
}
});
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`;