import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { Context } from "hono";
import { handle as awsHandle } from "hono/aws-lambda";
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
import { Hono } from "hono/tiny";
/**
* The `issuer` create an OpentAuth server, a [Hono](https://hono.dev) app that's
* designed to run anywhere.
*
* The `issuer` function requires a few things:
*
* ```ts title="issuer.ts"
* import { issuer } from "@aryalabs/openauth"
*
* const app = issuer({
* providers: { ... },
* storage,
* subjects,
* success: async (ctx, value) => { ... }
* })
* ```
*
* #### Add providers
*
* You start by specifying the auth providers you are going to use. Let's say you want your users
* to be able to authenticate with GitHub and with their email and password.
*
* ```ts title="issuer.ts"
* import { GithubProvider } from "@aryalabs/openauth/provider/github"
* import { PasswordProvider } from "@aryalabs/openauth/provider/password"
*
* const app = issuer({
* providers: {
* github: GithubProvider({
* // ...
* }),
* password: PasswordProvider({
* // ...
* }),
* },
* })
* ```
*
* #### Handle success
*
* The `success` callback receives the payload when a user completes a provider's auth flow.
*
* ```ts title="issuer.ts"
* const app = issuer({
* providers: { ... },
* subjects,
* async success(ctx, value) {
* let userID
* if (value.provider === "password") {
* console.log(value.email)
* userID = ... // lookup user or create them
* }
* if (value.provider === "github") {
* console.log(value.tokenset.access)
* userID = ... // lookup user or create them
* }
* return ctx.subject("user", {
* userID
* })
* }
* })
* ```
*
* Once complete, the `issuer` issues the access tokens that a client can use. The `ctx.subject`
* call is what is placed in the access token as a JWT.
*
* #### Define subjects
*
* You define the shape of these in the `subjects` field.
*
* ```ts title="subjects.ts"
* import { object, string } from "valibot"
* import { createSubjects } from "@aryalabs/openauth/subject"
*
* const subjects = createSubjects({
* user: object({
* userID: string()
* })
* })
* ```
*
* It's good to place this in a separate file since this'll be used in your client apps as well.
*
* ```ts title="issuer.ts"
* import { subjects } from "./subjects.js"
*
* const app = issuer({
* providers: { ... },
* subjects,
* // ...
* })
* ```
*
* #### Deploy
*
* Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
*
*
*
* ```ts title="issuer.ts"
* import { serve } from "@hono/node-server"
*
* serve(app)
* ```
*
*
* ```ts title="issuer.ts"
* import { handle } from "hono/aws-lambda"
*
* export const handler = handle(app)
* ```
*
*
* ```ts title="issuer.ts"
* export default app
* ```
*
*
* ```ts title="issuer.ts"
* export default app
* ```
*
*
*
* @packageDocumentation
*/
import type { Provider, ProviderOptions } from "./provider/provider.js";
import type { SubjectPayload, SubjectSchema } from "./subject.js";
/**
* Sets the subject payload in the JWT token and returns the response.
*
* ```ts
* ctx.subject("user", {
* userID
* })
* ```
*/
export interface OnSuccessResponder {
/**
* The `type` is the type of the subject, that was defined in the `subjects` field.
*
* The `properties` are the properties of the subject. This is the shape of the subject that
* you defined in the `subjects` field.
*/
subject(
type: Type,
properties: Extract["properties"],
opts?: {
ttl?: {
access?: number;
refresh?: number;
};
subject?: string;
},
): Promise;
}
/**
* @internal
*/
export interface AuthorizationState {
redirect_uri: string;
response_type: string;
state: string;
client_id: string;
audience?: string;
pkce?: {
challenge: string;
method: "S256";
};
}
/**
* @internal
*/
export type Prettify = {
[K in keyof T]: T[K];
} & {};
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { CompactEncrypt, SignJWT, compactDecrypt, jwtVerify } from "jose";
import { MissingParameterError, OauthError, UnauthorizedClientError, UnknownStateError } from "./error.js";
import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js";
import { validatePKCE } from "./pkce.js";
import { DynamoStorage } from "./storage/dynamo.js";
import { MemoryStorage } from "./storage/memory.js";
import { Storage, type StorageAdapter } from "./storage/storage.js";
import { Select } from "./ui/select.js";
import { type Theme, setTheme } from "./ui/theme.js";
import { getRelativeUrl, isDomainMatch, lazy } from "./util.js";
/** @internal */
export const aws = awsHandle;
export interface IssuerInput<
Providers extends Record>,
Subjects extends SubjectSchema,
Result = {
[key in keyof Providers]: Prettify<
{
provider: key;
} & (Providers[key] extends Provider ? T : {})
>;
}[keyof Providers],
> {
/**
* The shape of the subjects that you want to return.
*
* @example
*
* ```ts title="issuer.ts"
* import { object, string } from "valibot"
* import { createSubjects } from "@aryalabs/openauth/subject"
*
* issuer({
* subjects: createSubjects({
* user: object({
* userID: string()
* })
* })
* // ...
* })
* ```
*/
subjects: Subjects;
/**
* The storage adapter that you want to use.
*
* @example
* ```ts title="issuer.ts"
* import { DynamoStorage } from "@aryalabs/openauth/storage/dynamo"
*
* issuer({
* storage: DynamoStorage()
* // ...
* })
* ```
*/
storage?: StorageAdapter;
/**
* The providers that you want your OpenAuth server to support.
*
* @example
*
* ```ts title="issuer.ts"
* import { GithubProvider } from "@aryalabs/openauth/provider/github"
*
* issuer({
* providers: {
* github: GithubProvider()
* }
* })
* ```
*
* The key is just a string that you can use to identify the provider. It's passed back to
* the `success` callback.
*
* You can also specify multiple providers.
*
* ```ts
* {
* providers: {
* github: GithubProvider(),
* google: GoogleProvider()
* }
* }
* ```
*/
providers: Providers;
/**
* The theme you want to use for the UI.
*
* This includes the UI the user sees when selecting a provider. And the `PasswordUI` and
* `CodeUI` that are used by the `PasswordProvider` and `CodeProvider`.
*
* @example
* ```ts title="issuer.ts"
* import { THEME_SST } from "@aryalabs/openauth/ui/theme"
*
* issuer({
* theme: THEME_SST
* // ...
* })
* ```
*
* Or define your own.
*
* ```ts title="issuer.ts"
* import type { Theme } from "@aryalabs/openauth/ui/theme"
*
* const MY_THEME: Theme = {
* // ...
* }
*
* issuer({
* theme: MY_THEME
* // ...
* })
* ```
*/
theme?: Theme;
/**
* Set the TTL, in seconds, for access and refresh tokens.
*
* @example
* ```ts
* {
* ttl: {
* access: 60 * 60 * 24 * 30,
* refresh: 60 * 60 * 24 * 365
* }
* }
* ```
*/
ttl?: {
/**
* Interval in seconds where the access token is valid.
* @default 30d
*/
access?: number;
/**
* Interval in seconds where the refresh token is valid.
* @default 1y
*/
refresh?: number;
/**
* Interval in seconds where refresh token reuse is allowed. This helps mitigrate
* concurrency issues.
* @default 60s
*/
reuse?: number;
/**
* Interval in seconds to retain refresh tokens for reuse detection.
* @default 0s
*/
retention?: number;
};
/**
* Optionally, configure the UI that's displayed when the user visits the root URL of the
* of the OpenAuth server.
*
* ```ts title="issuer.ts"
* import { Select } from "@aryalabs/openauth/ui/select"
*
* issuer({
* select: Select({
* providers: {
* github: { hide: true },
* google: { display: "Google" }
* }
* })
* // ...
* })
* ```
*
* @default Select()
*/
select?(providers: Record, req: Request): Promise;
/**
* @internal
*/
start?(req: Request): Promise;
/**
* The success callback that's called when the user completes the flow.
*
* This is called after the user has been redirected back to your app after the OAuth flow.
*
* @example
* ```ts
* {
* success: async (ctx, value) => {
* let userID
* if (value.provider === "password") {
* console.log(value.email)
* userID = ... // lookup user or create them
* }
* if (value.provider === "github") {
* console.log(value.tokenset.access)
* userID = ... // lookup user or create them
* }
* return ctx.subject("user", {
* userID
* })
* },
* // ...
* }
* ```
*/
success(response: OnSuccessResponder>, input: Result, req: Request): Promise;
/**
* @internal
*/
error?(error: UnknownStateError, req: Request): Promise;
/**
* Override the logic for whether a client request is allowed to call the issuer.
*
* By default, it uses the following:
*
* - Allow if the `redirectURI` is localhost.
* - Compare `redirectURI` to the request's hostname or the `x-forwarded-host` header. If they
* are from the same sub-domain level, then allow.
*
* @example
* ```ts
* {
* allow: async (input, req) => {
* // Allow all clients
* return true
* }
* }
* ```
*/
allow?(
input: {
clientID: string;
redirectURI: string;
audience?: string;
},
req: Request,
): Promise;
}
/**
* Create an OpenAuth server, a Hono app.
*/
export function issuer<
Providers extends Record>,
Subjects extends SubjectSchema,
Result = {
[key in keyof Providers]: Prettify<
{
provider: key;
} & (Providers[key] extends Provider ? T : {})
>;
}[keyof Providers],
>(input: IssuerInput) {
const error =
input.error ??
((err) =>
new Response(err.message, {
status: 400,
headers: {
"Content-Type": "text/plain",
},
}));
const ttlAccess = input.ttl?.access ?? 60 * 60 * 24 * 30;
const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365;
const ttlRefreshReuse = input.ttl?.reuse ?? 60;
const ttlRefreshRetention = input.ttl?.retention ?? 0;
if (input.theme) {
setTheme(input.theme);
}
const select = lazy(() => input.select ?? Select());
const allow = lazy(
() =>
input.allow ??
(async (input: any, req: Request) => {
const redir = new URL(input.redirectURI).hostname;
if (redir === "localhost" || redir === "127.0.0.1") {
return true;
}
const forwarded = req.headers.get("x-forwarded-host");
const host = forwarded ? new URL(`https://${forwarded}`).hostname : new URL(req.url).hostname;
return isDomainMatch(redir, host);
}),
);
let storage = input.storage;
if (process.env.OPENAUTH_STORAGE) {
const parsed = JSON.parse(process.env.OPENAUTH_STORAGE);
if (parsed.type === "dynamo") storage = DynamoStorage(parsed.options);
if (parsed.type === "memory") storage = MemoryStorage();
if (parsed.type === "cloudflare") throw new Error("Cloudflare storage cannot be configured through env because it requires bindings.");
}
if (!storage) throw new Error("Store is not configured. Either set the `storage` option or set `OPENAUTH_STORAGE` environment variable.");
const allSigning = lazy(() => Promise.all([signingKeys(storage), legacySigningKeys(storage)]).then(([a, b]) => [...a, ...b]));
const allEncryption = lazy(() => encryptionKeys(storage));
const signingKey = lazy(() => allSigning().then((all) => all[0]));
const encryptionKey = lazy(() => allEncryption().then((all) => all[0]));
const auth: Omit, "name"> = {
async success(ctx: Context, properties: any, successOpts) {
return await input.success(
{
async subject(type, properties, subjectOpts) {
const authorization = await getAuthorization(ctx);
const subject = subjectOpts?.subject ? subjectOpts.subject : await resolveSubject(type, properties);
await successOpts?.invalidate?.(await resolveSubject(type, properties));
if (authorization.response_type === "token") {
const location = new URL(authorization.redirect_uri);
const tokens = await generateTokens(ctx, {
subject,
type: type as string,
properties,
clientID: authorization.client_id,
ttl: {
access: subjectOpts?.ttl?.access ?? ttlAccess,
refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
},
});
location.hash = new URLSearchParams({
access_token: tokens.access,
refresh_token: tokens.refresh,
state: authorization.state || "",
}).toString();
await auth.unset(ctx, "authorization");
return ctx.redirect(location.toString(), 302);
}
if (authorization.response_type === "code") {
const code = crypto.randomUUID();
await Storage.set(
storage,
["oauth:code", code],
{
type,
properties,
subject,
redirectURI: authorization.redirect_uri,
clientID: authorization.client_id,
pkce: authorization.pkce,
ttl: {
access: subjectOpts?.ttl?.access ?? ttlAccess,
refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
},
},
60,
);
const location = new URL(authorization.redirect_uri);
location.searchParams.set("code", code);
location.searchParams.set("state", authorization.state || "");
await auth.unset(ctx, "authorization");
return ctx.redirect(location.toString(), 302);
}
throw new OauthError("invalid_request", `Unsupported response_type: ${authorization.response_type}`);
},
},
{
provider: ctx.get("provider"),
...properties,
},
ctx.req.raw,
);
},
forward(ctx, response) {
return ctx.newResponse(response.body, response.status as any, Object.fromEntries(response.headers.entries()));
},
async set(ctx, key, maxAge, value) {
setCookie(ctx, key, await encrypt(value), {
maxAge,
httpOnly: true,
...(ctx.req.url.startsWith("https://") ? { secure: true, sameSite: "None" } : {}),
});
},
async get(ctx: Context, key: string) {
const raw = getCookie(ctx, key);
if (!raw) return;
return decrypt(raw).catch((ex) => {
console.error("failed to decrypt", key, ex);
});
},
async unset(ctx: Context, key: string) {
deleteCookie(ctx, key);
},
async invalidate(subject: string) {
// Resolve the scan in case modifications interfere with iteration
const keys = await Array.fromAsync(Storage.scan(this.storage, ["oauth:refresh", subject]));
for (const [key] of keys) {
await Storage.remove(this.storage, key);
}
},
storage,
};
async function getAuthorization(ctx: Context) {
const match = (await auth.get(ctx, "authorization")) || ctx.get("authorization");
if (!match) throw new UnknownStateError();
return match as AuthorizationState;
}
async function encrypt(value: any) {
return await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(value)))
.setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" })
.encrypt(await encryptionKey().then((k) => k.public));
}
async function resolveSubject(type: string, properties: any) {
const jsonString = JSON.stringify(properties);
const encoder = new TextEncoder();
const data = encoder.encode(jsonString);
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return `${type}:${hashHex.slice(0, 16)}`;
}
async function generateTokens(
ctx: Context,
value: {
type: string;
properties: any;
subject: string;
clientID: string;
ttl: {
access: number;
refresh: number;
};
timeUsed?: number;
nextToken?: string;
},
opts?: {
generateRefreshToken?: boolean;
},
) {
const refreshToken = value.nextToken ?? crypto.randomUUID();
if (opts?.generateRefreshToken ?? true) {
/**
* Generate and store the next refresh token after the one we are currently returning.
* Reserving these in advance avoids concurrency issues with multiple refreshes.
* Similar treatment should be given to any other values that may have race conditions,
* for example if a jti claim was added to the access token.
*/
const refreshValue = {
...value,
nextToken: crypto.randomUUID(),
};
refreshValue.timeUsed = undefined;
await Storage.set(storage!, ["oauth:refresh", value.subject, refreshToken], refreshValue, value.ttl.refresh);
}
const accessTimeUsed = Math.floor((value.timeUsed ?? Date.now()) / 1000);
return {
access: await new SignJWT({
mode: "access",
type: value.type,
properties: value.properties,
aud: value.clientID,
iss: issuer(ctx),
sub: value.subject,
})
.setExpirationTime(Math.floor(accessTimeUsed + value.ttl.access))
.setProtectedHeader(
await signingKey().then((k) => ({
alg: k.alg,
kid: k.id,
typ: "JWT",
})),
)
.sign(await signingKey().then((item) => item.private)),
expiresIn: Math.floor(accessTimeUsed + value.ttl.access - Date.now() / 1000),
refresh: [value.subject, refreshToken].join(":"),
};
}
async function decrypt(value: string) {
return JSON.parse(
new TextDecoder().decode(await compactDecrypt(value, await encryptionKey().then((v) => v.private)).then((value) => value.plaintext)),
);
}
function issuer(ctx: Context) {
return new URL(getRelativeUrl(ctx, "/")).origin;
}
const app = new Hono<{
Variables: {
authorization: AuthorizationState;
};
}>().use(logger());
for (const [name, value] of Object.entries(input.providers)) {
const route = new Hono();
route.use(async (c, next) => {
c.set("provider", name);
await next();
});
value.init(route, {
name,
...auth,
});
app.route(`/${name}`, route);
}
app.get(
"/.well-known/jwks.json",
cors({
origin: "*",
allowHeaders: ["*"],
allowMethods: ["GET"],
credentials: false,
}),
async (c) => {
const all = await allSigning();
return c.json({
keys: all.map((item) => ({
...item.jwk,
alg: item.alg,
exp: item.expired ? Math.floor(item.expired.getTime() / 1000) : undefined,
})),
});
},
);
app.get(
"/.well-known/oauth-authorization-server",
cors({
origin: "*",
allowHeaders: ["*"],
allowMethods: ["GET"],
credentials: false,
}),
async (c) => {
const iss = issuer(c);
return c.json({
issuer: iss,
authorization_endpoint: `${iss}/authorize`,
token_endpoint: `${iss}/token`,
jwks_uri: `${iss}/.well-known/jwks.json`,
response_types_supported: ["code", "token"],
});
},
);
app.post(
"/token",
cors({
origin: "*",
allowHeaders: ["*"],
allowMethods: ["POST"],
credentials: false,
}),
async (c) => {
const form = await c.req.formData();
const grantType = form.get("grant_type");
if (grantType === "authorization_code") {
const code = form.get("code");
if (!code)
return c.json(
{
error: "invalid_request",
error_description: "Missing code",
},
400,
);
const key = ["oauth:code", code.toString()];
const payload = await Storage.get<{
type: string;
properties: any;
clientID: string;
redirectURI: string;
subject: string;
ttl: {
access: number;
refresh: number;
};
pkce?: AuthorizationState["pkce"];
}>(storage, key);
if (!payload) {
return c.json(
{
error: "invalid_grant",
error_description: "Authorization code has been used or expired",
},
400,
);
}
if (payload.redirectURI !== form.get("redirect_uri")) {
return c.json(
{
error: "invalid_redirect_uri",
error_description: "Redirect URI mismatch",
},
400,
);
}
if (payload.clientID !== form.get("client_id")) {
return c.json(
{
error: "unauthorized_client",
error_description: "Client is not authorized to use this authorization code",
},
403,
);
}
if (payload.pkce) {
const codeVerifier = form.get("code_verifier")?.toString();
if (!codeVerifier)
return c.json(
{
error: "invalid_grant",
error_description: "Missing code_verifier",
},
400,
);
if (!(await validatePKCE(codeVerifier, payload.pkce.challenge, payload.pkce.method))) {
return c.json(
{
error: "invalid_grant",
error_description: "Code verifier does not match",
},
400,
);
}
}
const tokens = await generateTokens(c, payload);
await Storage.remove(storage, key);
return c.json({
access_token: tokens.access,
expires_in: tokens.expiresIn,
refresh_token: tokens.refresh,
});
}
if (grantType === "refresh_token") {
const refreshToken = form.get("refresh_token");
if (!refreshToken)
return c.json(
{
error: "invalid_request",
error_description: "Missing refresh_token",
},
400,
);
const splits = refreshToken.toString().split(":");
const token = splits.pop()!;
const subject = splits.join(":");
const key = ["oauth:refresh", subject, token];
const payload = await Storage.get<{
type: string;
properties: any;
clientID: string;
subject: string;
ttl: {
access: number;
refresh: number;
};
nextToken: string;
timeUsed?: number;
}>(storage, key);
if (!payload) {
return c.json(
{
error: "invalid_grant",
error_description: "Refresh token has been used or expired",
},
400,
);
}
const generateRefreshToken = !payload.timeUsed;
if (ttlRefreshReuse <= 0) {
// no reuse interval, remove the refresh token immediately
await Storage.remove(storage, key);
} else if (!payload.timeUsed) {
payload.timeUsed = Date.now();
await Storage.set(storage, key, payload, ttlRefreshReuse + ttlRefreshRetention);
} else if (Date.now() > payload.timeUsed + ttlRefreshReuse * 1000) {
// token was reused past the allowed interval
await auth.invalidate(subject);
return c.json(
{
error: "invalid_grant",
error_description: "Refresh token has been used or expired",
},
400,
);
}
const tokens = await generateTokens(c, payload, {
generateRefreshToken,
});
return c.json({
access_token: tokens.access,
refresh_token: tokens.refresh,
expires_in: tokens.expiresIn,
});
}
if (grantType === "client_credentials") {
const provider = form.get("provider")?.toString();
if (!provider) return c.json({ error: "missing `provider` form value" }, 400);
const match = input.providers[provider];
if (!match) {
return c.json({ error: "invalid_request", error_description: `Invalid provider: ${provider}` }, 400);
}
if (!match.client) {
return c.json(
{ error: "unsupported_grant_type", error_description: `Provider "${provider}" does not support client_credentials` },
400,
);
}
const clientID = form.get("client_id")?.toString();
const clientSecret = form.get("client_secret")?.toString();
if (!clientID) {
return c.json({ error: "missing `client_id` form value" }, 400);
}
try {
const response = await match.client({
clientID: clientID,
clientSecret: clientSecret,
params: Object.fromEntries(form) as Record,
});
return input.success(
{
async subject(type, properties, opts) {
const tokens = await generateTokens(c, {
type: type as string,
subject: opts?.subject || (await resolveSubject(type, properties)),
properties,
clientID: clientID,
ttl: {
access: opts?.ttl?.access ?? ttlAccess,
refresh: opts?.ttl?.refresh ?? ttlRefresh,
},
});
return c.json({
access_token: tokens.access,
refresh_token: tokens.refresh,
});
},
},
{
provider: provider,
...response,
},
c.req.raw,
);
} catch (err: any) {
// Handle errors from the provider's client function
const oauthError = err instanceof OauthError ? err : new OauthError("invalid_grant", err.message || "Client validation failed");
return c.json({ error: oauthError.error, error_description: oauthError.description }, 400);
}
}
throw new Error("Invalid grant_type");
},
);
app.get("/authorize", async (c) => {
const provider = c.req.query("provider");
const response_type = c.req.query("response_type");
const redirect_uri = c.req.query("redirect_uri");
const state = c.req.query("state");
const client_id = c.req.query("client_id");
const audience = c.req.query("audience");
const code_challenge = c.req.query("code_challenge");
const code_challenge_method = c.req.query("code_challenge_method");
const authorization: AuthorizationState = {
response_type,
redirect_uri,
state,
client_id,
audience,
pkce:
code_challenge && code_challenge_method
? {
challenge: code_challenge,
method: code_challenge_method,
}
: undefined,
} as AuthorizationState;
c.set("authorization", authorization);
if (!redirect_uri) {
return c.text("Missing redirect_uri", { status: 400 });
}
if (!response_type) {
throw new MissingParameterError("response_type");
}
if (!client_id) {
throw new MissingParameterError("client_id");
}
if (input.start) {
await input.start(c.req.raw);
}
if (
!(await allow()(
{
clientID: client_id,
redirectURI: redirect_uri,
audience,
},
c.req.raw,
))
)
throw new UnauthorizedClientError(client_id, redirect_uri);
await auth.set(c, "authorization", 60 * 60 * 24, authorization);
if (provider) return c.redirect(`/${provider}/authorize`);
const providers = Object.keys(input.providers);
if (providers.length === 1) return c.redirect(`/${providers[0]}/authorize`);
return auth.forward(
c,
await select()(Object.fromEntries(Object.entries(input.providers).map(([key, value]) => [key, value.type])), c.req.raw),
);
});
app.get("/userinfo", async (c) => {
const header = c.req.header("Authorization");
if (!header) {
return c.json(
{
error: "invalid_request",
error_description: "Missing Authorization header",
},
400,
);
}
const [type, token] = header.split(" ");
if (type !== "Bearer") {
return c.json(
{
error: "invalid_request",
error_description: "Missing or invalid Authorization header",
},
400,
);
}
if (!token) {
return c.json(
{
error: "invalid_request",
error_description: "Missing token",
},
400,
);
}
const result = await jwtVerify<{
mode: "access";
type: keyof SubjectSchema;
properties: StandardSchemaV1.InferInput;
}>(token, () => signingKey().then((item) => item.public), {
issuer: issuer(c),
});
const validated = await input.subjects[result.payload.type]["~standard"].validate(result.payload.properties);
if (!validated.issues && result.payload.mode === "access") {
return c.json(validated.value as SubjectSchema);
}
return c.json({
error: "invalid_token",
error_description: "Invalid token",
});
});
app.onError(async (err, c) => {
console.error(err);
if (err instanceof UnknownStateError) {
return auth.forward(c, await error(err, c.req.raw));
}
const authorization = await getAuthorization(c);
const url = new URL(authorization.redirect_uri);
const oauth = err instanceof OauthError ? err : new OauthError("server_error", err.message);
url.searchParams.set("error", oauth.error);
url.searchParams.set("error_description", oauth.description);
return c.redirect(url.toString());
});
return app;
}