add a signer claim to the aws alb auth provider
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-aws-alb-provider': patch
|
||||
---
|
||||
|
||||
Added a `signer` configuration option to validate against the token claims
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user