Add Bitbucket Cloud OAuth support

Signed-off-by: Jake Smith <jakemgsmith@gmail.com>
This commit is contained in:
Jake Smith
2025-11-22 01:47:59 +00:00
committed by Fredrik Adelöw
parent e404b12604
commit 959e6ecc5a
15 changed files with 398 additions and 46 deletions
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { BitbucketCloudIntegrationConfig } from '@backstage/integration';
import {
BitbucketCloudIntegrationConfig,
getBitbucketCloudOAuthToken,
} from '@backstage/integration';
import fetch, { Request } from 'cross-fetch';
import { Models } from './models';
import { WithPagination } from './pagination';
@@ -139,22 +142,32 @@ export class BitbucketCloudClient {
}
private async request(req: Request): Promise<Response> {
return fetch(req, { headers: this.getAuthHeaders() }).then(
(response: Response) => {
if (!response.ok) {
throw new Error(
`Unexpected response for ${req.method} ${req.url}. Expected 200 but got ${response.status} - ${response.statusText}`,
);
}
const headers = await this.getAuthHeaders();
return fetch(req, { headers }).then((response: Response) => {
if (!response.ok) {
throw new Error(
`Unexpected response for ${req.method} ${req.url}. Expected 200 but got ${response.status} - ${response.statusText}`,
);
}
return response;
},
);
return response;
});
}
private getAuthHeaders(): Record<string, string> {
private async getAuthHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {};
// OAuth authentication (clientId/clientSecret)
if (this.config.clientId && this.config.clientSecret) {
const token = await getBitbucketCloudOAuthToken(
this.config.clientId,
this.config.clientSecret,
);
headers.Authorization = `Bearer ${token}`;
return headers;
}
// Basic authentication (username/token or username/appPassword)
if (
this.config.username &&
(this.config.token ?? this.config.appPassword)
@@ -216,7 +216,7 @@ export function createPublishBitbucketCloudAction(options: {
);
}
const authorization = getAuthorizationHeader(
const authorization = await getAuthorizationHeader(
ctx.input.token ? { token: ctx.input.token } : integrationConfig.config,
);
@@ -22,11 +22,11 @@ jest.mock('bitbucket', () => ({
}));
describe('bitbucketCloud:branchRestriction:create', () => {
it('getBitbucketClient should return the correct headers with username and password', () => {
it('getBitbucketClient should return the correct headers with username and password', async () => {
expect.assertions(1);
const username = 'username';
const password = 'password';
getBitbucketClient({ username: username, appPassword: password });
await getBitbucketClient({ username: username, appPassword: password });
expect(Bitbucket).toHaveBeenCalledWith({
auth: {
username: username,
@@ -35,12 +35,14 @@ describe('bitbucketCloud:branchRestriction:create', () => {
});
});
it('getBitbucketClient should throw if only one of username or password is provided', () => {
it('getBitbucketClient should throw if only one of username or password is provided', async () => {
expect.assertions(2);
const username = 'username';
const password = 'password';
expect(() => getBitbucketClient({ username })).toThrow(Error);
expect(() => getBitbucketClient({ appPassword: password })).toThrow(Error);
await expect(getBitbucketClient({ username })).rejects.toThrow(Error);
await expect(getBitbucketClient({ appPassword: password })).rejects.toThrow(
Error,
);
});
});
@@ -37,6 +37,8 @@ const createBitbucketCloudBranchRestriction = async (opts: {
token?: string;
username?: string;
appPassword?: string;
clientId?: string;
clientSecret?: string;
};
}) => {
const {
@@ -52,7 +54,7 @@ const createBitbucketCloudBranchRestriction = async (opts: {
authorization,
} = opts;
const bitbucket = getBitbucketClient(authorization);
const bitbucket = await getBitbucketClient(authorization);
return await bitbucket.branchrestrictions.create({
_body: {
groups: groups,
@@ -68,7 +68,7 @@ export const createBitbucketPipelinesRunAction = (options: {
const host = 'bitbucket.org';
const integrationConfig = integrations.bitbucketCloud.byHost(host);
const authorization = getAuthorizationHeader(
const authorization = await getAuthorizationHeader(
token ? { token } : integrationConfig!.config,
);
let response: Response;
@@ -317,7 +317,7 @@ export function createPublishBitbucketCloudPullRequestAction(options: {
);
}
const authorization = getAuthorizationHeader(
const authorization = await getAuthorizationHeader(
ctx.input.token ? { token: ctx.input.token } : integrationConfig.config,
);
@@ -15,12 +15,28 @@
*/
import { Bitbucket } from 'bitbucket';
import { getBitbucketCloudOAuthToken } from '@backstage/integration';
export const getBitbucketClient = (config: {
export const getBitbucketClient = async (config: {
token?: string;
username?: string;
appPassword?: string;
clientId?: string;
clientSecret?: string;
}) => {
// If OAuth credentials provided, fetch token
if (config.clientId && config.clientSecret) {
const token = await getBitbucketCloudOAuthToken(
config.clientId,
config.clientSecret,
);
return new Bitbucket({
auth: {
token,
},
});
}
if (config.token) {
return new Bitbucket({
auth: {
@@ -38,15 +54,26 @@ export const getBitbucketClient = (config: {
});
}
throw new Error(
`Authorization has not been provided for Bitbucket Cloud. Please add either provide a username and token or username and appPassword to the Integrations config`,
`Authorization has not been provided for Bitbucket Cloud. Please provide either OAuth credentials (clientId/clientSecret), username and token, or username and appPassword in the Integrations config`,
);
};
export const getAuthorizationHeader = (config: {
export const getAuthorizationHeader = async (config: {
username?: string;
appPassword?: string;
token?: string;
}) => {
clientId?: string;
clientSecret?: string;
}): Promise<string> => {
// OAuth authentication
if (config.clientId && config.clientSecret) {
const token = await getBitbucketCloudOAuthToken(
config.clientId,
config.clientSecret,
);
return `Bearer ${token}`;
}
// TODO: appPassword can be removed once fully
// deprecated by BitBucket on 9th June 2026.
if (config.username && (config.token ?? config.appPassword)) {
@@ -62,6 +89,6 @@ export const getAuthorizationHeader = (config: {
}
throw new Error(
`Authorization has not been provided for Bitbucket Cloud. Please add either provide a username and token or username and appPassword to the Integrations config`,
`Authorization has not been provided for Bitbucket Cloud. Please provide either OAuth credentials (clientId/clientSecret), username and token, or username and appPassword in the Integrations config`,
);
};