There is a longstanding issue where there is no way to go from an AWS Cognito identity ID to a username. The workaround I found is complicated, but should work without posing a security risk (e.g. client setting the username must not be relied on otherwise they could hijack someone’s account). Using Cognito credentials (authenticated), make a call to a Lambda from the client app with an identity ID and ID token. In the Lambda compare the identity ID to the context’s identity ID, verify the ID token, then set a mapping in S3.
As a further optimization, rather than performing this operation every time the client app is loaded, use a custom attribute in the Cognito User Pool to set a field when the mapping is performed. The client can then check the user attribute on load and skip the operation if has already been performed.
import * as AWS from 'aws-sdk/global';
import S3 from 'aws-sdk/clients/s3';
import CognitoIdentity from 'aws-sdk/clients/cognitoidentity';
import jwt from 'jsonwebtoken';
import jwkToPem from 'jwk-to-pem';
import fetch from 'node-fetch';
const env = process.env;
const AWS_S3_BUCKET = env.AWS_S3_BUCKET as string;
const AWS_COGNITO_USER_POOL_URL = env.AWS_COGNITO_USER_POOL_URL as string;
const AWS_COGNITO_USER_POOL_CLIENT_ID = env.AWS_COGNITO_USER_POOL_CLIENT_ID as string;
export const handler = async (event: any, context: any) => {
const context_identity_id = context.identity.cognitoIdentityId;
const request_token = event.idToken;
const request_identity_id = event.identityId;
if (context_identity_id !== request_identity_id) {
console.error('Identity ID not verified', { context_identity_id, request_identity_id });
return;
}
// Verify the JWT to make sure it is valid
// https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
// WARNING: This is not verified, we need to
// get some info from the token to verify it
const decoded_jwt: any = jwt.decode(request_token, { complete: true })!;
if (decoded_jwt.payload.aud !== AWS_COGNITO_USER_POOL_CLIENT_ID) {
throw new Error(`JWT token has invalid audience: got ${decoded_jwt.payload.aud} expected ${AWS_COGNITO_USER_POOL_CLIENT_ID}`);
}
if (decoded_jwt.payload.iss !== AWS_COGNITO_USER_POOL_URL) {
throw new Error(`JWT token has invalid issuer: got ${decoded_jwt.payload.iss} expected ${AWS_COGNITO_USER_POOL_URL}`);
}
// Fetch the key used to sign the jwt
const response = await fetch(`${ AWS_COGNITO_USER_POOL_URL }/.well-known/jwks.json`, { method: 'GET' });
const jwk = await response.json();
const key = jwk.keys.find((key: any) => {
return key.kid === decoded_jwt.header.kid
});
const pem = jwkToPem(key);
jwt.verify(request_token, pem, function (err: any, decoded: any) {
if (err) {
throw new Error(err);
}
});
// Now we can trust the claims of the jwt
const username = decoded_jwt['cognito:username'];
// Write the mapping to S3
const s3 = new S3();
// Add a mapping from identity ID to username
const identity_id_to_username_key = `app/identity_id_to_username/${context_identity_id}/${username}`;
// This S3 location should never be writeable from the client app
// otherwise they could hijack someone's account by setting someone
// else's username!
await s3.putObject({
Bucket: AWS_S3_BUCKET,
Key: identity_id_to_username_key,
// There is no need to put a body in the object since all the
// information is encoded into the key.
Body: undefined
}).promise();
// Add a mapping from username to identity ID
// Not sure if this will be needed, but would be annoying to find
// out later and write a migration.
const username_to_identity_id_key = `app/username_to_identity_id/${username}/${context_identity_id}`;
await s3.putObject({
Bucket: AWS_S3_BUCKET,
Key: username_to_identity_id_key,
Body: undefined
}).promise();
};
Links to this note
-
Integrating Between AWS Services Adds Incidental Complexity
When using multiple AWS services together new problems emerge—IAM permissions and roles, configuration, and load-bearing quirks.