mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-03 00:26:01 +00:00
initial refactor
This commit is contained in:
@@ -1,31 +1,23 @@
|
||||
import { $console, type DB, Exception } from "core";
|
||||
import { addFlashMessage } from "core/server/flash";
|
||||
import {
|
||||
type Static,
|
||||
StringEnum,
|
||||
type TObject,
|
||||
parse,
|
||||
runtimeSupports,
|
||||
truncate,
|
||||
} from "core/utils";
|
||||
import { runtimeSupports, truncate } from "core/utils";
|
||||
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 type { ServerEnv } from "modules/Controller";
|
||||
import { pick } from "lodash-es";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import { InvalidConditionsException } from "auth/errors";
|
||||
const { Type } = tbbox;
|
||||
import { s, parse } from "core/object/schema";
|
||||
|
||||
type Input = any; // workaround
|
||||
export type JWTPayload = Parameters<typeof sign>[0];
|
||||
|
||||
export const strategyActions = ["create", "change"] as const;
|
||||
export type StrategyActionName = (typeof strategyActions)[number];
|
||||
export type StrategyAction<S extends TObject = TObject> = {
|
||||
export type StrategyAction<S extends s.ObjectSchema = s.ObjectSchema> = {
|
||||
schema: S;
|
||||
preprocess: (input: Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>;
|
||||
preprocess: (input: s.Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>;
|
||||
};
|
||||
export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>;
|
||||
|
||||
@@ -59,43 +51,44 @@ export interface UserPool {
|
||||
}
|
||||
|
||||
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
|
||||
export const cookieConfig = Type.Partial(
|
||||
Type.Object({
|
||||
path: Type.String({ default: "/" }),
|
||||
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }),
|
||||
secure: Type.Boolean({ default: true }),
|
||||
httpOnly: Type.Boolean({ default: true }),
|
||||
expires: Type.Number({ default: defaultCookieExpires }), // seconds
|
||||
renew: Type.Boolean({ default: true }),
|
||||
pathSuccess: Type.String({ default: "/" }),
|
||||
pathLoggedOut: Type.String({ default: "/" }),
|
||||
}),
|
||||
{ default: {}, additionalProperties: false },
|
||||
);
|
||||
export const cookieConfig = s
|
||||
.object({
|
||||
path: s.string({ default: "/" }),
|
||||
sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
|
||||
secure: s.boolean({ default: true }),
|
||||
httpOnly: s.boolean({ default: true }),
|
||||
expires: s.number({ default: defaultCookieExpires }), // seconds
|
||||
renew: s.boolean({ default: true }),
|
||||
pathSuccess: s.string({ default: "/" }),
|
||||
pathLoggedOut: s.string({ default: "/" }),
|
||||
})
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
|
||||
// see auth.integration test for further details
|
||||
|
||||
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(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"] }),
|
||||
},
|
||||
{
|
||||
default: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
export const authenticatorConfig = Type.Object({
|
||||
export const jwtConfig = s
|
||||
.object(
|
||||
{
|
||||
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
|
||||
secret: s.string({ default: "" }),
|
||||
alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
|
||||
expires: s.number().optional(), // seconds
|
||||
issuer: s.string().optional(),
|
||||
fields: s.array(s.string(), { default: ["id", "email", "role"] }),
|
||||
},
|
||||
{
|
||||
default: {},
|
||||
},
|
||||
)
|
||||
.strict();
|
||||
export const authenticatorConfig = s.object({
|
||||
jwt: jwtConfig,
|
||||
cookie: cookieConfig,
|
||||
});
|
||||
|
||||
type AuthConfig = Static<typeof authenticatorConfig>;
|
||||
type AuthConfig = s.Static<typeof authenticatorConfig>;
|
||||
export type AuthAction = "login" | "register";
|
||||
export type AuthResolveOptions = {
|
||||
identifier?: "email" | string;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { type Authenticator, InvalidCredentialsException, type User } from "auth";
|
||||
import { $console, tbValidator as tb } from "core";
|
||||
import { hash, parse, type Static, StrictObject, StringEnum } from "core/utils";
|
||||
import { $console } from "core";
|
||||
import { hash } from "core/utils";
|
||||
import { Hono } from "hono";
|
||||
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import { Strategy } from "./Strategy";
|
||||
import { s, parse, jsc } from "core/object/schema";
|
||||
|
||||
const { Type } = tbbox;
|
||||
const schema = s
|
||||
.object({
|
||||
hashing: s.string({ enum: ["plain", "sha256", "bcrypt"], default: "sha256" }),
|
||||
rounds: s.number({ minimum: 1, maximum: 10 }).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const schema = StrictObject({
|
||||
hashing: StringEnum(["plain", "sha256", "bcrypt"], { default: "sha256" }),
|
||||
rounds: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
|
||||
});
|
||||
|
||||
export type PasswordStrategyOptions = Static<typeof schema>;
|
||||
export type PasswordStrategyOptions = s.Static<typeof schema>;
|
||||
|
||||
export class PasswordStrategy extends Strategy<typeof schema> {
|
||||
constructor(config: Partial<PasswordStrategyOptions> = {}) {
|
||||
@@ -32,11 +32,11 @@ export class PasswordStrategy extends Strategy<typeof schema> {
|
||||
}
|
||||
|
||||
private getPayloadSchema() {
|
||||
return Type.Object({
|
||||
email: Type.String({
|
||||
return s.object({
|
||||
email: s.string({
|
||||
pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
||||
}),
|
||||
password: Type.String({
|
||||
password: s.string({
|
||||
minLength: 8, // @todo: this should be configurable
|
||||
}),
|
||||
});
|
||||
@@ -79,12 +79,12 @@ export class PasswordStrategy extends Strategy<typeof schema> {
|
||||
|
||||
getController(authenticator: Authenticator): Hono<any> {
|
||||
const hono = new Hono();
|
||||
const redirectQuerySchema = Type.Object({
|
||||
redirect: Type.Optional(Type.String()),
|
||||
const redirectQuerySchema = s.object({
|
||||
redirect: s.string().optional(),
|
||||
});
|
||||
const payloadSchema = this.getPayloadSchema();
|
||||
|
||||
hono.post("/login", tb("query", redirectQuerySchema), async (c) => {
|
||||
hono.post("/login", jsc("query", redirectQuerySchema), async (c) => {
|
||||
try {
|
||||
const body = parse(payloadSchema, await authenticator.getBody(c), {
|
||||
onError: (errors) => {
|
||||
@@ -102,7 +102,7 @@ export class PasswordStrategy extends Strategy<typeof schema> {
|
||||
}
|
||||
});
|
||||
|
||||
hono.post("/register", tb("query", redirectQuerySchema), async (c) => {
|
||||
hono.post("/register", jsc("query", redirectQuerySchema), async (c) => {
|
||||
try {
|
||||
const { redirect } = c.req.valid("query");
|
||||
const { password, email, ...body } = parse(
|
||||
|
||||
@@ -5,31 +5,31 @@ import type {
|
||||
StrategyActions,
|
||||
} from "../Authenticator";
|
||||
import type { Hono } from "hono";
|
||||
import type { Static, TSchema } from "@sinclair/typebox";
|
||||
import { parse, type TObject } from "core/utils";
|
||||
import { type s, parse } from "core/object/schema";
|
||||
|
||||
export type StrategyMode = "form" | "external";
|
||||
|
||||
export abstract class Strategy<Schema extends TSchema = TSchema> {
|
||||
export abstract class Strategy<Schema extends s.Schema = s.Schema> {
|
||||
protected actions: StrategyActions = {};
|
||||
|
||||
constructor(
|
||||
protected config: Static<Schema>,
|
||||
protected config: s.Static<Schema>,
|
||||
public type: string,
|
||||
public name: string,
|
||||
public mode: StrategyMode,
|
||||
) {
|
||||
// don't worry about typing, it'll throw if invalid
|
||||
this.config = parse(this.getSchema(), (config ?? {}) as any) as Static<Schema>;
|
||||
this.config = parse(this.getSchema(), (config ?? {}) as any) as s.Static<Schema>;
|
||||
}
|
||||
|
||||
protected registerAction<S extends TObject = TObject>(
|
||||
protected registerAction<S extends s.ObjectSchema = s.ObjectSchema>(
|
||||
name: StrategyActionName,
|
||||
schema: S,
|
||||
preprocess: StrategyAction<S>["preprocess"],
|
||||
): void {
|
||||
this.actions[name] = {
|
||||
schema,
|
||||
// @ts-expect-error - @todo: fix this
|
||||
preprocess,
|
||||
} as const;
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export abstract class Strategy<Schema extends TSchema = TSchema> {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
toJSON(secrets?: boolean): { type: string; config: Static<Schema> | {} | undefined } {
|
||||
toJSON(secrets?: boolean): { type: string; config: s.Static<Schema> | {} | undefined } {
|
||||
return {
|
||||
type: this.getType(),
|
||||
config: secrets ? this.config : undefined,
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
import { type Static, StrictObject, StringEnum } from "core/utils";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type * as oauth from "oauth4webapi";
|
||||
import { OAuthStrategy } from "./OAuthStrategy";
|
||||
const { Type } = tbbox;
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
type SupportedTypes = "oauth2" | "oidc";
|
||||
|
||||
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
|
||||
|
||||
const UrlString = Type.String({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" });
|
||||
const oauthSchemaCustom = StrictObject(
|
||||
const UrlString = s.string({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" });
|
||||
const oauthSchemaCustom = s.strictObject(
|
||||
{
|
||||
type: StringEnum(["oidc", "oauth2"] as const, { default: "oidc" }),
|
||||
name: Type.String(),
|
||||
client: StrictObject({
|
||||
client_id: Type.String(),
|
||||
client_secret: Type.String(),
|
||||
token_endpoint_auth_method: StringEnum(["client_secret_basic"]),
|
||||
type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oidc" }),
|
||||
name: s.string(),
|
||||
client: s.object({
|
||||
client_id: s.string(),
|
||||
client_secret: s.string(),
|
||||
token_endpoint_auth_method: s.string({ enum: ["client_secret_basic"] }),
|
||||
}),
|
||||
as: StrictObject({
|
||||
issuer: Type.String(),
|
||||
code_challenge_methods_supported: Type.Optional(StringEnum(["S256"])),
|
||||
scopes_supported: Type.Optional(Type.Array(Type.String())),
|
||||
scope_separator: Type.Optional(Type.String({ default: " " })),
|
||||
authorization_endpoint: Type.Optional(UrlString),
|
||||
token_endpoint: Type.Optional(UrlString),
|
||||
userinfo_endpoint: Type.Optional(UrlString),
|
||||
as: s.strictObject({
|
||||
issuer: s.string(),
|
||||
code_challenge_methods_supported: s.string({ enum: ["S256"] }).optional(),
|
||||
scopes_supported: s.array(s.string()).optional(),
|
||||
scope_separator: s.string({ default: " " }).optional(),
|
||||
authorization_endpoint: UrlString.optional(),
|
||||
token_endpoint: UrlString.optional(),
|
||||
userinfo_endpoint: UrlString.optional(),
|
||||
}),
|
||||
// @todo: profile mapping
|
||||
},
|
||||
{ title: "Custom OAuth" },
|
||||
);
|
||||
|
||||
type OAuthConfigCustom = Static<typeof oauthSchemaCustom>;
|
||||
type OAuthConfigCustom = s.Static<typeof oauthSchemaCustom>;
|
||||
|
||||
export type UserProfile = {
|
||||
sub: string;
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import type { AuthAction, Authenticator } from "auth";
|
||||
import { Exception, isDebug } from "core";
|
||||
import { type Static, StringEnum, filterKeys, StrictObject } from "core/utils";
|
||||
import { filterKeys } from "core/utils";
|
||||
import { type Context, Hono } from "hono";
|
||||
import { getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||
import * as oauth from "oauth4webapi";
|
||||
import * as issuers from "./issuers";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import { Strategy } from "auth/authenticate/strategies/Strategy";
|
||||
const { Type } = tbbox;
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
type ConfiguredIssuers = keyof typeof issuers;
|
||||
type SupportedTypes = "oauth2" | "oidc";
|
||||
|
||||
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
|
||||
|
||||
const schemaProvided = Type.Object(
|
||||
const schemaProvided = s.object(
|
||||
{
|
||||
name: StringEnum(Object.keys(issuers) as ConfiguredIssuers[]),
|
||||
type: StringEnum(["oidc", "oauth2"] as const, { default: "oauth2" }),
|
||||
client: StrictObject({
|
||||
client_id: Type.String(),
|
||||
client_secret: Type.String(),
|
||||
}),
|
||||
name: s.string({ enum: Object.keys(issuers) as ConfiguredIssuers[] }),
|
||||
type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oauth2" }),
|
||||
client: s
|
||||
.object({
|
||||
client_id: s.string(),
|
||||
client_secret: s.string(),
|
||||
})
|
||||
.strict(),
|
||||
},
|
||||
{ title: "OAuth" },
|
||||
);
|
||||
type ProvidedOAuthConfig = Static<typeof schemaProvided>;
|
||||
type ProvidedOAuthConfig = s.Static<typeof schemaProvided>;
|
||||
|
||||
export type CustomOAuthConfig = {
|
||||
type: SupportedTypes;
|
||||
|
||||
Reference in New Issue
Block a user