Added configurable signing algorithm
Signed-off-by: Manuel Scurti <manuel.scurti@agilelab.it>
This commit is contained in:
@@ -13,20 +13,20 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
SignJWT,
|
||||
generateKeyPair,
|
||||
decodeProtectedHeader,
|
||||
exportJWK,
|
||||
generateKeyPair,
|
||||
SignJWT,
|
||||
} from 'jose';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { IdentityClient } from './IdentityClient';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { IdentityClient } from './IdentityClient';
|
||||
|
||||
interface AnyJWK extends Record<string, string> {
|
||||
use: 'sig';
|
||||
alg: string;
|
||||
@@ -112,6 +112,54 @@ describe('IdentityClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('identity client configuration', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/.well-known/jwks.json`,
|
||||
async (_, res, ctx) => {
|
||||
const keys = await factory.listPublicKeys();
|
||||
return res(ctx.json(keys));
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should defaults to ES256 when no algorithm is supplied', async () => {
|
||||
const identityClient = IdentityClient.create({
|
||||
discovery,
|
||||
issuer: mockBaseUrl,
|
||||
});
|
||||
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const response = await identityClient.authenticate(token);
|
||||
|
||||
// expect that the authenticate is able to validate a token with ES256, which is the one set to FakeTokenFactory.
|
||||
// This means that IdentityClient set ES256 by default.
|
||||
expect(response).toEqual({
|
||||
token: token,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: 'foo',
|
||||
ownershipEntityRefs: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on empty algorithm string', async () => {
|
||||
const identityClient = IdentityClient.create({
|
||||
discovery,
|
||||
issuer: mockBaseUrl,
|
||||
algorithm: '',
|
||||
});
|
||||
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
return expect(
|
||||
async () => await identityClient.authenticate(token),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
|
||||
@@ -13,22 +13,32 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import {
|
||||
createRemoteJWKSet,
|
||||
decodeJwt,
|
||||
jwtVerify,
|
||||
decodeProtectedHeader,
|
||||
FlattenedJWSInput,
|
||||
JWSHeaderParameters,
|
||||
decodeProtectedHeader,
|
||||
jwtVerify,
|
||||
} from 'jose';
|
||||
import { GetKeyFunction } from 'jose/dist/types/types';
|
||||
|
||||
import { BackstageIdentityResponse } from './types';
|
||||
|
||||
const CLOCK_MARGIN_S = 10;
|
||||
|
||||
export type IdentityClientOptions = {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
issuer: string;
|
||||
|
||||
/** JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256.
|
||||
* Must match the algorithm defined in TokenFactory.
|
||||
* More info on supported algorithms: https://github.com/panva/jose */
|
||||
algorithm?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An identity client to interact with auth-backend and authenticate Backstage
|
||||
* tokens
|
||||
@@ -39,25 +49,21 @@ const CLOCK_MARGIN_S = 10;
|
||||
export class IdentityClient {
|
||||
private readonly discovery: PluginEndpointDiscovery;
|
||||
private readonly issuer: string;
|
||||
private readonly algorithm: string;
|
||||
private keyStore?: GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput>;
|
||||
private keyStoreUpdated: number = 0;
|
||||
|
||||
/**
|
||||
* Create a new {@link IdentityClient} instance.
|
||||
*/
|
||||
static create(options: {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
issuer: string;
|
||||
}): IdentityClient {
|
||||
static create(options: IdentityClientOptions): IdentityClient {
|
||||
return new IdentityClient(options);
|
||||
}
|
||||
|
||||
private constructor(options: {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
issuer: string;
|
||||
}) {
|
||||
private constructor(options: IdentityClientOptions) {
|
||||
this.discovery = options.discovery;
|
||||
this.issuer = options.issuer;
|
||||
this.algorithm = options.algorithm ?? 'ES256';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +88,7 @@ export class IdentityClient {
|
||||
throw new AuthenticationError('No keystore exists');
|
||||
}
|
||||
const decoded = await jwtVerify(token, this.keyStore, {
|
||||
algorithms: ['ES256'],
|
||||
algorithms: [this.algorithm],
|
||||
audience: 'backstage',
|
||||
issuer: this.issuer,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user