mirror of
https://github.com/versia-pub/server.git
synced 2026-01-26 04:06:01 +01:00
refactor(api): 🛂 Rewrite OpenID auth code to use state for data instead of query parameters
This commit is contained in:
parent
5436be0578
commit
a951a08073
3
bun.lock
3
bun.lock
|
|
@ -227,9 +227,6 @@
|
|||
"msgpackr-extract",
|
||||
"protobufjs",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"openid-client@6.8.1": "patches/openid-client@6.8.1.patch",
|
||||
},
|
||||
"catalog": {
|
||||
"@biomejs/biome": "2.3.4",
|
||||
"@bull-board/api": "~6.14.2",
|
||||
|
|
|
|||
|
|
@ -215,8 +215,5 @@
|
|||
"zod": "catalog:",
|
||||
"zod-openapi": "catalog:",
|
||||
"zod-validation-error": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"openid-client@6.8.1": "patches/openid-client@6.8.1.patch"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
|||
import { Client, db } from "@versia-server/kit/db";
|
||||
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import { sign } from "hono/jwt";
|
||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||
import * as client from "openid-client";
|
||||
import { z } from "zod";
|
||||
|
|
@ -114,10 +115,6 @@ export default apiRoute((app) => {
|
|||
code_challenge_method: "S256",
|
||||
};
|
||||
|
||||
if (!oidcConfig.serverMetadata().supportsPKCE()) {
|
||||
parameters.state = client.randomState();
|
||||
}
|
||||
|
||||
const redirectUri = oauthRedirectUri(
|
||||
context.get("config").http.base_url,
|
||||
issuerId,
|
||||
|
|
@ -149,14 +146,24 @@ export default apiRoute((app) => {
|
|||
.returning()
|
||||
)[0];
|
||||
|
||||
const jwt = await sign(
|
||||
{
|
||||
flow: newFlow.id,
|
||||
link: "true",
|
||||
user_id: user.id,
|
||||
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes expiration
|
||||
iss: config.http.base_url.toString(),
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
config.authentication.key,
|
||||
);
|
||||
|
||||
parameters.state = jwt;
|
||||
|
||||
parameters.redirect_uri = `${oauthRedirectUri(
|
||||
config.http.base_url,
|
||||
issuerId,
|
||||
)}?${new URLSearchParams({
|
||||
flow: newFlow.id,
|
||||
link: "true",
|
||||
user_id: user.id,
|
||||
})}`;
|
||||
)}`;
|
||||
|
||||
const redirectTo = client.buildAuthorizationUrl(
|
||||
oidcConfig,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Account as AccountSchema,
|
||||
RolePermission,
|
||||
zBoolean,
|
||||
} from "@versia/client/schemas";
|
||||
import { config } from "@versia-server/config";
|
||||
import { ApiError } from "@versia-server/kit";
|
||||
|
|
@ -16,7 +15,7 @@ import {
|
|||
import { randomUUIDv7 } from "bun";
|
||||
import { and, eq, isNull, type SQL } from "drizzle-orm";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import { sign } from "hono/jwt";
|
||||
import { sign, verify } from "hono/jwt";
|
||||
import { describeRoute, validator } from "hono-openapi";
|
||||
import * as client from "openid-client";
|
||||
import { z } from "zod";
|
||||
|
|
@ -48,15 +47,13 @@ export default apiRoute((app) => {
|
|||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
flow: z.string(),
|
||||
link: zBoolean.default(false),
|
||||
user_id: z.uuid().optional(),
|
||||
state: z.string(),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { issuer: issuerId } = context.req.valid("param");
|
||||
const { flow: flowId, user_id, link } = context.req.valid("query");
|
||||
const { state } = context.req.valid("query");
|
||||
|
||||
const issuer = config.authentication.openid_providers.find(
|
||||
(provider) => provider.id === issuerId,
|
||||
|
|
@ -66,6 +63,16 @@ export default apiRoute((app) => {
|
|||
throw new ApiError(422, "Unknown or invalid issuer");
|
||||
}
|
||||
|
||||
const jwtPayload = (await verify(state, config.authentication.key, {
|
||||
iss: config.http.base_url.toString(),
|
||||
})) as {
|
||||
flow: string;
|
||||
link?: boolean;
|
||||
user_id?: string;
|
||||
};
|
||||
|
||||
const { flow: flowId, link, user_id } = jwtPayload;
|
||||
|
||||
const flow = await db.query.OpenIdLoginFlows.findFirst({
|
||||
where: (flow): SQL | undefined => eq(flow.id, flowId),
|
||||
with: {
|
||||
|
|
@ -104,7 +111,7 @@ export default apiRoute((app) => {
|
|||
context.req.raw,
|
||||
{
|
||||
pkceCodeVerifier: flow.codeVerifier,
|
||||
expectedState: flow.state ?? undefined,
|
||||
expectedState: state,
|
||||
idTokenExpected: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
|||
import { Client, db } from "@versia-server/kit/db";
|
||||
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import { sign } from "hono/jwt";
|
||||
import { describeRoute, validator } from "hono-openapi";
|
||||
import * as client from "openid-client";
|
||||
import { z } from "zod";
|
||||
|
|
@ -43,8 +44,12 @@ export default apiRoute((app) => {
|
|||
),
|
||||
async (context) => {
|
||||
// This is the Versia client's client_id, not the external OAuth provider's client_id
|
||||
const { client_id, redirect_uri, scopes, state } =
|
||||
context.req.valid("json");
|
||||
const {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scopes,
|
||||
state: clientState,
|
||||
} = context.req.valid("json");
|
||||
const { issuer: issuerId } = context.req.valid("param");
|
||||
|
||||
const issuer = config.authentication.openid_providers.find(
|
||||
|
|
@ -84,10 +89,6 @@ export default apiRoute((app) => {
|
|||
code_challenge_method: "S256",
|
||||
};
|
||||
|
||||
if (!oidcConfig.serverMetadata().supportsPKCE()) {
|
||||
parameters.state = client.randomState();
|
||||
}
|
||||
|
||||
// Store into database
|
||||
const newFlow = (
|
||||
await db
|
||||
|
|
@ -96,7 +97,7 @@ export default apiRoute((app) => {
|
|||
id: randomUUIDv7(),
|
||||
codeVerifier,
|
||||
state: parameters.state,
|
||||
clientState: state,
|
||||
clientState,
|
||||
clientRedirectUri: redirect_uri,
|
||||
clientScopes: scopes,
|
||||
clientId: application.id,
|
||||
|
|
@ -105,12 +106,22 @@ export default apiRoute((app) => {
|
|||
.returning()
|
||||
)[0];
|
||||
|
||||
const jwt = await sign(
|
||||
{
|
||||
flow: newFlow.id,
|
||||
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes expiration
|
||||
iss: config.http.base_url.toString(),
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
config.authentication.key,
|
||||
);
|
||||
|
||||
parameters.state = jwt;
|
||||
|
||||
parameters.redirect_uri = `${oauthRedirectUri(
|
||||
context.get("config").http.base_url,
|
||||
issuerId,
|
||||
)}?${new URLSearchParams({
|
||||
flow: newFlow.id,
|
||||
})}`;
|
||||
)}`;
|
||||
|
||||
const redirectTo = client.buildAuthorizationUrl(
|
||||
oidcConfig,
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
diff --git a/build/index.js b/build/index.js
|
||||
index 8bea9f9d4413ecf2446ee5130b46e58d5ac37226..b1b9e89c1ac3b6bf6ac82fef94ccf92b55a40321 100644
|
||||
--- a/build/index.js
|
||||
+++ b/build/index.js
|
||||
@@ -888,7 +888,8 @@ export function useIdTokenResponseType(config) {
|
||||
}
|
||||
function stripParams(url) {
|
||||
url = new URL(url);
|
||||
- url.search = '';
|
||||
+ // Remove all params except user_id, link, and flow
|
||||
+ url.search = new URLSearchParams([...url.searchParams].filter(([k]) => ['user_id', 'link', 'flow'].includes(k))).toString();
|
||||
url.hash = '';
|
||||
return url.href;
|
||||
}
|
||||
Loading…
Reference in a new issue