Skip to content

OIDC PKCE support #16657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/backend-core/src/middleware/passport/sso/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export async function fetchStrategyConfig(
callbackUrl?: string
): Promise<OIDCStrategyConfiguration> {
try {
const { clientID, clientSecret, configUrl } = oidcConfig
const { clientID, clientSecret, configUrl, pkce } = oidcConfig

if (!clientID || !clientSecret || !callbackUrl || !configUrl) {
// check for remote config and all required elements
Expand All @@ -139,6 +139,7 @@ export async function fetchStrategyConfig(
clientID: clientID,
clientSecret: clientSecret,
callbackURL: callbackUrl,
pkce: pkce,
}
} catch (err) {
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export const useRecaptcha = () => {
return useFeature(Feature.RECAPTCHA)
}

export const usePkceOidc = () => {
return useFeature(Feature.PKCE_OIDC)
}

export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => {
return useFeature(Feature.BUDIBASE_AI, {
monthlyQuotas: [
Expand Down
17 changes: 15 additions & 2 deletions packages/backend-core/tests/core/utilities/structures/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
SSOProfile,
SSOProviderType,
User,
PKCEMethod,
} from "@budibase/types"
import { generator } from "./generator"
import { email, uuid } from "./common"
Expand Down Expand Up @@ -69,8 +70,8 @@ export function ssoProfile(user?: User): SSOProfile {

// OIDC

export function oidcConfig(): OIDCInnerConfig {
return {
export function oidcConfig(pkce?: PKCEMethod): OIDCInnerConfig {
const config: OIDCInnerConfig = {
uuid: uuid(),
activated: true,
logo: "",
Expand All @@ -80,6 +81,18 @@ export function oidcConfig(): OIDCInnerConfig {
clientSecret: generator.string(),
scopes: [],
}

if (pkce) {
config.pkce = pkce
}

return config
}

export function oidcConfigWithPKCE(
method: PKCEMethod = PKCEMethod.S256
): OIDCInnerConfig {
return oidcConfig(method)
}

// response from .well-known/openid-configuration
Expand Down
5 changes: 2 additions & 3 deletions packages/bbui/src/Form/Core/Picker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
</span>
{:else}
<span class="option-extra icon field-icon">
<img src={fieldIcon} alt="icon" width="15" height="15" />
<img src={fieldIcon} alt="icon" style="height: 15px; width: auto;" />
</span>
{/if}
{/if}
Expand Down Expand Up @@ -237,8 +237,7 @@
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="15"
height="15"
style="height: 15px; width: auto;"
/>
{:else}
<Icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import { onMount } from "svelte"
import { API } from "@/api"
import { organisation, admin, licensing } from "@/stores/portal"
import { PKCEMethod } from "@budibase/types"
import Scim from "./scim.svelte"
import Google from "./google.svelte"

Expand All @@ -37,6 +38,11 @@

$: enforcedSSO = $organisation.isSSOEnforced

const pkceOptions = [
{ label: "S256 (recommended)", value: PKCEMethod.S256 },
{ label: "Plain", value: PKCEMethod.PLAIN },
]

$: OIDCConfigFields = {
Oidc: [
{ name: "configUrl", label: "Config URL" },
Expand Down Expand Up @@ -361,6 +367,25 @@
on:change={e => onFileSelected(e)}
bind:this={fileinput}
/>
<div class="form-row">
<div class="lock">
<Label size="L">PKCE Method</Label>
{#if !$licensing.pkceOidcEnabled}
<Icon name="lock" size="S" />
{/if}
</div>
{#if $licensing.pkceOidcEnabled}
<Select
placeholder="None"
bind:value={providers.oidc.config.configs[0].pkce}
options={pkceOptions}
/>
{:else}
<Body size="XS">
PKCE support is only available on enterprise licenses.
</Body>
{/if}
</div>
<div class="form-row">
<Label size="L">Activated</Label>
<Toggle
Expand Down Expand Up @@ -520,4 +545,8 @@
align-items: center;
margin-left: 10px;
}
.lock {
display: flex;
gap: var(--spacing-s);
}
</style>
4 changes: 4 additions & 0 deletions packages/builder/src/stores/portal/licensing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface LicensingState {
syncAutomationsEnabled: boolean
triggerAutomationRunEnabled: boolean
recaptchaEnabled: boolean
pkceOidcEnabled: boolean
pdfEnabled: boolean
// the currently used quotas from the db
quotaUsage?: QuotaUsage
Expand Down Expand Up @@ -90,6 +91,7 @@ class LicensingStore extends BudiStore<LicensingState> {
syncAutomationsEnabled: false,
triggerAutomationRunEnabled: false,
recaptchaEnabled: false,
pkceOidcEnabled: false,
pdfEnabled: false,
// the currently used quotas from the db
quotaUsage: undefined,
Expand Down Expand Up @@ -219,6 +221,7 @@ class LicensingStore extends BudiStore<LicensingState> {
Constants.Features.CUSTOM_APP_SCRIPTS
)
const recaptchaEnabled = features.includes(Constants.Features.RECAPTCHA)
const pkceOidcEnabled = features.includes(Constants.Features.PKCE_OIDC)
const pdfEnabled = features.includes(Constants.Features.PDF)
this.update(state => {
return {
Expand All @@ -243,6 +246,7 @@ class LicensingStore extends BudiStore<LicensingState> {
customAppScriptsEnabled,
pdfEnabled,
recaptchaEnabled,
pkceOidcEnabled,
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/pro
Submodule pro updated from 9ac16a to 014389
7 changes: 7 additions & 0 deletions packages/types/src/documents/global/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface OIDCStrategyConfiguration {
clientID: string
clientSecret: string
callbackURL: string
pkce?: PKCEMethod
}

export interface OIDCConfigs {
Expand All @@ -94,6 +95,7 @@ export interface OIDCInnerConfig {
uuid: string
activated: boolean
scopes: string[]
pkce?: PKCEMethod
}

export interface OIDCConfig extends Config<OIDCConfigs> {}
Expand Down Expand Up @@ -166,6 +168,11 @@ export const isAIConfig = (config: Config): config is AIConfig =>
export const isRecaptchaConfig = (config: Config): config is RecaptchaConfig =>
config.type === ConfigType.RECAPTCHA

export enum PKCEMethod {
S256 = "S256",
PLAIN = "plain",
}

export enum ConfigType {
SETTINGS = "settings",
ACCOUNT = "account",
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/sdk/licensing/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum Feature {
AI_CUSTOM_CONFIGS = "aiCustomConfigs",
PWA = "pwa",
RECAPTCHA = "recaptcha",
PKCE_OIDC = "pkceOidc",
}

export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
1 change: 1 addition & 0 deletions packages/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"rimraf": "3.0.2",
"superagent": "^10.1.1",
"supertest": "6.3.3",
"testcontainers": "10.16.0",
"timekeeper": "2.2.0",
"typescript": "5.7.2"
},
Expand Down
5 changes: 5 additions & 0 deletions packages/worker/src/api/controllers/global/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ async function processGoogleConfig(
async function processOIDCConfig(config: OIDCConfigs, existing?: OIDCConfigs) {
await verifySSOConfig(ConfigType.OIDC, config.configs[0])

const anyPkceSettings = config.configs.find(cfg => cfg.pkce)
if (anyPkceSettings && !(await pro.features.isPkceOidcEnabled())) {
throw new Error("License does not allow OIDC PKCE method support")
}

if (existing) {
for (const c of config.configs) {
const existingConfig = existing.configs.find(e => e.uuid === c.uuid)
Expand Down
5 changes: 3 additions & 2 deletions packages/worker/src/api/routes/global/configs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as controller from "../../controllers/global/configs"
import { auth } from "@budibase/backend-core"
import Joi from "joi"
import { ConfigType } from "@budibase/types"
import { ConfigType, PKCEMethod } from "@budibase/types"
import { adminRoutes, loggedInRoutes } from "../endpointGroups"

function smtpValidation() {
Expand Down Expand Up @@ -50,7 +50,8 @@ function oidcValidation() {
name: Joi.string().allow("", null),
uuid: Joi.string().required(),
activated: Joi.boolean().required(),
scopes: Joi.array().optional()
scopes: Joi.array().optional(),
pkce: Joi.string().valid(...Object.values(PKCEMethod)).optional()
})
).required()
}).unknown(true)
Expand Down
Loading
Loading