Updates to redirect implementation after initial PR review.

Signed-off-by: headphonejames <generalfuzz@gmail.com>
This commit is contained in:
headphonejames
2023-02-03 12:03:01 -08:00
parent 9a9170047b
commit ae4d826fb2
29 changed files with 107 additions and 178 deletions
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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'),
});
},
}),
+3 -3
View File
@@ -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,