core-app-api: replace cookie auth provider with new implementation in AppIdentityProxy

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-04-09 13:31:50 +02:00
parent 2b57eac4b4
commit d11767c861
7 changed files with 52 additions and 162 deletions
-1
View File
@@ -44,7 +44,6 @@
"dependencies": {
"@backstage/config": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-auth-react": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@types/prop-types": "^15.7.3",
@@ -18,7 +18,11 @@ import {
IdentityApi,
ProfileInfo,
BackstageUserIdentity,
ErrorApi,
DiscoveryApi,
FetchApi,
} from '@backstage/core-plugin-api';
import { startCookieAuthRefresh } from './startCookieAuthRefresh';
function mkError(thing: string) {
return new Error(
@@ -50,7 +54,8 @@ export class AppIdentityProxy implements IdentityApi {
private waitForTarget: Promise<CompatibilityIdentityApi>;
private resolveTarget: (api: CompatibilityIdentityApi) => void = () => {};
private signOutTargetUrl = '/';
#signOutCallback?: () => Promise<void>;
#cookieAuthSignOut?: () => void;
constructor() {
this.waitForTarget = new Promise<CompatibilityIdentityApi>(resolve => {
@@ -68,10 +73,6 @@ export class AppIdentityProxy implements IdentityApi {
this.resolveTarget(identityApi);
}
setSignOutCallback(signOutCallback: () => Promise<void>): void {
this.#signOutCallback = signOutCallback;
}
getUserId(): string {
if (!this.target) {
throw mkError('getUserId');
@@ -129,7 +130,35 @@ export class AppIdentityProxy implements IdentityApi {
async signOut(): Promise<void> {
await this.waitForTarget.then(target => target.signOut());
await this.#signOutCallback?.();
await this.#cookieAuthSignOut?.();
window.location.href = this.signOutTargetUrl;
}
enableCookieAuth(ctx: {
errorApi: ErrorApi;
fetchApi: FetchApi;
discoveryApi: DiscoveryApi;
}) {
if (this.#cookieAuthSignOut) {
return;
}
const stopRefresh = startCookieAuthRefresh(ctx);
this.#cookieAuthSignOut = async () => {
stopRefresh();
// It is fine if we do NOT worry yet about deleting cookies for OTHER backends like techdocs
const appBaseUrl = await ctx.discoveryApi.getBaseUrl('app');
try {
await ctx.fetchApi.fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, {
method: 'DELETE',
});
} catch {
// Ignore the error for those who use static serving of the frontend
}
};
}
}
@@ -1,99 +0,0 @@
/*
* Copyright 2024 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 React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
import { AppAuthProvider } from './AppAuthProvider';
const now = 1710316886171;
const tenMinutesInMilliseconds = 10 * 60 * 1000;
const tenMinutesFromNowInMilliseconds = now + tenMinutesInMilliseconds;
const expiresAt = new Date(tenMinutesFromNowInMilliseconds).toISOString();
jest.mock('@backstage/core-plugin-api', () => {
return {
...jest.requireActual('@backstage/core-plugin-api'),
useApp: jest.fn().mockReturnValue({
getComponents: () => ({ Progress: () => <div data-testid="progress" /> }),
}),
useApi: jest.fn(ref => {
if (ref === discoveryApiRef) {
return {
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'),
};
}
if (ref === fetchApiRef) {
return {
fetch: jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ expiresAt }),
}),
};
}
// componentsApiRef
return {
getComponent: jest
.fn()
.mockReturnValue(() => <div data-testid="progress" />),
};
}),
};
});
describe('AppAuthProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers({ now });
});
afterEach(() => {
jest.useRealTimers();
});
it('should render the children when app mode is undefined', async () => {
render(<AppAuthProvider>Test content</AppAuthProvider>);
await waitFor(() =>
expect(screen.getByText('Test content')).toBeInTheDocument(),
);
});
it('should render the children the app mode is public', async () => {
render(
<>
<meta name="backstage-app-mode" content="public" />
<AppAuthProvider>Test content</AppAuthProvider>
</>,
);
await waitFor(() =>
expect(screen.getByText('Test content')).toBeInTheDocument(),
);
});
it('should render the children wrapped in the CookieAuthRefreshProvider', async () => {
render(
<>
<meta name="backstage-app-mode" content="protected" />
<AppAuthProvider>Test content</AppAuthProvider>
</>,
);
await waitFor(() =>
expect(screen.getByText('Test content')).toBeInTheDocument(),
);
});
});
@@ -1,33 +0,0 @@
/*
* Copyright 2024 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 React, { ReactNode } from 'react';
import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
import { isProtectedApp } from './isProtectedApp';
export function AppAuthProvider(props: { children: ReactNode }): JSX.Element {
const { children } = props;
if (isProtectedApp()) {
return (
<CookieAuthRefreshProvider pluginId="app">
{children}
</CookieAuthRefreshProvider>
);
}
return <>{children}</>;
}
+13 -11
View File
@@ -44,6 +44,7 @@ import {
FeatureFlag,
fetchApiRef,
discoveryApiRef,
errorApiRef,
} from '@backstage/core-plugin-api';
import {
AppLanguageApi,
@@ -359,20 +360,21 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
const apis = this.getApiHolder();
this.appIdentityProxy.setSignOutCallback(async () => {
if (isProtectedApp()) {
const errorApi = apis.get(errorApiRef);
const fetchApi = apis.get(fetchApiRef);
const discoveryApi = apis.get(discoveryApiRef);
if (!fetchApi || !discoveryApi || !isProtectedApp()) return;
// It is fine if we do NOT worry yet about deleting cookies for OTHER backends like techdocs
const appBaseUrl = await discoveryApi.getBaseUrl('app');
try {
await fetchApi.fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, {
method: 'DELETE',
});
} catch {
// Ignore the error for those who use static serving of the frontend
if (!errorApi || !fetchApi || !discoveryApi) {
throw new Error(
'App is running in protected mode but missing required APIs',
);
}
});
this.appIdentityProxy.enableCookieAuth({
errorApi,
fetchApi,
discoveryApi,
});
}
return (
<ApiProvider apis={apis}>
+4 -11
View File
@@ -29,7 +29,6 @@ import { isReactRouterBeta } from './isReactRouterBeta';
import { RouteTracker } from '../routing/RouteTracker';
import { Route, Routes } from 'react-router-dom';
import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy';
import { AppAuthProvider } from './AppAuthProvider';
/**
* Get the app base path from the configured app baseUrl.
@@ -146,10 +145,7 @@ export function AppRouter(props: AppRouterProps) {
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
<Routes>
<Route
path={mountPath}
element={<AppAuthProvider>{props.children}</AppAuthProvider>}
/>
<Route path={mountPath} element={<>{props.children}</>} />
</Routes>
</RouterComponent>
);
@@ -158,7 +154,7 @@ export function AppRouter(props: AppRouterProps) {
return (
<RouterComponent basename={basePath}>
<RouteTracker routeObjects={routeObjects} />
<AppAuthProvider>{props.children}</AppAuthProvider>
{props.children}
</RouterComponent>
);
}
@@ -172,10 +168,7 @@ export function AppRouter(props: AppRouterProps) {
appIdentityProxy={appIdentityProxy}
>
<Routes>
<Route
path={mountPath}
element={<AppAuthProvider>{props.children}</AppAuthProvider>}
/>
<Route path={mountPath} element={<>{props.children}</>} />
</Routes>
</SignInPageWrapper>
</RouterComponent>
@@ -189,7 +182,7 @@ export function AppRouter(props: AppRouterProps) {
component={SignInPageComponent}
appIdentityProxy={appIdentityProxy}
>
<AppAuthProvider>{props.children}</AppAuthProvider>
{props.children}
</SignInPageWrapper>
</RouterComponent>
);
-1
View File
@@ -3834,7 +3834,6 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/plugin-auth-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@backstage/version-bridge": "workspace:^"