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

verify() function #93

Closed wz2b closed 1 year ago

wz2b commented 1 year ago

This is more of a typescript question than anything, but I encountered this in aws-jwt-verify and I want to understand what it means when you declare a method parameter as ...[a, b]: SomeType

I'm also not entirely sure why this method is generic - what is the purpose of the type parameter T?

Would somebody mind pointing me toward some documentation? I have been searching, but those are hard search terms and I don't even know what to call this thing. Thanks.

    /**
     * Verify (asynchronously) a JWT that is signed by Amazon Cognito.
     * This call is asynchronous, and the JWKS will be fetched from the JWKS uri,
     * in case it is not yet available in the cache.
     *
     * @param jwt The JWT, as string
     * @param props Verification properties
     * @returns Promise that resolves to the payload of the JWT––if the JWT is valid, otherwise the promise rejects
     */
    verify<T extends SpecificVerifyProperties>(...[jwt, properties]: CognitoVerifyParameters<SpecificVerifyProperties>):
                Promise<CognitoIdOrAccessTokenPayload<IssuerConfig, T>>;
wz2b commented 1 year ago

Extending that question, am I close here? And do my names for these types make any sense?

/*
 * Declare extra things I want, some that are part of the standard claims,
 * others that are custom
 */
interface ExtraClaims {
    email: string,

    /* The actual custom property shoes up as custom:customer - is this right? */
    "custom:customer": string
}

type TokenPayload = CognitoIdOrAccessTokenPayload<CognitoJwtVerifierMultiProperties, ExtraClaims>;

/*
 * getVerifiedToken() - takes a token, verifies it, and returns the decoded contents
 * asynchronously
 */
async function getVerifiedToken(token: string): Promise<TokenPayload> {
    const verifier = CognitoJwtVerifier.create<CognitoJwtVerifierMultiProperties>({
        userPoolId: "<user_pool_id>",
        tokenUse: "id",
        clientId: null /* don't care? */
    });

    return verifier.verify(token)
        .then((data: TokenPayload) => {
            console.log("User is", data.username);
            console.log("His e-mail is", data.email);
            console.log("Groups this person is in", data.groups);
            return data;
        })
        .catch(error => {
            console.log("Token could not be verified", error);
            return Promise.reject("Nope");
        })
}
ottokruse commented 1 year ago

Here's some background first.

This lib uses some TypeScript magic (/shenanigans) to a.o. alert users to properly supply the required verification properties either at verifier level upon calling create, or later upon calling verify.

Notably, if you create the verifier without specifying e.g. clientId the TypeScript types will enforce that you must then provide clientId upon calling verify.

Example. If you provide all such parameters while calling create you can later just call verify with just the JWT:

const verifier = CognitoJwtVerifier.create({userPoolId: "xxx", clientId: "yyy", tokenUse: "access"})
verifier.verify("ey....") // just 1 parameter needed, the JWT

But if you did not provide e.g. the clientId while calling create:

const verifier = CognitoJwtVerifier.create({userPoolId: "yours", tokenUse: "access"}) // clientId not provided yet
verifier.verify("ey....", { clientId: "yours" }) // 2nd parameter is required now, or it won't compile

The intent of this is to help developers be explicit, and make it hard to do insecure things––such as not checking clientId. You should check that, unless you're quite sure you don't.

(If I'm very honest I'm no longer sure that this TypeScript magic is worth it. The explicitness it enforces is nice but it leads to impenetrable source code. But here you have it, this was the rationale of it all.)

Now to some concrete answers.

This is more of a typescript question than anything, but I encountered this in aws-jwt-verify and I want to understand what it means when you declare a method parameter as ...[a, b]: SomeType

See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements

I'm also not entirely sure why this method is generic - what is the purpose of the type parameter T?

The magic explained above works using this generic. The type of the parameters to verify depend on the type of the parameters that you provided to create. We use a generic to make this happen.

Extending that question, am I close here?

I'm not sure all the explicit typings you're putting in are helping you. You should be able to just call create without providing any generic type as TypeScript will infer it from your parameters.

If you just wanna check an extra claim that you put on the JWT, you can just do so, this compiles:

import { CognitoJwtVerifier } from "aws-jwt-verify";

const verifier = CognitoJwtVerifier.create({
  userPoolId: "xxx",
  clientId: "yyy",
  tokenUse: "access",
});

verifier
  .verify("ey...")
  .then((payload) => payload["custom:customer"] === "CustomerID"); // this field is of type Json

Otherwise use a cast:

verifier
  .verify("ey...")
  .then((payload) => acceptOnlyString(payload["custom:customer"] as string));

function acceptOnlyString(s: string) {
  return s;
}

We should document all of this. Might you be interested in submitting a PR ...

ottokruse commented 1 year ago

Did that help @wz2b ?

wz2b commented 1 year ago

Did that help @wz2b ?

YES! Very much! And I am glad you pinged me about this again because for some reason I missed the notification that you responded 9 days ago. Sorry about that, but thank you!!!

wz2b commented 1 year ago

I don't feel expert enough to add documentation and submit a PR yet, but let me get all this working and then I'll think about it!