From 74534f6d393ae002e73ce603c5ed02f06d7771b0 Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 23 Sep 2021 17:17:10 +0100 Subject: [PATCH 01/14] Bitbucket cloud authentication client added Signed-off-by: Filip Swiatczak --- app-config.yaml | 8 +- packages/app/src/identityProviders.ts | 7 + .../auth/bitbucket/BitbucketAuth.test.ts | 29 ++++ .../auth/bitbucket/BitbucketAuth.ts | 142 ++++++++++++++++++ .../implementations/auth/bitbucket/index.ts | 18 +++ .../implementations/auth/bitbucket/types.ts | 27 ++++ .../src/apis/implementations/auth/index.ts | 1 + packages/core-app-api/src/app/defaultApis.ts | 17 +++ .../src/apis/definitions/auth.ts | 12 ++ plugins/auth-backend/package.json | 1 + .../src/providers/bitbucket/index.ts | 18 +++ .../src/providers/bitbucket/provider.ts | 124 +++++++++++++++ .../auth-backend/src/providers/factories.ts | 2 + .../AuthProviders/DefaultProviderSettings.tsx | 9 ++ yarn.lock | 10 +- 15 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/bitbucket/index.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts create mode 100644 plugins/auth-backend/src/providers/bitbucket/index.ts create mode 100644 plugins/auth-backend/src/providers/bitbucket/provider.ts diff --git a/app-config.yaml b/app-config.yaml index 2fb75f718e..05e0440ac2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -1,6 +1,6 @@ app: title: Backstage Example App - baseUrl: http://localhost:3000 + baseUrl: http://localhost:7000 googleAnalyticsTrackingId: # UA-000000-0 #datadogRum: # clientToken: '123456789' @@ -32,7 +32,7 @@ backend: cache: store: memory cors: - origin: http://localhost:3000 + origin: http://localhost:7000 methods: [GET, POST, PUT, DELETE] credentials: true csp: @@ -358,6 +358,10 @@ auth: clientId: ${AUTH_ONELOGIN_CLIENT_ID} clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET} issuer: ${AUTH_ONELOGIN_ISSUER} + bitbucket: + development: + clientId: ${AUTH_BITBUCKET_CLIENT_ID} + clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} costInsights: engineerCost: 200000 products: diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 4cc7bab91a..24e02f61b0 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -24,6 +24,7 @@ import { oneloginAuthApiRef, oauth2ApiRef, oidcAuthApiRef, + bitbucketAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ @@ -81,4 +82,10 @@ export const providers = [ message: 'Sign In using OneLogin', apiRef: oneloginAuthApiRef, }, + { + id: 'bitbucket-auth-provider', + title: 'Bitbucket', + message: 'Sign In using Bitbucket Cloud', + apiRef: bitbucketAuthApiRef, + }, ]; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts new file mode 100644 index 0000000000..97de78f400 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts @@ -0,0 +1,29 @@ +/* + * 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 BitbucketAuth from './BitbucketAuth'; + +describe('BitbucketAuth', () => { + it('should get access token', async () => { + const getSession = jest + .fn() + .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); + const githubAuth = new BitbucketAuth({ getSession } as any); + + expect(await githubAuth.getAccessToken()).toBe('access-token'); + expect(getSession).toBeCalledTimes(1); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts new file mode 100644 index 0000000000..882f2cbeec --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -0,0 +1,142 @@ +/* + * 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 BitbucketIcon from '@material-ui/icons/FormatBold'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; +import { BitbucketSession } from './types'; +import { + OAuthApi, + SessionApi, + SessionState, + ProfileInfo, + BackstageIdentity, + AuthRequestOptions, + Observable, +} from '@backstage/core-plugin-api'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { + AuthSessionStore, + StaticAuthSessionManager, +} from '../../../../lib/AuthSessionManager'; +import { OAuthApiCreateOptions } from '../types'; + +export type BitbucketAuthResponse = { + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'bitbucket', + title: 'Bitbucket', + icon: BitbucketIcon, +}; + +class BitbucketAuth implements OAuthApi, SessionApi { + static create({ + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['team'], + }: OAuthApiCreateOptions) { + const connector = new DefaultAuthConnector({ + discoveryApi, + environment, + provider, + oauthRequestApi: oauthRequestApi, + sessionTransform(res: BitbucketAuthResponse): BitbucketSession { + return { + ...res, + providerInfo: { + accessToken: res.providerInfo.accessToken, + scopes: BitbucketAuth.normalizeScope(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, + }; + }, + }); + + const sessionManager = new StaticAuthSessionManager({ + connector, + defaultScopes: new Set(defaultScopes), + sessionScopes: (session: BitbucketSession) => session.providerInfo.scopes, + }); + + const authSessionStore = new AuthSessionStore({ + manager: sessionManager, + storageKey: `${provider.id}Session`, + sessionScopes: (session: BitbucketSession) => session.providerInfo.scopes, + }); + + return new BitbucketAuth(authSessionStore); + } + + constructor( + private readonly sessionManager: SessionManager, + ) {} + + async signIn() { + await this.getAccessToken(); + } + + async signOut() { + await this.sessionManager.removeSession(); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + + async getAccessToken(scope?: string, options?: AuthRequestOptions) { + const session = await this.sessionManager.getSession({ + ...options, + scopes: BitbucketAuth.normalizeScope(scope), + }); + return session?.providerInfo.accessToken ?? ''; + } + + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.profile; + } + + static normalizeScope(scope?: string): Set { + if (!scope) { + return new Set(); + } + + const scopeList = Array.isArray(scope) + ? scope + : scope.split(/[\s|,]/).filter(Boolean); + + return new Set(scopeList); + } +} +export default BitbucketAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/index.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/index.ts new file mode 100644 index 0000000000..33c6e75b3b --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './types'; +export { default as BitbucketAuth } from './BitbucketAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts new file mode 100644 index 0000000000..7babfdf686 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/types.ts @@ -0,0 +1,27 @@ +/* + * 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 { ProfileInfo, BackstageIdentity } from '@backstage/core-plugin-api'; + +export type BitbucketSession = { + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt?: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index 9889a33878..b622b90b8c 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -23,3 +23,4 @@ export * from './saml'; export * from './auth0'; export * from './microsoft'; export * from './onelogin'; +export * from './bitbucket'; diff --git a/packages/core-app-api/src/app/defaultApis.ts b/packages/core-app-api/src/app/defaultApis.ts index 2322f72e8d..6ed9a71265 100644 --- a/packages/core-app-api/src/app/defaultApis.ts +++ b/packages/core-app-api/src/app/defaultApis.ts @@ -25,6 +25,7 @@ import { GitlabAuth, Auth0Auth, MicrosoftAuth, + BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, @@ -51,6 +52,7 @@ import { samlAuthApiRef, oneloginAuthApiRef, oidcAuthApiRef, + bitbucketAuthApiRef, } from '@backstage/core-plugin-api'; import OAuth2Icon from '@material-ui/icons/AcUnit'; @@ -224,4 +226,19 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), + createApiFactory({ + api: bitbucketAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + BitbucketAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['team'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), ]; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index c585619ba5..a78414048c 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -340,3 +340,15 @@ export const oneloginAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.onelogin', }); + +/** + * Provides authentication towards Bitbucket APIs. + * + * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/ + * for a full list of supported scopes. + */ +export const bitbucketAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.bitbucket', +}); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 9293aec596..65804f242d 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -58,6 +58,7 @@ "node-cache": "^5.1.2", "openid-client": "^4.2.1", "passport": "^0.4.1", + "passport-bitbucket-oauth2": "^0.1.2", "passport-github2": "^0.1.12", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", diff --git a/plugins/auth-backend/src/providers/bitbucket/index.ts b/plugins/auth-backend/src/providers/bitbucket/index.ts new file mode 100644 index 0000000000..a863c452d9 --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucket/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { createBitbucketProvider } from './provider'; +export type { BitbucketProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts new file mode 100644 index 0000000000..d902595402 --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -0,0 +1,124 @@ +/* + * 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 express from 'express'; +// @ts-ignore passport-bitbucket-oauth2 does not have type definitions +import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + makeProfileInfo, + PassportDoneCallback, +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthEnvironmentHandler, + OAuthStartRequest, + encodeState, + OAuthResult, +} from '../../lib/oauth'; + +export type BitbucketAuthProviderOptions = OAuthProviderOptions & { + // extra options +}; + +export class BitbucketAuthProvider implements OAuthHandlers { + private readonly _strategy: BitbucketStrategy; + + constructor(options: BitbucketAuthProviderOptions) { + this._strategy = new BitbucketStrategy( + { + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + }, + ( + accessToken: any, + _refreshToken: any, + params: any, + fullProfile: any, + done: PassportDoneCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }); + }, + ); + } + + async start(req: OAuthStartRequest): Promise { + return await executeRedirectStrategy(req, this._strategy, { + scope: req.scope, + state: encodeState(req.state), + }); + } + + async handler(req: express.Request) { + const { + result: { fullProfile, accessToken, params }, + } = await executeFrameHandlerStrategy(req, this._strategy); + + const profile = makeProfileInfo( + { + ...fullProfile, + id: fullProfile.username || fullProfile.id, + displayName: + fullProfile.displayName || fullProfile.username || fullProfile.id, + }, + params.id_token, + ); + + return { + response: { + profile, + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + backstageIdentity: { + id: fullProfile.username || fullProfile.id, + }, + }, + }; + } +} + +export type BitbucketProviderOptions = {}; + +export const createBitbucketProvider = (): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const provider = new BitbucketAuthProvider({ + clientId, + clientSecret, + callbackUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + persistScopes: true, + providerId, + tokenIssuer, + }); + }); +}; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 070cdb38f3..c4894f2757 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -26,6 +26,7 @@ import { createMicrosoftProvider } from './microsoft'; import { createOneLoginProvider } from './onelogin'; import { AuthProviderFactory } from './types'; import { createAwsAlbProvider } from './aws-alb'; +import { createBitbucketProvider } from './bitbucket'; export const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider(), @@ -39,4 +40,5 @@ export const factories: { [providerId: string]: AuthProviderFactory } = { oidc: createOidcProvider(), onelogin: createOneLoginProvider(), awsalb: createAwsAlbProvider(), + bitbucket: createBitbucketProvider(), }; diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index 91438e5a5d..4b9cf9c783 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -24,6 +24,7 @@ import { oauth2ApiRef, oktaAuthApiRef, microsoftAuthApiRef, + bitbucketAuthApiRef, } from '@backstage/core-plugin-api'; type Props = { @@ -80,6 +81,14 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => ( icon={Star} /> )} + {configuredProviders.includes('bitbucket') && ( + + )} {configuredProviders.includes('oauth2') && ( Date: Thu, 23 Sep 2021 17:22:39 +0100 Subject: [PATCH 02/14] chore: cleanup Signed-off-by: Filip Swiatczak --- app-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 05e0440ac2..65f00a09b0 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -1,6 +1,6 @@ app: title: Backstage Example App - baseUrl: http://localhost:7000 + baseUrl: http://localhost:3000 googleAnalyticsTrackingId: # UA-000000-0 #datadogRum: # clientToken: '123456789' @@ -32,7 +32,7 @@ backend: cache: store: memory cors: - origin: http://localhost:7000 + origin: http://localhost:3000 methods: [GET, POST, PUT, DELETE] credentials: true csp: From 4c3eea7788cf36b82736aff5fdd619fa265c6fab Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 23 Sep 2021 17:43:30 +0100 Subject: [PATCH 03/14] changeset added Signed-off-by: Filip Swiatczak --- .changeset/gold-wombats-shop.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/gold-wombats-shop.md diff --git a/.changeset/gold-wombats-shop.md b/.changeset/gold-wombats-shop.md new file mode 100644 index 0000000000..6ce067da94 --- /dev/null +++ b/.changeset/gold-wombats-shop.md @@ -0,0 +1,12 @@ +--- +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-user-settings': patch +--- + +Bitbucket Cloud authentication - based on the existing github authentication + changes around BB apis and updated scope. + +- BitbucketAuth added to core-app-api. +- Bitbucket provider added to plugin-auth-backend. +- Cosmetic entry for Bitbucket connection in user-settings Authentication Providers tab. From dd89cd170e101b636c4fc6937a4f0a391f7af8f2 Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 23 Sep 2021 18:20:18 +0100 Subject: [PATCH 04/14] Documentation added Signed-off-by: Filip Swiatczak --- docs/auth/bitbucket/provider.md | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/auth/bitbucket/provider.md diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md new file mode 100644 index 0000000000..81b2e98f2c --- /dev/null +++ b/docs/auth/bitbucket/provider.md @@ -0,0 +1,66 @@ +--- +id: provider +title: GitHub Authentication Provider +sidebar_label: GitHub +description: Adding GitHub OAuth as an authentication provider in Backstage +--- + +The Backstage `core-api` package comes with a GitHub authentication provider +that can authenticate users using GitHub or GitHub Enterprise OAuth. + +## Create an OAuth App on GitHub + +To add GitHub authentication, you must create either a GitHub App, or an OAuth +App from the GitHub +[developer settings](https://github.com/settings/developers). The `Homepage URL` +should point to Backstage's frontend, while the `Authorization callback URL` +will point to the auth backend. + +Note that if you're using a GitHub App, the allowed scopes are configured as +part of that app. This means you need to verify what scopes the plugins you use +require, so be sure to check the plugin READMEs for that information. + +Settings for local development: + +- Application name: Backstage (or your custom app name) +- Homepage URL: `http://localhost:3000` +- Authorization callback URL: `http://localhost:7000/api/auth/github` + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the +root `auth` configuration: + +```yaml +auth: + environment: development + providers: + github: + development: + clientId: ${AUTH_GITHUB_CLIENT_ID} + clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} + ## uncomment if using GitHub Enterprise + # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} +``` + +The GitHub provider is a structure with three configuration keys: + +- `clientId`: The client ID that you generated on GitHub, e.g. + `b59241722e3c3b4816e2` +- `clientSecret`: The client secret tied to the generated client ID. +- `enterpriseInstanceUrl` (optional): The base URL for a GitHub Enterprise + instance, e.g. `https://ghe..com`. Only needed for GitHub Enterprise. + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `githubAuthApi` reference and +`SignInPage` component as shown in +[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). + +## Difference between GitHub Apps and GitHub OAuth Apps + +GitHub Apps handle OAuth scope at the app installation level, meaning that the +`scope` parameter for the call to `getAccessToken` in the frontend has no +effect. When calling `getAccessToken` in open source plugins, one should still +include the appropriate scope, but also document in the plugin README what +scopes are required for GitHub Apps. From 845d1081abd3c37b49d6168b352e876f01727160 Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 23 Sep 2021 18:25:48 +0100 Subject: [PATCH 05/14] documentation cd Signed-off-by: Filip Swiatczak --- docs/auth/bitbucket/provider.md | 74 +++++++++++++++++---------------- mkdocs.yml | 1 + 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index 81b2e98f2c..e827e0deab 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -1,30 +1,28 @@ --- id: provider -title: GitHub Authentication Provider -sidebar_label: GitHub -description: Adding GitHub OAuth as an authentication provider in Backstage +title: Bitbucket Authentication Provider +sidebar_label: Bitbucket +description: Adding Bitbucket OAuth as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a GitHub authentication provider -that can authenticate users using GitHub or GitHub Enterprise OAuth. +The Backstage `core-api` package comes with a Bitbucket authentication provider +that can authenticate users using Bitbucket Cloud. This does **NOT** work with +Bitbucket Server. -## Create an OAuth App on GitHub +## Create an OAuth Consumer in Bitbucket -To add GitHub authentication, you must create either a GitHub App, or an OAuth -App from the GitHub -[developer settings](https://github.com/settings/developers). The `Homepage URL` -should point to Backstage's frontend, while the `Authorization callback URL` -will point to the auth backend. +To add Bitbucket Cloud authentication, you must create an OAuth Consumer. -Note that if you're using a GitHub App, the allowed scopes are configured as -part of that app. This means you need to verify what scopes the plugins you use -require, so be sure to check the plugin READMEs for that information. +Go to `https://bitbucket.org//workspace/settings/api` . + +Click Add Consumer. Settings for local development: - Application name: Backstage (or your custom app name) -- Homepage URL: `http://localhost:3000` -- Authorization callback URL: `http://localhost:7000/api/auth/github` +- Callback URL: `http://localhost:7000/api/auth/bitbucket` +- Other are optional +- (IMPORTANT) **Permissions: Account - Read, Workspace membership - Read** ## Configuration @@ -35,32 +33,38 @@ root `auth` configuration: auth: environment: development providers: - github: + bitbucket: development: - clientId: ${AUTH_GITHUB_CLIENT_ID} - clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} - ## uncomment if using GitHub Enterprise - # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} + clientId: ${AUTH_BITBUCKET_CLIENT_ID} + clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} ``` -The GitHub provider is a structure with three configuration keys: +The Bitbucket provider is a structure with two configuration keys: -- `clientId`: The client ID that you generated on GitHub, e.g. +- `clientId`: The Key that you generated in Bitbucket, e.g. `b59241722e3c3b4816e2` -- `clientSecret`: The client secret tied to the generated client ID. -- `enterpriseInstanceUrl` (optional): The base URL for a GitHub Enterprise - instance, e.g. `https://ghe..com`. Only needed for GitHub Enterprise. +- `clientSecret`: The Secret tied to the generated Key. ## Adding the provider to the Backstage frontend -To add the provider to the frontend, add the `githubAuthApi` reference and -`SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +Ensure identityProviders contains Bitbucket at +`packages/app/src/identityProviders.ts`: -## Difference between GitHub Apps and GitHub OAuth Apps +```yaml +import { + ... + bitbucketAuthApiRef, + ... +} from '@backstage/core-plugin-api'; -GitHub Apps handle OAuth scope at the app installation level, meaning that the -`scope` parameter for the call to `getAccessToken` in the frontend has no -effect. When calling `getAccessToken` in open source plugins, one should still -include the appropriate scope, but also document in the plugin README what -scopes are required for GitHub Apps. +export const providers = [ +... + { + id: 'bitbucket-auth-provider', + title: 'Bitbucket', + message: 'Sign In using Bitbucket Cloud', + apiRef: bitbucketAuthApiRef, + }, +... +]; +``` diff --git a/mkdocs.yml b/mkdocs.yml index 48d1d49dcf..5527b3eede 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -140,6 +140,7 @@ nav: - Google: 'auth/google/provider.md' - Okta: 'auth/okta/provider.md' - OneLogin: 'auth/onelogin/provider.md' + - Bitbucket: 'auth/bitbucket/provider.md' - Adding authentication providers: 'auth/add-auth-provider.md' - Using authentication and identity: 'auth/using-auth.md' - Sign in resolvers: 'auth/identity-resolver.md' From f400ab45a14b144b2c24dbb44b08a92aeb111153 Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 23 Sep 2021 20:43:44 +0100 Subject: [PATCH 06/14] fix: vale run .md spelling correction Signed-off-by: Filip Swiatczak --- .changeset/gold-wombats-shop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/gold-wombats-shop.md b/.changeset/gold-wombats-shop.md index 6ce067da94..f29ed9cce1 100644 --- a/.changeset/gold-wombats-shop.md +++ b/.changeset/gold-wombats-shop.md @@ -5,7 +5,7 @@ '@backstage/plugin-user-settings': patch --- -Bitbucket Cloud authentication - based on the existing github authentication + changes around BB apis and updated scope. +Bitbucket Cloud authentication - based on the existing GitHub authentication + changes around BB apis and updated scope. - BitbucketAuth added to core-app-api. - Bitbucket provider added to plugin-auth-backend. From d830ebabcbe8514e1b7df2cd0339056bbcfb226f Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Fri, 24 Sep 2021 10:40:26 +0100 Subject: [PATCH 07/14] api-report.md changes from yarn build:api-reports Signed-off-by: Filip Swiatczak --- packages/core-app-api/api-report.md | 46 +++++++++++++++++++++++++- packages/core-plugin-api/api-report.md | 7 ++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 4b804c2cc2..f3d1cfb20e 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -269,6 +269,51 @@ export type BackstagePluginWithAnyOutput = Omit< output(): (PluginOutput | UnknownPluginOutput)[]; }; +// Warning: (ae-missing-release-tag) "BitbucketAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class BitbucketAuth implements OAuthApi, SessionApi { + // Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts + constructor(sessionManager: SessionManager); + // (undocumented) + static create({ + discoveryApi, + environment, + provider, + oauthRequestApi, + defaultScopes, + }: OAuthApiCreateOptions): BitbucketAuth; + // (undocumented) + getAccessToken(scope?: string, options?: AuthRequestOptions): Promise; + // (undocumented) + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; + // (undocumented) + getProfile(options?: AuthRequestOptions): Promise; + // (undocumented) + static normalizeScope(scope?: string): Set; + // (undocumented) + sessionState$(): Observable; + // (undocumented) + signIn(): Promise; + // (undocumented) + signOut(): Promise; +} + +// Warning: (ae-missing-release-tag) "BitbucketSession" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BitbucketSession = { + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt?: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + // Warning: (ae-missing-release-tag) "BootErrorPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -359,7 +404,6 @@ export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null; // // @public (undocumented) export class GithubAuth implements OAuthApi, SessionApi { - // Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts constructor(sessionManager: SessionManager); // (undocumented) static create({ diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 39040d1c91..40fa95dc34 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -226,6 +226,13 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; +// Warning: (ae-missing-release-tag) "bitbucketAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const bitbucketAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // Warning: (ae-missing-release-tag) "BootErrorPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From 42b897df3a1098a18d8b844bc853db8f973f3d8e Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Wed, 29 Sep 2021 12:52:10 +0100 Subject: [PATCH 08/14] cleanup + BitbucketAuth standardisation Signed-off-by: Filip Swiatczak --- docs/auth/bitbucket/provider.md | 30 +---- packages/core-app-api/api-report.md | 24 +--- .../auth/bitbucket/BitbucketAuth.test.ts | 34 ++++-- .../auth/bitbucket/BitbucketAuth.ts | 103 ++---------------- 4 files changed, 47 insertions(+), 144 deletions(-) diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index e827e0deab..ae09d5dbee 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -5,9 +5,9 @@ sidebar_label: Bitbucket description: Adding Bitbucket OAuth as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a Bitbucket authentication provider -that can authenticate users using Bitbucket Cloud. This does **NOT** work with -Bitbucket Server. +The Backstage `core-plugin-api` package comes with a Bitbucket authentication +provider that can authenticate users using Bitbucket Cloud. This does **NOT** +work with Bitbucket Server. ## Create an OAuth Consumer in Bitbucket @@ -47,24 +47,6 @@ The Bitbucket provider is a structure with two configuration keys: ## Adding the provider to the Backstage frontend -Ensure identityProviders contains Bitbucket at -`packages/app/src/identityProviders.ts`: - -```yaml -import { - ... - bitbucketAuthApiRef, - ... -} from '@backstage/core-plugin-api'; - -export const providers = [ -... - { - id: 'bitbucket-auth-provider', - title: 'Bitbucket', - message: 'Sign In using Bitbucket Cloud', - apiRef: bitbucketAuthApiRef, - }, -... -]; -``` +To add the provider to the frontend, add the `bitbucketAuthApi` reference and +`SignInPage` component as shown in +[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f3d1cfb20e..774be9dcdf 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -21,6 +21,7 @@ import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentity } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ConfigReader } from '@backstage/config'; import { DiscoveryApi } from '@backstage/core-plugin-api'; @@ -272,9 +273,7 @@ export type BackstagePluginWithAnyOutput = Omit< // Warning: (ae-missing-release-tag) "BitbucketAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class BitbucketAuth implements OAuthApi, SessionApi { - // Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts - constructor(sessionManager: SessionManager); +export class BitbucketAuth { // (undocumented) static create({ discoveryApi, @@ -282,23 +281,7 @@ export class BitbucketAuth implements OAuthApi, SessionApi { provider, oauthRequestApi, defaultScopes, - }: OAuthApiCreateOptions): BitbucketAuth; - // (undocumented) - getAccessToken(scope?: string, options?: AuthRequestOptions): Promise; - // (undocumented) - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; - // (undocumented) - getProfile(options?: AuthRequestOptions): Promise; - // (undocumented) - static normalizeScope(scope?: string): Set; - // (undocumented) - sessionState$(): Observable; - // (undocumented) - signIn(): Promise; - // (undocumented) - signOut(): Promise; + }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; } // Warning: (ae-missing-release-tag) "BitbucketSession" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -404,6 +387,7 @@ export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null; // // @public (undocumented) export class GithubAuth implements OAuthApi, SessionApi { + // Warning: (ae-forgotten-export) The symbol "SessionManager" needs to be exported by the entry point index.d.ts constructor(sessionManager: SessionManager); // (undocumented) static create({ diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts index 97de78f400..f4bd7cbe85 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts @@ -14,16 +14,34 @@ * limitations under the License. */ +import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; +import { UrlPatternDiscovery } from '../../DiscoveryApi'; import BitbucketAuth from './BitbucketAuth'; -describe('BitbucketAuth', () => { - it('should get access token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); - const githubAuth = new BitbucketAuth({ getSession } as any); +const getSession = jest.fn(); - expect(await githubAuth.getAccessToken()).toBe('access-token'); - expect(getSession).toBeCalledTimes(1); +jest.mock('../../../../lib/AuthSessionManager', () => ({ + ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), + RefreshingAuthSessionManager: class { + getSession = getSession; + }, +})); + +describe('BitbucketAuth', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each([ + ['team api write_repository', ['team', 'api', 'write_repository']], + ['read_repository sudo', ['read_repository', 'sudo']], + ])(`should normalize scopes correctly - %p`, (scope, scopes) => { + const gitlabAuth = BitbucketAuth.create({ + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + gitlabAuth.getAccessToken(scope); + expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); }); }); diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index 882f2cbeec..c88db2a2b0 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -15,23 +15,14 @@ */ import BitbucketIcon from '@material-ui/icons/FormatBold'; -import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; -import { BitbucketSession } from './types'; import { - OAuthApi, - SessionApi, - SessionState, - ProfileInfo, BackstageIdentity, - AuthRequestOptions, - Observable, + bitbucketAuthApiRef, + ProfileInfo, } from '@backstage/core-plugin-api'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { - AuthSessionStore, - StaticAuthSessionManager, -} from '../../../../lib/AuthSessionManager'; + import { OAuthApiCreateOptions } from '../types'; +import { OAuth2 } from '../oauth2'; export type BitbucketAuthResponse = { providerInfo: { @@ -49,94 +40,22 @@ const DEFAULT_PROVIDER = { icon: BitbucketIcon, }; -class BitbucketAuth implements OAuthApi, SessionApi { +class BitbucketAuth { static create({ discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, defaultScopes = ['team'], - }: OAuthApiCreateOptions) { - const connector = new DefaultAuthConnector({ + }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T { + return OAuth2.create({ discoveryApi, - environment, + oauthRequestApi, provider, - oauthRequestApi: oauthRequestApi, - sessionTransform(res: BitbucketAuthResponse): BitbucketSession { - return { - ...res, - providerInfo: { - accessToken: res.providerInfo.accessToken, - scopes: BitbucketAuth.normalizeScope(res.providerInfo.scope), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), - }, - }; - }, + environment, + defaultScopes, }); - - const sessionManager = new StaticAuthSessionManager({ - connector, - defaultScopes: new Set(defaultScopes), - sessionScopes: (session: BitbucketSession) => session.providerInfo.scopes, - }); - - const authSessionStore = new AuthSessionStore({ - manager: sessionManager, - storageKey: `${provider.id}Session`, - sessionScopes: (session: BitbucketSession) => session.providerInfo.scopes, - }); - - return new BitbucketAuth(authSessionStore); - } - - constructor( - private readonly sessionManager: SessionManager, - ) {} - - async signIn() { - await this.getAccessToken(); - } - - async signOut() { - await this.sessionManager.removeSession(); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - async getAccessToken(scope?: string, options?: AuthRequestOptions) { - const session = await this.sessionManager.getSession({ - ...options, - scopes: BitbucketAuth.normalizeScope(scope), - }); - return session?.providerInfo.accessToken ?? ''; - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; - } - - static normalizeScope(scope?: string): Set { - if (!scope) { - return new Set(); - } - - const scopeList = Array.isArray(scope) - ? scope - : scope.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeList); } } + export default BitbucketAuth; From 114fbb8e4a23e95186d9f7a22472c7ed8a67a56f Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Wed, 29 Sep 2021 18:18:49 +0100 Subject: [PATCH 09/14] sign-in resolver and auth handler added, plus profile icon Signed-off-by: Filip Swiatczak --- .../src/providers/bitbucket/index.ts | 5 +- .../src/providers/bitbucket/provider.test.ts | 92 ++++++ .../src/providers/bitbucket/provider.ts | 283 +++++++++++++++--- 3 files changed, 332 insertions(+), 48 deletions(-) create mode 100644 plugins/auth-backend/src/providers/bitbucket/provider.test.ts diff --git a/plugins/auth-backend/src/providers/bitbucket/index.ts b/plugins/auth-backend/src/providers/bitbucket/index.ts index a863c452d9..d9fcdf3a68 100644 --- a/plugins/auth-backend/src/providers/bitbucket/index.ts +++ b/plugins/auth-backend/src/providers/bitbucket/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { createBitbucketProvider } from './provider'; +export { + createBitbucketProvider, + bitbucketEmailSignInResolver, +} from './provider'; export type { BitbucketProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts new file mode 100644 index 0000000000..7ab803243a --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts @@ -0,0 +1,92 @@ +/* + * 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 } from './provider'; +import * as helpers from '../../lib/passport/PassportStrategyHelper'; +import { OAuthResult } from '../../lib/oauth'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; + +const mockFrameHandler = jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown as jest.MockedFunction< + () => Promise<{ result: OAuthResult; privateInfo: any }> +>; + +describe('createBitbucketProvider', () => { + it('should auth', async () => { + const tokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + }; + const catalogIdentityClient = { + findUser: jest.fn(), + }; + + const provider = new BitbucketAuthProvider({ + logger: getVoidLogger(), + catalogIdentityClient: + catalogIdentityClient as unknown as CatalogIdentityClient, + tokenIssuer: tokenIssuer as unknown as TokenIssuer, + 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: { + 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 d902595402..81dddba5d0 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -15,108 +15,297 @@ */ import express from 'express'; +import passport, { Profile as PassportProfile } from 'passport'; // @ts-ignore passport-bitbucket-oauth2 does not have type definitions import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; import { + encodeState, + OAuthAdapter, + OAuthEnvironmentHandler, + OAuthHandlers, + OAuthProviderOptions, + OAuthRefreshRequest, + OAuthResponse, + OAuthResult, + OAuthStartRequest, +} from '../../lib/oauth'; +import { + executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, + executeRefreshTokenStrategy, makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthEnvironmentHandler, - OAuthStartRequest, - encodeState, - OAuthResult, -} from '../../lib/oauth'; + AuthProviderFactory, + AuthHandler, + RedirectInfo, + SignInResolver, +} from '../types'; +import { Logger } from 'winston'; -export type BitbucketAuthProviderOptions = OAuthProviderOptions & { - // extra options +type PrivateInfo = { + refreshToken: string; +}; + +type Options = OAuthProviderOptions & { + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; +}; + +export type BitbucketOAuthResult = { + fullProfile: BitbucketPassportProfile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; +}; + +export type BitbucketPassportProfile = PassportProfile & { + avatarUrl?: string; + _json?: { + links?: { + avatar?: { + href?: string; + }; + }; + }; }; export class BitbucketAuthProvider implements OAuthHandlers { private readonly _strategy: BitbucketStrategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; - constructor(options: BitbucketAuthProviderOptions) { + constructor(options: Options) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this._strategy = new BitbucketStrategy( { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, + // We need passReqToCallback set to false to get params, but there's + // no matching type signature for that, so instead behold this beauty + passReqToCallback: false as true, }, ( accessToken: any, - _refreshToken: any, + refreshToken: any, params: any, - fullProfile: any, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - done(undefined, { fullProfile, params, accessToken }); + 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: { fullProfile, accessToken, params }, - } = await executeFrameHandlerStrategy(req, this._strategy); - - const profile = makeProfileInfo( - { - ...fullProfile, - id: fullProfile.username || fullProfile.id, - displayName: - fullProfile.displayName || fullProfile.username || fullProfile.id, - }, - params.id_token, - ); + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, + PrivateInfo + >(req, this._strategy); return { - response: { - profile, - providerInfo: { - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - backstageIdentity: { - id: fullProfile.username || fullProfile.id, - }, - }, + response: await this.handleResult(result), + refreshToken: privateInfo.refreshToken, }; } + + async refresh(req: OAuthRefreshRequest): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); + const fullProfile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + ); + return this.handleResult({ + fullProfile, + params, + accessToken, + refreshToken: req.refreshToken, + }); + } + + private async handleResult(result: BitbucketOAuthResult) { + result.fullProfile.avatarUrl = + result.fullProfile._json!.links!.avatar!.href; + const { profile } = await this.authHandler(result); + + 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, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); + } + + return response; + } } -export type BitbucketProviderOptions = {}; +export const bitbucketEmailSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; -export const createBitbucketProvider = (): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + if (!profile.email) { + throw new Error('Bitbucket profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'bitbucket/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; +}; + +const bitbucketDefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { fullProfile } = info.result; + + const userId = fullProfile.username || fullProfile.id; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: userId, ent: [`user:default/${userId}`] }, + }); + + return { id: userId, token }; +}; + +export type BitbucketProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * 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; + }; +}; + +export const createBitbucketProvider = ( + options?: BitbucketProviderOptions, +): AuthProviderFactory => { + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolverFn = + options?.signIn?.resolver ?? bitbucketDefaultSignInResolver; + + const signInResolver: SignInResolver = info => + signInResolverFn(info, { + catalogIdentityClient, + tokenIssuer, + logger, + }); + const provider = new BitbucketAuthProvider({ clientId, clientSecret, callbackUrl, + signInResolver, + authHandler, + tokenIssuer, + catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - persistScopes: true, + disableRefresh: false, providerId, tokenIssuer, }); From 96658c576d60842b7582cbb08356a130406a0680 Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Wed, 29 Sep 2021 20:24:34 +0100 Subject: [PATCH 10/14] signIn.resolver made mandatory + test fix Signed-off-by: Filip Swiatczak --- .../src/providers/bitbucket/provider.test.ts | 12 +++++++++--- .../auth-backend/src/providers/bitbucket/provider.ts | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts index 7ab803243a..690729a5fb 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BitbucketAuthProvider } from './provider'; +import { BitbucketAuthProvider, BitbucketOAuthResult } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { OAuthResult } from '../../lib/oauth'; import { getVoidLogger } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient } from '../../lib/catalog'; @@ -25,7 +24,7 @@ const mockFrameHandler = jest.spyOn( helpers, 'executeFrameHandlerStrategy', ) as unknown as jest.MockedFunction< - () => Promise<{ result: OAuthResult; privateInfo: any }> + () => Promise<{ result: BitbucketOAuthResult; privateInfo: any }> >; describe('createBitbucketProvider', () => { @@ -58,6 +57,13 @@ describe('createBitbucketProvider', () => { 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', diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 81dddba5d0..7f27e64fee 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -53,7 +53,7 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; - authHandler: AuthHandler; + authHandler: AuthHandler; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; logger: Logger; @@ -213,7 +213,7 @@ export const bitbucketEmailSignInResolver: SignInResolver = async ( const entity = await ctx.catalogIdentityClient.findUser({ annotations: { - 'bitbucket/email': profile.email, + 'bitbucket.org/email': profile.email, }, }); @@ -248,11 +248,11 @@ export type BitbucketProviderOptions = { /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. */ - signIn?: { + signIn: { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; From 22ef4c33075c71b417d33a3f7206f22bada47aec Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 30 Sep 2021 09:22:15 +0100 Subject: [PATCH 11/14] exporting bitbucket types Signed-off-by: Filip Swiatczak --- packages/app/src/identityProviders.ts | 2 +- plugins/auth-backend/api-report.md | 54 ++++++++++++++++++- .../src/providers/bitbucket/index.ts | 6 ++- plugins/auth-backend/src/providers/index.ts | 1 + 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 24e02f61b0..49204d2b5e 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -85,7 +85,7 @@ export const providers = [ { id: 'bitbucket-auth-provider', title: 'Bitbucket', - message: 'Sign In using Bitbucket Cloud', + message: 'Sign In using Bitbucket', apiRef: bitbucketAuthApiRef, }, ]; diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 5dfdd3df16..34a0d5c559 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -84,6 +84,57 @@ export type BackstageIdentity = { entity?: Entity; }; +// Warning: (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "bitbucketEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const bitbucketEmailSignInResolver: SignInResolver; + +// Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BitbucketOAuthResult = { + fullProfile: BitbucketPassportProfile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; +}; + +// Warning: (ae-missing-release-tag) "BitbucketPassportProfile" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BitbucketPassportProfile = Profile & { + avatarUrl?: string; + _json?: { + links?: { + avatar?: { + href?: string; + }; + }; + }; +}; + +// Warning: (ae-missing-release-tag) "BitbucketProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BitbucketProviderOptions = { + authHandler?: AuthHandler; + signIn: { + resolver: SignInResolver; + }; +}; + +// Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createBitbucketProvider: ( + options?: BitbucketProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -468,8 +519,7 @@ export type WebMessageResponse = // // src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:50:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:58:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts +// src/providers/bitbucket/provider.d.ts:57:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative diff --git a/plugins/auth-backend/src/providers/bitbucket/index.ts b/plugins/auth-backend/src/providers/bitbucket/index.ts index d9fcdf3a68..f39fc5e65b 100644 --- a/plugins/auth-backend/src/providers/bitbucket/index.ts +++ b/plugins/auth-backend/src/providers/bitbucket/index.ts @@ -18,4 +18,8 @@ export { createBitbucketProvider, bitbucketEmailSignInResolver, } from './provider'; -export type { BitbucketProviderOptions } from './provider'; +export type { + BitbucketProviderOptions, + BitbucketPassportProfile, + BitbucketOAuthResult, +} from './provider'; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 1f879b311b..90466a8094 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -20,6 +20,7 @@ export * from './google'; export * from './microsoft'; export * from './oauth2'; export * from './okta'; +export * from './bitbucket'; export { factories as defaultAuthProviderFactories } from './factories'; From 7105058bacef6da0be686575d1141e272b4c47ca Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 30 Sep 2021 10:30:47 +0100 Subject: [PATCH 12/14] bitbucket typed.d.ts added Signed-off-by: Filip Swiatczak --- .../src/providers/bitbucket/provider.ts | 1 - .../src/providers/bitbucket/types.d.ts | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 plugins/auth-backend/src/providers/bitbucket/types.d.ts diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 7f27e64fee..1c84258273 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -16,7 +16,6 @@ import express from 'express'; import passport, { Profile as PassportProfile } from 'passport'; -// @ts-ignore passport-bitbucket-oauth2 does not have type definitions import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; diff --git a/plugins/auth-backend/src/providers/bitbucket/types.d.ts b/plugins/auth-backend/src/providers/bitbucket/types.d.ts new file mode 100644 index 0000000000..70c4c8cba9 --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucket/types.d.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ +declare module 'passport-bitbucket-oauth2' { + import { StrategyCreated } from 'passport'; + import express = require('express'); + + export class Strategy { + name?: string | undefined; + authenticate( + this: StrategyCreated, + req: express.Request, + options?: any, + ): any; + constructor(options: any, verify: any); + } +} From ed528384719a8260eabc42df01fdc0946c99a82f Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Thu, 30 Sep 2021 21:11:00 +0100 Subject: [PATCH 13/14] Resolvers: Username, DisplayName, UUID added. Default removed Signed-off-by: Filip Swiatczak --- plugins/auth-backend/api-report.md | 21 +++- .../src/providers/bitbucket/index.ts | 4 +- .../src/providers/bitbucket/provider.ts | 102 +++++++++++------- .../auth-backend/src/providers/factories.ts | 9 +- 4 files changed, 90 insertions(+), 46 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 34a0d5c559..8a0d0a8823 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -85,10 +85,10 @@ export type BackstageIdentity = { }; // Warning: (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "bitbucketEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "bitbucketDisplayNameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const bitbucketEmailSignInResolver: SignInResolver; +export const bitbucketDisplayNameSignInResolver: SignInResolver; // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -108,8 +108,11 @@ export type BitbucketOAuthResult = { // // @public (undocumented) export type BitbucketPassportProfile = Profile & { + displayName?: string; + username?: string; avatarUrl?: string; _json?: { + uuid?: string; links?: { avatar?: { href?: string; @@ -128,11 +131,21 @@ export type BitbucketProviderOptions = { }; }; +// Warning: (ae-missing-release-tag) "bitbucketUsernameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const bitbucketUsernameSignInResolver: SignInResolver; + +// Warning: (ae-missing-release-tag) "bitbucketUuidSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const bitbucketUuidSignInResolver: SignInResolver; + // Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const createBitbucketProvider: ( - options?: BitbucketProviderOptions | undefined, + options: BitbucketProviderOptions, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -519,7 +532,7 @@ export type WebMessageResponse = // // src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/bitbucket/provider.d.ts:57:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts +// src/providers/bitbucket/provider.d.ts:62:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative diff --git a/plugins/auth-backend/src/providers/bitbucket/index.ts b/plugins/auth-backend/src/providers/bitbucket/index.ts index f39fc5e65b..288c2e0ab1 100644 --- a/plugins/auth-backend/src/providers/bitbucket/index.ts +++ b/plugins/auth-backend/src/providers/bitbucket/index.ts @@ -16,7 +16,9 @@ export { createBitbucketProvider, - bitbucketEmailSignInResolver, + bitbucketUsernameSignInResolver, + bitbucketUuidSignInResolver, + bitbucketDisplayNameSignInResolver, } from './provider'; export type { BitbucketProviderOptions, diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 1c84258273..e5b63ad3e3 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -70,8 +70,11 @@ export type BitbucketOAuthResult = { }; export type BitbucketPassportProfile = PassportProfile & { + displayName?: string; + username?: string; avatarUrl?: string; _json?: { + uuid?: string; links?: { avatar?: { href?: string; @@ -200,42 +203,65 @@ export class BitbucketAuthProvider implements OAuthHandlers { } } -export const bitbucketEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; +export const bitbucketUsernameSignInResolver: SignInResolver = + async (info, ctx) => { + const { result } = info; - if (!profile.email) { - throw new Error('Bitbucket profile contained no email'); - } + if (!result.fullProfile.username) { + throw new Error('Bitbucket profile contained no Username'); + } - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'bitbucket.org/email': profile.email, - }, - }); + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'bitbucket.org/username': result.fullProfile.username, + }, + }); - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); - return { id: entity.metadata.name, entity, token }; -}; + return { id: entity.metadata.name, entity, token }; + }; -const bitbucketDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { fullProfile } = info.result; +export const bitbucketDisplayNameSignInResolver: SignInResolver = + async (info, ctx) => { + const { result } = info; - const userId = fullProfile.username || fullProfile.id; + if (!result.fullProfile.displayName) { + throw new Error('Bitbucket profile contained no display name'); + } - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: userId, ent: [`user:default/${userId}`] }, - }); + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'bitbucket.org/displayName': result.fullProfile.displayName, + }, + }); - return { id: userId, token }; -}; + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; + +export const bitbucketUuidSignInResolver: SignInResolver = + async (info, ctx) => { + const { result } = info; + + if (!result.fullProfile?._json?.uuid) { + throw new Error('Bitbucket profile contained no UUID'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'bitbucket.org/uuid': result.fullProfile._json?.uuid, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; export type BitbucketProviderOptions = { /** @@ -256,7 +282,7 @@ export type BitbucketProviderOptions = { }; export const createBitbucketProvider = ( - options?: BitbucketProviderOptions, + options: BitbucketProviderOptions, ): AuthProviderFactory => { return ({ providerId, @@ -276,17 +302,15 @@ export const createBitbucketProvider = ( tokenIssuer, }); - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const authHandler: AuthHandler = + options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const signInResolverFn = - options?.signIn?.resolver ?? bitbucketDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { + const signInResolver: SignInResolver = info => + options.signIn.resolver(info, { catalogIdentityClient, tokenIssuer, logger, diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index c4894f2757..b8e4b37d0c 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -26,7 +26,10 @@ import { createMicrosoftProvider } from './microsoft'; import { createOneLoginProvider } from './onelogin'; import { AuthProviderFactory } from './types'; import { createAwsAlbProvider } from './aws-alb'; -import { createBitbucketProvider } from './bitbucket'; +import { + createBitbucketProvider, + bitbucketUsernameSignInResolver, +} from './bitbucket'; export const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider(), @@ -40,5 +43,7 @@ export const factories: { [providerId: string]: AuthProviderFactory } = { oidc: createOidcProvider(), onelogin: createOneLoginProvider(), awsalb: createAwsAlbProvider(), - bitbucket: createBitbucketProvider(), + bitbucket: createBitbucketProvider({ + signIn: { resolver: bitbucketUsernameSignInResolver }, + }), }; From ad7bfe6810e09857c376ca071df41627f5a5a071 Mon Sep 17 00:00:00 2001 From: Filip Swiatczak Date: Fri, 1 Oct 2021 17:05:48 +0100 Subject: [PATCH 14/14] dropped display name Resolver, uuid -> user-id Signed-off-by: Filip Swiatczak --- plugins/auth-backend/api-report.md | 21 +++++-------- .../src/providers/bitbucket/index.ts | 3 +- .../src/providers/bitbucket/provider.ts | 30 ++++--------------- 3 files changed, 14 insertions(+), 40 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 8a0d0a8823..3834388db9 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -84,12 +84,6 @@ export type BackstageIdentity = { entity?: Entity; }; -// Warning: (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "bitbucketDisplayNameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const bitbucketDisplayNameSignInResolver: SignInResolver; - // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -108,11 +102,11 @@ export type BitbucketOAuthResult = { // // @public (undocumented) export type BitbucketPassportProfile = Profile & { + id?: string; displayName?: string; username?: string; avatarUrl?: string; _json?: { - uuid?: string; links?: { avatar?: { href?: string; @@ -131,16 +125,16 @@ export type BitbucketProviderOptions = { }; }; +// Warning: (ae-missing-release-tag) "bitbucketUserIdSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const bitbucketUserIdSignInResolver: SignInResolver; + // Warning: (ae-missing-release-tag) "bitbucketUsernameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const bitbucketUsernameSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "bitbucketUuidSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const bitbucketUuidSignInResolver: SignInResolver; - // Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -532,7 +526,8 @@ export type WebMessageResponse = // // src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/bitbucket/provider.d.ts:62:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts +// src/providers/bitbucket/provider.d.ts:61:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts +// src/providers/bitbucket/provider.d.ts:69:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative diff --git a/plugins/auth-backend/src/providers/bitbucket/index.ts b/plugins/auth-backend/src/providers/bitbucket/index.ts index 288c2e0ab1..55a5735655 100644 --- a/plugins/auth-backend/src/providers/bitbucket/index.ts +++ b/plugins/auth-backend/src/providers/bitbucket/index.ts @@ -17,8 +17,7 @@ export { createBitbucketProvider, bitbucketUsernameSignInResolver, - bitbucketUuidSignInResolver, - bitbucketDisplayNameSignInResolver, + bitbucketUserIdSignInResolver, } from './provider'; export type { BitbucketProviderOptions, diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index e5b63ad3e3..352e0fc116 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -70,11 +70,11 @@ export type BitbucketOAuthResult = { }; export type BitbucketPassportProfile = PassportProfile & { + id?: string; displayName?: string; username?: string; avatarUrl?: string; _json?: { - uuid?: string; links?: { avatar?: { href?: string; @@ -223,37 +223,17 @@ export const bitbucketUsernameSignInResolver: SignInResolver = +export const bitbucketUserIdSignInResolver: SignInResolver = async (info, ctx) => { const { result } = info; - if (!result.fullProfile.displayName) { - throw new Error('Bitbucket profile contained no display name'); + if (!result.fullProfile.id) { + throw new Error('Bitbucket profile contained no User ID'); } const entity = await ctx.catalogIdentityClient.findUser({ annotations: { - 'bitbucket.org/displayName': result.fullProfile.displayName, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; - }; - -export const bitbucketUuidSignInResolver: SignInResolver = - async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile?._json?.uuid) { - throw new Error('Bitbucket profile contained no UUID'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'bitbucket.org/uuid': result.fullProfile._json?.uuid, + 'bitbucket.org/user-id': result.fullProfile.id, }, });