cleanup + BitbucketAuth standardisation
Signed-off-by: Filip Swiatczak <filip.swiatczak@gmail.com>
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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<BitbucketSession>);
|
||||
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<string>;
|
||||
// (undocumented)
|
||||
getBackstageIdentity(
|
||||
options?: AuthRequestOptions,
|
||||
): Promise<BackstageIdentity | undefined>;
|
||||
// (undocumented)
|
||||
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
|
||||
// (undocumented)
|
||||
static normalizeScope(scope?: string): Set<string>;
|
||||
// (undocumented)
|
||||
sessionState$(): Observable<SessionState>;
|
||||
// (undocumented)
|
||||
signIn(): Promise<void>;
|
||||
// (undocumented)
|
||||
signOut(): Promise<void>;
|
||||
}: 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<GithubSession>);
|
||||
// (undocumented)
|
||||
static create({
|
||||
|
||||
+26
-8
@@ -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) });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
Reference in New Issue
Block a user