Add code to enable authorization
Signed-off-by: Nataliya Issayeva <nissayeva@users.noreply.github.com>
This commit is contained in:
@@ -86,6 +86,52 @@ import { providers } from './identityProviders';
|
||||
import * as plugins from './plugins';
|
||||
|
||||
import { techDocsPage } from './components/techdocs/TechDocsPage';
|
||||
import { discoveryApiRef, useApi } 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(function (c) {
|
||||
return `%${`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
|
||||
async function setTokenCookie(url: string, getIdToken: () => Promise<string>) {
|
||||
const token = await getIdToken();
|
||||
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, getIdToken);
|
||||
},
|
||||
ms > 0 ? ms : 10000,
|
||||
);
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -96,12 +142,24 @@ 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"
|
||||
onResult={async result => {
|
||||
// When logged in, set a token cookie
|
||||
if (typeof result.getIdToken !== 'undefined') {
|
||||
setTokenCookie(
|
||||
await discoveryApi.getBaseUrl('cookie'),
|
||||
result.getIdToken,
|
||||
);
|
||||
}
|
||||
// Forward results
|
||||
props.onResult(result);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -55,11 +55,13 @@
|
||||
"@gitbeaker/node": "^30.2.0",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"azure-devops-node-api": "^11.0.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"dockerode": "^3.3.1",
|
||||
"example-app": "^0.2.53",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"express-prom-bundle": "^6.3.6",
|
||||
"jose": "^1.27.1",
|
||||
"knex": "^0.95.1",
|
||||
"pg": "^8.3.0",
|
||||
"pg-connection-string": "^2.3.0",
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
AuthIdentityTokenManager,
|
||||
IdentityClient,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import healthcheck from './plugins/healthcheck';
|
||||
@@ -55,11 +57,16 @@ import app from './plugins/app';
|
||||
import badges from './plugins/badges';
|
||||
import jenkins from './plugins/jenkins';
|
||||
import { PluginEnvironment } from './types';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { JWT } from 'jose';
|
||||
import { URL } from 'url';
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
const root = getRootLogger();
|
||||
const reader = UrlReaders.default({ logger: root, config });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
const tokenManager = new AuthIdentityTokenManager(discovery);
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
@@ -70,10 +77,31 @@ function makeCreateEnv(config: Config) {
|
||||
const logger = root.child({ type: 'plugin', plugin });
|
||||
const database = databaseManager.forPlugin(plugin);
|
||||
const cache = cacheManager.forPlugin(plugin);
|
||||
return { logger, cache, database, config, reader, discovery };
|
||||
return { logger, cache, database, config, reader, discovery, tokenManager };
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
secure: options.secure,
|
||||
sameSite: 'lax',
|
||||
domain: options.cookieDomain,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
});
|
||||
} catch (_err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
metricsInit();
|
||||
const logger = getRootLogger();
|
||||
@@ -112,7 +140,54 @@ async function main() {
|
||||
createEnv('tech-insights'),
|
||||
);
|
||||
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
const identity = new IdentityClient({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
});
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const secure = baseUrl.startsWith('https://');
|
||||
const cookieDomain = new URL(baseUrl).hostname;
|
||||
const authMiddleware = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token =
|
||||
IdentityClient.getBearerToken(req.headers.authorization) ||
|
||||
req.cookies.token;
|
||||
req.user = await identity.authenticate(token);
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (token !== req.cookies.token) {
|
||||
setTokenCookie(res, {
|
||||
token,
|
||||
secure,
|
||||
cookieDomain,
|
||||
});
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).send(`Unauthorized`);
|
||||
}
|
||||
};
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use(cookieParser());
|
||||
// The auth route must be publically 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());
|
||||
apiRouter.use('/catalog', await catalog(catalogEnv));
|
||||
apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv));
|
||||
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
AuthIdentityTokenManager,
|
||||
PluginCacheManager,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
@@ -30,4 +31,5 @@ export type PluginEnvironment = {
|
||||
config: Config;
|
||||
reader: UrlReader;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
tokenManager: AuthIdentityTokenManager;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user