awslabs / aws-jwt-verify

JS library for verifying JWTs signed by Amazon Cognito, and any OIDC-compatible IDP that signs JWTs with RS256, RS384, and RS512
Apache License 2.0
598 stars 43 forks source link

[FEATURE REQUEST] Support veryfing JWTs from AWS ALB #109

Open ottokruse opened 1 year ago

ottokruse commented 1 year ago

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)

ottokruse commented 1 year ago

Probably this regex that trips it up: https://github.com/awslabs/aws-jwt-verify/blob/d932662706894631207982b7a916b2a38cf7f02f/src/jwt.ts#L108

Need to reproduce and create a JWT signed by ALB to see what the actual padding characters are, I have yet to observe #71 myself.

ottokruse commented 5 months ago

Chatted about this issue with another user (Nicolas V) and to support ALB we need the following changes:

  1. 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)
  2. Ability to verify the signature of an ES256, ES384, ES512 algorithm (because of the JWT token in the ALB header x-amzn-oidc-data)
  3. Ability to read the custom padding (https://github.com/awslabs/aws-jwt-verify/issues/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-----
ottokruse commented 5 months ago

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>",
  },
ottokruse commented 5 months ago

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

speque commented 5 months ago

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. :(

NicolasViaud commented 5 months ago

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:

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.

ottokruse commented 5 months ago

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.

ottokruse commented 5 months ago

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

ottokruse commented 4 months ago

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):

So, looks like a simple LRU cache with size 2 will thus work, for caching ALB JWKs.

This is the current interface for JwksCache: https://github.com/awslabs/aws-jwt-verify/blob/8bb9b6e4be7b9186a279a35ca82cbbed70405f55/src/jwk.ts#L69-L74

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.

ahinkka commented 4 months ago

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.

ottokruse commented 4 months ago

Thanks @ahinkka and yes totally agree

mikepianka commented 3 months ago

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.