Change AuthIdentityTokenManager to ServerTokenManager

Co-authored-by: Joe Porpeglia <joeporpeglia@users.noreply.github.com>
Co-authored-by: Tim Hansen <timbonicus@gmail.com>
Signed-off-by: Nataliya Issayeva <nissayeva@users.noreply.github.com>
This commit is contained in:
Nataliya Issayeva
2021-11-09 17:57:03 -05:00
parent fa6a9b797f
commit b38d77f4f0
12 changed files with 109 additions and 161 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ app:
title: '#backstage'
backend:
auth:
authorization:
secret: ${BACKEND_SECRET}
baseUrl: http://localhost:7000
listen:
@@ -28,8 +28,6 @@ const CLOCK_MARGIN_S = 10;
* @experimental This is not a stable API yet
*/
// TODO: (b2b-auth) move IdentityClient into tokens
// perhaps also create an interface?
export class IdentityClient {
private readonly discovery: PluginEndpointDiscovery;
private readonly issuer: string;
@@ -93,7 +91,6 @@ export class IdentityClient {
* Returns the public signing key matching the given jwt token,
* or null if no matching key was found
*/
// TODO (b2b-auth): switch on type to identify server tokens?
private async getKey(rawJwtToken: string): Promise<JWK.Key | null> {
const { header, payload } = JWT.decode(rawJwtToken, {
complete: true,
@@ -1,70 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/core-app-api';
import { PluginEndpointDiscovery } from '..';
import { IdentityClient } from '../identity';
import { AuthIdentityTokenManager } from './AuthIdentityTokenManager';
const discovery: PluginEndpointDiscovery = {
async getBaseUrl() {
return 'url';
},
async getExternalBaseUrl() {
return 'url';
},
};
const config = new ConfigReader({
backend: { auth: { secret: 'a-secret-key' } },
});
beforeAll(() => {
jest
.spyOn(IdentityClient.prototype, 'authenticate')
.mockImplementation(async (_token?: string) => {
throw new Error('No');
});
});
describe('AuthIdentityTokenManager', () => {
it('should throw in getServerToken if there is no secret in the config', async () => {
const emptyConfig = new ConfigReader({});
const tokenManager = new AuthIdentityTokenManager(discovery, emptyConfig);
await expect(tokenManager.getServerToken()).rejects.toThrow(
'No server token defined in config',
);
});
it('should validate a valid server token', async () => {
const tokenManager = new AuthIdentityTokenManager(discovery, config);
const { token } = await tokenManager.getServerToken();
await expect(tokenManager.validateToken(token)).resolves.toBeUndefined();
});
it('should reject an invalid server token', async () => {
const differentConfig = new ConfigReader({
backend: { auth: { secret: 'a-different-key' } },
});
const tokenManager = new AuthIdentityTokenManager(discovery, config);
const differentTokenManager = new AuthIdentityTokenManager(
discovery,
differentConfig,
);
const { token } = await tokenManager.getServerToken();
await expect(differentTokenManager.validateToken(token)).rejects.toThrow(
'Invalid token',
);
});
});
@@ -1,76 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JWK, JWT } from 'jose';
import { Config } from '@backstage/config';
import { TokenManager } from './types';
import { IdentityClient } from '../identity';
import { PluginEndpointDiscovery } from '../discovery';
// TODO: (b2b-auth) rename this class
export class AuthIdentityTokenManager implements TokenManager {
private identityClient: IdentityClient;
private key?: JWK.OctKey;
constructor(discovery: PluginEndpointDiscovery, config: Config) {
this.identityClient = new IdentityClient({
discovery: discovery,
issuer: 'auth-identity-token-manager',
});
const secret = config.getOptionalString('backend.auth.secret');
if (secret) {
this.key = JWK.asKey({ kty: 'oct', k: secret });
}
}
async getServerToken(): Promise<{ token: string }> {
if (!this.key) {
throw new Error('No server token defined in config');
}
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
algorithm: 'HS256',
});
return { token: jwt };
}
// TODO: (b2b-auth) authenticate returns a Backstage Identity
// need to figure out what to return after validating a server token
async validateToken(token: string): Promise<void> {
let maybeUser;
let maybeServer;
try {
maybeUser = await this.identityClient.authenticate(token);
} catch (error) {
// invalid token
}
try {
if (!this.key) {
throw new Error('No server token defined in config');
}
maybeServer = JWT.verify(token, this.key);
} catch (error) {
// invalid token
}
if (!maybeUser && !maybeServer) {
throw new Error(`Invalid token`);
}
return;
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/core-app-api';
import { ServerTokenManager } from './ServerTokenManager';
const emptyConfig = new ConfigReader({});
const configWithSecret = new ConfigReader({
backend: { authorization: { secret: 'a-secret-key' } },
});
describe('ServerTokenManager', () => {
describe('getServerToken', () => {
it('should always return a token', async () => {
const tokenManager = new ServerTokenManager(configWithSecret);
expect((await tokenManager.getServerToken()).token).toBeDefined();
const emptyTokenManager = new ServerTokenManager(emptyConfig);
expect((await emptyTokenManager.getServerToken()).token).toBeDefined();
});
});
describe('isServerToken', () => {
it('should return true if token is valid', async () => {
const tokenManager = new ServerTokenManager(configWithSecret);
const { token } = await tokenManager.getServerToken();
const isServerToken = await tokenManager.isServerToken(token);
expect(isServerToken).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);
});
});
});
@@ -0,0 +1,45 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JWK, JWT } from 'jose';
import { Config } from '@backstage/config';
import { TokenManager } from './types';
export class ServerTokenManager implements TokenManager {
private key: JWK.OctKey;
constructor(config: Config) {
const secret =
config.getOptionalString('backend.authorization.secret') ?? 'no-secret';
this.key = JWK.asKey({ kty: 'oct', k: secret });
}
async isServerToken(token: string): Promise<boolean> {
try {
JWT.verify(token, this.key);
return true;
} catch (e) {
return false;
}
}
async getServerToken(): Promise<{ token: string }> {
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
algorithm: 'HS256',
});
return { token: jwt };
}
}
+1 -1
View File
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export { AuthIdentityTokenManager } from './AuthIdentityTokenManager';
export { ServerTokenManager } from './ServerTokenManager';
export type { TokenManager } from './types';
+1 -1
View File
@@ -15,6 +15,6 @@
*/
export interface TokenManager {
isServerToken: (token: string) => Promise<boolean>;
getServerToken: () => Promise<{ token: string }>;
validateToken: (token: string) => Promise<void>;
}
+6 -3
View File
@@ -33,7 +33,7 @@ import {
SingleHostDiscovery,
UrlReaders,
useHotMemoize,
AuthIdentityTokenManager,
ServerTokenManager,
IdentityClient,
} from '@backstage/backend-common';
import { Config } from '@backstage/config';
@@ -66,7 +66,7 @@ function makeCreateEnv(config: Config) {
const root = getRootLogger();
const reader = UrlReaders.default({ logger: root, config });
const discovery = SingleHostDiscovery.fromConfig(config);
const tokenManager = new AuthIdentityTokenManager(discovery, config);
const tokenManager = new ServerTokenManager(config);
root.info(`Created UrlReader ${reader}`);
@@ -158,7 +158,10 @@ async function main() {
IdentityClient.getBearerToken(req.headers.authorization) ||
req.cookies.token;
await authEnv.tokenManager.validateToken(token);
const isServerToken = await authEnv.tokenManager.isServerToken(token);
if (!isServerToken) {
await identity.authenticate(token);
}
if (!req.headers.authorization) {
// Authorization header may be forwarded by plugin requests
+2 -2
View File
@@ -17,10 +17,10 @@
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import {
AuthIdentityTokenManager,
PluginCacheManager,
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
UrlReader,
} from '@backstage/backend-common';
@@ -31,5 +31,5 @@ export type PluginEnvironment = {
config: Config;
reader: UrlReader;
discovery: PluginEndpointDiscovery;
tokenManager: AuthIdentityTokenManager;
tokenManager: TokenManager;
};
@@ -17,7 +17,7 @@ import {
DatabaseManager,
SingleHostDiscovery,
UrlReaders,
AuthIdentityTokenManager,
ServerTokenManager,
} from '@backstage/backend-common';
import { Config } from '@backstage/config';
import app from './plugins/app';
@@ -38,7 +38,7 @@ function makeCreateEnv(config: Config) {
const cacheManager = CacheManager.fromConfig(config);
const databaseManager = DatabaseManager.fromConfig(config);
const tokenManager = new AuthIdentityTokenManager(discovery, config);
const tokenManager = new ServerTokenManager(config);
return (plugin: string): PluginEnvironment => {
const logger = root.child({ type: 'plugin', plugin });
@@ -1,10 +1,10 @@
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import {
AuthIdentityTokenManager,
PluginCacheManager,
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
UrlReader,
} from '@backstage/backend-common';
@@ -15,5 +15,5 @@ export type PluginEnvironment = {
config: Config;
reader: UrlReader;
discovery: PluginEndpointDiscovery;
tokenManager: AuthIdentityTokenManager;
tokenManager: TokenManager;
};