diff --git a/.changeset/fast-experts-grin.md b/.changeset/fast-experts-grin.md new file mode 100644 index 0000000000..ef5f5b2488 --- /dev/null +++ b/.changeset/fast-experts-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Migrated the `Bitbucket Server` auth provider to be implemented using the new `@backstage/plugin-auth-backend-module-bitbucket-server-provider` module. diff --git a/.changeset/silent-oranges-trade.md b/.changeset/silent-oranges-trade.md new file mode 100644 index 0000000000..69d526817e --- /dev/null +++ b/.changeset/silent-oranges-trade.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a `Bitbucket Server` auth provider. diff --git a/docs/auth/bitbucketServer/provider--old.md b/docs/auth/bitbucketServer/provider--old.md new file mode 100644 index 0000000000..fdd8fa35be --- /dev/null +++ b/docs/auth/bitbucketServer/provider--old.md @@ -0,0 +1,60 @@ +--- +id: provider--od +title: Bitbucket Server Authentication Provider +sidebar_label: Bitbucket Server +description: Adding Bitbucket Server OAuth as an authentication provider in Backstage +--- + +:::info +This documentation is written for the old backend which has been replaced by +[the new backend system](../../backend-system/index.md), being the default since +Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new +backend system, you may want to read [its own article](./provider.md) +instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! +::: + +The Backstage `core-plugin-api` package comes with a Bitbucket Server authentication provider that can authenticate +users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. + +## Create an Application Link in Bitbucket Server + +To add Bitbucket Server authentication, you must create an incoming application link. Follow the steps described in +the [Bitbucket Server documentation](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html) +to create one. + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the root `auth` configuration: + +```yaml +auth: + environment: development + providers: + bitbucketServer: + development: + host: bitbucket.org + clientId: ${AUTH_BITBUCKET_SERVER_CLIENT_ID} + clientSecret: ${AUTH_BITBUCKET_SERVER_CLIENT_SECRET} +``` + +The Bitbucket Server provider is a structure with two configuration keys: + +- `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. +- `clientSecret`: The client secret tied to the generated client ID. + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `bitbucketServerAuthApi` reference and `SignInPage` component as shown +in [Adding the provider to the sign-in page](../index.md#sign-in-configuration). + +## Using Bitbucket Server for sign-in + +In order to use the Bitbucket Server provider for sign-in, you must configure it with a `signIn.resolver`. See +the [Sign-In Resolver documentation](../identity-resolver.md) for more details on how this is done. Note that for the +Bitbucket Server provider, you'll want to use `bitbucketServer` as the provider ID, +and `providers.bitbucketServer.create` for the provider factory. + +The `@backstage/plugin-auth-backend` plugin also comes with a built-in resolver that can be used if desired. +The `emailMatchingUserEntityProfileEmail` identifies users by matching their Bitbucket Server email address to the email +address of `User` entities in the catalog. Note that you must populate your catalog with matching entities or users will +not be able to sign in with this resolver. diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index 82aebc20b2..b15f0b206a 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -5,6 +5,13 @@ sidebar_label: Bitbucket Server description: Adding Bitbucket Server OAuth as an authentication provider in Backstage --- +:::info +This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage +[version 1.24](../../releases/v1.24.0.md). If you are still on the old backend +system, you may want to read [its own article](./provider--old.md) +instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +::: + The Backstage `core-plugin-api` package comes with a Bitbucket Server authentication provider that can authenticate users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. @@ -34,19 +41,43 @@ The Bitbucket Server provider is a structure with two configuration keys: - `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. - `clientSecret`: The client secret tied to the generated client ID. +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-bitbucket-server-provider +``` + +Then we will need to add this line: + +```ts title="packages/backend/src/index.ts" +//... +backend.add(import('@backstage/plugin-auth-backend')); +// highlight-add-start +backend.add( + import('@backstage/plugin-auth-backend-module-bitbucket-server-provider'), +); +// highlight-add-end +//... +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `bitbucketServerAuthApi` reference and `SignInPage` component as shown in [Adding the provider to the sign-in page](../index.md#sign-in-configuration). - -## Using Bitbucket Server for sign-in - -In order to use the Bitbucket Server provider for sign-in, you must configure it with a `signIn.resolver`. See -the [Sign-In Resolver documentation](../identity-resolver.md) for more details on how this is done. Note that for the -Bitbucket Server provider, you'll want to use `bitbucketServer` as the provider ID, -and `providers.bitbucketServer.create` for the provider factory. - -The `@backstage/plugin-auth-backend` plugin also comes with a built-in resolver that can be used if desired. -The `emailMatchingUserEntityProfileEmail` identifies users by matching their Bitbucket Server email address to the email -address of `User` entities in the catalog. Note that you must populate your catalog with matching entities or users will -not be able to sign in with this resolver. diff --git a/plugins/auth-backend-module-bitbucket-server-provider/.eslintrc.js b/plugins/auth-backend-module-bitbucket-server-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-bitbucket-server-provider/README.md b/plugins/auth-backend-module-bitbucket-server-provider/README.md new file mode 100644 index 0000000000..d3498ffc1b --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: Bitbucket Server Provider + +This module provides an Bitbucket Server auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Repository](https://gitlab.com/backstage/backstage/tree/master/plugins/auth-backend-module-bitbucket-server-provider) +- [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-bitbucket-server-provider/api-report.md b/plugins/auth-backend-module-bitbucket-server-provider/api-report.md new file mode 100644 index 0000000000..d5c484f6e3 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/api-report.md @@ -0,0 +1,33 @@ +## API Report File for "@backstage/plugin-auth-backend-module-bitbucket-server-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 authModuleBitbucketServerProvider: BackendFeature; +export default authModuleBitbucketServerProvider; + +// @public (undocumented) +export const bitbucketServerAuthenticator: OAuthAuthenticator< + { + helper: PassportOAuthAuthenticatorHelper; + host: string; + }, + PassportProfile +>; + +// @public +export namespace bitbucketServerSignInResolvers { + const emailMatchingUserEntityProfileEmail: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-bitbucket-server-provider/catalog-info.yaml b/plugins/auth-backend-module-bitbucket-server-provider/catalog-info.yaml new file mode 100644 index 0000000000..bcb659b1c2 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-bitbucket-server-provider + title: '@backstage/plugin-auth-backend-module-bitbucket-server-provider' + description: The bitbucket-server-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-server-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts new file mode 100644 index 0000000000..ee924f47f0 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts @@ -0,0 +1,34 @@ +/* + * 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 */ + bitbucketServer?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + host: string; + callbackUrl?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-bitbucket-server-provider/dev/index.ts b/plugins/auth-backend-module-bitbucket-server-provider/dev/index.ts new file mode 100644 index 0000000000..b52bbf830b --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/dev/index.ts @@ -0,0 +1,26 @@ +/* + * 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'; +import authPlugin from '@backstage/plugin-auth-backend'; +import authModuleBitbucketServerProvider from '../src'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleBitbucketServerProvider); + +backend.start(); diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json new file mode 100644 index 0000000000..cf100c6106 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/plugin-auth-backend-module-bitbucket-server-provider", + "version": "0.0.0", + "description": "The bitbucket-server-provider backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "auth", + "pluginPackage": "@backstage/plugin-auth-backend" + }, + "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-server-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:^", + "node-fetch": "^2.7.0", + "passport": "^0.7.0", + "passport-oauth2": "^1.6.1" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "@types/passport-oauth2": "^1.4.15", + "supertest": "^6.3.3" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/authenticator.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/authenticator.ts new file mode 100644 index 0000000000..052100cd40 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/authenticator.ts @@ -0,0 +1,93 @@ +/* + * 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 OAuth2Strategy, VerifyCallback } from 'passport-oauth2'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { fetchProfile } from './helpers'; + +/** @public */ +export const bitbucketServerAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientID = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const host = config.getString('host'); + const callbackURL = config.getOptionalString('callbackUrl') ?? callbackUrl; + + const helper = PassportOAuthAuthenticatorHelper.from( + new OAuth2Strategy( + { + clientID, + clientSecret, + callbackURL, + authorizationURL: `https://${host}/rest/oauth2/latest/authorize`, + tokenURL: `https://${host}/rest/oauth2/latest/token`, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: VerifyCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + + return { helper, host }; + }, + + async start(input, { helper }) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, { helper, host }) { + const result = await helper.authenticate(input); + + // The OAuth2 strategy does not return a user profile, so we fetch it manually + const fullProfile = await fetchProfile({ + host, + accessToken: result.session.accessToken, + }); + + return { ...result, fullProfile }; + }, + + async refresh(input, { helper, host }) { + const result = await helper.refresh(input); + + // The OAuth2 strategy does not return a user profile, so we fetch it manually + const fullProfile = await fetchProfile({ + host, + accessToken: result.session.accessToken, + }); + + return { ...result, fullProfile }; + }, +}); diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/helpers.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/helpers.ts new file mode 100644 index 0000000000..fc16253b27 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/helpers.ts @@ -0,0 +1,83 @@ +/* + * 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 fetch from 'node-fetch'; +import { PassportProfile } from '@backstage/plugin-auth-node'; + +export async function fetchProfile(options: { + host: string; + accessToken: string; +}): Promise { + const { host, accessToken } = options; + // Get current user name + let whoAmIResponse; + try { + whoAmIResponse = await fetch( + `https://${host}/plugins/servlet/applinks/whoami`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + } catch (e) { + throw new Error(`Failed to retrieve the username of the logged in user`); + } + + // A response.ok check here would be worthless as the Bitbucket API always returns 200 OK for this call + const username = whoAmIResponse.headers.get('X-Ausername'); + if (!username) { + throw new Error(`Failed to retrieve the username of the logged in user`); + } + + let userResponse; + try { + userResponse = await fetch( + `https://${host}/rest/api/latest/users/${username}?avatarSize=256`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + } catch (e) { + throw new Error(`Failed to retrieve the user '${username}'`); + } + + if (!userResponse.ok) { + throw new Error(`Failed to retrieve the user '${username}'`); + } + + const user = await userResponse.json(); + + const passportProfile = { + provider: 'bitbucketServer', + id: user.id.toString(), + displayName: user.displayName, + username: user.name, + emails: [ + { + value: user.emailAddress, + }, + ], + } as PassportProfile; + + if (user.avatarUrl) { + passportProfile.photos = [{ value: `https://${host}${user.avatarUrl}` }]; + } + + return passportProfile; +} diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/index.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/index.ts new file mode 100644 index 0000000000..0d32abfe22 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-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-server-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { bitbucketServerAuthenticator } from './authenticator'; +export { authModuleBitbucketServerProvider as default } from './module'; +export { bitbucketServerSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/module.test.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/module.test.ts new file mode 100644 index 0000000000..ec75df92f8 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/module.test.ts @@ -0,0 +1,81 @@ +/* + * 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 authPlugin from '@backstage/plugin-auth-backend'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authModuleBitbucketServerProvider } from './module'; +import request from 'supertest'; + +describe('authModuleBitbucketServerProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleBitbucketServerProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + bitbucketServer: { + development: { + clientId: 'cliendId', + clientSecret: 'clientSecret', + host: 'bitbucket.org', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/bitbucketServer/start?env=development', + ); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('bitbucketServer-nonce', { + domain: 'localhost', + path: '/api/auth/bitbucketServer/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('/rest/oauth2/latest/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'cliendId', + redirect_uri: `http://localhost:${server.port()}/api/auth/bitbucketServer/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-server-provider/src/module.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/module.ts new file mode 100644 index 0000000000..c49def87cd --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-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 { bitbucketServerAuthenticator } from './authenticator'; +import { bitbucketServerSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleBitbucketServerProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'bitbucket-server-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'bitbucketServer', + factory: createOAuthProviderFactory({ + authenticator: bitbucketServerAuthenticator, + signInResolverFactories: { + ...bitbucketServerSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts new file mode 100644 index 0000000000..dc2ceef7e9 --- /dev/null +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts @@ -0,0 +1,55 @@ +/* + * 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 Server auth provider. + * + * @public + */ +export namespace bitbucketServerSignInResolvers { + /** + * Looks up the user by matching their email to the entity email. + */ + export const emailMatchingUserEntityProfileEmail = + createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { profile } = info; + + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + + return ctx.signInWithCatalogUser({ + filter: { + 'spec.profile.email': profile.email, + }, + }); + }; + }, + }); +} diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 51f80024ae..20690528d1 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -108,7 +108,7 @@ export type BitbucketPassportProfile = Profile & { }; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketServerOAuthResult = { fullProfile: Profile; params: { diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index e874e1e805..9121f16d39 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -54,6 +54,7 @@ "@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^", "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "workspace:^", "@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "workspace:^", "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "workspace:^", "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts deleted file mode 100644 index 3756e00b38..0000000000 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ /dev/null @@ -1,390 +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 * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { makeProfileInfo } from '../../lib/passport'; -import { - bitbucketServer, - BitbucketServerAuthProvider, - BitbucketServerOAuthResult, -} from './provider'; -import { setupServer } from 'msw/node'; -import { registerMswTestHooks } from '@backstage/backend-test-utils'; -import { rest } from 'msw'; -import { AuthResolverContext } from '@backstage/plugin-auth-node'; - -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - ...jest.requireActual('../../lib/passport/PassportStrategyHelper'), - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ - result: BitbucketServerOAuthResult; - privateInfo: { refreshToken?: string }; - }> ->; - -const passportProfile = { - id: '123', - username: 'john.doe', - provider: 'bitubcketServer', - displayName: 'John Doe', - emails: [{ value: 'john@doe.com' }], - photos: [{ value: 'https://bitbucket.org/user/123/avatar' }], -}; - -const mockHost = 'bitbucket.org'; -const mockBaseUrl = `https://${mockHost}`; - -const whoAmIHandler = (options?: { fail?: boolean; value?: string }) => - rest.get( - `${mockBaseUrl}/plugins/servlet/applinks/whoami`, - (_req, res, ctx) => { - if (options?.fail) { - res.networkError('error'); - } - return res( - ctx.status(200), - ctx.set('X-Ausername', options?.value ?? passportProfile.username), - ); - }, - ); - -const getUserHandler = (options?: { - fail?: boolean; - status?: number; - avatarUrl?: string; - noDisplayName?: boolean; - noUserName?: boolean; -}) => - rest.get( - `${mockBaseUrl}/rest/api/latest/users/${passportProfile.username}`, - (_req, res, ctx) => { - if (options?.fail) { - res.networkError('error'); - } - return res( - ctx.status(options?.status ?? 200), - ctx.json({ - name: options?.noUserName ? undefined : 'john.doe', - emailAddress: 'john@doe.com', - id: 123, - displayName: options?.noDisplayName ? undefined : 'John Doe', - active: true, - slug: 'john.doe', - type: 'NORMAL', - links: { - self: [ - { - href: 'https://bitbucket.org/users/john.doe', - }, - ], - }, - avatarUrl: options?.avatarUrl ?? '/user/123/avatar', - }), - ); - }, - ); - -describe('BitbucketServerAuthProvider', () => { - const provider = new BitbucketServerAuthProvider({ - resolverContext: { - signInWithCatalogUser: jest.fn(info => { - return { - token: `token-for-user:${info.filter['spec.profile.email']}`, - }; - }), - } as unknown as AuthResolverContext, - signInResolver: - bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(), - authHandler: async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }), - callbackUrl: 'mock', - clientId: 'mock', - clientSecret: 'mock', - host: mockHost, - authorizationUrl: 'mock', - tokenUrl: 'mock', - }); - - describe('when transforming to type OAuthResponse', () => { - const server = setupServer(); - registerMswTestHooks(server); - - it('should map to a valid response', async () => { - server.use(whoAmIHandler(), getUserHandler()); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:john@doe.com', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'REPO_READ', - }, - profile: { - email: 'john@doe.com', - displayName: 'John Doe', - picture: 'https://bitbucket.org/user/123/avatar', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('should throw if whoami fails', async () => { - server.use(whoAmIHandler({ fail: true }), getUserHandler()); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - - await expect(provider.handler({} as any)).rejects.toThrow( - `Failed to retrieve the username of the logged in user`, - ); - }); - - it('should throw if whoami returns an invalid response', async () => { - server.use(whoAmIHandler({ value: '' }), getUserHandler()); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - - await expect(provider.handler({} as any)).rejects.toThrow( - `Failed to retrieve the username of the logged in user`, - ); - }); - - it('should throw if get user fails', async () => { - server.use(whoAmIHandler(), getUserHandler({ fail: true })); - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - - await expect(provider.handler({} as any)).rejects.toThrow( - `Failed to retrieve the user '${passportProfile.username}'`, - ); - }); - - it('should throw if get user is not ok', async () => { - server.use(whoAmIHandler(), getUserHandler({ status: 500 })); - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - - await expect(provider.handler({} as any)).rejects.toThrow( - `Failed to retrieve the user '${passportProfile.username}'`, - ); - }); - - it('should not set an avatar url if not given', async () => { - server.use(whoAmIHandler(), getUserHandler({ avatarUrl: '' })); - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:john@doe.com', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'REPO_READ', - }, - profile: { - email: 'john@doe.com', - displayName: 'John Doe', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('should fallback to the username if no displayName is given', async () => { - server.use(whoAmIHandler(), getUserHandler({ noDisplayName: true })); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:john@doe.com', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'REPO_READ', - }, - profile: { - email: 'john@doe.com', - displayName: 'john.doe', - picture: 'https://bitbucket.org/user/123/avatar', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('should fallback to the user id if no name is given', async () => { - server.use( - whoAmIHandler(), - getUserHandler({ noDisplayName: true, noUserName: true }), - ); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:john@doe.com', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'REPO_READ', - }, - profile: { - email: 'john@doe.com', - displayName: '123', - picture: 'https://bitbucket.org/user/123/avatar', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - }); - - describe('when authenticating', () => { - const server = setupServer(); - registerMswTestHooks(server); - - it('should forward the refresh token', async () => { - server.use(whoAmIHandler(), getUserHandler()); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: { refreshToken: 'refresh-token' }, - }); - - const response = await provider.handler({} as any); - - const expected = { - response: { - backstageIdentity: { - token: 'token-for-user:john@doe.com', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'REPO_READ', - }, - profile: { - email: 'john@doe.com', - displayName: 'John Doe', - picture: 'https://bitbucket.org/user/123/avatar', - }, - }, - refreshToken: 'refresh-token', - }; - - expect(response).toEqual(expected); - }); - - it('should forward a new refresh token on refresh', async () => { - server.use(whoAmIHandler(), getUserHandler()); - - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const params = { scope: 'REPO_READ' }; - const mockRefreshToken = jest.spyOn( - helpers, - 'executeRefreshTokenStrategy', - ) as unknown as jest.MockedFunction<() => Promise<{}>>; - mockRefreshToken.mockResolvedValueOnce({ - accessToken, - refreshToken: 'dont-forget-to-send-refresh', - params, - }); - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile: passportProfile, accessToken, params }, - privateInfo: { refreshToken: 'refresh-token' }, - }); - - const expected = { - response: { - backstageIdentity: { - token: 'token-for-user:john@doe.com', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'REPO_READ', - }, - profile: { - email: 'john@doe.com', - displayName: 'John Doe', - picture: 'https://bitbucket.org/user/123/avatar', - }, - }, - refreshToken: 'dont-forget-to-send-refresh', - }; - const response = await provider.refresh({ scope: 'REPO_WRITE' } as any); - - expect(response).toEqual(expected); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts index d66f7f33c0..c49cf5b7b1 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts @@ -14,40 +14,28 @@ * limitations under the License. */ -import { - encodeState, - OAuthAdapter, - OAuthEnvironmentHandler, - OAuthHandlers, - OAuthProviderOptions, - OAuthRefreshRequest, - OAuthResponse, - OAuthStartRequest, -} from '../../lib/oauth'; -import { Strategy as OAuth2Strategy, VerifyCallback } from 'passport-oauth2'; -import { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, -} from '../../lib/passport'; -import { AuthHandler, OAuthStartResponse } from '../types'; -import express from 'express'; -import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { Profile as PassportProfile } from 'passport'; -import { commonByEmailResolver } from '../resolvers'; -import fetch from 'node-fetch'; import { AuthResolverContext, + createOAuthProviderFactory, SignInResolver, } from '@backstage/plugin-auth-node'; +import { + bitbucketServerAuthenticator, + bitbucketServerSignInResolvers, +} from '@backstage/plugin-auth-backend-module-bitbucket-server-provider'; +import { OAuthProviderOptions } from '../../lib/oauth'; +import { + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, +} from '../../lib/legacy'; +import { AuthHandler } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -type PrivateInfo = { - refreshToken: string; -}; - -/** @public */ +/** + * @public + * @deprecated The Bitbucket Server auth provider was extracted to `@backstage/plugin-auth-backend-module-bitbucket-server-provider`. + */ export type BitbucketServerOAuthResult = { fullProfile: PassportProfile; params: { @@ -60,6 +48,10 @@ export type BitbucketServerOAuthResult = { refreshToken?: string; }; +/** + * @public + * @deprecated The Bitbucket Server auth provider was extracted to `@backstage/plugin-auth-backend-module-bitbucket-server-provider`. + */ export type BitbucketServerAuthProviderOptions = OAuthProviderOptions & { host: string; authorizationUrl: string; @@ -69,176 +61,6 @@ export type BitbucketServerAuthProviderOptions = OAuthProviderOptions & { resolverContext: AuthResolverContext; }; -export class BitbucketServerAuthProvider implements OAuthHandlers { - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - private readonly strategy: OAuth2Strategy; - private readonly host: string; - - constructor(options: BitbucketServerAuthProviderOptions) { - this.signInResolver = options.signInResolver; - this.authHandler = options.authHandler; - this.resolverContext = options.resolverContext; - this.strategy = new OAuth2Strategy( - { - authorizationURL: options.authorizationUrl, - tokenURL: options.tokenUrl, - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - }, - ( - accessToken: string, - refreshToken: string, - params: any, - fullProfile: PassportProfile, - done: VerifyCallback, - ) => { - done(undefined, { fullProfile, params, accessToken }, { refreshToken }); - }, - ); - this.host = options.host; - } - - 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, - ): Promise<{ response: OAuthResponse; refreshToken?: string }> { - const { result, privateInfo } = await executeFrameHandlerStrategy< - BitbucketServerOAuthResult, - PrivateInfo - >(req, this.strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async refresh( - req: OAuthRefreshRequest, - ): Promise<{ response: OAuthResponse; refreshToken?: string }> { - 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: BitbucketServerOAuthResult, - ): Promise { - // The OAuth2 strategy does not return a user profile -> let's fetch it before calling the auth handler - result.fullProfile = await this.fetchProfile(result); - const { profile } = await this.authHandler(result, this.resolverContext); - - let backstageIdentity = undefined; - if (this.signInResolver) { - backstageIdentity = await this.signInResolver( - { result, profile }, - this.resolverContext, - ); - } - - return { - providerInfo: { - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - backstageIdentity, - }; - } - - private async fetchProfile( - result: BitbucketServerOAuthResult, - ): Promise { - // Get current user name - let whoAmIResponse; - try { - whoAmIResponse = await fetch( - `https://${this.host}/plugins/servlet/applinks/whoami`, - { - headers: { - Authorization: `Bearer ${result.accessToken}`, - }, - }, - ); - } catch (e) { - throw new Error(`Failed to retrieve the username of the logged in user`); - } - - // A response.ok check here would be worthless as the Bitbucket API always returns 200 OK for this call - const username = whoAmIResponse.headers.get('X-Ausername'); - if (!username) { - throw new Error(`Failed to retrieve the username of the logged in user`); - } - - let userResponse; - try { - userResponse = await fetch( - `https://${this.host}/rest/api/latest/users/${username}?avatarSize=256`, - { - headers: { - Authorization: `Bearer ${result.accessToken}`, - }, - }, - ); - } catch (e) { - throw new Error(`Failed to retrieve the user '${username}'`); - } - - if (!userResponse.ok) { - throw new Error(`Failed to retrieve the user '${username}'`); - } - - const user = await userResponse.json(); - - const passportProfile = { - provider: 'bitbucketServer', - id: user.id.toString(), - displayName: user.displayName, - username: user.name, - emails: [ - { - value: user.emailAddress, - }, - ], - } as PassportProfile; - - if (user.avatarUrl) { - passportProfile.photos = [ - { value: `https://${this.host}${user.avatarUrl}` }, - ]; - } - - return passportProfile; - } -} - export const bitbucketServer = createAuthProviderIntegration({ create(options?: { /** @@ -257,48 +79,38 @@ export const bitbucketServer = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const host = envConfig.getString('host'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://${host}/rest/oauth2/latest/authorize`; - const tokenUrl = `https://${host}/rest/oauth2/latest/token`; - - const authHandler: AuthHandler = - options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }); - - const provider = new BitbucketServerAuthProvider({ - callbackUrl, - clientId, - clientSecret, - host, - authorizationUrl, - tokenUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); + return createOAuthProviderFactory({ + authenticator: bitbucketServerAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, resolvers: { /** * Looks up the user by matching their email to the entity email. */ emailMatchingUserEntityProfileEmail: - (): SignInResolver => commonByEmailResolver, + (): SignInResolver => { + const resolver = + bitbucketServerSignInResolvers.emailMatchingUserEntityProfileEmail(); + return async (info, ctx) => { + return resolver( + { + profile: info.profile, + result: { + fullProfile: info.result.fullProfile, + session: { + accessToken: info.result.accessToken, + tokenType: info.result.params.token_type ?? 'bearer', + scope: info.result.params.scope, + expiresInSeconds: info.result.params.expires_in, + refreshToken: info.result.refreshToken, + }, + }, + }, + ctx, + ); + }; + }, }, }); diff --git a/yarn.lock b/yarn.lock index e7b00fa157..b0fd3527bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4869,6 +4869,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-bitbucket-server-provider@workspace:^, @backstage/plugin-auth-backend-module-bitbucket-server-provider@workspace:plugins/auth-backend-module-bitbucket-server-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-bitbucket-server-provider@workspace:plugins/auth-backend-module-bitbucket-server-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:^" + "@types/passport-oauth2": ^1.4.15 + node-fetch: ^2.7.0 + passport: ^0.7.0 + passport-oauth2: ^1.6.1 + supertest: ^6.3.3 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-cloudflare-access-provider@workspace:^, @backstage/plugin-auth-backend-module-cloudflare-access-provider@workspace:plugins/auth-backend-module-cloudflare-access-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-cloudflare-access-provider@workspace:plugins/auth-backend-module-cloudflare-access-provider" @@ -5141,6 +5159,7 @@ __metadata: "@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^" "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "workspace:^" "@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "workspace:^" "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^"