diff --git a/bun.lock b/bun.lock index e719a635..ee1ecd27 100644 --- a/bun.lock +++ b/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", diff --git a/package.json b/package.json index 97c1c85c..4ca3c50a 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/packages/api/routes/api/v1/sso/index.ts b/packages/api/routes/api/v1/sso/index.ts index cd8e4228..33a50018 100644 --- a/packages/api/routes/api/v1/sso/index.ts +++ b/packages/api/routes/api/v1/sso/index.ts @@ -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, diff --git a/packages/api/routes/oauth/sso/[issuer]/callback.ts b/packages/api/routes/oauth/sso/[issuer]/callback.ts index 290c65ae..fe5d1164 100644 --- a/packages/api/routes/oauth/sso/[issuer]/callback.ts +++ b/packages/api/routes/oauth/sso/[issuer]/callback.ts @@ -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, }, ); diff --git a/packages/api/routes/oauth/sso/[issuer]/index.ts b/packages/api/routes/oauth/sso/[issuer]/index.ts index e0b54f42..a480da99 100644 --- a/packages/api/routes/oauth/sso/[issuer]/index.ts +++ b/packages/api/routes/oauth/sso/[issuer]/index.ts @@ -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, diff --git a/patches/openid-client@6.8.1.patch b/patches/openid-client@6.8.1.patch deleted file mode 100644 index 3f28cef8..00000000 --- a/patches/openid-client@6.8.1.patch +++ /dev/null @@ -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; - }