From 8c8473774ecb28f3bea164032e94736f1f07c7ba Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 19 Oct 2026 01:21:15 +0000 Subject: [PATCH] Add PKCE support --- server/server/internal/auth/oidc/index.ts | 38 +++++++++++++------ server/server/internal/config/sys-conf.ts | 7 ++++ server/server/internal/utils/query.ts | 10 +++-- .../content/docs/admin/authentication/oidc.md | 1 + 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/server/server/internal/auth/oidc/index.ts b/server/server/internal/auth/oidc/index.ts index 5ef0e23d4..9480df989 100644 --- a/server/server/internal/auth/oidc/index.ts +++ b/server/server/internal/auth/oidc/index.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "crypto"; +import { createHash, randomBytes, randomUUID } from "crypto"; import prisma from "../../db/database"; import type { UserModel } from "~/prisma/client/models"; import { AuthMec } from "~/prisma/client/enums"; @@ -43,6 +43,7 @@ interface OIDCAuthSession { redirectUrl: string; callbackUrl: string; state: string; + codeVerifier: string | undefined; options: OIDCAuthSessionOptions; claims: OIDCAuthSessionClaims; } @@ -283,18 +284,29 @@ export class OIDCManager { generateAuthSession(options?: OIDCAuthSessionOptions): OIDCAuthSession { const stateKey = randomUUID(); + let codeVerifier: string | undefined; + let codeChallenge: string | undefined; + if (systemConfig.shouldOidcUsePkce()) { + codeVerifier = randomBytes(32).toString("base64url"); + codeChallenge = createHash("sha256") + .update(codeVerifier) + .digest("base64url"); + } const normalisedUrl = new URL( this.oidcConfiguration.authorization_endpoint, ).toString(); - const queryParams = queryParamBuilder({ + const authParams: { [key: string]: string | undefined } = { client_id: this.clientId, redirect_uri: this.redirectUrl.toString(), state: stateKey, response_type: "code", scope: this.oidcConfiguration.scopes_supported.join(" "), - }); + code_challenge: codeChallenge, + code_challenge_method: codeChallenge && "S256", + }; + const queryParams = queryParamBuilder(authParams); const finalUrl = `${normalisedUrl}?${queryParams}`; @@ -302,6 +314,7 @@ export class OIDCManager { redirectUrl: finalUrl, callbackUrl: this.redirectUrl.toString(), state: stateKey, + codeVerifier, options: options ?? { redirect: undefined }, claims: { iss: this.oidcConfiguration.issuer.toString(), @@ -329,14 +342,17 @@ export class OIDCManager { const userinfoEndpoint = this.oidcConfiguration.userinfo_endpoint.toString(); - const requestBody = new URLSearchParams({ - client_id: this.clientId, - client_secret: this.clientSecret, - grant_type: "authorization_code", - code: code, - redirect_uri: session.callbackUrl, - scope: this.oidcConfiguration.scopes_supported.join(","), - }); + const requestBody = new URLSearchParams( + queryParamBuilder({ + client_id: this.clientId, + client_secret: this.clientSecret, + grant_type: "authorization_code", + code: code, + redirect_uri: session.callbackUrl, + scope: this.oidcConfiguration.scopes_supported.join(","), + code_verifier: session.codeVerifier, + }), + ); try { const rawTokenResponse = await $fetch(tokenEndpoint, { diff --git a/server/server/internal/config/sys-conf.ts b/server/server/internal/config/sys-conf.ts index 75ddbdde9..89dcae6b3 100644 --- a/server/server/internal/config/sys-conf.ts +++ b/server/server/internal/config/sys-conf.ts @@ -13,6 +13,7 @@ class SystemConfig { private dropVersion: string; private gitRef: string; private oidcRequireHttps; + private oidcPkce; private checkForUpdates = getUpdateCheckConfig(); @@ -32,6 +33,8 @@ class SystemConfig { oidcRequireHttps.toLocaleLowerCase() === "false" ? false : true; + + this.oidcPkce = process.env.OIDC_PKCE?.toLocaleLowerCase() !== "false"; } getLibraryFolder() { @@ -66,6 +69,10 @@ class SystemConfig { shouldOidcRequireHttps() { return this.oidcRequireHttps; } + + shouldOidcUsePkce() { + return this.oidcPkce; + } } export const systemConfig = new SystemConfig(); diff --git a/server/server/internal/utils/query.ts b/server/server/internal/utils/query.ts index bb62a68bb..149fce739 100644 --- a/server/server/internal/utils/query.ts +++ b/server/server/internal/utils/query.ts @@ -1,7 +1,9 @@ -export function queryParamBuilder(params: { [key: string]: string }) { - const list = Object.entries(params).map( - ([key, value]) => `${key}=${encodeURIComponent(value)}`, - ); +export function queryParamBuilder(params: { + [key: string]: string | undefined; +}) { + const list = Object.entries(params) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`); const str = list.join("&"); return str; } diff --git a/sites/docs/src/content/docs/admin/authentication/oidc.md b/sites/docs/src/content/docs/admin/authentication/oidc.md index 0d908a58e..51eacdcd4 100644 --- a/sites/docs/src/content/docs/admin/authentication/oidc.md +++ b/sites/docs/src/content/docs/admin/authentication/oidc.md @@ -18,6 +18,7 @@ To configure OIDC, you must set the following environment variables: | `OIDC_USERNAME_CLAIM` (optional) | Change the field that Drop pulls the username claim from. Users are merged based on their usernames. Defaults to "preferred_username". | | `OIDC_PROVIDER_NAME` (optional) | Change the name of the OIDC provider that is displayed on the sign-in page. Default is "external provider". | | `OIDC_DONT_REQUIRE_HTTPS` (optional) | Flag to disable HTTPS requirement for OIDC providers. | +| `OIDC_PKCE` (optional) | Enable PKCE for the authorization code flow. Defaults to `true`; set to `false` for providers that do not support PKCE. | ---