Bitbucket cloud authentication client added
Signed-off-by: Filip Swiatczak <filip.swiatczak@gmail.com>
This commit is contained in:
+6
-2
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user