Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ tests/cognito/cdk.out/*
tests/import-tests/typescript.js
tests/import-tests/should-not-compile.js
tests/vite-app/util/generateExampleTokens.js
tests/vite-app/src/style.css
101 changes: 81 additions & 20 deletions src/cognito-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export interface CognitoVerifyProperties {
graceSeconds?: number;
/**
* Your custom function with checks. It will be called, at the end of the verification,
* after standard verifcation checks have all passed.
* after standard verification checks have all passed.
* Throw an error in this function if you want to reject the JWT for whatever reason you deem fit.
* Your function will be called with a properties object that contains:
* - the decoded JWT header
Expand Down Expand Up @@ -223,31 +223,59 @@ export class CognitoJwtVerifier<
> extends JwtVerifierBase<SpecificVerifyProperties, IssuerConfig, MultiIssuer> {
private static USER_POOL_ID_REGEX =
/^(?<region>[a-z]{2}-(gov-)?[a-z]+-\d)_[a-zA-Z0-9]+$/;

private constructor(
props: CognitoJwtVerifierProperties | CognitoJwtVerifierMultiProperties[],
jwksCache?: JwksCache
) {
const issuerConfig = Array.isArray(props)
? (props.map((p) => ({
...p,
...CognitoJwtVerifier.parseUserPoolId(p.userPoolId),
audience: null, // checked instead by validateCognitoJwtFields
})) as IssuerConfig[])
: ({
...props,
...CognitoJwtVerifier.parseUserPoolId(props.userPoolId),
audience: null, // checked instead by validateCognitoJwtFields
} as IssuerConfig);
? (props.flatMap((p) => {
// For each user pool, create configs for BOTH region and global issuer formats
// This allows seamless verification during the transition period
const regionFormat = CognitoJwtVerifier.parseUserPoolId(p.userPoolId);
const globalFormatIssuer = `https://issuer.cognito-idp.${p.userPoolId.split("_")[0]}.amazonaws.com/${p.userPoolId}`;
const globalFormat = CognitoJwtVerifier.parseUserPoolId(
p.userPoolId,
globalFormatIssuer
);

// audience checked by validateCognitoJwtFields
return [
{ ...p, ...regionFormat, audience: null },
{ ...p, ...globalFormat, audience: null },
];
}) as IssuerConfig[])
: ([
// Single user pool - create configs for both formats
// audience checked by validateCognitoJwtFields
{
...props,
...CognitoJwtVerifier.parseUserPoolId(props.userPoolId),
audience: null,
},
{
...props,
...CognitoJwtVerifier.parseUserPoolId(
props.userPoolId,
`https://issuer.cognito-idp.${props.userPoolId.split("_")[0]}.amazonaws.com/${props.userPoolId}`
),
audience: null,
},
] as IssuerConfig[]);
super(issuerConfig, jwksCache);
}

/**
* Parse a User Pool ID, to extract the issuer and JWKS URI
*
* @param userPoolId The User Pool ID
* @param jwtIssuer Optional issuer claim from the JWT being verified, used to determine the issuer format
* @returns The issuer and JWKS URI for the User Pool
*/
public static parseUserPoolId(userPoolId: string): {
public static parseUserPoolId(
userPoolId: string,
jwtIssuer?: string
): {
issuer: string;
jwksUri: string;
} {
Expand All @@ -258,7 +286,23 @@ export class CognitoJwtVerifier<
);
}
const region = match.groups!.region;
const issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;

// Determine issuer format based on JWT's issuer claim if provided
// Global format: https://issuer.cognito-idp.<region>.amazonaws.com/<userPoolId>
// Region format: https://cognito-idp.<region>.amazonaws.com/<userPoolId>
let issuer: string;
if (jwtIssuer) {
// Use the format from the JWT's issuer claim
if (jwtIssuer.includes("issuer.cognito-idp")) {
issuer = `https://issuer.cognito-idp.${region}.amazonaws.com/${userPoolId}`;
} else {
issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;
}
} else {
// Default to region format for backward compatibility when no JWT issuer is provided
issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;
}

return {
issuer,
jwksUri: `${issuer}/.well-known/jwks.json`,
Expand Down Expand Up @@ -378,13 +422,30 @@ export class CognitoJwtVerifier<
? [jwks: Jwks, userPoolId?: string]
: [jwks: Jwks, userPoolId: string]
): void {
let issuer: string | undefined;
if (userPoolId !== undefined) {
issuer = CognitoJwtVerifier.parseUserPoolId(userPoolId).issuer;
} else if (Array.from(this.issuersConfig).length > 1) {
throw new ParameterValidationError("userPoolId must be provided");
let poolId: string | undefined = userPoolId;

if (!poolId) {
// Get unique user pool IDs from configs (since we store both region and global format)
const uniqueUserPoolIds = new Set(
Array.from(this.issuersConfig.values()).map(
(config) => config.userPoolId
)
);
if (uniqueUserPoolIds.size > 1) {
throw new ParameterValidationError("userPoolId must be provided");
}
poolId = Array.from(uniqueUserPoolIds)[0];
}
const issuerConfig = this.getIssuerConfig(issuer);
super.cacheJwks(jwks, issuerConfig.issuer);

// Cache for both region and global issuer formats
const regionFormatIssuer =
CognitoJwtVerifier.parseUserPoolId(poolId).issuer;
const globalFormatIssuer = CognitoJwtVerifier.parseUserPoolId(
poolId,
`https://issuer.cognito-idp.${poolId.split("_")[0]}.amazonaws.com/${poolId}`
).issuer;

super.cacheJwks(jwks, regionFormatIssuer);
super.cacheJwks(jwks, globalFormatIssuer);
}
}
103 changes: 103 additions & 0 deletions tests/unit/cognito-verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,109 @@ describe("unit tests cognito verifier", () => {
CognitoJwtVerifier.parseUserPoolId("foo-central-bar_cfE3xfsaf")
).toThrow("Invalid Cognito User Pool ID");
});

test("parseUserPoolId with region format (default)", () => {
const userPoolId = "us-east-1_123456";
const { issuer, jwksUri } =
CognitoJwtVerifier.parseUserPoolId(userPoolId);
expect(issuer).toBe(
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_123456"
);
expect(jwksUri).toBe(
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_123456/.well-known/jwks.json"
);
});

test("parseUserPoolId with global format from JWT", () => {
const userPoolId = "us-east-1_123456";
const jwtIssuer =
"https://issuer.cognito-idp.us-east-1.amazonaws.com/us-east-1_123456";
const { issuer, jwksUri } = CognitoJwtVerifier.parseUserPoolId(
userPoolId,
jwtIssuer
);
expect(issuer).toBe(
"https://issuer.cognito-idp.us-east-1.amazonaws.com/us-east-1_123456"
);
expect(jwksUri).toBe(
"https://issuer.cognito-idp.us-east-1.amazonaws.com/us-east-1_123456/.well-known/jwks.json"
);
});

test("parseUserPoolId with region format from JWT", () => {
const userPoolId = "us-east-1_123456";
const jwtIssuer =
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_123456";
const { issuer, jwksUri } = CognitoJwtVerifier.parseUserPoolId(
userPoolId,
jwtIssuer
);
expect(issuer).toBe(
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_123456"
);
expect(jwksUri).toBe(
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_123456/.well-known/jwks.json"
);
});

test("verify JWT with global issuer format", () => {
const userPoolId = "us-east-1_123456";
const globalIssuer =
"https://issuer.cognito-idp.us-east-1.amazonaws.com/us-east-1_123456";
const signedJwt = signJwt(
{ kid: keypair.jwk.kid },
{
hello: "world",
iss: globalIssuer,
token_use: "access",
client_id: "test-client",
},
keypair.privateKey
);
const cognitoVerifier = CognitoJwtVerifier.create({
userPoolId,
tokenUse: "access",
clientId: "test-client",
});

// The JWKS cache is keyed by jwksUri, so we need to cache for the global format URL
// In practice, this would be fetched automatically on first verification
const globalFormatJwksUri = `${globalIssuer}/.well-known/jwks.json`;
cognitoVerifier["jwksCache"].addJwks(globalFormatJwksUri, keypair.jwks);

expect.assertions(1);
expect(cognitoVerifier.verifySync(signedJwt)).toMatchObject({
hello: "world",
iss: globalIssuer,
});
});

test("verify JWT with region issuer format", async () => {
const userPoolId = "us-east-1_123456";
const regionIssuer =
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_123456";
const signedJwt = signJwt(
{ kid: keypair.jwk.kid },
{
hello: "world",
iss: regionIssuer,
token_use: "access",
client_id: "test-client",
},
keypair.privateKey
);
const cognitoVerifier = CognitoJwtVerifier.create({
userPoolId,
tokenUse: "access",
clientId: "test-client",
});
cognitoVerifier.cacheJwks(keypair.jwks);
expect.assertions(1);
expect(await cognitoVerifier.verify(signedJwt)).toMatchObject({
hello: "world",
iss: regionIssuer,
});
});
});
});

Expand Down
6 changes: 3 additions & 3 deletions tests/vite-app/src/style.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#app {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
Expand Down