From 6f3ec43408800bb179f455455751d6d802e03487 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Tue, 24 Feb 2026 15:31:16 -0800 Subject: [PATCH 1/4] feat(web): add Bitbucket Server (Data Center) SSO identity provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `bitbucket-server` as a custom OAuth 2.0 SSO identity provider for self-hosted Bitbucket Server / Data Center instances. Uses a two-step userinfo fetch (whoami → user profile API) and client_secret_post for token exchange, as required by Bitbucket Server's OAuth 2.0 implementation. Co-Authored-By: Claude Sonnet 4.6 --- docs/docs/configuration/idp.mdx | 45 +++++ .../schemas/v3/identityProvider.schema.mdx | 174 ++++++++++++++++++ docs/snippets/schemas/v3/index.schema.mdx | 174 ++++++++++++++++++ .../schemas/src/v3/identityProvider.schema.ts | 174 ++++++++++++++++++ .../schemas/src/v3/identityProvider.type.ts | 37 +++- packages/schemas/src/v3/index.schema.ts | 174 ++++++++++++++++++ packages/schemas/src/v3/index.type.ts | 37 +++- packages/web/src/ee/features/sso/sso.ts | 78 ++++++-- packages/web/src/lib/utils.ts | 9 + schemas/v3/identityProvider.json | 28 +++ 10 files changed, 916 insertions(+), 14 deletions(-) diff --git a/docs/docs/configuration/idp.mdx b/docs/docs/configuration/idp.mdx index ee2c4a2fd..071b51a42 100644 --- a/docs/docs/configuration/idp.mdx +++ b/docs/docs/configuration/idp.mdx @@ -218,6 +218,51 @@ in the Bitbucket Cloud identity provider config. +### Bitbucket Server + +A Bitbucket Server (Data Center) connection can be used for [authentication](/docs/configuration/auth). + + + + + To begin, you must register an OAuth 2.0 application in your Bitbucket Server instance to facilitate the identity provider connection. + + In your Bitbucket Server admin panel, navigate to **Administration → Application Links** and create a new incoming external application link. + + When configuring your application: + - Set the redirect URL to `/api/auth/callback/bitbucket-server` (ex. https://sourcebot.coolcorp.com/api/auth/callback/bitbucket-server) + + The result of creating the application is a `CLIENT_ID` and `CLIENT_SECRET` which you'll provide to Sourcebot. + + + To provide Sourcebot the client id and secret for your OAuth application you must set them as environment variables. These can be named whatever you like + (ex. `BITBUCKET_SERVER_IDENTITY_PROVIDER_CLIENT_ID` and `BITBUCKET_SERVER_IDENTITY_PROVIDER_CLIENT_SECRET`) + + + Finally, pass the client id, client secret, and your Bitbucket Server base URL to Sourcebot by defining a `identityProvider` object in the [config file](/docs/configuration/config-file): + + ```json wrap icon="code" + { + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "identityProviders": [ + { + "provider": "bitbucket-server", + "purpose": "sso", + "baseUrl": "https://bitbucket.example.com", + "clientId": { + "env": "YOUR_CLIENT_ID_ENV_VAR" + }, + "clientSecret": { + "env": "YOUR_CLIENT_SECRET_ENV_VAR" + } + } + ] + } + ``` + + + + ### Google [Auth.js Google Provider Docs](https://authjs.dev/getting-started/providers/google) diff --git a/docs/snippets/schemas/v3/identityProvider.schema.mdx b/docs/snippets/schemas/v3/identityProvider.schema.mdx index 04c3c0de2..67b55ff51 100644 --- a/docs/snippets/schemas/v3/identityProvider.schema.mdx +++ b/docs/snippets/schemas/v3/identityProvider.schema.mdx @@ -841,6 +841,93 @@ "clientSecret", "issuer" ] + }, + "BitbucketServerIdentityProviderConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } }, "oneOf": [ @@ -1681,6 +1768,93 @@ "clientId", "clientSecret" ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } ] } diff --git a/docs/snippets/schemas/v3/index.schema.mdx b/docs/snippets/schemas/v3/index.schema.mdx index 0af7aa45a..4c65b7477 100644 --- a/docs/snippets/schemas/v3/index.schema.mdx +++ b/docs/snippets/schemas/v3/index.schema.mdx @@ -5381,6 +5381,93 @@ "clientSecret", "issuer" ] + }, + "BitbucketServerIdentityProviderConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } }, "oneOf": [ @@ -6221,6 +6308,93 @@ "clientId", "clientSecret" ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } ] } diff --git a/packages/schemas/src/v3/identityProvider.schema.ts b/packages/schemas/src/v3/identityProvider.schema.ts index b0308726b..28b540ed7 100644 --- a/packages/schemas/src/v3/identityProvider.schema.ts +++ b/packages/schemas/src/v3/identityProvider.schema.ts @@ -840,6 +840,93 @@ const schema = { "clientSecret", "issuer" ] + }, + "BitbucketServerIdentityProviderConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } }, "oneOf": [ @@ -1680,6 +1767,93 @@ const schema = { "clientId", "clientSecret" ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } ] } as const; diff --git a/packages/schemas/src/v3/identityProvider.type.ts b/packages/schemas/src/v3/identityProvider.type.ts index 85b465ca3..17aea05e6 100644 --- a/packages/schemas/src/v3/identityProvider.type.ts +++ b/packages/schemas/src/v3/identityProvider.type.ts @@ -9,7 +9,8 @@ export type IdentityProviderConfig = | MicrosoftEntraIDIdentityProviderConfig | GCPIAPIdentityProviderConfig | AuthentikIdentityProviderConfig - | BitbucketCloudIdentityProviderConfig; + | BitbucketCloudIdentityProviderConfig + | BitbucketServerIdentityProviderConfig; export interface GitHubIdentityProviderConfig { provider: "github"; @@ -331,3 +332,37 @@ export interface BitbucketCloudIdentityProviderConfig { }; accountLinkingRequired?: boolean; } +export interface BitbucketServerIdentityProviderConfig { + provider: "bitbucket-server"; + purpose: "sso"; + clientId: + | { + /** + * The name of the environment variable that contains the token. + */ + env: string; + } + | { + /** + * The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets + */ + googleCloudSecret: string; + }; + clientSecret: + | { + /** + * The name of the environment variable that contains the token. + */ + env: string; + } + | { + /** + * The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets + */ + googleCloudSecret: string; + }; + /** + * The URL of the Bitbucket Server/Data Center host. + */ + baseUrl: string; +} diff --git a/packages/schemas/src/v3/index.schema.ts b/packages/schemas/src/v3/index.schema.ts index 5293052e5..0c823a600 100644 --- a/packages/schemas/src/v3/index.schema.ts +++ b/packages/schemas/src/v3/index.schema.ts @@ -5380,6 +5380,93 @@ const schema = { "clientSecret", "issuer" ] + }, + "BitbucketServerIdentityProviderConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } }, "oneOf": [ @@ -6220,6 +6307,93 @@ const schema = { "clientId", "clientSecret" ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "clientSecret": { + "anyOf": [ + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "googleCloudSecret": { + "type": "string", + "description": "The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets" + } + }, + "required": [ + "googleCloudSecret" + ], + "additionalProperties": false + } + ] + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": [ + "https://bitbucket.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": [ + "provider", + "purpose", + "clientId", + "clientSecret", + "baseUrl" + ] } ] } diff --git a/packages/schemas/src/v3/index.type.ts b/packages/schemas/src/v3/index.type.ts index 5e603d606..ba064cd57 100644 --- a/packages/schemas/src/v3/index.type.ts +++ b/packages/schemas/src/v3/index.type.ts @@ -35,7 +35,8 @@ export type IdentityProviderConfig = | MicrosoftEntraIDIdentityProviderConfig | GCPIAPIdentityProviderConfig | AuthentikIdentityProviderConfig - | BitbucketCloudIdentityProviderConfig; + | BitbucketCloudIdentityProviderConfig + | BitbucketServerIdentityProviderConfig; export interface SourcebotConfig { $schema?: string; @@ -1489,3 +1490,37 @@ export interface BitbucketCloudIdentityProviderConfig { }; accountLinkingRequired?: boolean; } +export interface BitbucketServerIdentityProviderConfig { + provider: "bitbucket-server"; + purpose: "sso"; + clientId: + | { + /** + * The name of the environment variable that contains the token. + */ + env: string; + } + | { + /** + * The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets + */ + googleCloudSecret: string; + }; + clientSecret: + | { + /** + * The name of the environment variable that contains the token. + */ + env: string; + } + | { + /** + * The resource name of a Google Cloud secret. Must be in the format `projects//secrets//versions/`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets + */ + googleCloudSecret: string; + }; + /** + * The URL of the Bitbucket Server/Data Center host. + */ + baseUrl: string; +} diff --git a/packages/web/src/ee/features/sso/sso.ts b/packages/web/src/ee/features/sso/sso.ts index 8a74e2aa7..0043c8b77 100644 --- a/packages/web/src/ee/features/sso/sso.ts +++ b/packages/web/src/ee/features/sso/sso.ts @@ -1,12 +1,13 @@ import type { IdentityProvider } from "@/auth"; import { onCreateUser } from "@/lib/authUtils"; import { prisma } from "@/prisma"; -import { AuthentikIdentityProviderConfig, BitbucketCloudIdentityProviderConfig, GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type"; +import { AuthentikIdentityProviderConfig, BitbucketCloudIdentityProviderConfig, BitbucketServerIdentityProviderConfig, GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type"; import type { IdentityProviderType } from "@sourcebot/shared"; import { createLogger, env, getTokenFromConfig, hasEntitlement, loadConfig } from "@sourcebot/shared"; import { OAuth2Client } from "google-auth-library"; import type { User as AuthJsUser } from "next-auth"; import type { Provider } from "next-auth/providers"; +import type { TokenSet } from "@auth/core/types"; import Authentik from "next-auth/providers/authentik"; import Bitbucket from "next-auth/providers/bitbucket"; import Credentials from "next-auth/providers/credentials"; @@ -33,27 +34,27 @@ export const getEEIdentityProviders = async (): Promise => { const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const baseUrl = providerConfig.baseUrl; - providers.push({ provider: createGitHubProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.accountLinkingRequired ?? false}); + providers.push({ provider: createGitHubProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.accountLinkingRequired ?? false }); } if (identityProvider.provider === "gitlab") { const providerConfig = identityProvider as GitLabIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const baseUrl = providerConfig.baseUrl; - providers.push({ provider: createGitLabProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.accountLinkingRequired ?? false}); + providers.push({ provider: createGitLabProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.accountLinkingRequired ?? false }); } if (identityProvider.provider === "google") { const providerConfig = identityProvider as GoogleIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); - providers.push({ provider: createGoogleProvider(clientId, clientSecret), purpose: providerConfig.purpose}); + providers.push({ provider: createGoogleProvider(clientId, clientSecret), purpose: providerConfig.purpose }); } if (identityProvider.provider === "okta") { const providerConfig = identityProvider as OktaIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const issuer = await getTokenFromConfig(providerConfig.issuer); - providers.push({ provider: createOktaProvider(clientId, clientSecret, issuer), purpose: providerConfig.purpose}); + providers.push({ provider: createOktaProvider(clientId, clientSecret, issuer), purpose: providerConfig.purpose }); } if (identityProvider.provider === "keycloak") { const providerConfig = identityProvider as KeycloakIdentityProviderConfig; @@ -87,6 +88,13 @@ export const getEEIdentityProviders = async (): Promise => { const issuer = await getTokenFromConfig(providerConfig.issuer); providers.push({ provider: createAuthentikProvider(clientId, clientSecret, issuer), purpose: providerConfig.purpose }); } + if (identityProvider.provider === "bitbucket-server") { + const providerConfig = identityProvider as BitbucketServerIdentityProviderConfig; + const clientId = await getTokenFromConfig(providerConfig.clientId); + const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); + const baseUrl = providerConfig.baseUrl; + providers.push({ provider: createBitbucketServerProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose }); + } } // @deprecate in favor of defining identity providers throught the identityProvider object in the config file. This was done to allow for more control over @@ -97,32 +105,32 @@ export const getEEIdentityProviders = async (): Promise => { if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) { providers.push({ provider: createGitHubProvider(env.AUTH_EE_GITHUB_CLIENT_ID, env.AUTH_EE_GITHUB_CLIENT_SECRET, env.AUTH_EE_GITHUB_BASE_URL), purpose: "sso" }); } - + if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) { providers.push({ provider: createGitLabProvider(env.AUTH_EE_GITLAB_CLIENT_ID, env.AUTH_EE_GITLAB_CLIENT_SECRET, env.AUTH_EE_GITLAB_BASE_URL), purpose: "sso" }); } - + if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) { providers.push({ provider: createGoogleProvider(env.AUTH_EE_GOOGLE_CLIENT_ID, env.AUTH_EE_GOOGLE_CLIENT_SECRET), purpose: "sso" }); } - + if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) { providers.push({ provider: createOktaProvider(env.AUTH_EE_OKTA_CLIENT_ID, env.AUTH_EE_OKTA_CLIENT_SECRET, env.AUTH_EE_OKTA_ISSUER), purpose: "sso" }); } - + if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) { providers.push({ provider: createKeycloakProvider(env.AUTH_EE_KEYCLOAK_CLIENT_ID, env.AUTH_EE_KEYCLOAK_CLIENT_SECRET, env.AUTH_EE_KEYCLOAK_ISSUER), purpose: "sso" }); } - + if (env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID && env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET && env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER) { providers.push({ provider: createMicrosoftEntraIDProvider(env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID, env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET, env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER), purpose: "sso" }); } - + if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) { providers.push({ provider: createGCPIAPProvider(env.AUTH_EE_GCP_IAP_AUDIENCE), purpose: "sso" }); } } - + return providers; } @@ -245,6 +253,52 @@ const createBitbucketCloudProvider = (clientId: string, clientSecret: string): P }); } +const createBitbucketServerProvider = (clientId: string, clientSecret: string, baseUrl: string): Provider => { + return { + id: 'bitbucket-server' satisfies IdentityProviderType, + name: "Bitbucket Server", + type: "oauth", + clientId, + clientSecret, + authorization: { + url: `${baseUrl}/rest/oauth2/latest/authorize`, + params: { + response_type: "code", + // @see: https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html + scope: [ + "PUBLIC_REPOS" + ].join(' ') + }, + }, + token: { url: `${baseUrl}/rest/oauth2/latest/token` }, + // Bitbucket Server expects client credentials as body params, not Basic Auth header + client: { token_endpoint_auth_method: "client_secret_post" }, + userinfo: { + // url is required by Auth.js endpoint validation; the request function overrides the actual fetch + url: `${baseUrl}/plugins/servlet/applinks/whoami`, + async request({ tokens }: { tokens: TokenSet }) { + const whoamiRes = await fetch(`${baseUrl}/plugins/servlet/applinks/whoami`, { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + const username = (await whoamiRes.text()).trim(); + const profileRes = await fetch(`${baseUrl}/rest/api/1.0/users/${username}`, { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + return profileRes.json(); + }, + }, + profile(profile) { + return { + id: String(profile.id), + name: profile.displayName, + email: profile.emailAddress, + image: null, + }; + }, + allowDangerousEmailAccountLinking: env.AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING === 'true', + } as Provider; +} + export const createAuthentikProvider = (clientId: string, clientSecret: string, issuer: string): Provider => { return Authentik({ id: 'authentik' satisfies IdentityProviderType, diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index 55cd5249d..e240c5a06 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -160,6 +160,15 @@ export const getAuthProviderInfo = (providerId: string): AuthProviderInfo => { src: bitbucketLogo, }, }; + case "bitbucket-server": + return { + id: "bitbucket-server", + name: "Bitbucket Server", + displayName: "Bitbucket Server", + icon: { + src: bitbucketLogo, + }, + }; default: return { id: providerId, diff --git a/schemas/v3/identityProvider.json b/schemas/v3/identityProvider.json index ac8989fa1..b6b913066 100644 --- a/schemas/v3/identityProvider.json +++ b/schemas/v3/identityProvider.json @@ -215,6 +215,31 @@ } }, "required": ["provider", "purpose", "clientId", "clientSecret", "issuer"] + }, + "BitbucketServerIdentityProviderConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "bitbucket-server" + }, + "purpose": { + "const": "sso" + }, + "clientId": { + "$ref": "./shared.json#/definitions/Token" + }, + "clientSecret": { + "$ref": "./shared.json#/definitions/Token" + }, + "baseUrl": { + "type": "string", + "description": "The URL of the Bitbucket Server/Data Center host.", + "examples": ["https://bitbucket.example.com"], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + } + }, + "required": ["provider", "purpose", "clientId", "clientSecret", "baseUrl"] } }, "oneOf": [ @@ -244,6 +269,9 @@ }, { "$ref": "#/definitions/BitbucketCloudIdentityProviderConfig" + }, + { + "$ref": "#/definitions/BitbucketServerIdentityProviderConfig" } ] } From 764147feb853f75bfbacf8761c10bae2463ced68 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Tue, 24 Feb 2026 15:31:44 -0800 Subject: [PATCH 2/4] chore: update CHANGELOG for #934 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb05995bc..7080fa16d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added PostHog events for chat UI interactions (details card expand/collapse, copy answer, table of contents toggle) and repo tracking in `wa_chat_message_sent`. [#922](https://github.com/sourcebot-dev/sourcebot/pull/922) - Added Bitbucket Cloud OAuth identity provider support (`provider: "bitbucket-cloud"`) for SSO and account-linked permission syncing. [#924](https://github.com/sourcebot-dev/sourcebot/pull/924) - Added permission syncing support for Bitbucket Cloud. [#925](https://github.com/sourcebot-dev/sourcebot/pull/925) +- Added Bitbucket Server (Data Center) OAuth 2.0 SSO identity provider support (`provider: "bitbucket-server"`). [#934](https://github.com/sourcebot-dev/sourcebot/pull/934) ### Changed - Hide version upgrade toast for askgithub deployment (`EXPERIMENT_ASK_GH_ENABLED`). [#931](https://github.com/sourcebot-dev/sourcebot/pull/931) From e1ecf25f15cbc3435d6e6f79d2a778b0c0fb096b Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Tue, 24 Feb 2026 16:01:50 -0800 Subject: [PATCH 3/4] Update packages/web/src/ee/features/sso/sso.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/web/src/ee/features/sso/sso.ts | 28 +++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/web/src/ee/features/sso/sso.ts b/packages/web/src/ee/features/sso/sso.ts index 0043c8b77..dd2fce03a 100644 --- a/packages/web/src/ee/features/sso/sso.ts +++ b/packages/web/src/ee/features/sso/sso.ts @@ -277,14 +277,34 @@ const createBitbucketServerProvider = (clientId: string, clientSecret: string, b // url is required by Auth.js endpoint validation; the request function overrides the actual fetch url: `${baseUrl}/plugins/servlet/applinks/whoami`, async request({ tokens }: { tokens: TokenSet }) { + const accessToken = tokens.access_token; + if (!accessToken) { + throw new Error("Missing access token for Bitbucket Server userinfo request"); + } + const whoamiRes = await fetch(`${baseUrl}/plugins/servlet/applinks/whoami`, { - headers: { Authorization: `Bearer ${tokens.access_token}` }, + headers: { Authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(10_000), }); + if (!whoamiRes.ok) { + throw new Error(`Bitbucket whoami failed (${whoamiRes.status})`); + } + const username = (await whoamiRes.text()).trim(); - const profileRes = await fetch(`${baseUrl}/rest/api/1.0/users/${username}`, { - headers: { Authorization: `Bearer ${tokens.access_token}` }, + if (!username) { + throw new Error("Bitbucket whoami returned an empty username"); + } + + const profileRes = await fetch(`${baseUrl}/rest/api/1.0/users/${encodeURIComponent(username)}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(10_000), }); - return profileRes.json(); + if (!profileRes.ok) { + throw new Error(`Bitbucket profile lookup failed (${profileRes.status})`); + } + + return await profileRes.json(); + } }, }, profile(profile) { From 0cc09c1e67a22f61f2d643c898b99c403da6f0ce Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Tue, 24 Feb 2026 16:05:59 -0800 Subject: [PATCH 4/4] nit --- packages/web/src/ee/features/sso/sso.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web/src/ee/features/sso/sso.ts b/packages/web/src/ee/features/sso/sso.ts index dd2fce03a..53761c3c5 100644 --- a/packages/web/src/ee/features/sso/sso.ts +++ b/packages/web/src/ee/features/sso/sso.ts @@ -305,7 +305,6 @@ const createBitbucketServerProvider = (clientId: string, clientSecret: string, b return await profileRes.json(); } - }, }, profile(profile) { return {