From ffd71105a6e34481854d6926b5b81ed3f205e7b7 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 3 Apr 2024 13:03:53 +0200 Subject: [PATCH] refactor: apply review suggestions Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- .changeset/fifty-cameras-shake.md | 2 +- .changeset/nine-rabbits-exist.md | 2 +- .changeset/old-parents-try.md | 7 - docs/tutorials/enable-public-entry.md | 48 ++---- packages/app/src/App.tsx | 7 +- .../app/src/index-public-experimental.tsx | 4 +- .../httpAuth/httpAuthServiceFactory.ts | 52 +++--- .../httpRouter/httpRouterServiceFactory.ts | 2 +- .../src/auth/createLegacyAuthAdapters.ts | 4 - packages/backend-plugin-api/api-report.md | 4 +- .../services/definitions/HttpAuthService.ts | 10 +- .../src/next/services/MockHttpAuthService.ts | 4 - .../src/next/services/mockServices.ts | 1 - packages/cli/src/lib/bundler/bundle.ts | 6 +- packages/cli/src/lib/bundler/server.ts | 7 +- packages/core-app-api/package.json | 1 + .../core-app-api/src/app/AppManager.test.tsx | 2 +- packages/core-app-api/src/app/AppRouter.tsx | 15 +- packages/frontend-app-api/package.json | 1 + .../src/extensions/AppRoot.tsx | 5 +- .../app-backend/src/service/appPlugin.test.ts | 4 +- plugins/app-backend/src/service/router.ts | 30 ++-- plugins/auth-node/api-report.md | 2 + .../createCookieAuthRefreshMiddleware.test.ts | 7 +- .../createCookieAuthRefreshMiddleware.ts | 10 +- plugins/auth-react/api-report.md | 10 +- plugins/auth-react/package.json | 2 + .../src/components/AppMode/AppMode.test.tsx | 103 ++++++++++++ .../src/components/AppMode/AppMode.tsx | 48 ++++++ .../{RedirectToRoot => AppMode}/index.ts | 2 +- .../CompatAppProgress/CompatAppProgress.tsx | 40 +++++ .../index.ts | 2 +- .../CookieAuthRefreshProvider.test.tsx | 2 +- .../CookieAuthRefreshProvider.tsx | 6 +- .../CookieAuthRootRedirect.test.tsx} | 8 +- .../CookieAuthRootRedirect.tsx} | 2 +- .../CookieAuthRootRedirect/index.ts | 17 ++ .../ExperimentalAppProtection.test.tsx | 150 ------------------ .../ExperimentalAppProtection.tsx | 63 -------- plugins/auth-react/src/components/index.ts | 4 +- .../useCookieAuthRefresh.test.tsx | 2 +- .../useCookieAuthRefresh.tsx | 4 +- yarn.lock | 18 ++- 43 files changed, 351 insertions(+), 369 deletions(-) delete mode 100644 .changeset/old-parents-try.md create mode 100644 plugins/auth-react/src/components/AppMode/AppMode.test.tsx create mode 100644 plugins/auth-react/src/components/AppMode/AppMode.tsx rename plugins/auth-react/src/components/{RedirectToRoot => AppMode}/index.ts (92%) create mode 100644 plugins/auth-react/src/components/CompatAppProgress/CompatAppProgress.tsx rename plugins/auth-react/src/components/{ExperimentalAppProtection => CompatAppProgress}/index.ts (89%) rename plugins/auth-react/src/components/{RedirectToRoot/RedirectToRoot.test.tsx => CookieAuthRootRedirect/CookieAuthRootRedirect.test.tsx} (91%) rename plugins/auth-react/src/components/{RedirectToRoot/RedirectToRoot.tsx => CookieAuthRootRedirect/CookieAuthRootRedirect.tsx} (97%) create mode 100644 plugins/auth-react/src/components/CookieAuthRootRedirect/index.ts delete mode 100644 plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.test.tsx delete mode 100644 plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.tsx diff --git a/.changeset/fifty-cameras-shake.md b/.changeset/fifty-cameras-shake.md index 49515502a3..b87d4e92c2 100644 --- a/.changeset/fifty-cameras-shake.md +++ b/.changeset/fifty-cameras-shake.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-react': patch --- -Update the default cookie base path and create a experimental redirect to root and protected app components, the components should be used only when the public entry is enabled. +Update the default cookie base path and create a experimental redirect to root and app mode components, the components authenticate and keep a cookie refresh loop on the client side. diff --git a/.changeset/nine-rabbits-exist.md b/.changeset/nine-rabbits-exist.md index 9d14a9f417..a26f1f59c3 100644 --- a/.changeset/nine-rabbits-exist.md +++ b/.changeset/nine-rabbits-exist.md @@ -2,4 +2,4 @@ '@backstage/core-app-api': patch --- -Clear the app auth cookie when the when the user sign out. +Clear the app auth cookie after a sign out. diff --git a/.changeset/old-parents-try.md b/.changeset/old-parents-try.md deleted file mode 100644 index 866cdaee21..0000000000 --- a/.changeset/old-parents-try.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-plugin-api': patch -'@backstage/backend-test-utils': patch -'@backstage/backend-common': patch ---- - -Add a `removeUserCookie` to the http auth service interface. diff --git a/docs/tutorials/enable-public-entry.md b/docs/tutorials/enable-public-entry.md index a25d1a8704..79c2d33e68 100644 --- a/docs/tutorials/enable-public-entry.md +++ b/docs/tutorials/enable-public-entry.md @@ -1,6 +1,6 @@ --- id: enable-public-entry -title: Enabling public entry point +title: Enabling a public entry point description: A guide for how to experiment with public and protected Backstage app bundles --- @@ -21,11 +21,14 @@ With that, Backstage's cli and backend will detect public entry point and serve - The tutorial will only work for those using backstage-cli to build and serve their Backstage app. -## Trying out this feature +## Step-by-step -1. Add a `index-public-experimental.tsx` to your app `src` folder; +1. Create a `index-public-experimental.tsx` in your app `src` folder; + :::note + The filename is a convention, so it is not currently configurable. + ::: -2. Prepare an unauthenticated version of your application. This will be the public entry point for your site: +2. This file is the public entry point for your application, and it should only contain what unauthenticated users should see: ```tsx title="in packages/app/src/index-public-experimental.tsx" import React from 'react'; @@ -42,7 +45,7 @@ With that, Backstage's cli and backend will detect public entry point and serve createApiFactory, discoveryApiRef, } from '@backstage/core-plugin-api'; - import { RedirectToRoot } from '@backstage/plugin-auth-react'; + import { CookieAuthRootRedirect } from '@backstage/plugin-auth-react'; import { providers } from '../src/identityProviders'; import { AuthProxyDiscoveryApi } from '../src/AuthProxyDiscoveryApi'; @@ -76,7 +79,7 @@ With that, Backstage's cli and backend will detect public entry point and serve {/* This is a special component that does the magic to redirect users to access the home page of your authenticated application version */} - + , ); @@ -84,35 +87,12 @@ With that, Backstage's cli and backend will detect public entry point and serve ReactDOM.createRoot(document.getElementById('root')!).render(); ``` -3) Then ensure your main entry is also protected by the experimental app provider, as shown in the following example: +3. The frontend will handle cookie refreshing automatically, so you don't have to worry about it; -```tsx title="in packages/app/src/index-public-experimental.tsx" -// ... -export default app.createRoot( - <> - - - - {/* For now, this is just a temporary solution to ensure the user remains logged in properly and has access to the main app content. */} - - - {routes} - - - , -); -``` +4. You're now ready to build and serve your frontend and backend as usual; -4. You're now ready to build your front-end app: +5. After that, access your backend index endpoint to see the public app being served (note that only a minimal app is being served). -```sh -yarn workspace example-app build -``` +6. Log in and you will be redirected to the main app home page (check the protected bundle being served from the app-backend after the redirect). -5. And also serve it from your Backstage app backend: - -```sh -yarn start-backend:next -``` - -6. Finally, access http://localhost:7007 to see the public app being served (note that only a minimal app is being served). Log in and you will be redirected to the main app home page (check the protected bundle being served from the app-backend after the redirect). +That's it! diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 2639f2add8..3d8bd45e5a 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -108,7 +108,6 @@ import { DevToolsPage } from '@backstage/plugin-devtools'; import { customDevToolsPage } from './components/devtools/CustomDevToolsPage'; import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities'; import { NotificationsPage } from '@backstage/plugin-notifications'; -import { ExperimentalAppProtection } from '@backstage/plugin-auth-react'; const app = createApp({ apis, @@ -283,10 +282,8 @@ export default app.createRoot( - - - {routes} - + + {routes} , ); diff --git a/packages/app/src/index-public-experimental.tsx b/packages/app/src/index-public-experimental.tsx index 59f89c1157..3de164fb64 100644 --- a/packages/app/src/index-public-experimental.tsx +++ b/packages/app/src/index-public-experimental.tsx @@ -21,7 +21,7 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; -import { RedirectToRoot } from '@backstage/plugin-auth-react'; +import { CookieAuthRootRedirect } from '@backstage/plugin-auth-react'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { providers } from '../src/identityProviders'; @@ -59,7 +59,7 @@ const App = app.createRoot( - + , ); diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index 807e31513e..fe927eaa5d 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -181,6 +181,13 @@ class DefaultHttpAuthService implements HttpAuthService { let credentials: BackstageCredentials; if (options?.credentials) { + if (this.#auth.isPrincipal(options.credentials, 'none')) { + res.clearCookie( + BACKSTAGE_AUTH_COOKIE, + await this.#getCookieOptions(res.req), + ); + return { expiresAt: new Date() }; + } if (!this.#auth.isPrincipal(options.credentials, 'user')) { throw new AuthenticationError( 'Refused to issue cookie for non-user principal', @@ -196,16 +203,6 @@ class DefaultHttpAuthService implements HttpAuthService { return { expiresAt: existingExpiresAt }; } - const originHeader = res.req.headers.origin; - const origin = - !originHeader || originHeader === 'null' ? undefined : originHeader; - - // https://backstage.example.com/api/catalog - const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl( - this.#pluginId, - ); - const externalBaseUrl = new URL(origin ?? externalBaseUrlStr); - const { token, expiresAt } = await this.#auth.getLimitedUserToken( credentials, ); @@ -213,20 +210,41 @@ class DefaultHttpAuthService implements HttpAuthService { throw new Error('User credentials is unexpectedly missing token'); } + res.cookie(BACKSTAGE_AUTH_COOKIE, token, { + ...(await this.#getCookieOptions(res.req)), + expires: expiresAt, + }); + + return { expiresAt }; + } + + async #getCookieOptions(req: Request): Promise<{ + domain: string; + httpOnly: true; + secure: boolean; + priority: 'high'; + sameSite: 'none' | 'lax'; + }> { + const originHeader = req.headers.origin; + const origin = + !originHeader || originHeader === 'null' ? undefined : originHeader; + + const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl( + this.#pluginId, + ); + const externalBaseUrl = new URL(origin ?? externalBaseUrlStr); + const secure = externalBaseUrl.protocol === 'https:' || externalBaseUrl.hostname === 'localhost'; - res.cookie(BACKSTAGE_AUTH_COOKIE, token, { + return { domain: externalBaseUrl.hostname, httpOnly: true, - expires: expiresAt, secure, priority: 'high', sameSite: secure ? 'none' : 'lax', - }); - - return { expiresAt }; + }; } async #existingCookieExpiration(req: Request): Promise { @@ -254,10 +272,6 @@ class DefaultHttpAuthService implements HttpAuthService { throw error; } } - - removeUserCookie(res: Response): void { - res.clearCookie(BACKSTAGE_AUTH_COOKIE); - } } /** @public */ diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 5f32eb8d00..fe8cd0298b 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -93,7 +93,7 @@ export const httpRouterServiceFactory = createServiceFactory( ) { // Only add the cookie refresh middleware once hasRegistedCookieAuthRefreshMiddleware = true; - router.use(createCookieAuthRefreshMiddleware({ httpAuth })); + router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth })); } }, }; diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index 7718f74ee8..0ba23661ef 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -259,10 +259,6 @@ class HttpAuthCompat implements HttpAuthService { async issueUserCookie(_res: Response): Promise<{ expiresAt: Date }> { return { expiresAt: new Date(Date.now() + 3600_000) }; } - - removeUserCookie(res: Response): void { - res.clearCookie('backstage-auth'); - } } export class UserInfoCompat implements UserInfoService { diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index d91f4a2450..3d20b789c1 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -325,13 +325,11 @@ export interface HttpAuthService { issueUserCookie( res: Response_2, options?: { - credentials?: BackstageCredentials; + credentials?: BackstageCredentials; }, ): Promise<{ expiresAt: Date; }>; - // (undocumented) - removeUserCookie(res: Response_2): void; } // @public (undocumented) diff --git a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts index eb6ba944f5..5d1a2090d4 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts @@ -15,11 +15,7 @@ */ import { Request, Response } from 'express'; -import { - BackstageCredentials, - BackstagePrincipalTypes, - BackstageUserPrincipal, -} from './AuthService'; +import { BackstageCredentials, BackstagePrincipalTypes } from './AuthService'; /** @public */ export interface HttpAuthService { @@ -34,9 +30,7 @@ export interface HttpAuthService { issueUserCookie( res: Response, options?: { - credentials?: BackstageCredentials; + credentials?: BackstageCredentials; }, ): Promise<{ expiresAt: Date }>; - - removeUserCookie(res: Response): void; } diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts index 8934def5f0..9a69620473 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts @@ -140,8 +140,4 @@ export class MockHttpAuthService implements HttpAuthService { return { expiresAt: new Date(Date.now() + 3600_000) }; } - - removeUserCookie(res: Response): void { - res.clearCookie(MOCK_AUTH_COOKIE); - } } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 00f1aa901a..a858bf7dbf 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -275,7 +275,6 @@ export namespace mockServices { export const mock = simpleMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), issueUserCookie: jest.fn(), - removeUserCookie: jest.fn(), })); } diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 06249774fd..b99891d70d 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -70,11 +70,7 @@ export async function buildBundle(options: BuildOptions) { `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, ), ); - configs.push( - await createConfig(publicPaths, { - ...commonConfigOptions, - }), - ); + configs.push(await createConfig(publicPaths, commonConfigOptions)); } const isCi = yn(process.env.CI, { default: false }); diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 2fe72cd471..2b7311a34c 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -216,12 +216,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be ); } const compiler = publicPaths - ? webpack([ - config, - await createConfig(publicPaths, { - ...commonConfigOptions, - }), - ]) + ? webpack([config, await createConfig(publicPaths, commonConfigOptions)]) : webpack(config); webpackServer = new WebpackDevServer( diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index bb3566062c..dcdb5a78f1 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -44,6 +44,7 @@ "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", diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 9f81d0b62e..9300e81567 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -864,7 +864,7 @@ describe('Integration Test', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7007/app/.backstage/v1-cookie', + 'http://localhost:7007/app/.backstage/auth/v1/cookie', { method: 'DELETE' }, ), ); diff --git a/packages/core-app-api/src/app/AppRouter.tsx b/packages/core-app-api/src/app/AppRouter.tsx index 7481f6662a..c4bedea4a7 100644 --- a/packages/core-app-api/src/app/AppRouter.tsx +++ b/packages/core-app-api/src/app/AppRouter.tsx @@ -29,6 +29,7 @@ import { isReactRouterBeta } from './isReactRouterBeta'; import { RouteTracker } from '../routing/RouteTracker'; import { Route, Routes } from 'react-router-dom'; import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy'; +import { AppMode } from '@backstage/plugin-auth-react'; /** * Get the app base path from the configured app baseUrl. @@ -145,7 +146,10 @@ export function AppRouter(props: AppRouterProps) { - {props.children}} /> + {props.children}} + /> ); @@ -154,7 +158,7 @@ export function AppRouter(props: AppRouterProps) { return ( - {props.children} + {props.children} ); } @@ -168,7 +172,10 @@ export function AppRouter(props: AppRouterProps) { appIdentityProxy={appIdentityProxy} > - {props.children}} /> + {props.children}} + /> @@ -182,7 +189,7 @@ export function AppRouter(props: AppRouterProps) { component={SignInPageComponent} appIdentityProxy={appIdentityProxy} > - {props.children} + {props.children} ); diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index fb5e982058..097f5a3611 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -38,6 +38,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-auth-react": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index f3a962dd10..a25953ed49 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -42,6 +42,7 @@ import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations import { BrowserRouter } from 'react-router-dom'; import { RouteTracker } from '../routing/RouteTracker'; import { getBasePath } from '../routing/getBasePath'; +import { AppMode } from '@backstage/plugin-auth-react'; export const AppRoot = createExtension({ namespace: 'app', @@ -190,7 +191,7 @@ export function AppRouter(props: AppRouterProps) { return ( - {children} + {children} ); } @@ -202,7 +203,7 @@ export function AppRouter(props: AppRouterProps) { component={SignInPageComponent} appIdentityProxy={appIdentityProxy} > - {children} + {children} ); diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index 647697c204..1507e43b46 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -62,9 +62,9 @@ describe('appPlugin', () => { fetch(`http://localhost:${server.port()}/api/app/derp.html`).then(res => res.text(), ), - ).resolves.toBe('winning'); + ).resolves.toMatch('winning'); await expect( fetch(`http://localhost:${server.port()}`).then(res => res.text()), - ).resolves.toBe('winning'); + ).resolves.toMatch('winning'); }); }); diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index 70466da001..531fad8179 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -168,12 +168,13 @@ export async function createRouter( const publicDistDir = resolvePath(appDistDir, 'public'); - const enablePublicEntryPoint = await fs.pathExists(publicDistDir); + const enablePublicEntryPoint = + (await fs.pathExists(publicDistDir)) && auth && httpAuth; if (enablePublicEntryPoint && auth && httpAuth) { const publicRouter = Router(); - publicRouter.use(createCookieAuthRefreshMiddleware({ httpAuth })); + publicRouter.use(createCookieAuthRefreshMiddleware({ auth, httpAuth })); publicRouter.use(async (req, _res, next) => { const credentials = await httpAuth.credentials(req, { @@ -214,6 +215,7 @@ export async function createRouter( publicRouter.use( await createEntryPointRouter({ + appMode: 'public', logger: logger.child({ entry: 'public' }), rootDir: publicDistDir, assetStore: assetStore?.withNamespace('public'), @@ -226,6 +228,7 @@ export async function createRouter( router.use( await createEntryPointRouter({ + appMode: enablePublicEntryPoint ? 'protected' : 'public', logger: logger.child({ entry: 'main' }), rootDir: appDistDir, assetStore, @@ -243,6 +246,7 @@ async function createEntryPointRouter({ rootDir, assetStore, staticFallbackHandler, + appMode, appConfigs, injectedConfigPath, }: { @@ -250,6 +254,7 @@ async function createEntryPointRouter({ rootDir: string; assetStore?: StaticAssetsStore; staticFallbackHandler?: express.Handler; + appMode: 'public' | 'protected'; appConfigs?: AppConfig[]; injectedConfigPath?: string; }) { @@ -303,14 +308,21 @@ async function createEntryPointRouter({ }, }), ); + + const indexHtmlContent = Buffer.from( + (await fs.readFile(resolvePath(rootDir, 'index.html'), 'utf8')).replace( + //, + ``, + ), + 'utf8', + ); + router.get('/*', (_req, res) => { - res.sendFile(resolvePath(rootDir, 'index.html'), { - headers: { - // The Cache-Control header instructs the browser to not cache the index.html since it might - // link to static assets from recently deployed versions. - 'cache-control': CACHE_CONTROL_NO_CACHE, - }, - }); + // The Cache-Control header instructs the browser to not cache the index.html since it might + // link to static assets from recently deployed versions. + res.setHeader('Cache-Control', CACHE_CONTROL_NO_CACHE); + res.setHeader('Content-Type', 'text/html;charset=utf-8'); + res.send(indexHtmlContent); }); return router; diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index 4cb9db70f3..f0696ecd9f 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackstageIdentityResponse as BackstageIdentityResponse_2 } from '@backstage/plugin-auth-node'; import { BackstageSignInResult as BackstageSignInResult_2 } from '@backstage/plugin-auth-node'; import { Config } from '@backstage/config'; @@ -152,6 +153,7 @@ export type CookieConfigurer = (ctx: { // @public export function createCookieAuthRefreshMiddleware(options: { + auth: AuthService; httpAuth: HttpAuthService; }): Router; diff --git a/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.test.ts b/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.test.ts index 65426b4ca5..aa5f1a995a 100644 --- a/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.test.ts +++ b/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.test.ts @@ -23,8 +23,9 @@ describe('createCookieAuthRefreshMiddleware', () => { let app: express.Express; beforeAll(async () => { + const auth = mockServices.auth(); const httpAuth = mockServices.httpAuth(); - const router = createCookieAuthRefreshMiddleware({ httpAuth }); + const router = createCookieAuthRefreshMiddleware({ auth, httpAuth }); app = express().use(router); }); @@ -33,7 +34,7 @@ describe('createCookieAuthRefreshMiddleware', () => { }); it('should issue the user cookie', async () => { - const response = await request(app).get('/.backstage/v1-cookie'); + const response = await request(app).get('/.backstage/auth/v1/cookie'); expect(response.status).toBe(200); expect(response.header['set-cookie'][0]).toMatch( `backstage-auth=${mockCredentials.limitedUser.token()}`, @@ -41,7 +42,7 @@ describe('createCookieAuthRefreshMiddleware', () => { }); it('should remove the user cookie', async () => { - const response = await request(app).delete('/.backstage/v1-cookie'); + const response = await request(app).delete('/.backstage/auth/v1/cookie'); expect(response.status).toBe(200); expect(response.header['set-cookie'][0]).toMatch('backstage-auth='); }); diff --git a/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.ts b/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.ts index efb58345a1..939fa9197d 100644 --- a/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.ts +++ b/plugins/auth-node/src/identity/createCookieAuthRefreshMiddleware.ts @@ -14,19 +14,20 @@ * limitations under the License. */ -import { HttpAuthService } from '@backstage/backend-plugin-api'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; -const WELL_KNOWN_COOKIE_PATH_V1 = '/.backstage/v1-cookie'; +const WELL_KNOWN_COOKIE_PATH_V1 = '/.backstage/auth/v1/cookie'; /** * @public * Creates a middleware that can be used to refresh the cookie for the user. */ export function createCookieAuthRefreshMiddleware(options: { + auth: AuthService; httpAuth: HttpAuthService; }) { - const { httpAuth } = options; + const { auth, httpAuth } = options; const router = Router(); // Endpoint that sets the cookie for the user @@ -37,7 +38,8 @@ export function createCookieAuthRefreshMiddleware(options: { // Endpoint that removes the cookie for the user router.delete(WELL_KNOWN_COOKIE_PATH_V1, async (_, res) => { - httpAuth.removeUserCookie(res); + const credentials = await auth.getNoneCredentials(); + await httpAuth.issueUserCookie(res, { credentials }); res.send(200); }); diff --git a/plugins/auth-react/api-report.md b/plugins/auth-react/api-report.md index d6b7d2e17f..ef8ae58267 100644 --- a/plugins/auth-react/api-report.md +++ b/plugins/auth-react/api-report.md @@ -6,6 +6,9 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; +// @public +export function AppMode(props: { children: ReactNode }): JSX.Element; + // @public export function CookieAuthRefreshProvider( props: CookieAuthRefreshProviderProps, @@ -19,12 +22,7 @@ export type CookieAuthRefreshProviderProps = { }; // @public -export function ExperimentalAppProtection(props: { - children: ReactNode; -}): JSX.Element; - -// @public -export function RedirectToRoot(): React_2.JSX.Element | null; +export function CookieAuthRootRedirect(): React_2.JSX.Element | null; // @public export function useCookieAuthRefresh(options: { diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 9de3aade21..e5eb7db192 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -35,6 +35,8 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/version-bridge": "workspace:^", "@material-ui/core": "^4.9.13", "@react-hookz/web": "^24.0.0", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0" diff --git a/plugins/auth-react/src/components/AppMode/AppMode.test.tsx b/plugins/auth-react/src/components/AppMode/AppMode.test.tsx new file mode 100644 index 0000000000..71f6ce9434 --- /dev/null +++ b/plugins/auth-react/src/components/AppMode/AppMode.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 { AppMode } from './AppMode'; +import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; +import { componentsApiRef } from '@backstage/frontend-plugin-api'; + +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: () =>
}), + }), + 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 }), + }), + }; + } + + if (ref === componentsApiRef) { + return { + getComponent: jest + .fn() + .mockReturnValue(() =>
), + }; + } + + throw new Error(`Attempted to use an unmocked API reference: ${ref}`); + }), + }; +}); + +describe('AppMode', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ now }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should render the children when app mode is undefined', async () => { + render(Test content); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + }); + + it('should render the children the app mode is public', async () => { + render( + <> + + Test content + , + ); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + }); + + it('should render the children wrapped in the CookieAuthRefreshProvider', async () => { + render( + <> + + Test content + , + ); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/auth-react/src/components/AppMode/AppMode.tsx b/plugins/auth-react/src/components/AppMode/AppMode.tsx new file mode 100644 index 0000000000..af99bcb23b --- /dev/null +++ b/plugins/auth-react/src/components/AppMode/AppMode.tsx @@ -0,0 +1,48 @@ +/* + * 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, useEffect, useState } from 'react'; +import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; +import { CompatAppProgress } from '../CompatAppProgress'; + +/** + * @public + * A provider that will protect the app when running in protected experimental mode. + */ +export function AppMode(props: { children: ReactNode }): JSX.Element { + const { children } = props; + + const [appMode, setAppMode] = useState(null); + + useEffect(() => { + const element = document.querySelector('meta[name="backstage-app-mode"]'); + setAppMode(element?.getAttribute('content') ?? 'public'); + }, [setAppMode]); + + if (!appMode) { + return ; + } + + if (appMode === 'protected') { + return ( + + {children} + + ); + } + + return <>{children}; +} diff --git a/plugins/auth-react/src/components/RedirectToRoot/index.ts b/plugins/auth-react/src/components/AppMode/index.ts similarity index 92% rename from plugins/auth-react/src/components/RedirectToRoot/index.ts rename to plugins/auth-react/src/components/AppMode/index.ts index 1e9e211adc..11d64b282a 100644 --- a/plugins/auth-react/src/components/RedirectToRoot/index.ts +++ b/plugins/auth-react/src/components/AppMode/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { RedirectToRoot } from './RedirectToRoot'; +export { AppMode } from './AppMode'; diff --git a/plugins/auth-react/src/components/CompatAppProgress/CompatAppProgress.tsx b/plugins/auth-react/src/components/CompatAppProgress/CompatAppProgress.tsx new file mode 100644 index 0000000000..e624d46304 --- /dev/null +++ b/plugins/auth-react/src/components/CompatAppProgress/CompatAppProgress.tsx @@ -0,0 +1,40 @@ +/* + * 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 { useApp } from '@backstage/core-plugin-api'; +import { useVersionedContext } from '@backstage/version-bridge'; + +import { + coreComponentRefs, + useComponentRef, +} from '@backstage/frontend-plugin-api'; + +function LegacyAppProgress() { + const app = useApp(); + const { Progress } = app.getComponents(); + return ; +} + +function NewAppProgress() { + const Progress = useComponentRef(coreComponentRefs.progress); + return ; +} + +export function CompatAppProgress() { + const isInNewApp = !useVersionedContext<{ 1: unknown }>('app-context'); + return isInNewApp ? : ; +} diff --git a/plugins/auth-react/src/components/ExperimentalAppProtection/index.ts b/plugins/auth-react/src/components/CompatAppProgress/index.ts similarity index 89% rename from plugins/auth-react/src/components/ExperimentalAppProtection/index.ts rename to plugins/auth-react/src/components/CompatAppProgress/index.ts index c29b1e438d..22fe51dd85 100644 --- a/plugins/auth-react/src/components/ExperimentalAppProtection/index.ts +++ b/plugins/auth-react/src/components/CompatAppProgress/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { ExperimentalAppProtection } from './ExperimentalAppProtection'; +export { CompatAppProgress } from './CompatAppProgress'; diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx index ab88361773..77710dc7a4 100644 --- a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx @@ -119,7 +119,7 @@ describe('CookieAuthRefreshProvider', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7000/techdocs/api/.backstage/v1-cookie', + 'http://localhost:7000/techdocs/api/.backstage/auth/v1/cookie', { credentials: 'include' }, ), ); diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx index 7919207692..18dd3c88a0 100644 --- a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx @@ -16,9 +16,9 @@ import React, { ReactNode } from 'react'; import { ErrorPanel } from '@backstage/core-components'; -import { useApp } from '@backstage/core-plugin-api'; import { Button } from '@material-ui/core'; import { useCookieAuthRefresh } from '../../hooks'; +import { CompatAppProgress } from '../CompatAppProgress/CompatAppProgress'; /** * @public @@ -41,13 +41,11 @@ export function CookieAuthRefreshProvider( props: CookieAuthRefreshProviderProps, ): JSX.Element { const { children, ...options } = props; - const app = useApp(); - const { Progress } = app.getComponents(); const result = useCookieAuthRefresh(options); if (result.status === 'loading') { - return ; + return ; } if (result.status === 'error') { diff --git a/plugins/auth-react/src/components/RedirectToRoot/RedirectToRoot.test.tsx b/plugins/auth-react/src/components/CookieAuthRootRedirect/CookieAuthRootRedirect.test.tsx similarity index 91% rename from plugins/auth-react/src/components/RedirectToRoot/RedirectToRoot.test.tsx rename to plugins/auth-react/src/components/CookieAuthRootRedirect/CookieAuthRootRedirect.test.tsx index d1171a4f81..109c65d4ed 100644 --- a/plugins/auth-react/src/components/RedirectToRoot/RedirectToRoot.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRootRedirect/CookieAuthRootRedirect.test.tsx @@ -18,9 +18,9 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; import { identityApiRef } from '@backstage/core-plugin-api'; -import { RedirectToRoot } from './RedirectToRoot'; +import { CookieAuthRootRedirect } from './CookieAuthRootRedirect'; -describe('RedirectToRoot', () => { +describe('CookieAuthRootRedirect', () => { const identityApiMock = { getCredentials: jest.fn() }; beforeEach(() => { @@ -32,7 +32,7 @@ describe('RedirectToRoot', () => { await renderInTestApp( - + , ); @@ -50,7 +50,7 @@ describe('RedirectToRoot', () => { await renderInTestApp( - + , ); diff --git a/plugins/auth-react/src/components/RedirectToRoot/RedirectToRoot.tsx b/plugins/auth-react/src/components/CookieAuthRootRedirect/CookieAuthRootRedirect.tsx similarity index 97% rename from plugins/auth-react/src/components/RedirectToRoot/RedirectToRoot.tsx rename to plugins/auth-react/src/components/CookieAuthRootRedirect/CookieAuthRootRedirect.tsx index 86b40b44e1..15a0f7a801 100644 --- a/plugins/auth-react/src/components/RedirectToRoot/RedirectToRoot.tsx +++ b/plugins/auth-react/src/components/CookieAuthRootRedirect/CookieAuthRootRedirect.tsx @@ -22,7 +22,7 @@ import { useAsync, useMountEffect } from '@react-hookz/web'; * @public * A component that redirects to the root of the app after a successful sign-in. */ -export function RedirectToRoot() { +export function CookieAuthRootRedirect() { const identityApi = useApi(identityApiRef); const [state, actions] = useAsync(async () => { diff --git a/plugins/auth-react/src/components/CookieAuthRootRedirect/index.ts b/plugins/auth-react/src/components/CookieAuthRootRedirect/index.ts new file mode 100644 index 0000000000..3e0bc26084 --- /dev/null +++ b/plugins/auth-react/src/components/CookieAuthRootRedirect/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { CookieAuthRootRedirect } from './CookieAuthRootRedirect'; diff --git a/plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.test.tsx b/plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.test.tsx deleted file mode 100644 index 2884400ea9..0000000000 --- a/plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.test.tsx +++ /dev/null @@ -1,150 +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 { screen, waitFor } from '@testing-library/react'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { - MockConfigApi, - TestApiProvider, - renderInTestApp, - setupRequestMockHandlers, -} from '@backstage/test-utils'; -import { ExperimentalAppProtection } from './ExperimentalAppProtection'; -import { - configApiRef, - discoveryApiRef, - fetchApiRef, -} from '@backstage/core-plugin-api'; - -describe('ExperimentalAppProtection', () => { - const worker = setupServer(); - setupRequestMockHandlers(worker); - - const configApiMock = new MockConfigApi({ - backend: { - baseUrl: 'http://localhost:7000', - }, - }); - - const fetchApiMock = { - fetch: jest.fn(), - }; - - const discoveryApiMock = { - getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'), - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should render the progress component while loading', async () => { - fetchApiMock.fetch.mockReturnValueOnce(new Promise(() => {})); - await renderInTestApp( - - Test content - , - ); - expect(screen.getByTestId('progress')).toBeVisible(); - expect(screen.queryByText('Test Content')).not.toBeInTheDocument(); - }); - - it('should render the children even when there is an error', async () => { - const error = new Error('Failed to fetch'); - fetchApiMock.fetch.mockRejectedValueOnce(error); - await renderInTestApp( - - Test content - , - ); - await waitFor(() => - expect(screen.getByText('Test content')).toBeInTheDocument(), - ); - expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); - expect(screen.queryByText('Failed to fetch')).not.toBeInTheDocument(); - }); - - it('should render the children even when the public index is not available', async () => { - fetchApiMock.fetch.mockResolvedValueOnce({ ok: false }); - await renderInTestApp( - - Test content - , - ); - await waitFor(() => - expect(screen.getByText('Test content')).toBeInTheDocument(), - ); - expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); - }); - - it('should render the children also when the public index is available', async () => { - fetchApiMock.fetch.mockResolvedValueOnce({ ok: true }); - await renderInTestApp( - Test content, - ); - await waitFor(() => - expect(screen.getByText('Test content')).toBeInTheDocument(), - ); - expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); - }); - - it('should render the children wrapped in the CookieAuthRefreshProvider', async () => { - worker.use( - rest.get('http://localhost:7000/public/index.html', (_, res, ctx) => { - return res(ctx.status(200)); - }), - rest.get( - 'http://localhost:7000/app/.backstage/v1-cookie', - (_, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ expiresAt: Date.now() + 10 * 60 * 1000 }), - ); - }, - ), - ); - await renderInTestApp( - - Test content - , - ); - await waitFor(() => - expect(screen.getByText('Test content')).toBeInTheDocument(), - ); - }); -}); diff --git a/plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.tsx b/plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.tsx deleted file mode 100644 index f76b287d97..0000000000 --- a/plugins/auth-react/src/components/ExperimentalAppProtection/ExperimentalAppProtection.tsx +++ /dev/null @@ -1,63 +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 { - configApiRef, - fetchApiRef, - useApi, - useApp, -} from '@backstage/core-plugin-api'; -import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; -import { useAsync, useMountEffect } from '@react-hookz/web'; - -/** - * @public - * A provider that will protect the app when running in public experimental mode. - */ -export function ExperimentalAppProtection(props: { - children: ReactNode; -}): JSX.Element { - const { children } = props; - const fetchApi = useApi(fetchApiRef); - const configApi = useApi(configApiRef); - const Components = useApp().getComponents(); - - const [state, actions] = useAsync(async () => { - const baseUrl = configApi.getString('backend.baseUrl'); - const response = await fetchApi.fetch(`${baseUrl}/public/index.html`); - return response.ok; - }); - - useMountEffect(actions.execute); - - if (state.status === 'not-executed' || state.status === 'loading') { - return ; - } - - // Request failed, or the public index is not available - if (state.status === 'error' || !state.result) { - return <>{children}; - } - - // The public index is available - // That means the app is running in public experimental mode - return ( - - {children} - - ); -} diff --git a/plugins/auth-react/src/components/index.ts b/plugins/auth-react/src/components/index.ts index a85fad95ae..03345df968 100644 --- a/plugins/auth-react/src/components/index.ts +++ b/plugins/auth-react/src/components/index.ts @@ -17,6 +17,6 @@ // The index file in ./components/ is typically responsible for selecting // which components are public API and should be exported from the package. -export * from './RedirectToRoot'; +export * from './CookieAuthRootRedirect'; export * from './CookieAuthRefreshProvider'; -export * from './ExperimentalAppProtection'; +export * from './AppMode'; diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx index aeff0be3b0..cd61b9d94b 100644 --- a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx @@ -236,7 +236,7 @@ describe('useCookieAuthRefresh', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7000/techdocs/api/.backstage/v1-cookie', + 'http://localhost:7000/techdocs/api/.backstage/auth/v1/cookie', { credentials: 'include' }, ), ); diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx index 45753d7460..b26d45447e 100644 --- a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx @@ -31,13 +31,13 @@ import { ResponseError } from '@backstage/errors'; export function useCookieAuthRefresh(options: { // The plugin id used for discovering the API origin pluginId: string; - // The path used for calling the refresh cookie endpoint, default to '/.backstage/v1-cookie' + // The path used for calling the refresh cookie endpoint, default to '/.backstage/auth/v1/cookie' path?: string; }): | { status: 'loading' } | { status: 'error'; error: Error; retry: () => void } | { status: 'success'; data: { expiresAt: string } } { - const { pluginId, path = '/.backstage/v1-cookie' } = options ?? {}; + const { pluginId, path = '/.backstage/auth/v1/cookie' } = options ?? {}; const fetchApi = useApi(fetchApiRef); const discoveryApi = useApi(discoveryApiRef); diff --git a/yarn.lock b/yarn.lock index 666548dd95..db7491ccf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3834,6 +3834,7 @@ __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:^" @@ -4193,6 +4194,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/plugin-auth-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" @@ -5112,7 +5114,9 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" + "@backstage/version-bridge": "workspace:^" "@material-ui/core": ^4.9.13 "@react-hookz/web": ^24.0.0 "@testing-library/jest-dom": ^6.0.0 @@ -25863,7 +25867,7 @@ __metadata: languageName: node linkType: hard -"domhandler@npm:^5.0.1, domhandler@npm:^5.0.2": +"domhandler@npm:^5.0.1, domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": version: 5.0.3 resolution: "domhandler@npm:5.0.3" dependencies: @@ -26269,7 +26273,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.2.0, entities@npm:^4.3.0, entities@npm:^4.4.0": +"entities@npm:^4.2.0, entities@npm:^4.4.0": version: 4.4.0 resolution: "entities@npm:4.4.0" checksum: 84d250329f4b56b40fa93ed067b194db21e8815e4eb9b59f43a086f0ecd342814f6bc483de8a77da5d64e0f626033192b1b4f1792232a7ea6b970ebe0f3187c2 @@ -30033,14 +30037,14 @@ __metadata: linkType: hard "htmlparser2@npm:^8.0.0": - version: 8.0.1 - resolution: "htmlparser2@npm:8.0.1" + version: 8.0.2 + resolution: "htmlparser2@npm:8.0.2" dependencies: domelementtype: ^2.3.0 - domhandler: ^5.0.2 + domhandler: ^5.0.3 domutils: ^3.0.1 - entities: ^4.3.0 - checksum: 06d5c71e8313597722bc429ae2a7a8333d77bd3ab07ccb916628384b37332027b047f8619448d8f4a3312b6609c6ea3302a4e77435d859e9e686999e6699ca39 + entities: ^4.4.0 + checksum: 29167a0f9282f181da8a6d0311b76820c8a59bc9e3c87009e21968264c2987d2723d6fde5a964d4b7b6cba663fca96ffb373c06d8223a85f52a6089ced942700 languageName: node linkType: hard