mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
switched to hono/jwt to save some kb
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
Reference in New Issue
Block a user