[FEATURE REQUEST] Support veryfing JWTs from AWS ALB
ottokruse opened this issue ยท 19 comments
See #71
Let's look into the padding issue and figure out if we can support verifying ALB JWTs?
(Or conclude we don't want that feature in this lib, as long as we have look into it and make up our minds about what is right)
Chatted about this issue with another user (Nicolas V) and to support ALB we need the following changes:
- Ability to read and parse PEM/PKCS8 public key, because the ALB public key endpoint does not expose a JWKS but rather exposes the public key in PEM format (see below)
- Ability to verify the signature of an ES256, ES384, ES512 algorithm (because of the JWT token in the ALB header x-amzn-oidc-data)
- Ability to read the custom padding (#109 this issue originally)
ALB public key example, as hosted on https://public-keys.auth.elb.<region>.amazonaws.com/<id>
:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGBJCbjNusVteS//606LS3fgYrhQy
vfAh+GbOfy2n7rWgG433Rtb4C/Gxyh6xVoTuvI8hKOqx4qCKjoflk7nGaQ==
-----END PUBLIC KEY-----
We are working on (2) supporting Elliptic Curve and (3) dealing with the weird padding seems simple enough, but (1) is to be designed and implemented: how to "nicely" deal with the non-standard JWKS.
Current thinking is to create a new file and subpath, aws-jwt-verify/jwks-adapters
, where we would predefine a number of JWKS adapters for known parties that do not expose their JWKS in standard way, such as ALB but also Firebase (see #152 ). So that you can do:
import { JwtRsaVerifier } from "aws-jwt-verify";
import { AwsAlbJwksCache } from "aws-jwt-verify/jwks-adapters";
const verifier = JwtRsaVerifier.create(
{
issuer: "<issuer",
audience: "<audience>",
},
{
jwksCache: new AwsAlbJwksCache(),
}
);
Or for Firebase:
import { JwtRsaVerifier } from "aws-jwt-verify";
import { GoogleFirebaseJwksCache } from "aws-jwt-verify/jwks-adapters";
const verifier = JwtRsaVerifier.create(
{
issuer: "<issuer",
audience: "<audience>",
},
{
jwksCache: new GoogleJwksCache(),
}
);
An implementation of such a JWKS cache is pretty simple, here's one for Firebase (adapted from #152 using Firebase as example because we already typed this one out earlier):
import { JsonFetcher, SimpleJsonFetcher } from "aws-jwt-verify/https";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";
import crypto from "crypto";
type GoogleFirebaseJwks = Record<string, string>;
class GoogleFirebaseJwksFetcher implements JsonFetcher {
private fetcher = new SimpleJsonFetcher();
public async fetch(...params: Parameters<JsonFetcher["fetch"]>) {
return this.fetcher.fetch(...params).then((response) => {
const keys = Object.entries(response as GoogleFirebaseJwks).map(
([kid, x509cert]) => {
return {
kid,
kty: "RSA",
use: "sig",
...new crypto.X509Certificate(x509cert).publicKey.export({
format: "jwk",
}),
};
}
);
return { keys };
});
}
}
export class GoogleFirebaseJwksCache extends SimpleJwksCache {
constructor() {
super({ fetcher: new GoogleFirebaseJwksFetcher() });
}
}
Of course having done that we could also expose the complete assembled JWT verifiers at module level (well, at least the AWS ones ;) ):
import { AwsAlbJwtVerifier } from "aws-jwt-verify";
const verifier = AwsAlbJwtVerifier.create(
{
issuer: "<issuer",
audience: "<audience>",
},
Above the current thoughts we have on this, brought in the interest of full disclosure, and to collect feedback! If you have an opinion please share
Already at this point (and even if this does not get implemented, after all) I want to say: thank you for working on this! Unfortunately my knowledge regarding the details is not deep enough for contributing. :(
I would like to thank you again Otto for working on this feature.
I like your proposal about implementing a specific cache and fetcher for the feature 1) Ability to read and parse PEM/PKCS8
like the Google Firebase feature.
According to the proposal, I would like to add what I have in mind about how this feature would be integrated into an real API protected by the ALB cognito feature.
//Verification of the access token (signed by cognito) in the header x-amzn-oidc-accesstoken"
const accessTokenVerifier = CognitoJwtVerifier.create({
userPoolId: 'xxx',
clientId: 'xxx',
tokenUser:'access'
});
const accessTokenPayload = accessTokenVerifier.verify(request.headers["x-amzn-oidc-accesstoken"]);
//Verification of the data token (signed by the ALB) in the header x-amzn-oidc-data"
const dataTokenVerifier = AlbJwtVerifier.create({
region: 'eu-west-1',
});
const dataTokenPayload = dataTokenVerifier.verify(request.headers["x-amzn-oidc-data"]);
The code above assume that the developper want to parse the access and data token. Of course we could imagine that only one of them could be verified if the other is not need.
I have started looking about how to implement the ALB cache and the ALB fetcher. It seems a little bit more complexe than the Google Firebase example. The current SimpleJwksCache algorithm is roughly the one below:
- 1 request all jwks url and put the result in cache (when hydrate)
- 2 when a jwt token need to be verified:
- 2.1 the cache send the jwk associated to the token jwks url
- 2.2 if the entry not exist in the cache, request the jwks url and put the result in cache
- 2.3 a loop is executed to find which jwk is associated to the jwt kid header
However, whean dealing with the ALB, the jwks uri is templated (the kid is inside the uri path). It means that the hydratation (step 1
) feature can't work and the jwks need to be retreive dynamically before a token verification. And the cache is not compatible for now with templated URI (it would cache a request with potentially 2 differents results).
It's why the Alb cache can't reuse the SimpleJwksCache and need to reimplemente is own logic (step 2
). And for the step 1
, the method getJwks
(hydratation feature) need to be empty. I am not sure also that the method getCachedJwk
(verifySync feature) make much more sens on this context, and should be also leave empty I guess. Same for addJwks
.
Finally, the ALB verifier will have only one method verify
. The method cacheJwks
, hydrate
, verifySync
won't be present.
Thanks @NicolasViaud for all the details! I have not yet looked closely at how ALB actually "does it" and was expecting it to be easier.
I want to have a chat with the ALB team about this. Maybe they have some insights to share that could be helpful. I'll report back here once I've done that.
I wonder how many unique kids there can potentially be, and if the associated public keys are indeed immutable as you would expect, but that I will ask the ALB team.
A thing we could start building, if you want to get hands dirty already, is adding an ALB with OIDC auth to the CDK stack for the end to end test: https://github.com/awslabs/aws-jwt-verify/blob/main/tests/cognito/lib/cognito-stack.ts
Update: had a chat with ALB team.
The following is my summary (note: do not treat this as product documentation for ALB, officially this might change at any time):
- For an ALB, at 1 point in time there is only 1 JWK in use for signing JWTs
- However, this JWK gets auto rotated, and during the short rotation window there might actually be 2 JWKs in use, the new one and the old one (both with their own
kid
). - A JWK is immutable, that is, if the JWK changes (key rotation) the new JWK will have a different key ID (
kid
). So you can use thekid
to cache the JWK indefinitely
So, looks like a simple LRU cache with size 2 will thus work, for caching ALB JWKs.
This is the current interface for JwksCache
:
Lines 69 to 74 in 8bb9b6e
Agree with your reasoning above @NicolasViaud and I think we should create a new Jwks cache, we can't reuse the SimpleJwksCache here. The new JWKS cache for AWS ALB, should support getJwk
and getCachedJwk
but it should raise an error if getJwks
or addJwks
is called.
Commenting to signal interest: it would be really great if this library supported JWT verification from ALB The Right Way. We've got all sorts of implementations in various non-JS/TS languages lying around, but having one provided by AWS would set at least my mind at ease.
I am very interested in this. I currently have a Node implementation for verifying Firebase JWT's in lambda@edge with the jsonwebtoken lib. Currently handling fetching and caching of the Google public certs with some custom code. I need to add support for Cognito tokens, so it would be great to consolidate on one library to do it all.
I'm bumping my head into this right now, after authoring a webapp with Cognito auth on the ALB.. I simply want to define a /user
endpoint on my webserver but I could not for the life of me figure out how to validate the token on the backend. Now I understand why, it seems to be pretty complicated and not supported by common JWT libraries.
What are we as users supposed to do in the meantime? Is it safe to just accept the unverified claims from the x-amzn-oidc-data
token if we are behind the ALB? Could this token be overridden somehow by the user?
Here are the ALB docs that specify how to verify their JWT: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html#user-claims-encoding
There is a Python code sample in there that shows how to do it.
I'm very happy to find this thread, along with #166. +1 to the thanks others have already offered.
my experience to this point
I've been trying to use the jsonwebtoken library's decode
and verify
methods on the 'x-amzn-oidc-data'
header value returned from an ALB. My understanding is that the 2 methods essentially do the same thing, but decode
unsafely skips verification.
I can get the decode
method to work by stripping out the non-standard padding, but doing so makes the verify
method error with "invalid signature". I believe that's essentially why we're all here. ๐
(If seeing some example code would be helpful, let me know, and I can share.)
confirming your plans
My initial understanding is that the planned v5.0.0
would entirely supplant my need for using jsonwebtoken - i.e. that this library would verify, and assuming verified, return the payload. Is that correct? Basically, I'm trying to understand how literally to interpret the first bullet of the library's philosophy ...
Do one thing and do it well. Focus solely on verifying JWTs.
Do you have any sense of timeline yet? No worries if it's too early to say, but it would be helpful to know whether help is imminent, or if we need to pivot to a non-ALB-based solution.
AWS references to current state of play
Just within the last week, the Networking & Content Delivery blog posted Security best practices when using ALB authentication. Similar to the docs @ottokruse linked, they offer a Python example. Apparently the Python library can handle the non-standard padding? ๐คท
The blog states ...
There are multiple libraries in various programming languages that validate JWT signatures. We provide a sample using the PyJWT library for Python 3.x, with comments at each step to assist you in building upon it.
They don't offer a mention of how many of those "multiple libraries" will actually support ALB JWTs. I've not yet found any for JavaScript. I'll eventually be searching for a .NET solution as well. If anyone here is aware of libraries that do already work, please share!
The docs seem a little more forthright about the current state of play ...
Standard libraries are not compatible with the padding that is included in the Application Load Balancer authentication token in JWT format.
Yes we aim to make ALB JWT verification work in the lib here, aws-jwt-verify, so you wouldn't need any other lib for it then. (It would eg also include fetching the public key, caching it, and checking standard claims such as exp, iss, and the specifics for AWS ALB).
Timeline without guarantees: this year. It's stalling a bit now due to holiday season, but we're not far off and already did some pre work (support ES256).
Thanks for linking the blog.
Another reason why we should add support for AWS ALB: https://www.miggo.io/resources/uncovering-auth-vulnerability-in-aws-alb-albeast
TL;DR --> AWS ALB has quirks when signing JWTs, makes sure you check iss field (pretty standard although AWS ALB puts this claim in the JWT header instead of in the payload which is non-standard) but ALSO make sure the signer
claim matches your ALB arn (this is specific to AWS ALB, many JWT verification implementations miss this).
AWS ALB puts this claim in the JWT header instead of in the payload which is non-standard
FWIW, I'm seeing an iss
property in the payload, with a value I'd expect.
Interesting, their docs say it's in the header, along with exp
and client
but I didn't check it yet myself. Thanks @rhbecker-uw