Default to noop tokenManager and no backend secret
Signed-off-by: Nataliya Issayeva <nissayeva@users.noreply.github.com>
This commit is contained in:
+3
-2
@@ -24,8 +24,9 @@ app:
|
||||
|
||||
backend:
|
||||
# Used for enabling authorization, secret is shared by all backend plugins
|
||||
authorization:
|
||||
secret: ${BACKEND_SECRET}
|
||||
# See authenticate-api-requests.md in the contrib docs for information on the format
|
||||
# authorization:
|
||||
# secret: ${BACKEND_SECRET}
|
||||
baseUrl: http://localhost:7000
|
||||
listen:
|
||||
port: 7000
|
||||
|
||||
@@ -69,8 +69,10 @@ async function main() {
|
||||
req.cookies['token'];
|
||||
|
||||
// Authenticate all requests originating from backends by default
|
||||
const isServerToken = await authEnv.tokenManager.isServerToken(token);
|
||||
if (!isServerToken) {
|
||||
const isValidServerToken = await authEnv.tokenManager.validateServerToken(
|
||||
token,
|
||||
);
|
||||
if (!isValidServerToken) {
|
||||
req.user = await identity.authenticate(token);
|
||||
}
|
||||
|
||||
@@ -275,7 +277,25 @@ export const plugin = createPlugin({
|
||||
});
|
||||
```
|
||||
|
||||
In the (probably unlikely) case that you need to authenticate from a backend plugin, the plugin environment contains a `tokenManager` that will provide a server token to use in the request:
|
||||
In the (probably unlikely) case that you need to authenticate from a backend plugin, the plugin environment contains a `tokenManager` that will provide a server token to use in the request. It is a noop `tokenManager` by default -- you'll need to instantiate a new one using the secret from your config instead:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
const root = getRootLogger();
|
||||
const reader = UrlReaders.default({ logger: root, config });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
- const tokenManager = ServerTokenManager.noop();
|
||||
+ const tokenManager = ServerTokenManager.fromConfig(config);
|
||||
```
|
||||
|
||||
With this `tokenManager`, you can then generate a server token for requests:
|
||||
|
||||
```
|
||||
const { token } = await this.tokenManager.getServerToken();
|
||||
|
||||
Vendored
+9
@@ -20,6 +20,15 @@ export interface Config {
|
||||
};
|
||||
|
||||
backend: {
|
||||
/** Backend configuration for when request authentication is enabled */
|
||||
authorization?: {
|
||||
/**
|
||||
* Secret shared by all backends for generating tokens
|
||||
* Format is base64 24-bit key
|
||||
*/
|
||||
secret?: string;
|
||||
};
|
||||
|
||||
baseUrl: string; // defined in core, but repeated here without doc
|
||||
|
||||
/** Address that the backend should listen to. */
|
||||
|
||||
@@ -22,32 +22,49 @@ const configWithSecret = new ConfigReader({
|
||||
});
|
||||
|
||||
describe('ServerTokenManager', () => {
|
||||
it('should throw if secret in config does not exist', () => {
|
||||
expect(() => ServerTokenManager.fromConfig(emptyConfig)).toThrowError();
|
||||
});
|
||||
|
||||
describe('getServerToken', () => {
|
||||
it('should return a token if secret in config exists', async () => {
|
||||
const tokenManager = new ServerTokenManager(configWithSecret);
|
||||
const tokenManager = ServerTokenManager.fromConfig(configWithSecret);
|
||||
expect((await tokenManager.getServerToken()).token).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return a token if secret in config does not exist', async () => {
|
||||
const tokenManagerWithEmptyConfig = new ServerTokenManager(emptyConfig);
|
||||
expect(
|
||||
(await tokenManagerWithEmptyConfig.getServerToken()).token,
|
||||
).toBeDefined();
|
||||
it('should return an empty string if using a noop TokenManager', async () => {
|
||||
const tokenManager = ServerTokenManager.noop();
|
||||
expect((await tokenManager.getServerToken()).token).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isServerToken', () => {
|
||||
describe('validateServerToken', () => {
|
||||
it('should return true if token is valid', async () => {
|
||||
const tokenManager = new ServerTokenManager(configWithSecret);
|
||||
const tokenManager = ServerTokenManager.fromConfig(configWithSecret);
|
||||
const { token } = await tokenManager.getServerToken();
|
||||
const isServerToken = await tokenManager.isServerToken(token);
|
||||
expect(isServerToken).toBe(true);
|
||||
const isValidServerToken = await tokenManager.validateServerToken(token);
|
||||
expect(isValidServerToken).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if token is invalid', async () => {
|
||||
const tokenManager = new ServerTokenManager(configWithSecret);
|
||||
const isServerToken = await tokenManager.isServerToken('random-string');
|
||||
expect(isServerToken).toBe(false);
|
||||
const tokenManager = ServerTokenManager.fromConfig(configWithSecret);
|
||||
const isValidServerToken = await tokenManager.validateServerToken(
|
||||
'random-string',
|
||||
);
|
||||
expect(isValidServerToken).toBe(false);
|
||||
});
|
||||
|
||||
it('should always return true if using noop TokenManager', async () => {
|
||||
const tokenManager = ServerTokenManager.noop();
|
||||
const { token } = await tokenManager.getServerToken();
|
||||
const isValidServerToken0 = await tokenManager.validateServerToken(token);
|
||||
const isValidServerToken1 = await tokenManager.validateServerToken(
|
||||
'random-string',
|
||||
);
|
||||
const isValidServerToken2 = await tokenManager.validateServerToken('');
|
||||
expect(isValidServerToken0).toBe(true);
|
||||
expect(isValidServerToken1).toBe(true);
|
||||
expect(isValidServerToken2).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,15 +19,40 @@ import { Config } from '@backstage/config';
|
||||
import { TokenManager } from './types';
|
||||
|
||||
export class ServerTokenManager implements TokenManager {
|
||||
private key: JWK.OctKey;
|
||||
private key: JWK.OctKey | JWK.NoneKey;
|
||||
|
||||
constructor(config: Config) {
|
||||
const secret =
|
||||
config.getOptionalString('backend.authorization.secret') ?? 'no-secret';
|
||||
this.key = JWK.asKey({ kty: 'oct', k: secret });
|
||||
static noop() {
|
||||
return new ServerTokenManager();
|
||||
}
|
||||
|
||||
async isServerToken(token: string): Promise<boolean> {
|
||||
static fromConfig(config: Config) {
|
||||
const secret = config.getOptionalString('backend.authorization.secret');
|
||||
if (!secret) {
|
||||
throw new Error('No backend auth secret set in app-config');
|
||||
}
|
||||
return new ServerTokenManager(secret);
|
||||
}
|
||||
|
||||
private constructor(secret: string = '') {
|
||||
this.key = secret ? JWK.asKey({ kty: 'oct', k: secret }) : JWK.None;
|
||||
}
|
||||
|
||||
async getServerToken(): Promise<{ token: string }> {
|
||||
if (this.key === JWK.None) {
|
||||
return { token: '' };
|
||||
}
|
||||
|
||||
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
|
||||
algorithm: 'HS256',
|
||||
});
|
||||
return { token: jwt };
|
||||
}
|
||||
|
||||
async validateServerToken(token: string): Promise<boolean> {
|
||||
if (this.key === JWK.None) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
JWT.verify(token, this.key);
|
||||
return true;
|
||||
@@ -35,11 +60,4 @@ export class ServerTokenManager implements TokenManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getServerToken(): Promise<{ token: string }> {
|
||||
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
|
||||
algorithm: 'HS256',
|
||||
});
|
||||
return { token: jwt };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
export interface TokenManager {
|
||||
isServerToken: (token: string) => Promise<boolean>;
|
||||
getServerToken: () => Promise<{ token: string }>;
|
||||
validateServerToken: (token: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ function makeCreateEnv(config: Config) {
|
||||
const root = getRootLogger();
|
||||
const reader = UrlReaders.default({ logger: root, config });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
const tokenManager = new ServerTokenManager(config);
|
||||
const tokenManager = ServerTokenManager.noop();
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ organization:
|
||||
name: My Company
|
||||
|
||||
backend:
|
||||
# Used for enabling authorization, secret is shared by all backend plugins
|
||||
# See authenticate-api-requests.md in the contrib docs for information on the format
|
||||
# authorization:
|
||||
# secret: ${BACKEND_SECRET}
|
||||
baseUrl: http://localhost:7000
|
||||
listen:
|
||||
port: 7000
|
||||
|
||||
@@ -38,7 +38,7 @@ function makeCreateEnv(config: Config) {
|
||||
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
const tokenManager = new ServerTokenManager(config);
|
||||
const tokenManager = ServerTokenManager.noop();
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = root.child({ type: 'plugin', plugin });
|
||||
|
||||
Reference in New Issue
Block a user