Bitbucket cloud authentication client added

Signed-off-by: Filip Swiatczak <filip.swiatczak@gmail.com>
This commit is contained in:
Filip Swiatczak
2021-09-23 17:17:10 +01:00
parent a45b2dfeb5
commit 74534f6d39
15 changed files with 422 additions and 3 deletions
+6 -2
View File
@@ -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:
+7
View File
@@ -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,
},
];
@@ -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);
});
});
@@ -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<BitbucketSession>({
manager: sessionManager,
storageKey: `${provider.id}Session`,
sessionScopes: (session: BitbucketSession) => session.providerInfo.scopes,
});
return new BitbucketAuth(authSessionStore);
}
constructor(
private readonly sessionManager: SessionManager<BitbucketSession>,
) {}
async signIn() {
await this.getAccessToken();
}
async signOut() {
await this.sessionManager.removeSession();
}
sessionState$(): Observable<SessionState> {
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<BackstageIdentity | undefined> {
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<string> {
if (!scope) {
return new Set();
}
const scopeList = Array.isArray(scope)
? scope
: scope.split(/[\s|,]/).filter(Boolean);
return new Set(scopeList);
}
}
export default BitbucketAuth;
@@ -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';
@@ -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<string>;
expiresAt?: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
@@ -23,3 +23,4 @@ export * from './saml';
export * from './auth0';
export * from './microsoft';
export * from './onelogin';
export * from './bitbucket';
@@ -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'),
}),
}),
];
@@ -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',
});
+1
View File
@@ -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",
@@ -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';
@@ -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<OAuthResult>,
) => {
done(undefined, { fullProfile, params, accessToken });
},
);
}
async start(req: OAuthStartRequest): Promise<RedirectInfo> {
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<OAuthResult>(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,
});
});
};
@@ -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(),
};
@@ -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') && (
<ProviderSettingsItem
title="Bitbucket"
description="Provides authentication towards Bitbucket APIs"
apiRef={bitbucketAuthApiRef}
icon={Star}
/>
)}
{configuredProviders.includes('oauth2') && (
<ProviderSettingsItem
title="YourOrg"
+9 -1
View File
@@ -21332,6 +21332,14 @@ pascalcase@^0.1.1:
resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
passport-bitbucket-oauth2@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/passport-bitbucket-oauth2/-/passport-bitbucket-oauth2-0.1.2.tgz#eb3af5cdd0d06830adc49b76acae4ad82290693b"
integrity sha1-6zr1zdDQaDCtxJt2rK5K2CKQaTs=
dependencies:
passport-oauth2 "^1.1.2"
pkginfo "0.2.x"
passport-github2@^0.1.12:
version "0.1.12"
resolved "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz#a72ebff4fa52a35bc2c71122dcf470d1116f772c"
@@ -21379,7 +21387,7 @@ passport-oauth2@1.2.0:
passport-strategy "1.x.x"
uid2 "0.0.x"
passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0:
passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0:
version "1.6.0"
resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.0.tgz#5f599735e0ea40ea3027643785f81a3a9b4feb50"
integrity sha512-emXPLqLcVEcLFR/QvQXZcwLmfK8e9CqvMgmOFJxcNT3okSFMtUbRRKpY20x5euD+01uHsjjCa07DYboEeLXYiw==