add a signer claim to the aws alb auth provider

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-08-19 09:29:38 +02:00
parent 3c196411e3
commit 4ea354f480
6 changed files with 83 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-aws-alb-provider': patch
---
Added a `signer` configuration option to validate against the token claims
+9 -3
View File
@@ -11,14 +11,18 @@ and get the user seamlessly authenticated.
## Configuration
The provider configuration can be added to your `app-config.yaml` under the root
`auth` configuration:
`auth` configuration, similar to the following example:
```yaml title="app-config.yaml"
auth:
providers:
awsalb:
issuer: 'https://example.okta.com/oauth2/default' # optional
region: 'us-west-2' # required, use your actual region here
# this is the URL of the IdP you configured
issuer: 'https://example.okta.com/oauth2/default'
# this is the ARN of your ALB instance
signer: 'arn:aws:elasticloadbalancing:us-east-2:123456789012:loadbalancer/app/my-load-balancer/1234567890123456'
# this is the region where your ALB instance resides
region: 'us-west-2'
signIn:
resolvers:
# typically you would pick one of these
@@ -26,6 +30,8 @@ auth:
- resolver: emailLocalPartMatchingUserEntityName
```
Ensure that you have set the signer correctly. It is also recommended that you restrict your target groups' security policy to only accept connections from that ALB.
### Resolvers
This provider includes several resolvers out of the box that you can use:
+19 -1
View File
@@ -19,7 +19,25 @@ export interface Config {
providers?: {
/** @visibility frontend */
awsalb?: {
issuer?: string;
/**
* The issuer IdP URL that was configured in your ALB; this corresponds
* to the `iss` claim in your tokens.
*
* @example https://example.okta.com/oauth2/default
*/
issuer: string;
/**
* The ARN of the ALB that signs the tokens; this corresponds to the
* `signer` claim in your tokens.
*
* @example arn:aws:elasticloadbalancing:us-east-2:123456789012:loadbalancer/app/my-load-balancer/1234567890123456
*/
signer?: string;
/**
* The AWS region where the ALB is located.
*
* @example us-east-2
*/
region: string;
signIn?: {
resolvers: Array<
@@ -21,7 +21,7 @@ import {
ALB_JWT_HEADER,
awsAlbAuthenticator,
} from './authenticator';
import { Config } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import { AuthenticationError } from '@backstage/errors';
describe('AwsAlbProvider', () => {
@@ -35,6 +35,7 @@ describe('AwsAlbProvider', () => {
email: 'user.name@email.test',
exp: Date.now() + 10000,
iss: 'ISSUER_URL',
signer: 'SIGNER_ARN',
};
const signingKey = new TextEncoder().encode('signingKey');
let mockJwt: string;
@@ -87,6 +88,7 @@ describe('AwsAlbProvider', () => {
{ req: mockRequest },
{
issuer: 'ISSUER_URL',
signer: 'SIGNER_ARN',
getKey: jest.fn().mockResolvedValue(signingKey),
},
);
@@ -114,12 +116,13 @@ describe('AwsAlbProvider', () => {
});
});
});
describe('should fail when', () => {
it('Access token is missing', async () => {
await expect(
awsAlbAuthenticator.authenticate(
{ req: mockRequestWithoutAccessToken },
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
{ issuer: 'ISSUER_URL', signer: 'SIGNER_ARN', getKey: jest.fn() },
),
).rejects.toThrow(AuthenticationError);
});
@@ -128,7 +131,7 @@ describe('AwsAlbProvider', () => {
await expect(
awsAlbAuthenticator.authenticate(
{ req: mockRequestWithoutJwt },
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
{ issuer: 'ISSUER_URL', signer: 'SIGNER_ARN', getKey: jest.fn() },
),
).rejects.toThrow(AuthenticationError);
});
@@ -137,7 +140,7 @@ describe('AwsAlbProvider', () => {
await expect(
awsAlbAuthenticator.authenticate(
{ req: mockRequestWithInvalidJwt },
{ issuer: 'ISSUER_URL', getKey: jest.fn() },
{ issuer: 'ISSUER_URL', signer: 'SIGNER_ARN', getKey: jest.fn() },
),
).rejects.toThrow(
'Exception occurred during JWT processing: JWSInvalid: Invalid Compact JWS',
@@ -164,6 +167,7 @@ describe('AwsAlbProvider', () => {
{ req },
{
issuer: 'ISSUER_URL',
signer: undefined,
getKey: jest.fn().mockResolvedValue(signingKey),
},
),
@@ -192,6 +196,36 @@ describe('AwsAlbProvider', () => {
{ req },
{
issuer: 'ISSUER_URL',
signer: 'SIGNER_ARN',
getKey: jest.fn().mockResolvedValue(signingKey),
},
),
).rejects.toThrow(
'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token',
);
});
it('signer is invalid', async () => {
const jwt = await new SignJWT({ signer: 'INVALID_SIGNER_ARN' })
.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 },
{
issuer: 'ISSUER_URL',
signer: 'SIGNER_ARN',
getKey: jest.fn().mockResolvedValue(signingKey),
},
),
@@ -200,15 +234,14 @@ describe('AwsAlbProvider', () => {
);
});
});
describe('should initialize', () => {
it('with default options', async () => {
const config = {
config: {
getString: jest
.fn()
.mockReturnValueOnce('ISSUER_URL')
.mockReturnValueOnce('TEST_REGION'),
} as unknown as Config,
config: new ConfigReader({
issuer: 'ISSUER_URL',
region: 'TEST_REGION',
}),
};
expect(awsAlbAuthenticator.initialize(config)).toEqual({
@@ -36,12 +36,13 @@ export const awsAlbAuthenticator = createProxyAuthenticator({
},
initialize({ config }) {
const issuer = config.getString('issuer');
const signer = config.getOptionalString('signer');
const region = config.getString('region');
const keyCache = new NodeCache({ stdTTL: 3600 });
const getKey = provisionKeyCache(region, keyCache);
return { issuer, getKey };
return { issuer, signer, getKey };
},
async authenticate({ req }, { issuer, getKey }) {
async authenticate({ req }, { issuer, signer, getKey }) {
const jwt = req.header(ALB_JWT_HEADER);
const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER);
@@ -61,8 +62,10 @@ export const awsAlbAuthenticator = createProxyAuthenticator({
const verifyResult = await jwtVerify(jwt, getKey);
const claims = verifyResult.payload as AwsAlbClaims;
if (issuer && claims?.iss !== issuer) {
if (claims?.iss !== issuer) {
throw new AuthenticationError('Issuer mismatch on JWT token');
} else if (signer && claims?.signer !== signer) {
throw new AuthenticationError('Signer mismatch on JWT token');
}
const fullProfile: PassportProfile = {
@@ -38,4 +38,5 @@ export type AwsAlbClaims = {
email: string;
exp: number;
iss: string;
signer: string;
};