diff --git a/CHANGELOG.md b/CHANGELOG.md
index 584f6aa3b..44d267999 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added permission syncing support for Bitbucket Cloud. [#925](https://github.com/sourcebot-dev/sourcebot/pull/925)
- Added `wa_user_created` PostHog event fired on successful user sign-up. [#933](https://github.com/sourcebot-dev/sourcebot/pull/933)
- Added `wa_askgh_login_wall_prompted` PostHog event fired when an unauthenticated user attempts to ask a question on Ask GitHub. [#933](https://github.com/sourcebot-dev/sourcebot/pull/933)
+- 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)
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..53761c3c5 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,71 @@ 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 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 ${accessToken}` },
+ signal: AbortSignal.timeout(10_000),
+ });
+ if (!whoamiRes.ok) {
+ throw new Error(`Bitbucket whoami failed (${whoamiRes.status})`);
+ }
+
+ const username = (await whoamiRes.text()).trim();
+ 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),
+ });
+ if (!profileRes.ok) {
+ throw new Error(`Bitbucket profile lookup failed (${profileRes.status})`);
+ }
+
+ return await 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"
}
]
}