backend-common: allow for multiple signing keys in ServerTokenManager

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2021-11-25 18:42:07 +00:00
parent 51608d2021
commit 189f7f8a72
4 changed files with 64 additions and 23 deletions
+2 -1
View File
@@ -26,7 +26,8 @@ backend:
# Used for enabling authentication, secret is shared by all backend plugins
# See backend-to-backend-auth.md in the docs for information on the format
# auth:
# secret: ${BACKEND_SECRET}
# keys:
# - secret: ${BACKEND_SECRET}
baseUrl: http://localhost:7007
listen:
port: 7007
+10 -7
View File
@@ -22,13 +22,16 @@ export interface Config {
backend: {
/** Backend configuration for when request authentication is enabled */
auth?: {
/**
* Secret shared by all backends for generating tokens. Should be
* a base64 string, recommended length is 24 bytes.
*
* @visibility secret
*/
secret: string;
/** Keys shared by all backends for signing and validating backend tokens. */
keys: {
/**
* Secret for generating tokens. Should be a base64 string, recommended
* length is 24 bytes.
*
* @visibility secret
*/
secret: string;
}[];
};
baseUrl: string; // defined in core, but repeated here without doc
@@ -18,7 +18,7 @@ import { ServerTokenManager } from './ServerTokenManager';
const emptyConfig = new ConfigReader({});
const configWithSecret = new ConfigReader({
backend: { auth: { secret: 'a-secret-key' } },
backend: { auth: { keys: [{ secret: 'a-secret-key' }] } },
});
describe('ServerTokenManager', () => {
@@ -61,12 +61,42 @@ describe('ServerTokenManager', () => {
expect(() => tokenManager2.validateToken(token)).not.toThrow();
});
it('should throw for server tokens created using a different secret', async () => {
it('should validate server tokens created using any of the secrets', async () => {
const tokenManager1 = ServerTokenManager.fromConfig(
new ConfigReader({ backend: { auth: { secret: 'a1b2c3' } } }),
new ConfigReader({
backend: { auth: { keys: [{ secret: 'a1b2c3' }] } },
}),
);
const tokenManager2 = ServerTokenManager.fromConfig(
new ConfigReader({ backend: { auth: { secret: 'd4e5f6' } } }),
new ConfigReader({
backend: { auth: { keys: [{ secret: 'd4e5f6' }] } },
}),
);
const tokenManager3 = ServerTokenManager.fromConfig(
new ConfigReader({
backend: {
auth: { keys: [{ secret: 'a1b2c3' }, { secret: 'd4e5f6' }] },
},
}),
);
const { token: token1 } = await tokenManager1.getToken();
expect(() => tokenManager3.validateToken(token1)).not.toThrow();
const { token: token2 } = await tokenManager2.getToken();
expect(() => tokenManager3.validateToken(token2)).not.toThrow();
});
it('should throw for server tokens created using a different secret', async () => {
const tokenManager1 = ServerTokenManager.fromConfig(
new ConfigReader({
backend: { auth: { keys: [{ secret: 'a1b2c3' }] } },
}),
);
const tokenManager2 = ServerTokenManager.fromConfig(
new ConfigReader({
backend: { auth: { keys: [{ secret: 'd4e5f6' }] } },
}),
);
const { token } = await tokenManager1.getToken();
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JWK, JWT } from 'jose';
import { JWKS, JWK, JWT } from 'jose';
import { Config } from '@backstage/config';
import { AuthenticationError } from '@backstage/errors';
import { TokenManager } from './types';
@@ -26,40 +26,47 @@ import { TokenManager } from './types';
* @public
*/
export class ServerTokenManager implements TokenManager {
private key: JWK.OctKey | JWK.NoneKey;
private readonly keyStore: JWKS.KeyStore;
static noop() {
return new ServerTokenManager();
}
static fromConfig(config: Config) {
const secret = config.getString('backend.auth.secret');
return new ServerTokenManager(secret);
return new ServerTokenManager(
config
.getConfigArray('backend.auth.keys')
.map(key => key.getString('secret')),
);
}
private constructor(secret: string = '') {
this.key = secret ? JWK.asKey({ kty: 'oct', k: secret }) : JWK.None;
private constructor(secrets?: string[]) {
this.keyStore = new JWKS.KeyStore(
secrets?.length
? secrets.map(secret => JWK.asKey({ kty: 'oct', k: secret }))
: [],
);
}
async getToken(): Promise<{ token: string }> {
if (this.key === JWK.None) {
if (this.keyStore.size === 0) {
return { token: '' };
}
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
const jwt = JWT.sign({ sub: 'backstage-server' }, this.keyStore.all()[0], {
algorithm: 'HS256',
});
return { token: jwt };
}
validateToken(token: string): void {
if (this.key === JWK.None) {
if (this.keyStore.size === 0) {
return;
}
try {
JWT.verify(token, this.key);
JWT.verify(token, this.keyStore);
return;
} catch (e) {
throw new AuthenticationError('Invalid server token');