From 1869f859c440de3654b8d2f66cc078d44e3e5ab3 Mon Sep 17 00:00:00 2001 From: Jacob Bolda Date: Fri, 6 Jan 2023 09:31:23 -0600 Subject: [PATCH] enable auth for the endpoints with support --- app-config.production.yaml | 3 ++ packages/app/src/App.tsx | 37 ++++++++++---- packages/backend/src/authMiddleware.ts | 71 ++++++++++++++++++++++++++ packages/backend/src/index.ts | 35 +++++++------ 4 files changed, 120 insertions(+), 26 deletions(-) create mode 100644 packages/backend/src/authMiddleware.ts diff --git a/app-config.production.yaml b/app-config.production.yaml index b5889f0285..81efca8824 100644 --- a/app-config.production.yaml +++ b/app-config.production.yaml @@ -17,6 +17,9 @@ auth: # see https://backstage.io/docs/auth/ to learn about auth providers session: secret: ${AUTH_SESSION_CLIENT_SECRET} + # see https://backstage.io/docs/auth/service-to-service-auth#setup + keys: + - secret: ${BACKEND_SECRET} environment: production providers: auth0: diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index e7bbdac618..07090dd160 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -40,24 +40,41 @@ import { GraphiQLPage } from '@backstage/plugin-graphiql'; import { SignInPage } from '@backstage/core-components'; import { auth0AuthApiRef } from './internal'; import Star from '@material-ui/icons/Star'; +import { + discoveryApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import type { IdentityApi } from '@backstage/core-plugin-api'; +import { setTokenCookie } from './cookieAuth'; const app = createApp({ apis, components: { - SignInPage: props => ( - { + const discoveryApi = useApi(discoveryApiRef); + return ( + - ), + }} + onSignInSuccess={async (identityApi: IdentityApi) => { + // As techdocs HTML pages load assets without an Authorization header + // the code below also sets a token cookie when the user logs in + // (and when the token is about to expire). + setTokenCookie( + await discoveryApi.getBaseUrl('cookie'), + identityApi, + ); + + props.onSignInSuccess(identityApi); + }} + /> + ); + }, }, bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { diff --git a/packages/backend/src/authMiddleware.ts b/packages/backend/src/authMiddleware.ts new file mode 100644 index 0000000000..c2b77bee1b --- /dev/null +++ b/packages/backend/src/authMiddleware.ts @@ -0,0 +1,71 @@ +import type { Config } from '@backstage/config'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +// this dep is required in @backstage/plugin-auth-node, but it doesn't export this functionality +// eslint-disable-next-line import/no-extraneous-dependencies +import { decodeJwt } from 'jose'; +import { NextFunction, Request, Response, RequestHandler } from 'express'; +import { URL } from 'url'; +import { PluginEnvironment } from './types'; + +function setTokenCookie( + res: Response, + options: { token: string; secure: boolean; cookieDomain: string }, +) { + try { + const payload = decodeJwt(options.token); + res.cookie('token', options.token, { + expires: new Date(payload.exp ? payload.exp * 1000 : 0), + secure: options.secure, + sameSite: 'lax', + domain: options.cookieDomain, + path: '/', + httpOnly: true, + }); + } catch (_err) { + // Ignore + } +} + +export const createAuthMiddleware = async ( + config: Config, + appEnv: PluginEnvironment, +) => { + const baseUrl = config.getString('backend.baseUrl'); + const secure = baseUrl.startsWith('https://'); + const cookieDomain = new URL(baseUrl).hostname; + const authMiddleware: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { + try { + const token = + getBearerTokenFromAuthorizationHeader(req.headers.authorization) || + (req.cookies?.token as string | undefined); + if (!token) { + res.status(401).send('Unauthorized'); + return; + } + try { + req.user = await appEnv.identity.getIdentity({ request: req }); + } catch { + await appEnv.tokenManager.authenticate(token); + } + if (!req.headers.authorization) { + // Authorization header may be forwarded by plugin requests + req.headers.authorization = `Bearer ${token}`; + } + if (token && token !== req.cookies?.token) { + setTokenCookie(res, { + token, + secure, + cookieDomain, + }); + } + next(); + } catch (error) { + res.status(401).send('Unauthorized'); + } + }; + return authMiddleware; +}; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 79890a0713..5ba36b3340 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,11 +1,3 @@ -/* - * Hi! - * - * Note that this is an EXAMPLE Backstage backend. Please check the README. - * - * Happy hacking! - */ - import Router from 'express-promise-router'; import { createServiceBuilder, @@ -22,6 +14,7 @@ import { import { TaskScheduler } from '@backstage/backend-tasks'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +import { createAuthMiddleware } from './authMiddleware'; import { Config } from '@backstage/config'; import app from './plugins/app'; import auth from './plugins/auth'; @@ -46,7 +39,7 @@ function makeCreateEnv(config: Config) { const cacheManager = CacheManager.fromConfig(config); const databaseManager = DatabaseManager.fromConfig(config, { logger: root }); - const tokenManager = ServerTokenManager.noop(); + const tokenManager = ServerTokenManager.fromConfig(config, { logger: root }); const taskScheduler = TaskScheduler.fromConfig(config); const identity = DefaultIdentityClient.create({ @@ -100,18 +93,28 @@ async function main() { const appEnv = useHotMemoize(module, () => createEnv('app')); const humanitecEnv = useHotMemoize(module, () => createEnv('humanitec')); + const authMiddleware = await createAuthMiddleware(config, appEnv); const apiRouter = Router(); - apiRouter.use('/catalog', await catalog(catalogEnv)); - apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + // The auth route must be publicly available as it is used during login apiRouter.use('/auth', await auth(authEnv)); - apiRouter.use('/techdocs', await techdocs(techdocsEnv)); - apiRouter.use('/proxy', await proxy(proxyEnv)); - apiRouter.use('/search', await search(searchEnv)); - apiRouter.use('/healthcheck', await healthcheck(healthcheckEnv)); + // Add a simple endpoint to be used when setting a token cookie + apiRouter.use('/cookie', authMiddleware, (_req, res) => { + res.status(200).send(`Coming right up`); + }); + // Only authenticated requests are allowed to the routes below + apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv)); + apiRouter.use('/scaffolder', authMiddleware, await scaffolder(scaffolderEnv)); + apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv)); + apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv)); + apiRouter.use('/search', authMiddleware, await search(searchEnv)); + apiRouter.use('/healthcheck', authMiddleware, await healthcheck(healthcheckEnv)); apiRouter.use('/effection-inspector', await effectionInspector(effectionInspectorEnv)); + // apiRouter.use('/effection-inspector', authMiddleware, await effectionInspector(effectionInspectorEnv)); apiRouter.use('/humanitec', await humanitec(humanitecEnv)); + // apiRouter.use('/humanitec', authMiddleware, await humanitec(humanitecEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); - apiRouter.use(notFoundHandler()); + // apiRouter.use('/graphql', authMiddleware, await graphql(graphqlEnv)); + apiRouter.use(authMiddleware, notFoundHandler()); const service = createServiceBuilder(module) .loadConfig(config)