Merge pull request #21116 from kuangp/docs/newBackendAuth
docs(auth): add a contrib example hooking up api auth in new backend system
This commit is contained in:
@@ -8,6 +8,8 @@ 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).
|
||||
|
||||
## Old Backend System Setup
|
||||
|
||||
Create `packages/backend/src/authMiddleware.ts`:
|
||||
|
||||
```typescript
|
||||
@@ -122,6 +124,151 @@ async function main() {
|
||||
}
|
||||
```
|
||||
|
||||
## New Backend System Setup
|
||||
|
||||
Create `packages/backend/src/authMiddlewareFactory.ts`:
|
||||
|
||||
```typescript
|
||||
import { HostDiscovery } from '@backstage/backend-app-api';
|
||||
import { ServerTokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
DefaultIdentityClient,
|
||||
getBearerTokenFromAuthorizationHeader,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import lzstring from 'lz-string';
|
||||
import { URL } from 'url';
|
||||
|
||||
type AuthMiddlewareFactoryOptions = {
|
||||
config: RootConfigService;
|
||||
logger: LoggerService;
|
||||
};
|
||||
|
||||
export const authMiddlewareFactory = ({
|
||||
config,
|
||||
logger,
|
||||
}: AuthMiddlewareFactoryOptions): RequestHandler => {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const discovery = HostDiscovery.fromConfig(config);
|
||||
const identity = DefaultIdentityClient.create({ discovery });
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const fullPath = `${req.baseUrl}${req.path}`;
|
||||
|
||||
// Only apply auth to /api routes & skip auth for the following endpoints
|
||||
// Add any additional plugin routes you want to whitelist eg. events
|
||||
const nonAuthWhitelist = ['app', 'auth'];
|
||||
const nonAuthRegex = new RegExp(
|
||||
`^\/api\/(${nonAuthWhitelist.join('|')})(?=\/|$)\S*`,
|
||||
);
|
||||
if (!fullPath.startsWith('/api/') || nonAuthRegex.test(fullPath)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Token cookies are compressed to reduce size
|
||||
const cookieToken = lzstring.decompressFromEncodedURIComponent(
|
||||
req.cookies.token,
|
||||
);
|
||||
const token =
|
||||
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ??
|
||||
cookieToken;
|
||||
|
||||
try {
|
||||
// Attempt to authenticate as a frontend request token
|
||||
await identity.authenticate(token);
|
||||
} catch (err) {
|
||||
// Attempt to authenticate as a backend request token
|
||||
await tokenManager.authenticate(token);
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (token !== cookieToken) {
|
||||
try {
|
||||
const payload = decodeJwt(token);
|
||||
res.cookie('token', token, {
|
||||
// Compress token to reduce cookie size
|
||||
encode: lzstring.compressToEncodedURIComponent,
|
||||
expires: new Date((payload?.exp ?? 0) * 1000),
|
||||
secure: baseUrl.startsWith('https://'),
|
||||
sameSite: 'lax',
|
||||
domain: new URL(baseUrl).hostname,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).send(`Unauthorized`);
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Install cookie-parser:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend cookie-parser @types/cookie-parser
|
||||
```
|
||||
|
||||
Create a custom configured `rootHttpRouterService` in `packages/backend/src/customRootHttpRouterService.ts`:
|
||||
|
||||
```typescript
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { authMiddlewareFactory } from './authMiddlewareFactory';
|
||||
|
||||
export default rootHttpRouterServiceFactory({
|
||||
configure: ({ app, config, logger, middleware, routes }) => {
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
app.use(cookieParser());
|
||||
app.use(middleware.logging());
|
||||
|
||||
app.use(authMiddlewareFactory({ config, logger }));
|
||||
|
||||
// Simple handler to set auth cookie for user
|
||||
app.use('/api/cookie', (_, res) => {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.use(routes);
|
||||
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Update `packages/backend/src/index.ts` to add the custom `rootHttpRouterService` and override the default:
|
||||
|
||||
```typescript
|
||||
// ...
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('./customRootHttpRouterService'));
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
Create `packages/app/src/cookieAuth.ts`:
|
||||
|
||||
```typescript
|
||||
|
||||
Reference in New Issue
Block a user