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:
@@ -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}</>;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user