@@ -422,5 +422,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
};
|
||||
}
|
||||
|
||||
console.log('retry');
|
||||
console.log(`args: ${JSON.stringify(args)}`);
|
||||
|
||||
await runJest(args);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { AuthRequestOptions } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityResponse } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { BackstageUserIdentity } from '@backstage/core-plugin-api';
|
||||
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
@@ -547,7 +548,9 @@ export class OAuth2
|
||||
SessionApi
|
||||
{
|
||||
// (undocumented)
|
||||
static create(options: OAuth2CreateOptions): OAuth2;
|
||||
static create(
|
||||
options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,
|
||||
): OAuth2;
|
||||
// (undocumented)
|
||||
getAccessToken(
|
||||
scope?: string | string[],
|
||||
@@ -562,6 +565,11 @@ export class OAuth2
|
||||
// (undocumented)
|
||||
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
|
||||
// (undocumented)
|
||||
static normalizeScopes(
|
||||
scopeTransform: (scopes: string[]) => string[],
|
||||
scopes?: string | string[],
|
||||
): Set<string>;
|
||||
// (undocumented)
|
||||
sessionState$(): Observable<SessionState>;
|
||||
// (undocumented)
|
||||
signIn(): Promise<void>;
|
||||
@@ -573,7 +581,29 @@ export class OAuth2
|
||||
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
popupOptions?: PopupOptions;
|
||||
authConnector?: AuthConnector<OAuth2Session>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type OAuth2CreateOptionsWithAuthConnector = {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
defaultScopes?: string[];
|
||||
authConnector: AuthConnector<OAuth2Session>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OAuth2Response = {
|
||||
providerInfo: {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
scope: string;
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: {
|
||||
token: string;
|
||||
expiresInSeconds?: number;
|
||||
identity: BackstageUserIdentity;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -18,6 +18,14 @@ import OAuth2 from './OAuth2';
|
||||
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
|
||||
import { UrlPatternDiscovery } from '../../DiscoveryApi';
|
||||
import { mockApis } from '@backstage/test-utils';
|
||||
import {
|
||||
OAuth2Session,
|
||||
AuthConnector,
|
||||
AuthConnectorRefreshSessionOptions,
|
||||
openLoginPopup,
|
||||
// OAuth2Response,
|
||||
OAuth2CreateOptionsWithAuthConnector,
|
||||
} from '../../../../index';
|
||||
|
||||
const theFuture = new Date(Date.now() + 3600000);
|
||||
const thePast = new Date(Date.now() - 10);
|
||||
@@ -37,6 +45,25 @@ jest.mock('../../../../lib/AuthSessionManager', () => ({
|
||||
|
||||
const configApi = mockApis.config();
|
||||
|
||||
class CustomAuthConnector implements AuthConnector<OAuth2Session> {
|
||||
async createSession() {
|
||||
const s: OAuth2Session = {
|
||||
providerInfo: {
|
||||
idToken: '',
|
||||
accessToken: 'accessToken',
|
||||
scopes: new Set(['myScope']),
|
||||
},
|
||||
profile: {},
|
||||
};
|
||||
await openLoginPopup({ url: 'http://localhost', name: 'myPopup' });
|
||||
return Promise.resolve(s);
|
||||
}
|
||||
|
||||
async refreshSession(_?: AuthConnectorRefreshSessionOptions): Promise<any> {}
|
||||
|
||||
async removeSession(): Promise<void> {}
|
||||
}
|
||||
|
||||
describe('OAuth2', () => {
|
||||
it('should get refreshed access token', async () => {
|
||||
getSession = jest.fn().mockResolvedValue({
|
||||
@@ -215,4 +242,25 @@ describe('OAuth2', () => {
|
||||
await expect(promise3).resolves.toBe('token2');
|
||||
expect(getSession).toHaveBeenCalledTimes(4); // De-duping of session requests happens in client
|
||||
});
|
||||
it('should use provided auth provider', async () => {
|
||||
getSession = jest.fn().mockResolvedValue({
|
||||
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
|
||||
});
|
||||
|
||||
const customAuthConnector = new CustomAuthConnector();
|
||||
|
||||
const options: OAuth2CreateOptionsWithAuthConnector = {
|
||||
scopeTransform,
|
||||
defaultScopes: ['myScope'],
|
||||
authConnector: customAuthConnector,
|
||||
};
|
||||
const oauth2 = OAuth2.create(options);
|
||||
|
||||
expect(await oauth2.getAccessToken('my-scope my-scope2')).toBe(
|
||||
'access-token',
|
||||
);
|
||||
expect(getSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ scopes: new Set(['my-scope', 'my-scope2']) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,53 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthConnector,
|
||||
DefaultAuthConnector,
|
||||
PopupOptions,
|
||||
} from '../../../../lib/AuthConnector';
|
||||
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import {
|
||||
AuthRequestOptions,
|
||||
BackstageIdentityApi,
|
||||
BackstageIdentityResponse,
|
||||
OAuthApi,
|
||||
OpenIdConnectApi,
|
||||
ProfileInfo,
|
||||
ProfileInfoApi,
|
||||
SessionState,
|
||||
SessionApi,
|
||||
BackstageIdentityApi,
|
||||
BackstageUserIdentity,
|
||||
SessionState,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { OAuth2Session } from './types';
|
||||
import { OAuthApiCreateOptions } from '../types';
|
||||
|
||||
/**
|
||||
* OAuth2 create options.
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
popupOptions?: PopupOptions;
|
||||
authConnector?: AuthConnector<OAuth2Session>;
|
||||
};
|
||||
|
||||
export type OAuth2Response = {
|
||||
providerInfo: {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
scope: string;
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: {
|
||||
token: string;
|
||||
expiresInSeconds?: number;
|
||||
identity: BackstageUserIdentity;
|
||||
};
|
||||
};
|
||||
import {
|
||||
OAuth2CreateOptions,
|
||||
OAuth2CreateOptionsWithAuthConnector,
|
||||
OAuth2Response,
|
||||
OAuth2Session,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'oauth2',
|
||||
@@ -81,61 +54,67 @@ export default class OAuth2
|
||||
BackstageIdentityApi,
|
||||
SessionApi
|
||||
{
|
||||
static create(options: OAuth2CreateOptions) {
|
||||
private static createAuthConnector(
|
||||
options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,
|
||||
) {
|
||||
if ('authConnector' in options) {
|
||||
return options.authConnector;
|
||||
}
|
||||
const {
|
||||
scopeTransform = x => x,
|
||||
configApi,
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
defaultScopes = [],
|
||||
scopeTransform = x => x,
|
||||
popupOptions,
|
||||
} = options;
|
||||
|
||||
const connector =
|
||||
options.authConnector ??
|
||||
new DefaultAuthConnector({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
sessionTransform({
|
||||
backstageIdentity,
|
||||
...res
|
||||
}: OAuth2Response): OAuth2Session {
|
||||
const session: OAuth2Session = {
|
||||
...res,
|
||||
providerInfo: {
|
||||
idToken: res.providerInfo.idToken,
|
||||
accessToken: res.providerInfo.accessToken,
|
||||
scopes: OAuth2.normalizeScopes(
|
||||
scopeTransform,
|
||||
res.providerInfo.scope,
|
||||
),
|
||||
expiresAt: res.providerInfo.expiresInSeconds
|
||||
? new Date(
|
||||
Date.now() + res.providerInfo.expiresInSeconds * 1000,
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
return new DefaultAuthConnector({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
sessionTransform({
|
||||
backstageIdentity,
|
||||
...res
|
||||
}: OAuth2Response): OAuth2Session {
|
||||
const session: OAuth2Session = {
|
||||
...res,
|
||||
providerInfo: {
|
||||
idToken: res.providerInfo.idToken,
|
||||
accessToken: res.providerInfo.accessToken,
|
||||
scopes: OAuth2.normalizeScopes(
|
||||
scopeTransform,
|
||||
res.providerInfo.scope,
|
||||
),
|
||||
expiresAt: res.providerInfo.expiresInSeconds
|
||||
? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
if (backstageIdentity) {
|
||||
session.backstageIdentity = {
|
||||
token: backstageIdentity.token,
|
||||
identity: backstageIdentity.identity,
|
||||
expiresAt: backstageIdentity.expiresInSeconds
|
||||
? new Date(Date.now() + backstageIdentity.expiresInSeconds * 1000)
|
||||
: undefined,
|
||||
};
|
||||
if (backstageIdentity) {
|
||||
session.backstageIdentity = {
|
||||
token: backstageIdentity.token,
|
||||
identity: backstageIdentity.identity,
|
||||
expiresAt: backstageIdentity.expiresInSeconds
|
||||
? new Date(
|
||||
Date.now() + backstageIdentity.expiresInSeconds * 1000,
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
return session;
|
||||
},
|
||||
popupOptions,
|
||||
});
|
||||
}
|
||||
return session;
|
||||
},
|
||||
popupOptions,
|
||||
});
|
||||
}
|
||||
|
||||
static create(
|
||||
options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,
|
||||
) {
|
||||
const { defaultScopes = [], scopeTransform = x => x } = options;
|
||||
|
||||
const connector = OAuth2.createAuthConnector(options);
|
||||
|
||||
const sessionManager = new RefreshingAuthSessionManager({
|
||||
connector,
|
||||
@@ -218,7 +197,10 @@ export default class OAuth2
|
||||
return session?.profile;
|
||||
}
|
||||
|
||||
private static normalizeScopes(
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
public static normalizeScopes(
|
||||
scopeTransform: (scopes: string[]) => string[],
|
||||
scopes?: string | string[],
|
||||
): Set<string> {
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 OAuth2 from './OAuth2';
|
||||
import {
|
||||
OAuth2Session,
|
||||
AuthConnector,
|
||||
AuthConnectorRefreshSessionOptions,
|
||||
openLoginPopup,
|
||||
OAuth2CreateOptionsWithAuthConnector,
|
||||
OAuth2Response,
|
||||
} from '../../../../index';
|
||||
|
||||
const scopeTransform = (x: string[]) => x;
|
||||
|
||||
type Options = {
|
||||
/**
|
||||
* Function used to transform an auth response into the session type.
|
||||
*/
|
||||
sessionTransform?(response: any): OAuth2Session | Promise<OAuth2Session>;
|
||||
};
|
||||
|
||||
class CustomAuthConnector implements AuthConnector<OAuth2Session> {
|
||||
private readonly sessionTransform: (response: any) => Promise<OAuth2Session>;
|
||||
|
||||
constructor(options: Options) {
|
||||
const { sessionTransform = id => id } = options;
|
||||
|
||||
this.sessionTransform = sessionTransform;
|
||||
}
|
||||
|
||||
async createSession() {
|
||||
return await this.sessionTransform(
|
||||
await openLoginPopup({ url: 'http://my-origin', name: 'myPopup' }),
|
||||
);
|
||||
}
|
||||
|
||||
async refreshSession(_?: AuthConnectorRefreshSessionOptions): Promise<any> {}
|
||||
|
||||
async removeSession(): Promise<void> {}
|
||||
}
|
||||
|
||||
describe('OAuth2', () => {
|
||||
it('should use provided auth provider', async () => {
|
||||
const popupMock = { closed: false };
|
||||
|
||||
jest.spyOn(window, 'open').mockReturnValue(popupMock as Window);
|
||||
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const customAuthConnector = new CustomAuthConnector({
|
||||
sessionTransform(res: OAuth2Response): OAuth2Session {
|
||||
return {
|
||||
...res,
|
||||
providerInfo: {
|
||||
idToken: res.providerInfo.idToken,
|
||||
accessToken: res.providerInfo.accessToken,
|
||||
scopes: OAuth2.normalizeScopes(
|
||||
scopeTransform,
|
||||
res.providerInfo.scope,
|
||||
),
|
||||
expiresAt: res.providerInfo.expiresInSeconds
|
||||
? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const options: OAuth2CreateOptionsWithAuthConnector = {
|
||||
scopeTransform,
|
||||
defaultScopes: ['myScope'],
|
||||
authConnector: customAuthConnector,
|
||||
};
|
||||
const oauth2 = OAuth2.create(options);
|
||||
|
||||
// so that AuthConnector calls openLoginPopup synchronously (not try to refresh the token)
|
||||
const accessToken = oauth2.getAccessToken('myScope', {
|
||||
instantPopup: true,
|
||||
optional: false,
|
||||
});
|
||||
|
||||
const listener = addEventListenerSpy.mock.calls[0][1] as EventListener;
|
||||
|
||||
const accessTokenValue = 'myAccessToken';
|
||||
const myResponse = {
|
||||
providerInfo: {
|
||||
accessToken: accessTokenValue,
|
||||
scope: 'myScope',
|
||||
expiresInSeconds: 900,
|
||||
},
|
||||
profile: { displayName: 'John Doe' },
|
||||
};
|
||||
|
||||
// A valid sessions response
|
||||
listener({
|
||||
source: popupMock,
|
||||
origin: 'http://my-origin',
|
||||
data: {
|
||||
type: 'authorization_response',
|
||||
response: myResponse,
|
||||
},
|
||||
} as MessageEvent);
|
||||
|
||||
return expect(accessToken).resolves.toBe(accessTokenValue);
|
||||
});
|
||||
});
|
||||
@@ -15,11 +15,13 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ProfileInfo,
|
||||
BackstageIdentityResponse,
|
||||
BackstageUserIdentity,
|
||||
ProfileInfo,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { OAuthApiCreateOptions } from '../types.ts';
|
||||
import { AuthConnector, PopupOptions } from '../../../../lib';
|
||||
|
||||
export type { OAuth2CreateOptions } from './OAuth2';
|
||||
export type { PopupOptions } from '../../../../lib/AuthConnector';
|
||||
/**
|
||||
* Session information for generic OAuth2 auth.
|
||||
@@ -36,3 +38,40 @@ export type OAuth2Session = {
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity?: BackstageIdentityResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2Response = {
|
||||
providerInfo: {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
scope: string;
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: {
|
||||
token: string;
|
||||
expiresInSeconds?: number;
|
||||
identity: BackstageUserIdentity;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* OAuth2 create options.
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
popupOptions?: PopupOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* OAuth2 create options with custom auth connector.
|
||||
* @public
|
||||
*/
|
||||
export type OAuth2CreateOptionsWithAuthConnector = {
|
||||
scopeTransform?: (scopes: string[]) => string[];
|
||||
defaultScopes?: string[];
|
||||
authConnector: AuthConnector<OAuth2Session>;
|
||||
};
|
||||
|
||||
@@ -27,6 +27,6 @@ export type {
|
||||
AuthConnector,
|
||||
AuthConnectorCreateSessionOptions,
|
||||
AuthConnectorRefreshSessionOptions,
|
||||
openLoginPopup,
|
||||
OpenLoginPopupOptions,
|
||||
} from './lib';
|
||||
export { openLoginPopup } from './lib';
|
||||
|
||||
Reference in New Issue
Block a user