backend-common: allow for multiple signing keys in ServerTokenManager
Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
Vendored
+10
-7
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user