awslabs / aws-support-tools

Tools and sample code provided by AWS Premium Support.
https://aws.amazon.com/premiumsupport/
Apache License 2.0
1.44k stars 796 forks source link

decode-verify-jwt.ts does not validate audience or client Id #121

Open jvarnado opened 4 years ago

jvarnado commented 4 years ago

In the readme it states "To verify the signature of an Amazon Cognito JWT ... Be sure to also verify that ... The audience ("aud") specified in the payload matches the app client ID created in the Amazon Cognito user pool."

This is done in the python example here: https://github.com/awslabs/aws-support-tools/blob/master/Cognito/decode-verify-jwt/decode-verify-jwt.py#L63

In the typescript version, we return the value but do not verify it: https://github.com/awslabs/aws-support-tools/blob/master/Cognito/decode-verify-jwt/decode-verify-jwt.ts#L103

Is this intentional? Can some details about this be shared? Thank you.

amorfica commented 2 years ago

Hi @jvarnado,

I have tried the current code and you have reason. I have opened a pull request (#201) with the necessary changes to check the audience.

You can try with this code:

import {promisify} from 'util';
import * as Axios from 'axios';
import * as jsonwebtoken from 'jsonwebtoken';
const jwkToPem = require('jwk-to-pem');

export interface ClaimVerifyRequest {
  readonly token?: string;
}

export interface ClaimVerifyResult {
  readonly userName: string;
  readonly clientId: string;
  readonly isValid: boolean;
  readonly error?: any;
}

interface TokenHeader {
  kid: string;
  alg: string;
}
interface PublicKey {
  alg: string;
  e: string;
  kid: string;
  kty: string;
  n: string;
  use: string;
}
interface PublicKeyMeta {
  instance: PublicKey;
  pem: string;
}

interface PublicKeys {
  keys: PublicKey[];
}

interface MapOfKidToPublicKey {
  [key: string]: PublicKeyMeta;
}

interface Claim {
  token_use: string;
  auth_time: number;
  iss: string;
  exp: number;
  username: string;
  client_id: string;
}

const USERPOOL_ID = process.env.COGNITO_POOL_ID || '';
if (!USERPOOL_ID) {
  throw new Error('USERPOOL_ID env var required');
}

const CLIENT_ID = process.env.CLIENT_ID || '';
if (!CLIENT_ID) {
  throw new Error('CLIENT_ID env var required');
}

const AWS_REGION = process.env.AWS_REGION || '';
if (!AWS_REGION) {
  throw new Error('AWS_REGION env var required');
}

const cognitoIssuer = `https://cognito-idp.${AWS_REGION}.amazonaws.com/${USERPOOL_ID}`;

let cacheKeys: MapOfKidToPublicKey | undefined;
const getPublicKeys = async (): Promise<MapOfKidToPublicKey> => {
  if (!cacheKeys) {
    const url = `${cognitoIssuer}/.well-known/jwks.json`;
    const publicKeys = await Axios.default.get<PublicKeys>(url);
    cacheKeys = publicKeys.data.keys.reduce((agg, current) => {
      const pem = jwkToPem(current);
      agg[current.kid] = {instance: current, pem};
      return agg;
    }, {} as MapOfKidToPublicKey);
    return cacheKeys;
  } else {
    return cacheKeys;
  }
};

const verifyPromised = promisify(jsonwebtoken.verify.bind(jsonwebtoken));

const handler = async (request: ClaimVerifyRequest): Promise<ClaimVerifyResult> => {
  let result: ClaimVerifyResult;
  try {
    console.log(`user claim verify invoked for ${JSON.stringify(request)}`);
    const token = request.token;
    const tokenSections = (token || '').split('.');
    if (tokenSections.length < 2) {
      throw new Error('requested token is invalid');
    }
    const headerJSON = Buffer.from(tokenSections[0], 'base64').toString('utf8');
    const header = JSON.parse(headerJSON) as TokenHeader;
    const keys = await getPublicKeys();
    const key = keys[header.kid];
    if (key === undefined) {
      throw new Error('claim made for unknown kid');
    }
    const claim = await verifyPromised(token, key.pem) as Claim;
    const currentSeconds = Math.floor( (new Date()).valueOf() / 1000);
    if (currentSeconds > claim.exp || currentSeconds < claim.auth_time) {
      throw new Error('claim is expired or invalid');
    }
    if (claim.iss !== cognitoIssuer) {
      throw new Error('claim issuer is invalid');
    }
    if (claim.token_use !== 'access') {
      throw new Error('claim use is not access');
    }
    // Verify the Audience (use claims['client_id'] if verifying an access token)
    if (claim.client_id !== CLIENT_ID) {
      throw new Error('token was not issued for this audience');
    }

    console.log(`claim confirmed for ${claim.username}`);
    result = {userName: claim.username, clientId: claim.client_id, isValid: true};
  } catch (error) {
    result = {userName: '', clientId: '', error, isValid: false};
  }
  return result;
};

export {handler};
alexwillingham commented 1 year ago

I just got access to a codebase that uses the authorization code from this repo and found the authorization code not checking audience. Is this not a security risk, propagating into AWS Cognito customers' projects as they copy the sample code from this repo?