TL;DR; Firebase > Cognito
We started with Cognito first, but we eventually realized it has an atrocious smell to it when it comes using Federated Identities (e.g., Google Sign-in, Facebook Login, etc.). For Cognito User Pools (i.e., allowing a user to sign up/in with a username and password), you can use the built-in API Gateway Cognito User Pool Authorizer and it works beautifully. You don't need to write your own custom authorizer or anything.
However, if you want to support Federated Identities, you need to change the authentication on your API gateway to IAM Auth, and then have EVERY client sigv4 sign the requests, which turned out to be a thorn in our side and cost significant development time. Option 2 was to have API Gateway generate you code for your API calls for every client... which in my opinion is a testament to the cumbersome integration with Cognito.
We got Firebase working through the custom authorizer for API Gateway. Was a breeze on all clients (iOS, Android and Web). The API Gateway endpoints were linked up to Lambda functions, which were able to communicate with DynamoDB, S3, and other web services on behalf of the user calling the endpoint. The lambda functions knew who the calling user was because the custom authorizer returned the email address in the JWT.
Here's a pretty basic Firebase custom authorizer that returns the user email in the JWT as the principalId:
'use strict';
console.log('Loading function');
var admin = require('firebase-admin');
var serviceAccount = require('./my-secret-json.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://my-app.firebaseio.com'
});
exports.handler = (event, context, callback) => {
var token = event.authorizationToken;
if (token == null) {
callback('Invalid token');
}
else {
admin.auth().verifyIdToken(token)
.then(function (decodedToken) {
var email = decodedToken.email;
var policy = generatePolicy(email);
callback(null, policy);
}).catch(function (error) {
console.log(error);
callback('Unauthorized');
});
}
};
var generatePolicy = function (email) {
return {
principalId: email,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: email ? 'allow' : 'deny',
Resource: '*'
}
]
}
};
}
You can then use $context.authorizer.principalId
in your API Gateway mapping template to retrieve the email and pass it to lambda X.
I initially thought latency would be an issue, but that really doesn't seem to be the case. Any and all latency I've encountered is due to latency of the lambda that is being called due to cold start. I've noticed authorization lambdas live much longer than other lambdas.
This lambda gets called for every backend request. There are couple of things though:
- caching is enabled for 1hr for each JWT, so that greatly simplifies the calls.
- The lambda is being called constantly, so there shouldn't be a cold start, and
- The first MILLION lambda requests/month are free, and then it's $0.20 for every million requests/month after that. So unless you're having your API called BILLION times per month, you aren't going to incur outrageous cost.