diff --git a/.changeset/modern-hounds-give.md b/.changeset/modern-hounds-give.md new file mode 100644 index 0000000000000..d02c993ee0f84 --- /dev/null +++ b/.changeset/modern-hounds-give.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend-module-bitbucket-provider': minor +'@backstage/plugin-auth-backend': patch +--- + +Migrate the Bitbucket auth provider to the new `@backstage/plugin-auth-backend-module-bitbucket-provider` module package. diff --git a/plugins/auth-backend-module-bitbucket-provider/.eslintrc.js b/plugins/auth-backend-module-bitbucket-provider/.eslintrc.js new file mode 100644 index 0000000000000..e2a53a6ad283f --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-bitbucket-provider/README.md b/plugins/auth-backend-module-bitbucket-provider/README.md new file mode 100644 index 0000000000000..e6937f9a96b4a --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: Bitbucket Provider + +This module provides an Bitbucket.org auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-bitbucket-provider) +- [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-bitbucket-provider/api-report.md b/plugins/auth-backend-module-bitbucket-provider/api-report.md new file mode 100644 index 0000000000000..11df004d0e3ab --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/api-report.md @@ -0,0 +1,34 @@ +## API Report File for "@backstage/plugin-auth-backend-module-bitbucket-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleBitbucketProvider: () => BackendFeature; +export default authModuleBitbucketProvider; + +// @public (undocumented) +export const bitbucketAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace bitbucketSignInResolvers { + const userIdMatchingUserEntityAnnotation: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; + const usernameMatchingUserEntityAnnotation: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-bitbucket-provider/catalog-info.yaml b/plugins/auth-backend-module-bitbucket-provider/catalog-info.yaml new file mode 100644 index 0000000000000..817eb561a57ce --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-bitbucket-provider + title: '@backstage/plugin-auth-backend-module-bitbucket-provider' + description: The bitbucket-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-bitbucket-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-provider/config.d.ts new file mode 100644 index 0000000000000..81e835a5e6a56 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/config.d.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + bitbucket?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-bitbucket-provider/dev/index.ts b/plugins/auth-backend-module-bitbucket-provider/dev/index.ts new file mode 100644 index 0000000000000..6b255f0f1522b --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/dev/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('@backstage/plugin-auth-backend')); +backend.add(import('../src')); + +backend.start(); diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json new file mode 100644 index 0000000000000..15f80547d4cca --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", + "version": "0.0.0", + "description": "The bitbucket-provider backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-bitbucket-provider" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "express": "^4.18.2", + "passport": "^0.7.0", + "passport-bitbucket-oauth2": "^0.1.2" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "supertest": "^6.3.3" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts b/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts new file mode 100644 index 0000000000000..bc962ec773164 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; + +/** @public */ +export const bitbucketAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientID = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const baseURL = 'https://bitbucket.org/site/oauth2'; + + return PassportOAuthAuthenticatorHelper.from( + new BitbucketStrategy( + { + clientID, + clientSecret, + callbackURL: callbackUrl, + passReqToCallback: false, + baseURL, + authorizationURL: `${baseURL}/authorize`, + tokenURL: `${baseURL}/access_token`, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + return helper.authenticate(input); + }, + + async refresh(input, helper) { + return helper.refresh(input); + }, +}); diff --git a/plugins/auth-backend-module-bitbucket-provider/src/index.ts b/plugins/auth-backend-module-bitbucket-provider/src/index.ts new file mode 100644 index 0000000000000..9ed8bcf0b88ef --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The bitbucket-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { bitbucketAuthenticator } from './authenticator'; +export { authModuleBitbucketProvider as default } from './module'; +export { bitbucketSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts b/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts new file mode 100644 index 0000000000000..e761369a9c3da --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authModuleBitbucketProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleBitbucketProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleBitbucketProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + bitbucket: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/bitbucket/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('bitbucket-nonce', { + domain: 'localhost', + path: '/api/auth/bitbucket/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://bitbucket.org'); + expect(startUrl.pathname).toBe('/site/oauth2/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/bitbucket/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +}); diff --git a/plugins/auth-backend-module-bitbucket-provider/src/module.ts b/plugins/auth-backend-module-bitbucket-provider/src/module.ts new file mode 100644 index 0000000000000..77d7e1e7a7740 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/src/module.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { bitbucketAuthenticator } from './authenticator'; +import { bitbucketSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleBitbucketProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'bitbucket-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'bitbucket', + factory: createOAuthProviderFactory({ + authenticator: bitbucketAuthenticator, + signInResolverFactories: { + ...bitbucketSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts new file mode 100644 index 0000000000000..f9d834a3a702f --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the Bitbucket auth provider. + * + * @public + */ +export namespace bitbucketSignInResolvers { + /** + * Looks up the user by matching their Bitbucket user ID with the `bitbucket.org/user-id` annotation. + */ + export const userIdMatchingUserEntityAnnotation = createSignInResolverFactory( + { + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { result } = info; + + const id = result.fullProfile.id; + if (!id) { + throw new Error('Bitbucket user profile does not contain an ID'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'bitbucket.org/user-id': id, + }, + }); + }; + }, + }, + ); + + /** + * Looks up the user by matching their Bitbucket username with the `bitbucket.org/username` annotation. + */ + export const usernameMatchingUserEntityAnnotation = + createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { result } = info; + + const username = result.fullProfile.username; + if (!username) { + throw new Error( + 'Bitbucket user profile does not contain a Username', + ); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'bitbucket.org/username': username, + }, + }); + }; + }, + }); +} diff --git a/plugins/auth-backend/src/providers/bitbucket/types.d.ts b/plugins/auth-backend-module-bitbucket-provider/src/types.d.ts similarity index 100% rename from plugins/auth-backend/src/providers/bitbucket/types.d.ts rename to plugins/auth-backend-module-bitbucket-provider/src/types.d.ts diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 1beddb6419b3c..9d240dd88d392 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -78,7 +78,7 @@ export type AuthResponse = ClientAuthResponse; // @public @deprecated export type AwsAlbResult = AwsAlbResult_2; -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketOAuthResult = { fullProfile: BitbucketPassportProfile; params: { @@ -90,7 +90,7 @@ export type BitbucketOAuthResult = { refreshToken?: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketPassportProfile = Profile & { id?: string; displayName?: string; @@ -425,8 +425,8 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - usernameMatchingUserEntityAnnotation(): SignInResolver_2; - userIdMatchingUserEntityAnnotation(): SignInResolver_2; + userIdMatchingUserEntityAnnotation: () => SignInResolver_2; + usernameMatchingUserEntityAnnotation: () => SignInResolver_2; }>; }>; bitbucketServer: Readonly<{ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 001c05b41169a..58fbc7d8d0b5a 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -45,6 +45,7 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^", "@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^", "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^", @@ -81,7 +82,6 @@ "openid-client": "^5.2.1", "passport": "^0.7.0", "passport-auth0": "^1.4.3", - "passport-bitbucket-oauth2": "^0.1.2", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "passport-microsoft": "^1.0.0", diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts deleted file mode 100644 index 66a8f6e396e5d..0000000000000 --- a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BitbucketAuthProvider, BitbucketOAuthResult } from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { AuthResolverContext } from '@backstage/plugin-auth-node'; - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ result: BitbucketOAuthResult; privateInfo: any }> ->; - -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -describe('createBitbucketProvider', () => { - it('should auth', async () => { - const provider = new BitbucketAuthProvider({ - resolverContext: {} as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://google.com/lols', - }, - }), - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'mock', - }); - - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - _json: { - links: { - avatar: { - href: 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }, - }, - emails: [{ value: 'conrad@example.com' }], - displayName: 'Conrad', - id: 'conrad', - provider: 'google', - }, - params: { - id_token: 'idToken', - scope: 'scope', - expires_in: 123, - }, - accessToken: 'accessToken', - }, - privateInfo: { - refreshToken: 'wacka', - }, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual({ - providerInfo: { - accessToken: 'accessToken', - expiresInSeconds: 123, - idToken: 'idToken', - scope: 'scope', - }, - profile: { - email: 'conrad@example.com', - displayName: 'Conrad', - picture: 'http://google.com/lols', - }, - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 4a7f3770a4a7c..2cebb15861c0e 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -14,46 +14,28 @@ * limitations under the License. */ -import express from 'express'; -import passport, { Profile as PassportProfile } from 'passport'; -import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; import { - encodeState, - OAuthAdapter, - OAuthEnvironmentHandler, - OAuthHandlers, - OAuthProviderOptions, - OAuthRefreshRequest, - OAuthResponse, - OAuthResult, - OAuthStartRequest, -} from '../../lib/oauth'; + bitbucketAuthenticator, + bitbucketSignInResolvers, +} from '@backstage/plugin-auth-backend-module-bitbucket-provider'; import { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; -import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { AuthHandler, OAuthStartResponse } from '../types'; -import { - AuthResolverContext, SignInResolver, + createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; +import { Profile as PassportProfile } from 'passport'; +import { + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, + adaptOAuthSignInResolverToLegacy, +} from '../../lib/legacy'; +import { OAuthResult } from '../../lib/oauth'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { AuthHandler } from '../types'; -type PrivateInfo = { - refreshToken: string; -}; - -type Options = OAuthProviderOptions & { - signInResolver?: SignInResolver; - authHandler: AuthHandler; - resolverContext: AuthResolverContext; -}; - -/** @public */ +/** + * @public + * @deprecated The Bitbucket auth provider was extracted to `@backstage/plugin-auth-backend-module-bitbucket-provider`. + */ export type BitbucketOAuthResult = { fullProfile: BitbucketPassportProfile; params: { @@ -65,7 +47,10 @@ export type BitbucketOAuthResult = { refreshToken?: string; }; -/** @public */ +/** + * @public + * @deprecated The Bitbucket auth provider was extracted to `@backstage/plugin-auth-backend-module-bitbucket-provider`. + */ export type BitbucketPassportProfile = PassportProfile & { id?: string; displayName?: string; @@ -80,119 +65,8 @@ export type BitbucketPassportProfile = PassportProfile & { }; }; -export class BitbucketAuthProvider implements OAuthHandlers { - private readonly _strategy: BitbucketStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - - constructor(options: Options) { - this.signInResolver = options.signInResolver; - this.authHandler = options.authHandler; - this.resolverContext = options.resolverContext; - this._strategy = new BitbucketStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - passReqToCallback: false, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: passport.Profile, - done: PassportDoneCallback, - ) => { - done( - undefined, - { - fullProfile, - params, - accessToken, - refreshToken, - }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { - accessType: 'offline', - prompt: 'consent', - scope: req.scope, - state: encodeState(req.state), - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - OAuthResult, - PrivateInfo - >(req, this._strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ); - return { - response: await this.handleResult({ - fullProfile, - params, - accessToken, - }), - refreshToken, - }; - } - - private async handleResult(result: BitbucketOAuthResult) { - result.fullProfile.avatarUrl = - result.fullProfile._json!.links!.avatar!.href; - const { profile } = await this.authHandler(result, this.resolverContext); - - const response: OAuthResponse = { - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; - } -} - /** - * Auth provider integration for BitBucket auth + * Auth provider integration for Bitbucket auth * * @public */ @@ -208,79 +82,19 @@ export const bitbucket = createAuthProviderIntegration({ * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = - options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); - - const provider = new BitbucketAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); - }, - resolvers: { - /** - * Looks up the user by matching their username to the `bitbucket.org/username` annotation. - */ - usernameMatchingUserEntityAnnotation(): SignInResolver { - return async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile.username) { - throw new Error('Bitbucket profile contained no Username'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'bitbucket.org/username': result.fullProfile.username, - }, - }); - }; - }, - /** - * Looks up the user by matching their user ID to the `bitbucket.org/user-id` annotation. - */ - userIdMatchingUserEntityAnnotation(): SignInResolver { - return async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile.id) { - throw new Error('Bitbucket profile contained no User ID'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'bitbucket.org/user-id': result.fullProfile.id, - }, - }); - }; - }, + return createOAuthProviderFactory({ + authenticator: bitbucketAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, + resolvers: adaptOAuthSignInResolverToLegacy({ + userIdMatchingUserEntityAnnotation: + bitbucketSignInResolvers.userIdMatchingUserEntityAnnotation(), + usernameMatchingUserEntityAnnotation: + bitbucketSignInResolvers.usernameMatchingUserEntityAnnotation(), + }), }); diff --git a/yarn.lock b/yarn.lock index a05765d50a39a..bbeab7408128a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4793,6 +4793,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-bitbucket-provider@workspace:^, @backstage/plugin-auth-backend-module-bitbucket-provider@workspace:plugins/auth-backend-module-bitbucket-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-bitbucket-provider@workspace:plugins/auth-backend-module-bitbucket-provider" + dependencies: + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + express: ^4.18.2 + passport: ^0.7.0 + passport-bitbucket-oauth2: ^0.1.2 + supertest: ^6.3.3 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-gcp-iap-provider@workspace:^, @backstage/plugin-auth-backend-module-gcp-iap-provider@workspace:plugins/auth-backend-module-gcp-iap-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-gcp-iap-provider@workspace:plugins/auth-backend-module-gcp-iap-provider" @@ -5025,6 +5042,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^" "@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^" @@ -5072,7 +5090,6 @@ __metadata: openid-client: ^5.2.1 passport: ^0.7.0 passport-auth0: ^1.4.3 - passport-bitbucket-oauth2: ^0.1.2 passport-github2: ^0.1.12 passport-google-oauth20: ^2.0.0 passport-microsoft: ^1.0.0