0

In the AWS re-Invent video, the solution uses Cognito pool + identity pool

It also uses a lambda authorizer at the API gateway to validate the token and generate the policy. I was reading How to authenticate API Gateway calls with Facebook?

and it says: To use a federated identity, you set the API Gateway method to use “AWS_IAM” authorization. You use Cognito to create a role and associate it with your Cognito identity pool. You then use the Identity and Access Management (IAM) service to grant this role permission to call your API Gateway method.

-> If that's the case, How are we using lambda authorizer instead of IAM authorizer while we are using identity pool as well -> What's the difference between using IAM authorizer and generating IAM policies in custom authorizer as I see happening here:

https://github.com/aws-quickstart/saas-identity-cognito/blob/96531568b5bd30106d115ad7437b2b1886379e57/functions/source/custom-authorizer/index.js or

const Promise = require('bluebird');
const jws = require('jws');
const jwkToPem = require('jwk-to-pem');
const request = require('request-promise');
const AWS = require('aws-sdk');

AWS.config.setPromisesDependency(Promise);
const s3 = new AWS.S3({ apiVersion: '2006-03-01' });

const { env: { s3bucket }} = process
// cache for certificates of issuers
const certificates = {};

// time tenant data was loaded
let tenantLoadTime = 0;
// promise containt tenant data
let tenantPromise;

// this function returns tenant data promise
// refreshes the data if older than a minute
function tenants() {
    if (new Date().getTime() - tenantLoadTime > 1000 * 60) {
        console.log('Tenant info outdated, reloading');
        tenantPromise = s3.getObject({
            Bucket: s3bucket,
            Key: 'tenants.json'
        }).promise().then((data) => {
            const config = JSON.parse(data.Body.toString());
            console.log('Tenant config: %j', config);

            const tenantMap = {};
            config.forEach((t) => { tenantMap[t.iss] = t.id; });

            return tenantMap;
        });
        tenantLoadTime = new Date().getTime();
    }
    return tenantPromise;
}

// helper function to load certificate of issuer
function getCertificate(iss, kid) {
    if (certificates[iss]) {
        // resolve with cached certificate, if exists
        return Promise.resolve(certificates[iss][kid]);
    }
    return request({
        url: `${iss}/.well-known/jwks.json`,
        method: 'GET'
    }).then((rawBody) => {
        const { keys } = JSON.parse(rawBody);
        const pems = keys.map(k => ({ kid: k.kid, pem: jwkToPem(k) }));
        const map = {};
        pems.forEach((e) => { map[e.kid] = e.pem; });
        certificates[iss] = map;
        return map[kid];
    });
}

// extract tenant from a payload
function getTenant(payload) {
    return tenants().then(config => config[payload.iss]);
}

// Help function to generate an IAM policy
function generatePolicy(payload, effect, resource) {
    return getTenant(payload).then((tenant) => {
        if (!tenant) {
            return Promise.reject(new Error('Unknown tenant'));
        }
        const authResponse = {};

        authResponse.principalId = payload.sub;
        if (effect && resource) {
            authResponse.policyDocument = {
                Version: '2012-10-17',
                Statement: [{
                    Action: 'execute-api:Invoke',
                    Effect: effect,
                    Resource: resource
                }]
            };
        }

        // extract tenant id from iss
        payload.tenant = tenant;

        authResponse.context = { payload: JSON.stringify(payload) };

        console.log('%j', authResponse);

        return authResponse;
    });
}

function verifyPayload(payload) {
    if (payload.token_use !== 'id') {
        console.log('Invalid token use');
        return Promise.reject(new Error('Invalid token use'));
    }

    if (parseInt(payload.exp || 0, 10) * 1000 < new Date().getTime()) {
        console.log('Token expired');
        return Promise.reject(new Error('Token expired'));
    }

    // check if iss is a known tenant
    return tenants().then((config) => {
        if (config[payload.iss]) {
            return Promise.resolve();
        }
        console.log('Invalid issuer');
        return Promise.reject();
    });
}

function verifyToken(token, alg, pem) {
    if (!jws.verify(token, alg, pem)) {
        console.log('Invalid Signature');
        return Promise.reject(new Error('Token invalid'));
    }
    return Promise.resolve();
}

exports.handle = function handle(e, context, callback) {
    console.log('processing event: %j', e);

    const { authorizationToken: token } = e;

    if (!token) {
        console.log('No token found');
        return callback('Unauthorized');
    }

    const { header: { alg, kid }, payload: rawToken } = jws.decode(token);
    const payload = JSON.parse(rawToken);

    return verifyPayload(payload)
        .then(() => getCertificate(payload.iss, kid))
        .then(pem => verifyToken(token, alg, pem))
        .then(() => generatePolicy(payload, 'Allow', e.methodArn))
        .then(policy => callback(null, policy))
        .catch((err) => {
            console.log(err);
            return callback('Unauthorized');
        });
};
systemdebt
  • 4,589
  • 10
  • 55
  • 116
  • It is not clear! I think the code is for a Cognito User Pool token verification! (not an identity pool). Also, it seems that the code is for a multi-tenant system. Then, if you use an identity pool per tenant, I think you still need to use a lambda custom authorizer to add tenant info to claims before hitting the target lambda/service. – G. Bahaa Jan 06 '21 at 07:28
  • That's correct. It is for a multi-tenant system. What is IAM authorizer for identity pool then and how is it different from adding tenant info claims? I am a little confused here with what can and cannot be done with these pools – systemdebt Jan 06 '21 at 17:47
  • @G.Bahaa What are we using identity pool for here? From what I see custom claims and tenant context can be managed through user pool itself – systemdebt Jan 06 '21 at 17:56
  • sorry for being late. usually, I think in your case too, there is one API for all tenants. Also, for each tenant, there is a separate Cognito Pool (per the same tutorial). Then, How can you link multiple Cognito to one authorizer? – G. Bahaa Jan 12 '21 at 05:58
  • 1
    Using an identity pool will allow you to grant authenticated users a policy to call the API. Then, I am not sure too how can you add custom claims per tenant. So, the procedure is to verify the token against the appropriate tenant's user pool in the custom authorizer and add whatever necessary claims based on this. The identity pool as designated for authorization is necessary for federated identities in my opinion. read this for understanding the difference between user and identity pools https://thenewstack.io/understanding-aws-cognito-user-and-identity-pools-for-serverless-apps/ – G. Bahaa Jan 12 '21 at 06:08

0 Answers0