diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 45ae38b8b7..c8f4083a0c 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -8,30 +8,28 @@ API requests from frontend plugins include an authorization header with a Backst 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). +Create `packages/backend/src/authMiddleware.ts`: + ```typescript -// packages/backend/src/index.ts from a create-app deployment - -import cookieParser from 'cookie-parser'; -import { Request, Response, NextFunction } from 'express'; -import { JWT } from 'jose'; -import { URL } from 'url'; +import { SingleHostDiscovery } from '@backstage/backend-common'; +import type { Config } from '@backstage/config'; import { - IdentityClient, getBearerTokenFromAuthorizationHeader, + IdentityClient, } from '@backstage/plugin-auth-node'; - -// ... +import { NextFunction, Request, Response, RequestHandler } from 'express'; +import { decodeJwt } from 'jose'; +import { URL } from 'url'; +import { PluginEnvironment } from './types'; function setTokenCookie( res: Response, options: { token: string; secure: boolean; cookieDomain: string }, ) { try { - const payload = JWT.decode(options.token) as object & { - exp: number; - }; - res.cookie(`token`, options.token, { - expires: new Date(payload?.exp ? payload?.exp * 1000 : 0), + 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, @@ -43,9 +41,10 @@ function setTokenCookie( } } -async function main() { - // ... - +export const createAuthMiddleware = async ( + config: Config, + appEnv: PluginEnvironment, +) => { const discovery = SingleHostDiscovery.fromConfig(config); const identity = IdentityClient.create({ discovery, @@ -54,7 +53,7 @@ async function main() { const baseUrl = config.getString('backend.baseUrl'); const secure = baseUrl.startsWith('https://'); const cookieDomain = new URL(baseUrl).hostname; - const authMiddleware = async ( + const authMiddleware: RequestHandler = async ( req: Request, res: Response, next: NextFunction, @@ -62,13 +61,21 @@ async function main() { try { const token = getBearerTokenFromAuthorizationHeader(req.headers.authorization) || - req.cookies['token']; - req.user = await identity.authenticate(token); + (req.cookies.token as string | undefined); + if (!token) { + res.status(401).send('Unauthorized'); + return; + } + try { + req.user = await identity.authenticate(token); + } 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 !== req.cookies['token']) { + if (token && token !== req.cookies.token) { setTokenCookie(res, { token, secure, @@ -77,9 +84,24 @@ async function main() { } next(); } catch (error) { - res.status(401).send(`Unauthorized`); + res.status(401).send('Unauthorized'); } }; + return authMiddleware; +}; +``` + +```typescript +// packages/backend/src/index.ts from a create-app deployment + +import { createAuthMiddleware } from './authMiddleware'; + +// ... + +async function main() { + // ... + + const authMiddleware = await createAuthMiddleware(config, appEnv); const apiRouter = Router(); apiRouter.use(cookieParser()); @@ -99,12 +121,10 @@ async function main() { } ``` +Create `packages/app/src/cookieAuth.ts`: + ```typescript -// packages/app/src/App.tsx from a create-app deployment - -import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; - -// ... +import type { IdentityApi } from '@backstage/core-plugin-api'; // Parses supplied JWT token and returns the payload function parseJwt(token: string): { exp: number } { @@ -113,9 +133,11 @@ function parseJwt(token: string): { exp: number } { const jsonPayload = decodeURIComponent( atob(base64) .split('') - .map(function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }) + .map( + c => + // eslint-disable-next-line prefer-template + '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2), + ) .join(''), ); @@ -132,7 +154,7 @@ function msUntilExpiry(token: string): number { // Calls the specified url regularly using an auth token to set a token cookie // to authorize regular HTTP requests when loading techdocs -async function setTokenCookie(url: string, identityApi: IdentityApi) { +export async function setTokenCookie(url: string, identityApi: IdentityApi) { const { token } = await identityApi.getCredentials(); if (!token) { return; @@ -155,6 +177,14 @@ async function setTokenCookie(url: string, identityApi: IdentityApi) { ms > 0 ? ms : 10000, ); } +``` + +```typescript +// packages/app/src/App.tsx from a create-app deployment + +import { setTokenCookie } from './cookieAuth'; + +// ... const app = createApp({ // ...