remove jwt-decode from aws-alb provider
Signed-off-by: Jamie Klassen <jamie.klassen@broadcom.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-aws-alb-provider': patch
|
||||
---
|
||||
|
||||
Refactored to use the `jose` library for JWT handling.
|
||||
@@ -38,14 +38,15 @@
|
||||
"@backstage/plugin-auth-backend": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"jose": "^4.6.0",
|
||||
"jwt-decode": "^3.1.0",
|
||||
"node-cache": "^5.1.2"
|
||||
"node-cache": "^5.1.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"msw": "^2.0.8"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -14,36 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { SignJWT } from 'jose';
|
||||
import {
|
||||
ALB_ACCESS_TOKEN_HEADER,
|
||||
ALB_JWT_HEADER,
|
||||
awsAlbAuthenticator,
|
||||
} from './authenticator';
|
||||
import { jwtVerify } from 'jose';
|
||||
import express from 'express';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { Config } from '@backstage/config';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
|
||||
const jwtMock = jwtVerify as jest.Mocked<any>;
|
||||
const mockJwt =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
|
||||
const mockAccessToken = 'ACCESS_TOKEN';
|
||||
const mockClaims = {
|
||||
sub: '1234567890',
|
||||
name: 'User Name',
|
||||
family_name: 'Name',
|
||||
given_name: 'User',
|
||||
picture: 'PICTURE_URL',
|
||||
email: 'user.name@email.test',
|
||||
exp: 1632833763,
|
||||
iss: 'ISSUER_URL',
|
||||
};
|
||||
jest.mock('jose');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('AwsAlbProvider', () => {
|
||||
const mockAccessToken = 'ACCESS_TOKEN';
|
||||
const mockClaims = {
|
||||
sub: '1234567890',
|
||||
name: 'User Name',
|
||||
family_name: 'Name',
|
||||
given_name: 'User',
|
||||
picture: 'PICTURE_URL',
|
||||
email: 'user.name@email.test',
|
||||
exp: Date.now() + 10000,
|
||||
iss: 'ISSUER_URL',
|
||||
};
|
||||
const signingKey = new TextEncoder().encode('signingKey');
|
||||
let mockJwt: string;
|
||||
const mockRequest = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_JWT_HEADER) {
|
||||
@@ -54,6 +48,16 @@ describe('AwsAlbProvider', () => {
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
const mockRequestWithInvalidJwt = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_JWT_HEADER) {
|
||||
return 'invalid.jwt';
|
||||
} else if (name === ALB_ACCESS_TOKEN_HEADER) {
|
||||
return mockAccessToken;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
const mockRequestWithoutJwt = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_ACCESS_TOKEN_HEADER) {
|
||||
@@ -71,13 +75,20 @@ describe('AwsAlbProvider', () => {
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockJwt = await new SignJWT(mockClaims)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.sign(signingKey);
|
||||
});
|
||||
|
||||
describe('should transform to type AwsAlbResponse', () => {
|
||||
it('when JWT is valid and identity is resolved successfully', async () => {
|
||||
jwtMock.mockReturnValueOnce(Promise.resolve({ payload: mockClaims }));
|
||||
|
||||
const response = await awsAlbAuthenticator.authenticate(
|
||||
{ req: mockRequest },
|
||||
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
|
||||
{
|
||||
issuer: 'ISSUER_URL',
|
||||
getKey: jest.fn().mockResolvedValue(signingKey),
|
||||
},
|
||||
);
|
||||
expect(response).toEqual({
|
||||
result: {
|
||||
@@ -119,27 +130,38 @@ describe('AwsAlbProvider', () => {
|
||||
});
|
||||
|
||||
it('JWT is invalid', async () => {
|
||||
jwtMock.mockImplementationOnce(() => {
|
||||
throw new Error('bad JWT');
|
||||
});
|
||||
|
||||
await expect(
|
||||
awsAlbAuthenticator.authenticate(
|
||||
{ req: mockRequest },
|
||||
{ req: mockRequestWithInvalidJwt },
|
||||
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Exception occurred during JWT processing: Error: bad JWT',
|
||||
'Exception occurred during JWT processing: JWSInvalid: Invalid Compact JWS',
|
||||
);
|
||||
});
|
||||
|
||||
it('issuer is missing', async () => {
|
||||
jwtMock.mockReturnValueOnce({});
|
||||
const jwt = await new SignJWT({})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.sign(signingKey);
|
||||
const req = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_JWT_HEADER) {
|
||||
return jwt;
|
||||
} else if (name === ALB_ACCESS_TOKEN_HEADER) {
|
||||
return mockAccessToken;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
|
||||
await expect(
|
||||
awsAlbAuthenticator.authenticate(
|
||||
{ req: mockRequest },
|
||||
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
|
||||
{ req },
|
||||
{
|
||||
issuer: 'ISSUER_URL',
|
||||
getKey: jest.fn().mockResolvedValue(signingKey),
|
||||
},
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token',
|
||||
@@ -147,14 +169,27 @@ describe('AwsAlbProvider', () => {
|
||||
});
|
||||
|
||||
it('issuer is invalid', async () => {
|
||||
jwtMock.mockReturnValueOnce({
|
||||
iss: 'INVALID_ISSUE_URL',
|
||||
});
|
||||
const jwt = await new SignJWT({ iss: 'INVALID_ISSUER_URL' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.sign(signingKey);
|
||||
const req = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_JWT_HEADER) {
|
||||
return jwt;
|
||||
} else if (name === ALB_ACCESS_TOKEN_HEADER) {
|
||||
return mockAccessToken;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
|
||||
await expect(
|
||||
awsAlbAuthenticator.authenticate(
|
||||
{ req: mockRequest },
|
||||
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
|
||||
{ req },
|
||||
{
|
||||
issuer: 'ISSUER_URL',
|
||||
getKey: jest.fn().mockResolvedValue(signingKey),
|
||||
},
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token',
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import NodeCache from 'node-cache';
|
||||
import { makeProfileInfo, provisionKeyCache } from './helpers';
|
||||
import * as crypto from 'crypto';
|
||||
import { JWTHeaderParameters } from 'jose';
|
||||
import { PassportProfile } from '@backstage/plugin-auth-node';
|
||||
import jwtDecoder from 'jwt-decode';
|
||||
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
@@ -21,40 +14,47 @@ import jwtDecoder from 'jwt-decode';
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const mockKey = async () => {
|
||||
return `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
|
||||
yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
|
||||
-----END PUBLIC KEY-----
|
||||
`;
|
||||
};
|
||||
import * as crypto from 'crypto';
|
||||
import { JWTHeaderParameters, UnsecuredJWT } from 'jose';
|
||||
import NodeCache from 'node-cache';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { PassportProfile } from '@backstage/plugin-auth-node';
|
||||
import { makeProfileInfo, provisionKeyCache } from './helpers';
|
||||
|
||||
jest.mock('crypto');
|
||||
const cryptoMock = crypto as jest.Mocked<any>;
|
||||
jest.mock('node-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: async () => {
|
||||
return {
|
||||
text: async () => {
|
||||
return mockKey();
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const jwtMock = jwtDecoder as jest.Mocked<any>;
|
||||
jest.mock('jwt-decode');
|
||||
|
||||
describe('helpers', () => {
|
||||
const server = setupServer();
|
||||
setupRequestMockHandlers(server);
|
||||
|
||||
const nodeCache = jest.fn() as unknown as NodeCache;
|
||||
nodeCache.set = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.use(
|
||||
http.get(
|
||||
'https://public-keys.auth.elb.eu-west-1.amazonaws.com/kid',
|
||||
() =>
|
||||
new HttpResponse(
|
||||
`-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
|
||||
yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
|
||||
-----END PUBLIC KEY-----
|
||||
`,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a key', () => {
|
||||
const getKey = provisionKeyCache('eu-west-1', nodeCache);
|
||||
expect(getKey).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return a key from cache', async () => {
|
||||
const getKey = provisionKeyCache('eu-west-1', nodeCache);
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('helpers', () => {
|
||||
|
||||
expect(key).toBe('key');
|
||||
});
|
||||
|
||||
it('should update cache if key is not found', async () => {
|
||||
const getKey = provisionKeyCache('eu-west-1', nodeCache);
|
||||
|
||||
@@ -77,6 +78,7 @@ describe('helpers', () => {
|
||||
await getKey({ kid: 'kid' } as unknown as JWTHeaderParameters);
|
||||
expect(nodeCache.set).toHaveBeenCalledWith('kid', 'key');
|
||||
});
|
||||
|
||||
it('should throw error if key is not found', async () => {
|
||||
const getKey = provisionKeyCache('eu-west-1', nodeCache);
|
||||
|
||||
@@ -87,6 +89,7 @@ describe('helpers', () => {
|
||||
getKey({ kid: 'kid' } as unknown as JWTHeaderParameters),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw if key is not present in request header', async () => {
|
||||
const getKey = provisionKeyCache('eu-west-1', nodeCache);
|
||||
|
||||
@@ -119,19 +122,19 @@ describe('makeProfileInfo', () => {
|
||||
};
|
||||
expect(makeProfileInfo(profile, accessToken)).toEqual(result);
|
||||
});
|
||||
|
||||
it('should return profile info from id token', () => {
|
||||
jwtMock.mockReturnValueOnce({
|
||||
email: 'email',
|
||||
picture: 'picture',
|
||||
name: 'displayName',
|
||||
});
|
||||
const profile = {
|
||||
name: {
|
||||
familyName: 'familyName',
|
||||
givenName: 'givenName',
|
||||
},
|
||||
} as PassportProfile;
|
||||
const idToken = 'idToken';
|
||||
const idToken = new UnsecuredJWT({
|
||||
email: 'email',
|
||||
picture: 'picture',
|
||||
name: 'displayName',
|
||||
}).encode();
|
||||
const result = {
|
||||
email: 'email',
|
||||
picture: 'picture',
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { PassportProfile } from '@backstage/plugin-auth-node/';
|
||||
import { ProfileInfo } from '@backstage/plugin-auth-node';
|
||||
import { KeyObject } from 'crypto';
|
||||
import jwtDecoder from 'jwt-decode';
|
||||
import NodeCache from 'node-cache';
|
||||
import * as crypto from 'crypto';
|
||||
import { JWTHeaderParameters } from 'jose';
|
||||
import { JWTHeaderParameters, decodeJwt } from 'jose';
|
||||
import NodeCache from 'node-cache';
|
||||
import fetch from 'node-fetch';
|
||||
import { PassportProfile, ProfileInfo } from '@backstage/plugin-auth-node';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
|
||||
export const makeProfileInfo = (
|
||||
@@ -45,7 +44,11 @@ export const makeProfileInfo = (
|
||||
|
||||
if ((!email || !picture || !displayName) && idToken) {
|
||||
try {
|
||||
const decoded: Record<string, string> = jwtDecoder(idToken);
|
||||
const decoded: Record<string, string> = decodeJwt(idToken) as {
|
||||
email?: string;
|
||||
picture?: string;
|
||||
name?: string;
|
||||
};
|
||||
if (!email && decoded.email) {
|
||||
email = decoded.email;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express-session": "^1.17.2",
|
||||
"@types/jwt-decode": "^3.1.0",
|
||||
"@types/passport-auth0": "^1.0.5",
|
||||
"@types/passport-github2": "^1.2.4",
|
||||
"@types/passport-google-oauth20": "^2.0.3",
|
||||
|
||||
@@ -4677,8 +4677,9 @@ __metadata:
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
express: ^4.18.2
|
||||
jose: ^4.6.0
|
||||
jwt-decode: ^3.1.0
|
||||
msw: ^2.0.8
|
||||
node-cache: ^5.1.2
|
||||
node-fetch: ^2.6.7
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -4913,7 +4914,6 @@ __metadata:
|
||||
"@types/cookie-parser": ^1.4.2
|
||||
"@types/express": ^4.17.6
|
||||
"@types/express-session": ^1.17.2
|
||||
"@types/jwt-decode": ^3.1.0
|
||||
"@types/passport": ^1.0.3
|
||||
"@types/passport-auth0": ^1.0.5
|
||||
"@types/passport-github2": ^1.2.4
|
||||
@@ -18482,15 +18482,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jwt-decode@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@types/jwt-decode@npm:3.1.0"
|
||||
dependencies:
|
||||
jwt-decode: "*"
|
||||
checksum: 82ff0b3826e5d9da48be1f11e16fec96e56dd946995edaa100682202b9b7beb30fb04a353cbabd378d3b13a24241b48a0b662a4f181bae7f758147d92368d930
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/keyv@npm:*, @types/keyv@npm:^3.1.1":
|
||||
version: 3.1.4
|
||||
resolution: "@types/keyv@npm:3.1.4"
|
||||
@@ -32363,13 +32354,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jwt-decode@npm:*, jwt-decode@npm:^3.1.0":
|
||||
version: 3.1.2
|
||||
resolution: "jwt-decode@npm:3.1.2"
|
||||
checksum: 20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kafkajs@npm:^2.0.0":
|
||||
version: 2.2.4
|
||||
resolution: "kafkajs@npm:2.2.4"
|
||||
|
||||
Reference in New Issue
Block a user