mirror of
https://github.com/versia-pub/server.git
synced 2026-01-26 12:16: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",
|
"msgpackr-extract",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
|
||||||
"openid-client@6.8.1": "patches/openid-client@6.8.1.patch",
|
|
||||||
},
|
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@biomejs/biome": "2.3.4",
|
"@biomejs/biome": "2.3.4",
|
||||||
"@bull-board/api": "~6.14.2",
|
"@bull-board/api": "~6.14.2",
|
||||||
|
|
|
||||||
|
|
@ -215,8 +215,5 @@
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zod-openapi": "catalog:",
|
"zod-openapi": "catalog:",
|
||||||
"zod-validation-error": "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 { Client, db } from "@versia-server/kit/db";
|
||||||
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { sign } from "hono/jwt";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import * as client from "openid-client";
|
import * as client from "openid-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -114,10 +115,6 @@ export default apiRoute((app) => {
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!oidcConfig.serverMetadata().supportsPKCE()) {
|
|
||||||
parameters.state = client.randomState();
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirectUri = oauthRedirectUri(
|
const redirectUri = oauthRedirectUri(
|
||||||
context.get("config").http.base_url,
|
context.get("config").http.base_url,
|
||||||
issuerId,
|
issuerId,
|
||||||
|
|
@ -149,14 +146,24 @@ export default apiRoute((app) => {
|
||||||
.returning()
|
.returning()
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
parameters.redirect_uri = `${oauthRedirectUri(
|
const jwt = await sign(
|
||||||
config.http.base_url,
|
{
|
||||||
issuerId,
|
|
||||||
)}?${new URLSearchParams({
|
|
||||||
flow: newFlow.id,
|
flow: newFlow.id,
|
||||||
link: "true",
|
link: "true",
|
||||||
user_id: user.id,
|
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,
|
||||||
|
)}`;
|
||||||
|
|
||||||
const redirectTo = client.buildAuthorizationUrl(
|
const redirectTo = client.buildAuthorizationUrl(
|
||||||
oidcConfig,
|
oidcConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
zBoolean,
|
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "@versia-server/config";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { ApiError } from "@versia-server/kit";
|
||||||
|
|
@ -16,7 +15,7 @@ import {
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { and, eq, isNull, type SQL } from "drizzle-orm";
|
import { and, eq, isNull, type SQL } from "drizzle-orm";
|
||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
import { sign } from "hono/jwt";
|
import { sign, verify } from "hono/jwt";
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import * as client from "openid-client";
|
import * as client from "openid-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -48,15 +47,13 @@ export default apiRoute((app) => {
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
flow: z.string(),
|
state: z.string(),
|
||||||
link: zBoolean.default(false),
|
|
||||||
user_id: z.uuid().optional(),
|
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { issuer: issuerId } = context.req.valid("param");
|
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(
|
const issuer = config.authentication.openid_providers.find(
|
||||||
(provider) => provider.id === issuerId,
|
(provider) => provider.id === issuerId,
|
||||||
|
|
@ -66,6 +63,16 @@ export default apiRoute((app) => {
|
||||||
throw new ApiError(422, "Unknown or invalid issuer");
|
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({
|
const flow = await db.query.OpenIdLoginFlows.findFirst({
|
||||||
where: (flow): SQL | undefined => eq(flow.id, flowId),
|
where: (flow): SQL | undefined => eq(flow.id, flowId),
|
||||||
with: {
|
with: {
|
||||||
|
|
@ -104,7 +111,7 @@ export default apiRoute((app) => {
|
||||||
context.req.raw,
|
context.req.raw,
|
||||||
{
|
{
|
||||||
pkceCodeVerifier: flow.codeVerifier,
|
pkceCodeVerifier: flow.codeVerifier,
|
||||||
expectedState: flow.state ?? undefined,
|
expectedState: state,
|
||||||
idTokenExpected: true,
|
idTokenExpected: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
||||||
import { Client, db } from "@versia-server/kit/db";
|
import { Client, db } from "@versia-server/kit/db";
|
||||||
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { sign } from "hono/jwt";
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import * as client from "openid-client";
|
import * as client from "openid-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -43,8 +44,12 @@ export default apiRoute((app) => {
|
||||||
),
|
),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
// This is the Versia client's client_id, not the external OAuth provider's client_id
|
// This is the Versia client's client_id, not the external OAuth provider's client_id
|
||||||
const { client_id, redirect_uri, scopes, state } =
|
const {
|
||||||
context.req.valid("json");
|
client_id,
|
||||||
|
redirect_uri,
|
||||||
|
scopes,
|
||||||
|
state: clientState,
|
||||||
|
} = context.req.valid("json");
|
||||||
const { issuer: issuerId } = context.req.valid("param");
|
const { issuer: issuerId } = context.req.valid("param");
|
||||||
|
|
||||||
const issuer = config.authentication.openid_providers.find(
|
const issuer = config.authentication.openid_providers.find(
|
||||||
|
|
@ -84,10 +89,6 @@ export default apiRoute((app) => {
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!oidcConfig.serverMetadata().supportsPKCE()) {
|
|
||||||
parameters.state = client.randomState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store into database
|
// Store into database
|
||||||
const newFlow = (
|
const newFlow = (
|
||||||
await db
|
await db
|
||||||
|
|
@ -96,7 +97,7 @@ export default apiRoute((app) => {
|
||||||
id: randomUUIDv7(),
|
id: randomUUIDv7(),
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
state: parameters.state,
|
state: parameters.state,
|
||||||
clientState: state,
|
clientState,
|
||||||
clientRedirectUri: redirect_uri,
|
clientRedirectUri: redirect_uri,
|
||||||
clientScopes: scopes,
|
clientScopes: scopes,
|
||||||
clientId: application.id,
|
clientId: application.id,
|
||||||
|
|
@ -105,12 +106,22 @@ export default apiRoute((app) => {
|
||||||
.returning()
|
.returning()
|
||||||
)[0];
|
)[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(
|
parameters.redirect_uri = `${oauthRedirectUri(
|
||||||
context.get("config").http.base_url,
|
context.get("config").http.base_url,
|
||||||
issuerId,
|
issuerId,
|
||||||
)}?${new URLSearchParams({
|
)}`;
|
||||||
flow: newFlow.id,
|
|
||||||
})}`;
|
|
||||||
|
|
||||||
const redirectTo = client.buildAuthorizationUrl(
|
const redirectTo = client.buildAuthorizationUrl(
|
||||||
oidcConfig,
|
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