Updates to redirect implementation after initial PR review.
Signed-off-by: headphonejames <generalfuzz@gmail.com>
This commit is contained in:
+1
-1
@@ -293,7 +293,7 @@ auth:
|
||||
# path: my-sessions
|
||||
|
||||
environment: development
|
||||
usePopup: false
|
||||
authFlow: redirect
|
||||
### Providing an auth.session.secret will enable session support in the auth-backend
|
||||
# session:
|
||||
# secret: custom session secret
|
||||
|
||||
@@ -131,7 +131,7 @@ export const apis = [
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -146,7 +146,7 @@ export const apis = [
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -162,7 +162,7 @@ export const apis = [
|
||||
oauthRequestApi,
|
||||
defaultScopes: ['read:user'],
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -177,7 +177,7 @@ export const apis = [
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -192,7 +192,7 @@ export const apis = [
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -207,7 +207,7 @@ export const apis = [
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -223,7 +223,7 @@ export const apis = [
|
||||
oauthRequestApi,
|
||||
defaultScopes: ['team'],
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
@@ -238,7 +238,7 @@ export const apis = [
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
usePopup: configApi.getOptionalBoolean('auth.usePopup'),
|
||||
authFlow: configApi.getOptionalString('auth.authFlow'),
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
Vendored
+3
-3
@@ -115,10 +115,10 @@ export interface Config {
|
||||
*/
|
||||
environment?: string;
|
||||
/**
|
||||
$ The authentication flow type - either using a popup to authenticate or perform a http redirect
|
||||
* default value: 'true'
|
||||
$ The authentication flow type - currently supports 'popup' and 'redirect'
|
||||
* default value: 'popup'
|
||||
* @visibility frontend
|
||||
*/
|
||||
usePopup?: boolean;
|
||||
authFlow?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,8 +37,10 @@ export class OAuthRequestManager implements OAuthRequestApi {
|
||||
private readonly subject = new BehaviorSubject<PendingOAuthRequest[]>([]);
|
||||
private currentRequests: PendingOAuthRequest[] = [];
|
||||
private handlerCount = 0;
|
||||
private authFlowStr: string = 'popup';
|
||||
|
||||
createAuthRequester<T>(options: OAuthRequesterOptions<T>): OAuthRequester<T> {
|
||||
this.authFlowStr = options.authFlow;
|
||||
const handler = new OAuthPendingRequests<T>();
|
||||
|
||||
const index = this.handlerCount;
|
||||
@@ -91,4 +93,8 @@ export class OAuthRequestManager implements OAuthRequestApi {
|
||||
authRequest$(): Observable<PendingOAuthRequest[]> {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
authFlow(): string {
|
||||
return this.authFlowStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class AtlassianAuth {
|
||||
const {
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
} = options;
|
||||
@@ -44,7 +44,7 @@ export default class AtlassianAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export type BitbucketAuthResponse = {
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'bitbucket',
|
||||
title: 'Bitbucket',
|
||||
provider_id: 'bitbucket-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -50,7 +49,7 @@ export default class BitbucketAuth {
|
||||
const {
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
defaultScopes = ['team'],
|
||||
@@ -61,7 +60,7 @@ export default class BitbucketAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow,
|
||||
defaultScopes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { OAuthApiCreateOptions } from '../types';
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'github',
|
||||
title: 'GitHub',
|
||||
provider_id: 'github-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -38,7 +37,7 @@ export default class GithubAuth {
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
defaultScopes = ['read:user'],
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
} = options;
|
||||
|
||||
return OAuth2.create({
|
||||
@@ -47,7 +46,7 @@ export default class GithubAuth {
|
||||
provider,
|
||||
environment,
|
||||
defaultScopes,
|
||||
usePopup,
|
||||
authFlow,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { OAuthApiCreateOptions } from '../types';
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'gitlab',
|
||||
title: 'GitLab',
|
||||
provider_id: 'gitlab-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -35,7 +34,7 @@ export default class GitlabAuth {
|
||||
const {
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
defaultScopes = ['read_user'],
|
||||
@@ -46,7 +45,7 @@ export default class GitlabAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow,
|
||||
defaultScopes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { OAuthApiCreateOptions } from '../types';
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'google',
|
||||
title: 'Google',
|
||||
provider_id: 'google-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -38,7 +37,7 @@ export default class GoogleAuth {
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
defaultScopes = [
|
||||
'openid',
|
||||
@@ -52,7 +51,7 @@ export default class GoogleAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow,
|
||||
defaultScopes,
|
||||
scopeTransform(scopes: string[]) {
|
||||
return scopes.map(scope => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { OAuthApiCreateOptions } from '../types';
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'microsoft',
|
||||
title: 'Microsoft',
|
||||
provider_id: 'microsoft-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -34,7 +33,7 @@ export default class MicrosoftAuth {
|
||||
static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T {
|
||||
const {
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
discoveryApi,
|
||||
@@ -52,7 +51,7 @@ export default class MicrosoftAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow,
|
||||
defaultScopes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class OAuth2
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
defaultScopes = [],
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
scopeTransform = x => x,
|
||||
} = options;
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class OAuth2
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
usePopup,
|
||||
authFlow,
|
||||
sessionTransform(res: OAuth2Response): OAuth2Session {
|
||||
return {
|
||||
...res,
|
||||
|
||||
@@ -21,7 +21,6 @@ import { OAuthApiCreateOptions } from '../types';
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'okta',
|
||||
title: 'Okta',
|
||||
provider_id: 'okta-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -47,7 +46,7 @@ export default class OktaAuth {
|
||||
const {
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
defaultScopes = ['openid', 'email', 'profile', 'offline_access'],
|
||||
@@ -58,7 +57,7 @@ export default class OktaAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow,
|
||||
defaultScopes,
|
||||
scopeTransform(scopes) {
|
||||
return scopes.map(scope => {
|
||||
|
||||
@@ -30,14 +30,13 @@ export type OneLoginAuthCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
environment?: string;
|
||||
usePopup?: boolean;
|
||||
authFlow?: string;
|
||||
provider?: AuthProviderInfo;
|
||||
};
|
||||
|
||||
const DEFAULT_PROVIDER = {
|
||||
id: 'onelogin',
|
||||
title: 'onelogin',
|
||||
provider_id: 'onelogin-auth-provider',
|
||||
icon: () => null,
|
||||
};
|
||||
|
||||
@@ -65,7 +64,7 @@ export default class OneLoginAuth {
|
||||
const {
|
||||
discoveryApi,
|
||||
environment = 'development',
|
||||
usePopup = true,
|
||||
authFlow = 'popup',
|
||||
provider = DEFAULT_PROVIDER,
|
||||
oauthRequestApi,
|
||||
} = options;
|
||||
@@ -75,7 +74,7 @@ export default class OneLoginAuth {
|
||||
oauthRequestApi,
|
||||
provider,
|
||||
environment,
|
||||
usePopup,
|
||||
authFlow: authFlow,
|
||||
defaultScopes: ['openid', 'email', 'profile', 'offline_access'],
|
||||
scopeTransform(scopes) {
|
||||
return scopes.map(scope => {
|
||||
|
||||
@@ -37,5 +37,5 @@ export type AuthApiCreateOptions = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
environment?: string;
|
||||
provider?: AuthProviderInfo;
|
||||
usePopup?: boolean;
|
||||
authFlow?: string;
|
||||
};
|
||||
|
||||
@@ -34,16 +34,15 @@ const defaultOptions = {
|
||||
provider: {
|
||||
id: 'my-provider',
|
||||
title: 'My Provider',
|
||||
provider_id: 'myprovider-auth-provider',
|
||||
icon: () => null,
|
||||
},
|
||||
usePopup: true,
|
||||
oauthRequestApi: new MockOAuthApi(),
|
||||
sessionTransform: ({ expiresInSeconds, ...res }: any) => ({
|
||||
...res,
|
||||
scopes: new Set(res.scopes.split(' ')),
|
||||
expiresAt: new Date(Date.now() + expiresInSeconds * 1000),
|
||||
}),
|
||||
authFlow: 'popup',
|
||||
};
|
||||
|
||||
describe('DefaultAuthConnector', () => {
|
||||
@@ -133,7 +132,7 @@ describe('DefaultAuthConnector', () => {
|
||||
|
||||
expect(popupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&env=production',
|
||||
url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&authFlow=popup&env=production',
|
||||
});
|
||||
|
||||
await expect(sessionPromise).resolves.toEqual({
|
||||
@@ -181,40 +180,7 @@ describe('DefaultAuthConnector', () => {
|
||||
|
||||
expect(popupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&env=production',
|
||||
url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&authFlow=popup&env=production',
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to api server', async () => {
|
||||
const mockOauth = new MockOAuthApi();
|
||||
const mockResponse = jest.fn();
|
||||
// replace the window.location object
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
hash: {
|
||||
endsWith: mockResponse,
|
||||
includes: mockResponse,
|
||||
},
|
||||
assign: mockResponse,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
const helper = new DefaultAuthConnector({
|
||||
...defaultOptions,
|
||||
usePopup: false,
|
||||
oauthRequestApi: mockOauth,
|
||||
});
|
||||
|
||||
const sessionPromise = helper.createSession({
|
||||
scopes: new Set(['a', 'b']),
|
||||
});
|
||||
|
||||
await mockOauth.triggerAll();
|
||||
|
||||
await expect(sessionPromise).resolves.toEqual({});
|
||||
// redirect to the auth api
|
||||
expect(window.location.href).toMatch(
|
||||
'http://my-host/api/auth/my-provider/start?scope=a%20b&authType=redirect&env=production',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,10 +32,6 @@ type Options<AuthSession> = {
|
||||
* Environment hint passed on to auth backend, for example 'production' or 'development'
|
||||
*/
|
||||
environment: string;
|
||||
/**
|
||||
* use a popup or a redirect for authentication with backend authentication api
|
||||
*/
|
||||
usePopup: boolean;
|
||||
/**
|
||||
* Information about the auth provider to be shown to the user.
|
||||
* The ID Must match the backend auth plugin configuration, for example 'google'.
|
||||
@@ -53,6 +49,10 @@ type Options<AuthSession> = {
|
||||
* Function used to transform an auth response into the session type.
|
||||
*/
|
||||
sessionTransform?(response: any): AuthSession | Promise<AuthSession>;
|
||||
/**
|
||||
* The UI authentication flow with backend authentication api. Supports either 'popup' or 'redirect'.
|
||||
*/
|
||||
authFlow: string;
|
||||
};
|
||||
|
||||
function defaultJoinScopes(scopes: Set<string>) {
|
||||
@@ -73,6 +73,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
private readonly joinScopesFunc: (scopes: Set<string>) => string;
|
||||
private readonly authRequester: OAuthRequester<AuthSession>;
|
||||
private readonly sessionTransform: (response: any) => Promise<AuthSession>;
|
||||
private readonly authFlow: string;
|
||||
|
||||
constructor(options: Options<AuthSession>) {
|
||||
const {
|
||||
@@ -81,39 +82,22 @@ export class DefaultAuthConnector<AuthSession>
|
||||
provider,
|
||||
joinScopes = defaultJoinScopes,
|
||||
oauthRequestApi,
|
||||
usePopup,
|
||||
authFlow,
|
||||
sessionTransform = id => id,
|
||||
} = options;
|
||||
|
||||
this.authRequester = oauthRequestApi.createAuthRequester({
|
||||
provider,
|
||||
onAuthRequest: async scopes => {
|
||||
if (!usePopup) {
|
||||
// modal before redirect
|
||||
const scope = this.joinScopesFunc(scopes);
|
||||
const redirectUrl = await this.buildUrl('/start', {
|
||||
scope,
|
||||
origin: window.location.origin,
|
||||
redirectUrl: window.location.href,
|
||||
authType: 'redirect',
|
||||
});
|
||||
|
||||
if (provider.hasOwnProperty('provider_id')) {
|
||||
// set the sign in provider here
|
||||
localStorage.setItem(
|
||||
'@backstage/core:SignInPage:provider',
|
||||
provider.provider_id!,
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
// we need to return to exit function or else popup occurs
|
||||
return Promise.resolve({} as AuthSession);
|
||||
if (authFlow === 'popup') {
|
||||
return this.showPopup(scopes);
|
||||
}
|
||||
return this.showPopup(scopes);
|
||||
return this.executeRedirect(scopes);
|
||||
},
|
||||
authFlow,
|
||||
});
|
||||
|
||||
this.authFlow = authFlow;
|
||||
this.discoveryApi = discoveryApi;
|
||||
this.environment = environment;
|
||||
this.provider = provider;
|
||||
@@ -122,7 +106,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
}
|
||||
|
||||
async createSession(options: CreateSessionOptions): Promise<AuthSession> {
|
||||
if (options.instantPopup) {
|
||||
if (options.instantPopup && this.authFlow === 'popup') {
|
||||
return this.showPopup(options.scopes);
|
||||
}
|
||||
return this.authRequester(options.scopes);
|
||||
@@ -184,6 +168,7 @@ export class DefaultAuthConnector<AuthSession>
|
||||
const popupUrl = await this.buildUrl('/start', {
|
||||
scope,
|
||||
origin: window.location.origin,
|
||||
authFlow: 'popup',
|
||||
});
|
||||
|
||||
const payload = await showLoginPopup({
|
||||
@@ -197,6 +182,20 @@ export class DefaultAuthConnector<AuthSession>
|
||||
return await this.sessionTransform(payload);
|
||||
}
|
||||
|
||||
private async executeRedirect(scopes: Set<string>): Promise<AuthSession> {
|
||||
const scope = this.joinScopesFunc(scopes);
|
||||
const redirectUrl = await this.buildUrl('/start', {
|
||||
scope,
|
||||
origin: window.location.origin,
|
||||
redirectUrl: window.location.href,
|
||||
authFlow: 'redirect',
|
||||
});
|
||||
// redirect to auth api
|
||||
window.location.href = redirectUrl;
|
||||
// return a promise that never resolves
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
private async buildUrl(
|
||||
path: string,
|
||||
query?: { [key: string]: string | boolean | undefined },
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
export type CreateSessionOptions = {
|
||||
scopes: Set<string>;
|
||||
instantPopup?: boolean;
|
||||
redirectUrl?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,11 +24,7 @@ import Button from '@material-ui/core/Button';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import LoginRequestListItem from './LoginRequestListItem';
|
||||
import {
|
||||
useApi,
|
||||
oauthRequestApiRef,
|
||||
configApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useApi, oauthRequestApiRef } from '@backstage/core-plugin-api';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
export type OAuthRequestDialogClassKey =
|
||||
@@ -62,11 +58,10 @@ export function OAuthRequestDialog(_props: {}) {
|
||||
const classes = useStyles();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const oauthRequestApi = useApi(oauthRequestApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const usePopup = configApi.getOptionalBoolean('auth.usePopup') ?? true;
|
||||
const redirectMessage = usePopup
|
||||
? ''
|
||||
: 'This will trigger a http redirect to OAuth Login.';
|
||||
const redirectMessage =
|
||||
oauthRequestApi.authFlow() === 'popup'
|
||||
? ''
|
||||
: 'This will trigger a http redirect to OAuth Login.';
|
||||
|
||||
const requests = useObservable(
|
||||
useMemo(() => oauthRequestApi.authRequest$(), [oauthRequestApi]),
|
||||
|
||||
@@ -24,27 +24,25 @@ import {
|
||||
SignInProvider,
|
||||
SignInProviderConfig,
|
||||
} from './types';
|
||||
import { useApi, errorApiRef, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
|
||||
import { GridItem } from './styles';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
import { PROVIDER_STORAGE_KEY } from './providers';
|
||||
|
||||
const Component: ProviderComponent = ({ config, onSignInSuccess }) => {
|
||||
const { apiRef, title, message } = config as SignInProviderConfig;
|
||||
const { apiRef, title, message, id } = config as SignInProviderConfig;
|
||||
const authApi = useApi(apiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const usePopup = configApi.getOptionalBoolean('auth.usePopup') ?? true;
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
localStorage.setItem(PROVIDER_STORAGE_KEY, id);
|
||||
const identityResponse = await authApi.getBackstageIdentity({
|
||||
instantPopup: usePopup,
|
||||
instantPopup: true,
|
||||
});
|
||||
if (!identityResponse) {
|
||||
if (!usePopup) {
|
||||
return;
|
||||
}
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
throw new Error(
|
||||
`The ${title} provider is not configured to support sign-in`,
|
||||
);
|
||||
@@ -60,6 +58,7 @@ const Component: ProviderComponent = ({ config, onSignInSuccess }) => {
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
errorApi.post(new ForwardedError('Login failed', error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ import { guestProvider } from './guestProvider';
|
||||
import { customProvider } from './customProvider';
|
||||
import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy';
|
||||
|
||||
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
|
||||
export const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
|
||||
|
||||
export type SignInProviderType = {
|
||||
[key: string]: {
|
||||
@@ -134,6 +134,7 @@ export const useSignInProviders = (
|
||||
.loader(apiHolder, provider.config?.apiRef!)
|
||||
.then(result => {
|
||||
if (didCancel) {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
@@ -143,10 +144,10 @@ export const useSignInProviders = (
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
if (didCancel) {
|
||||
return;
|
||||
}
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
errorApi.post(error);
|
||||
setLoading(false);
|
||||
});
|
||||
@@ -172,8 +173,6 @@ export const useSignInProviders = (
|
||||
const { Component } = provider.components;
|
||||
|
||||
const handleSignInSuccess = (result: IdentityApi) => {
|
||||
localStorage.setItem(PROVIDER_STORAGE_KEY, provider.id);
|
||||
|
||||
handleWrappedResult(result);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ export type OAuthRequesterOptions<TOAuthResponse> = {
|
||||
* trigger() is called on an auth requests.
|
||||
*/
|
||||
onAuthRequest(scopes: Set<string>): Promise<TOAuthResponse>;
|
||||
|
||||
/**
|
||||
* The authentication flow type
|
||||
*/
|
||||
authFlow: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -119,6 +124,11 @@ export type OAuthRequestApi = {
|
||||
* AuthRequester calls will resolve to the value returned by the onAuthRequest call.
|
||||
*/
|
||||
authRequest$(): Observable<PendingOAuthRequest[]>;
|
||||
|
||||
/**
|
||||
* The authentication flow type
|
||||
*/
|
||||
authFlow(): string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,11 +54,6 @@ export type AuthProviderInfo = {
|
||||
* Icon for the auth provider.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
|
||||
/**
|
||||
* The provider id from the list of provider configured in the login page
|
||||
*/
|
||||
provider_id?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
safelyEncodeURIComponent,
|
||||
ensuresXRequestedWith,
|
||||
postMessageResponse,
|
||||
redirectMessageResponse,
|
||||
} from './authFlowHelpers';
|
||||
import { WebMessageResponse } from './types';
|
||||
|
||||
@@ -179,22 +178,6 @@ describe('oauth helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirectMessageResponse', () => {
|
||||
const redirectUrl = 'http://localhost:3000/catalog';
|
||||
it('should perform redirect', () => {
|
||||
const mockResponse = {
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
redirect: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
redirectMessageResponse(mockResponse, redirectUrl);
|
||||
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.end).not.toHaveBeenCalled();
|
||||
expect(mockResponse.setHeader).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensuresXRequestedWith', () => {
|
||||
it('should return false if no header present', () => {
|
||||
const mockRequest = {
|
||||
|
||||
@@ -69,14 +69,6 @@ export const postMessageResponse = (
|
||||
res.end(`<html><body><script>${script}</script></body></html>`);
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const redirectMessageResponse = (
|
||||
res: express.Response,
|
||||
redirectUrl: string,
|
||||
) => {
|
||||
res.redirect(redirectUrl);
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const ensuresXRequestedWith = (req: express.Request) => {
|
||||
const requiredHeader = req.header('X-Requested-With');
|
||||
|
||||
@@ -74,7 +74,6 @@ describe('OAuthAdapter', () => {
|
||||
providerId: 'test-provider',
|
||||
appOrigin: 'http://localhost:3000',
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
isPopupAuthenticationRequest: true,
|
||||
cookieConfigurer: mockCookieConfigurer,
|
||||
tokenIssuer: {
|
||||
issueToken: async () => 'my-id-token',
|
||||
@@ -174,10 +173,14 @@ describe('OAuthAdapter', () => {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
isOriginAllowed: () => false,
|
||||
isPopupAuthenticationRequest: false,
|
||||
});
|
||||
|
||||
const mockRequest = createEncodedQueryMockRequest(defaultState);
|
||||
const state = {
|
||||
...defaultState,
|
||||
redirectUrl: 'http://localhost:3000',
|
||||
authFlow: 'redirect',
|
||||
};
|
||||
const mockRequest = createEncodedQueryMockRequest(state);
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
|
||||
@@ -343,7 +346,6 @@ describe('OAuthAdapter', () => {
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
isPopupAuthenticationRequest: true,
|
||||
};
|
||||
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
@@ -365,7 +367,6 @@ describe('OAuthAdapter', () => {
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
isPopupAuthenticationRequest: true,
|
||||
};
|
||||
|
||||
const mockStartRequestWithOrigin = {
|
||||
@@ -505,7 +506,7 @@ describe('OAuthAdapter', () => {
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('executed a response redirect when isPopupAuthenticationRequest is false', async () => {
|
||||
it('executed a response redirect when authFlow query string is set to "redirect"', async () => {
|
||||
const handlers = {
|
||||
start: jest.fn(async (_req: { state: OAuthState }) => ({
|
||||
url: '/url',
|
||||
@@ -516,7 +517,6 @@ describe('OAuthAdapter', () => {
|
||||
};
|
||||
const configWithNoPopupEnabled = {
|
||||
...configOriginAllowed,
|
||||
isPopupAuthenticationRequest: false,
|
||||
};
|
||||
const oauthProvider = OAuthAdapter.fromConfig(
|
||||
configWithNoPopupEnabled,
|
||||
@@ -531,6 +531,7 @@ describe('OAuthAdapter', () => {
|
||||
...defaultState,
|
||||
origin: 'http://other.domain',
|
||||
redirectUrl: 'http://domain.org',
|
||||
authFlow: 'redirect',
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
@@ -540,7 +541,6 @@ describe('OAuthAdapter', () => {
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockRequest.get).not.toHaveBeenCalled();
|
||||
expect(mockResponse.end).not.toHaveBeenCalled();
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
import { defaultCookieConfigurer, readState, verifyNonce } from './helpers';
|
||||
import {
|
||||
postMessageResponse,
|
||||
redirectMessageResponse,
|
||||
ensuresXRequestedWith,
|
||||
WebMessageResponse,
|
||||
} from '../flow';
|
||||
@@ -61,7 +60,6 @@ export type OAuthAdapterOptions = {
|
||||
cookieConfigurer: CookieConfigurer;
|
||||
isOriginAllowed: (origin: string) => boolean;
|
||||
callbackUrl: string;
|
||||
isPopupAuthenticationRequest: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
@@ -74,8 +72,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
'providerId' | 'persistScopes' | 'callbackUrl' | 'redirectUrl'
|
||||
>,
|
||||
): OAuthAdapter {
|
||||
const { appUrl, baseUrl, isOriginAllowed, isPopupAuthenticationRequest } =
|
||||
config;
|
||||
const { appUrl, baseUrl, isOriginAllowed } = config;
|
||||
const { origin: appOrigin } = new URL(appUrl);
|
||||
|
||||
const cookieConfigurer = config.cookieConfigurer ?? defaultCookieConfigurer;
|
||||
@@ -86,7 +83,6 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
baseUrl,
|
||||
cookieConfigurer,
|
||||
isOriginAllowed,
|
||||
isPopupAuthenticationRequest: isPopupAuthenticationRequest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,6 +104,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
const env = req.query.env?.toString();
|
||||
const origin = req.query.origin?.toString();
|
||||
const redirectUrl = req.query.redirectUrl?.toString();
|
||||
const authFlow = req.query.authFlow?.toString();
|
||||
if (!env) {
|
||||
throw new InputError('No env provided in request query parameters');
|
||||
}
|
||||
@@ -118,7 +115,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
// set a nonce cookie before redirecting to oauth provider
|
||||
this.setNonceCookie(res, nonce, cookieConfig);
|
||||
|
||||
const state: OAuthState = { nonce, env, origin, redirectUrl };
|
||||
const state: OAuthState = { nonce, env, origin, redirectUrl, authFlow };
|
||||
|
||||
// If scopes are persisted then we pass them through the state so that we
|
||||
// can set the cookie on successful auth
|
||||
@@ -184,8 +181,8 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
response: { ...response, backstageIdentity: identity },
|
||||
};
|
||||
|
||||
if (!this.options.isPopupAuthenticationRequest) {
|
||||
return redirectMessageResponse(res, redirectUrl);
|
||||
if (state.authFlow === 'redirect') {
|
||||
res.redirect(redirectUrl);
|
||||
}
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, appOrigin, responseObj);
|
||||
|
||||
@@ -91,6 +91,7 @@ export type OAuthState = {
|
||||
origin?: string;
|
||||
scope?: string;
|
||||
redirectUrl?: string;
|
||||
authFlow?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -131,8 +131,6 @@ export type AuthProviderConfig = {
|
||||
* The function used to resolve cookie configuration based on the auth provider options.
|
||||
*/
|
||||
cookieConfigurer?: CookieConfigurer;
|
||||
|
||||
isPopupAuthenticationRequest: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -107,8 +107,6 @@ export async function createRouter(
|
||||
...providerFactories,
|
||||
};
|
||||
const providersConfig = config.getConfig('auth.providers');
|
||||
const isPopupAuthenticationRequest =
|
||||
config.getOptionalBoolean('auth.usePopup') ?? true;
|
||||
|
||||
const configuredProviders = providersConfig.keys();
|
||||
|
||||
@@ -126,7 +124,6 @@ export async function createRouter(
|
||||
baseUrl: authUrl,
|
||||
appUrl,
|
||||
isOriginAllowed,
|
||||
isPopupAuthenticationRequest: isPopupAuthenticationRequest,
|
||||
},
|
||||
config: providersConfig.getConfig(providerId),
|
||||
logger,
|
||||
|
||||
Reference in New Issue
Block a user