diff --git a/app/__test__/auth/Authenticator.spec.ts b/app/__test__/auth/Authenticator.spec.ts index d622739..620ab03 100644 --- a/app/__test__/auth/Authenticator.spec.ts +++ b/app/__test__/auth/Authenticator.spec.ts @@ -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"; diff --git a/app/package.json b/app/package.json index ff0d45a..58210db 100644 --- a/app/package.json +++ b/app/package.json @@ -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": { diff --git a/app/src/Api.ts b/app/src/Api.ts index 30d58aa..0595557 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -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; diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 5876daf..ff98c41 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -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[0]; // @todo: add schema to interface to ensure proper inference export interface Strategy { @@ -50,6 +52,7 @@ export interface UserPool { create: (user: CreateUser) => Promise; } +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 = 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 { - 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( + 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) }; } diff --git a/app/src/modules/migrations.ts b/app/src/modules/migrations.ts index 69de5c6..9cf0beb 100644 --- a/app/src/modules/migrations.ts +++ b/app/src/modules/migrations.ts @@ -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 + } + }; } } ]; diff --git a/bun.lockb b/bun.lockb index 74b8c5e..565dd37 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/cloudflare-worker/build.ts b/examples/cloudflare-worker/build.ts index d643c41..746b59e 100644 --- a/examples/cloudflare-worker/build.ts +++ b/examples/cloudflare-worker/build.ts @@ -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`;