Skip to content

Commit

Permalink
core-app-api: switch GithubAuth to use the common OAuth2 implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
  • Loading branch information
Rugvip committed Feb 2, 2022
1 parent 648606b commit 40775bd
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 139 deletions.
7 changes: 7 additions & 0 deletions .changeset/tasty-pandas-design.md
@@ -0,0 +1,7 @@
---
'@backstage/core-app-api': patch
---

Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`.

> **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change.
21 changes: 4 additions & 17 deletions packages/core-app-api/api-report.md
Expand Up @@ -35,6 +35,7 @@ import { FeatureFlag } from '@backstage/core-plugin-api';
import { FeatureFlagsApi } from '@backstage/core-plugin-api';
import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { githubAuthApiRef } from '@backstage/core-plugin-api';
import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
Expand Down Expand Up @@ -379,25 +380,11 @@ export type FlatRoutesProps = {
};

// @public
export class GithubAuth implements OAuthApi, SessionApi {
// (undocumented)
static create(options: OAuthApiCreateOptions): GithubAuth;
// (undocumented)
getAccessToken(scope?: string, options?: AuthRequestOptions): Promise<string>;
// (undocumented)
getBackstageIdentity(
options?: AuthRequestOptions,
): Promise<BackstageIdentityResponse | undefined>;
// (undocumented)
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
export class GithubAuth {
// (undocumented)
static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T;
// @deprecated (undocumented)
static normalizeScope(scope?: string): Set<string>;
// (undocumented)
sessionState$(): Observable<SessionState>;
// (undocumented)
signIn(): Promise<void>;
// (undocumented)
signOut(): Promise<void>;
}

// @public @deprecated
Expand Down
Expand Up @@ -14,16 +14,33 @@
* limitations under the License.
*/

import { UrlPatternDiscovery } from '../../DiscoveryApi';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import GithubAuth from './GithubAuth';

const getSession = jest.fn();

jest.mock('../../../../lib/AuthSessionManager', () => ({
...(jest.requireActual('../../../../lib/AuthSessionManager') as any),
RefreshingAuthSessionManager: class {
getSession = getSession;
},
}));

describe('GithubAuth', () => {
it('should get access token', async () => {
const getSession = jest
.fn()
.mockResolvedValue({ providerInfo: { accessToken: 'access-token' } });
const githubAuth = new (GithubAuth as any)({ getSession }) as GithubAuth;
afterEach(() => {
jest.resetAllMocks();
});

it('should forward access token request to session manager', async () => {
const githubAuth = GithubAuth.create({
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});

expect(await githubAuth.getAccessToken()).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
githubAuth.getAccessToken('repo');
expect(getSession).toHaveBeenCalledWith({
scopes: new Set(['repo']),
});
});
});
126 changes: 11 additions & 115 deletions packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts
Expand Up @@ -14,35 +14,9 @@
* limitations under the License.
*/

import {
AuthRequestOptions,
BackstageIdentityResponse,
OAuthApi,
ProfileInfo,
SessionApi,
SessionState,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import {
AuthSessionStore,
RefreshingAuthSessionManager,
StaticAuthSessionManager,
} from '../../../../lib/AuthSessionManager';
import { OptionalRefreshSessionManagerMux } from '../../../../lib/AuthSessionManager/OptionalRefreshSessionManagerMux';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { githubAuthApiRef } from '@backstage/core-plugin-api';
import { OAuth2 } from '../oauth2';
import { OAuthApiCreateOptions } from '../types';
import { GithubSession, githubSessionSchema } from './types';

export type GithubAuthResponse = {
providerInfo: {
accessToken: string;
scope: string;
expiresInSeconds?: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentityResponse;
};

const DEFAULT_PROVIDER = {
id: 'github',
Expand All @@ -55,8 +29,8 @@ const DEFAULT_PROVIDER = {
*
* @public
*/
export default class GithubAuth implements OAuthApi, SessionApi {
static create(options: OAuthApiCreateOptions) {
export default class GithubAuth {
static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T {
const {
discoveryApi,
environment = 'development',
Expand All @@ -65,96 +39,18 @@ export default class GithubAuth implements OAuthApi, SessionApi {
defaultScopes = ['read:user'],
} = options;

const connector = new DefaultAuthConnector({
return OAuth2.create({
discoveryApi,
environment,
oauthRequestApi,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: GithubAuthResponse): GithubSession {
return {
...res,
providerInfo: {
accessToken: res.providerInfo.accessToken,
scopes: GithubAuth.normalizeScope(res.providerInfo.scope),
expiresAt: res.providerInfo.expiresInSeconds
? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)
: undefined,
},
};
},
});

const refreshingSessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set(defaultScopes),
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
sessionShouldRefresh: (session: GithubSession) => {
const { expiresAt } = session.providerInfo;
if (!expiresAt) {
return false;
}
const expiresInSec = (expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
});

const staticSessionManager = new AuthSessionStore<GithubSession>({
manager: new StaticAuthSessionManager({
connector,
defaultScopes: new Set(defaultScopes),
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
}),
storageKey: `${provider.id}Session`,
schema: githubSessionSchema,
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
});

const sessionManagerMux = new OptionalRefreshSessionManagerMux({
refreshingSessionManager,
staticSessionManager,
sessionCanRefresh: session =>
session.providerInfo.expiresAt !== undefined,
});

return new GithubAuth(sessionManagerMux);
}

private constructor(
private readonly sessionManager: SessionManager<GithubSession>,
) {}

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: GithubAuth.normalizeScope(scope),
environment,
defaultScopes,
});
return session?.providerInfo.accessToken ?? '';
}

async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentityResponse | 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;
}

/**
* @deprecated This method is deprecated and will be removed in a future release.
*/
static normalizeScope(scope?: string): Set<string> {
if (!scope) {
return new Set();
Expand Down

0 comments on commit 40775bd

Please sign in to comment.