Clean up of the contrib folder

Signed-off-by: Andre Wanlin <awanlin@spotify.com>
This commit is contained in:
Andre Wanlin
2026-03-18 08:59:09 -05:00
parent a4b9c45277
commit 5cbd39e980
9 changed files with 6 additions and 1138 deletions
@@ -1,459 +0,0 @@
# Authenticate API requests
> [!CAUTION]
> This entire guide MUST NOT BE USED by users of Backstage 1.26 and
> newer. If you have applied the changes in this guide, you need to remove them
> again as you upgrade to recent versions of Backstage. When [the new auth changes](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution)
> landed backends became natively secured through the framework, and the
> instructions outlined in here can interfere with the backend functioning
> correctly.
The Backstage backend APIs are by default available without authentication. To avoid evil-doers from accessing or modifying data, one might use a network protection mechanism such as a firewall or an authenticating reverse proxy. For Backstage instances that are available on the Internet one can instead use the experimental IdentityClient as outlined below.
API requests from frontend plugins include an authorization header with a Backstage identity token acquired when the user logs in. By adding a middleware that verifies said token to be valid and signed by Backstage, non-authenticated requests can be blocked with a 401 Unauthorized response.
**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. If you have not done so already, you will also need to implement [service-to-service auth](https://backstage.io/docs/auth/service-to-service-auth).
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
import type { Config } from '@backstage/config';
import { getBearerTokenFromAuthorizationHeader } 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 = 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;
};
```
Install cookie-parser:
```bash
# From your Backstage root directory
yarn --cwd packages/backend add cookie-parser
```
Update routes in `packages/backend/src/index.ts`:
```typescript
// packages/backend/src/index.ts from a create-app deployment
import { createAuthMiddleware } from './authMiddleware';
import cookieParser from 'cookie-parser';
// ...
async function main() {
// ...
const authMiddleware = await createAuthMiddleware(config, appEnv);
const apiRouter = Router();
apiRouter.use(cookieParser());
// The auth route must be publicly available as it is used during login
apiRouter.use('/auth', await auth(authEnv));
// 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('/techdocs', authMiddleware, await techdocs(techdocsEnv));
apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv));
apiRouter.use(authMiddleware, notFoundHandler());
// ...
}
```
## 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 --cwd packages/backend add 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
import type { IdentityApi } from '@backstage/core-plugin-api';
// Parses supplied JWT token and returns the payload
function parseJwt(token: string): { exp: number } {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(
c =>
// eslint-disable-next-line prefer-template
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
)
.join(''),
);
return JSON.parse(jsonPayload);
}
// Returns milliseconds until the supplied JWT token expires
function msUntilExpiry(token: string): number {
const payload = parseJwt(token);
const remaining =
new Date(payload.exp * 1000).getTime() - new Date().getTime();
return remaining;
}
// Calls the specified url regularly using an auth token to set a token cookie
// to authorize regular HTTP requests when loading techdocs
export async function setTokenCookie(url: string, identityApi: IdentityApi) {
const { token } = await identityApi.getCredentials();
if (!token) {
return;
}
await fetch(url, {
mode: 'cors',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
},
});
// Call this function again a few minutes before the token expires
const ms = msUntilExpiry(token) - 4 * 60 * 1000;
setTimeout(
() => {
setTokenCookie(url, identityApi);
},
ms > 0 ? ms : 10000,
);
}
```
```typescript
// required types and packages for example below
import type { IdentityApi } from '@backstage/core-plugin-api';
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
// additional packages/app/src/App.tsx from a create-app deployment
import { setTokenCookie } from './cookieAuth';
// ...
const app = createApp({
// ...
components: {
SignInPage: props => {
const discoveryApi = useApi(discoveryApiRef);
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
onSignInSuccess={async (identityApi: IdentityApi) => {
setTokenCookie(
await discoveryApi.getBaseUrl('cookie'),
identityApi,
);
props.onSignInSuccess(identityApi);
}}
/>
);
},
},
// ...
});
// ...
```
**NOTE**: Most Backstage frontend plugins come with the support for the `IdentityApi`.
In case you already have a dozen of internal ones, you may need to update those too.
Assuming you follow the common plugin structure, the changes to your front-end may look like:
```diff
// plugins/internal-plugin/src/api.ts
- import { createApiRef } from '@backstage/core-plugin-api';
+ import { createApiRef, IdentityApi } from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
// ...
type MyApiOptions = {
configApi: Config;
+ identityApi: IdentityApi;
// ...
}
interface MyInterface {
getData(): Promise<MyData[]>;
}
export class MyApi implements MyInterface {
private configApi: Config;
+ private identityApi: IdentityApi;
// ...
constructor(options: MyApiOptions) {
this.configApi = options.configApi;
+ this.identityApi = options.identityApi;
}
async getMyData() {
const backendUrl = this.configApi.getString('backend.baseUrl');
+ const { token } = await this.identityApi.getCredentials();
const requestUrl = `${backendUrl}/api/data/`;
- const response = await fetch(requestUrl);
+ const response = await fetch(
requestUrl,
{ headers: { Authorization: `Bearer ${token}` } },
);
// ...
}
```
and
```diff
// plugins/internal-plugin/src/plugin.ts
import {
configApiRef,
createApiFactory,
createPlugin,
+ identityApiRef,
} from '@backstage/core-plugin-api';
import { myPluginPageRouteRef } from './routeRefs';
import { MyApi, myApiRef } from './api';
export const plugin = createPlugin({
id: 'my-plugin',
routes: {
mainPage: myPluginPageRouteRef,
},
apis: [
createApiFactory({
api: myApiRef,
deps: {
configApi: configApiRef,
+ identityApi: identityApiRef,
},
- factory: ({ configApi }) =>
- new MyApi({ configApi }),
+ factory: ({ configApi, identityApi }) =>
+ new MyApi({ configApi, identityApi }),
}),
],
});
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

@@ -1,115 +0,0 @@
# Prometheus
> [!NOTE]
> The Prometheus metrics have been marked as deprecated and will be removed at a later point. The recommendation is to use the OpenTelemetry metrics by following the [Setup OpenTelemetry](https://backstage.io/docs/tutorials/setup-opentelemetry) documentation
## Overview
This is a small tutorial that goes over how to setup your Backstage instance to output metrics in a format that can be pulled in by Prometheus.
## How to Setup Prometheus Metrics
1. First we need to add the needed dependencies to the `package.json` in the `\packages\backend`:
```diff
// packages/backend/package.json
"dependencies": {
+ "express-prom-bundle": "^7.0.0",
+ "prom-client": "^15.0.0",
```
2. Now we want to run `yarn install` from the root of the project to get those dependencies in place
3. Then we need to add a handler for the metrics by creating a file called `metrics.ts` in the `\packages\backend\src` folder
4. Next we add the following content to the `metrics.ts` file:
```ts
// packages/backend/src/metrics.ts
import { useHotCleanup } from '@backstage/backend-common';
import { RequestHandler } from 'express';
import promBundle from 'express-prom-bundle';
import prom from 'prom-client';
import * as url from 'url';
const rootRegEx = new RegExp('^/([^/]*)/.*');
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
export function normalizePath(req: any): string {
const path = url.parse(req.originalUrl || req.url).pathname || '/';
// Capture /api/ and the plugin name
if (apiRegEx.test(path)) {
return path.replace(apiRegEx, '/api/$1');
}
// Only the first path segment at root level
return path.replace(rootRegEx, '/$1');
}
/**
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
*/
export function metricsHandler(): RequestHandler {
// We can only initialize the metrics once and have to clean them up between hot reloads
useHotCleanup(module, () => prom.register.clear());
return promBundle({
includeMethod: true,
includePath: true,
// Using includePath alone is problematic, as it will include path labels with high
// cardinality (e.g. path params). Instead we would have to template them. However, this
// is difficult, as every backend plugin might use different routes. Instead we only take
// the first directory of the path, to have at least an idea how each plugin performs:
normalizePath,
promClient: { collectDefaultMetrics: {} },
});
}
```
5. Now we will extend the router configuration with the `metricsHandler`:
```diff
+import { metricsHandler } from './metrics';
...
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('', await healthcheck(healthcheckEnv))
+ .addRouter('', metricsHandler())
.addRouter('/api', apiRouter);
```
6. You now have everything setup, from the `\packages\backend` folder run `yarn start` this will start up the backend
7. Now in a browser load up `http://localhost:7007/metrics`, if everything went smoothly you should see metrics in your browser something like this:
![Prometheus Metrics Example Output](prometheus-metrics-output.png)
## Metrics
The following sections goes over the included and experimental metrics available once you have completed this tutorial
## Included
This tutorials uses the [`express-prom-bundle`](https://github.com/jochen-schweizer/express-prom-bundle) and the [`prom-client`](https://github.com/siimon/prom-client) to make this all work. They both come with some built in metrics:
- `express-prom-bundle` comes with 2 metrics:
- `up`: this normally will be just 1
- `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
- `prom-client` comes with a collection of metrics around memory, CPU, processes, etc. You can see the supported metrics in the `prom-client's` [`lib/metrics`](https://github.com/siimon/prom-client/tree/master/lib/metrics) folder.
### Experimental
There are some custom metrics that have been added to Backstage will be output for you, these are currently deemed experimental and may be changed or removed in a future release. Here is a rough list, again subject to changes:
- `catalog_entities_count`: Total amount of entities in the catalog
- `catalog_registered_locations_count`: Total amount of registered locations in the catalog
- `catalog_relations_count`: Total amount of relations between entities
- `catalog_stitched_entities_count`: Amount of entities stitched
- `catalog_processed_entities_count`: Amount of entities processed
- `catalog_processing_duration_seconds`: Time spent executing the full processing flow
- `catalog_processors_duration_seconds`: Time spent executing catalog processors
- `catalog_processing_queue_delay_seconds`: The amount of delay between being scheduled for processing, and the start of actually being processed
- `scaffolder_task_count`: Tracks successful task runs.
- `scaffolder_task_duration`: a histogram which tracks the duration of a task run
- `scaffolder_step_count`: a count that tracks each step run
- `scaffolder_step_duration`: a histogram which tracks the duration of each step run
@@ -1,53 +0,0 @@
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
ExampleComponent.tsx reference
```tsx
import { Typography, Grid } from '@material-ui/core';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
import {
InfoCard,
Header,
Page,
Content,
ContentHeader,
HeaderLabel,
SupportButton,
} from '@backstage/core-components';
import { ExampleFetchComponent } from '../ExampleFetchComponent';
export const ExampleComponent = () => {
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
return (
<Page themeId="tool">
<Header
title="Welcome to github-playground!"
subtitle="Optional subtitle"
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Plugin title">
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title={userId}>
<Typography variant="body1">
{`${profile.displayName} | ${profile.email}`}
</Typography>
</InfoCard>
</Grid>
<Grid item>
<ExampleFetchComponent />
</Grid>
</Grid>
</Content>
</Page>
);
};
```
@@ -1,103 +0,0 @@
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
ExampleFetchComponent.tsx reference
```tsx
import useAsync from 'react-use/lib/useAsync';
import Alert from '@material-ui/lab/Alert';
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import { graphql } from '@octokit/graphql';
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
type Node = {
name: string;
createdAt: string;
description: string;
diskUsage: number;
isFork: boolean;
};
type Viewer = {
repositories: {
totalCount: number;
nodes: Node[];
pageInfo: {
endCursor: string;
hasNextPage: boolean;
};
};
};
type DenseTableProps = {
viewer: Viewer;
};
export const DenseTable = ({ viewer }: DenseTableProps) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'name' },
{ title: 'Created', field: 'createdAt' },
{ title: 'Description', field: 'description' },
{ title: 'Disk Usage', field: 'diskUsage' },
{ title: 'Fork', field: 'isFork' },
];
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={columns}
data={viewer.repositories.nodes}
/>
);
};
export const ExampleFetchComponent = () => {
const auth = useApi(githubAuthApiRef);
const { value, loading, error } = useAsync(async (): Promise<any> => {
const token = await auth.getAccessToken();
const gqlEndpoint = graphql.defaults({
// Uncomment baseUrl if using enterprise
// baseUrl: 'https://github.MY-BIZ.com/api',
headers: {
authorization: `token ${token}`,
},
});
const { viewer } = await gqlEndpoint(query);
return viewer;
}, []);
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
};
```
@@ -1,3 +1,6 @@
# Basic Kubernetes example with Helm
> [!NOTE]
> This documentation is deprecated and will be removed at a future date, please use the well maintained [Backstage Helm Charts](https://github.com/backstage/charts) for this.
Note that these examples aim to show a minimal setup and do not include best practices for secure Kubernetes deployments. See the [Kubernetes documentation](https://kubernetes.io/docs/concepts/security/) for more information, or resources provided by your own organization.
+3
View File
@@ -1,5 +1,8 @@
# Confluence
> [!NOTE]
> This documentation is deprecated and will be removed at a future date, please use the well maintained [`@backstage-community/plugin-search-backend-module-confluence-collator` Community Plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/confluence/plugins/search-backend-module-confluence-collator) for this.
These files help you add Confluence as a source to the Backstage Search plugin.
To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app.
Then, add the following code to your packages/app/src/components/search/SearchPage.tsx: