Firebase authentication vs AWS Cognito
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.
We are doing the same.
We started with Cognito but moved to Firebase because we were not satisfied with the way AWS Android SDK implements the authentication flow with Google and Facebook: the code is quite old, it makes use of deprecated methods and generally requires rewriting. On the other hand, Firebase authentication is obviously working seamlessly.
When you don't use Cognito, you need to implement your custom authenticator in AWS API Gateway which is quite easy and is described in https://aws.amazon.com/blogs/mobile/integrating-amazon-cognito-user-pools-with-api-gateway/. Firebase instructions for token validation are in https://firebase.google.com/docs/auth/admin/verify-id-tokens
The following is an excerpt of my authenticator's code:
'use strict';
// Firebase initialization
// console.log('Loading function');
const admin = require("firebase-admin");
admin.initializeApp({
credential: admin.credential.cert("xxx.json"),
databaseURL: "https://xxx.firebaseio.com"
});
// Standard AWS AuthPolicy - don't touch !!
...
// END Standard AWS AuthPolicy - don't touch !!
exports.handler = (event, context, callback) => {
// console.log('Client token:', event.authorizationToken);
// console.log('Method ARN:', event.methodArn);
// validate the incoming token
// and produce the principal user identifier associated with the token
// this is accomplished by Firebase Admin
admin.auth().verifyIdToken(event.authorizationToken)
.then(function(decodedToken) {
let principalId = decodedToken.uid;
// console.log(JSON.stringify(decodedToken));
// if the token is valid, a policy must be generated which will allow or deny access to the client
// if access is denied, the client will recieve a 403 Access Denied response
// if access is allowed, API Gateway will proceed with the backend integration configured on the method that was called
// build apiOptions for the AuthPolicy
const apiOptions = {};
const tmp = event.methodArn.split(':');
const apiGatewayArnTmp = tmp[5].split('/');
const awsAccountId = tmp[4];
apiOptions.region = tmp[3];
apiOptions.restApiId = apiGatewayArnTmp[0];
apiOptions.stage = apiGatewayArnTmp[1];
const method = apiGatewayArnTmp[2];
let resource = '/'; // root resource
if (apiGatewayArnTmp[3]) {
resource += apiGatewayArnTmp[3];
}
// this function must generate a policy that is associated with the recognized principal user identifier.
// depending on your use case, you might store policies in a DB, or generate them on the fly
// keep in mind, the policy is cached for 5 minutes by default (TTL is configurable in the authorizer)
// and will apply to subsequent calls to any method/resource in the RestApi
// made with the same token
// the policy below grants access to all resources in the RestApi
const policy = new AuthPolicy(principalId, awsAccountId, apiOptions);
policy.allowAllMethods();
// policy.denyAllMethods();
// policy.allowMethod(AuthPolicy.HttpVerb.GET, "/users/username");
// finally, build the policy and exit the function
callback(null, policy.build());
})
.catch(function(error) {
// Firebase throws an error when the token is not valid
// you can send a 401 Unauthorized response to the client by failing like so:
console.error(error);
callback("Unauthorized");
});
};
We are not in production, yet, but tests on the authenticator show that it behaves correctly with Google, Facebook and password authentication and it is also very quick (60 - 200 ms). The only drawback I can see is that you will be charged for the authenticator lambda function, while the Cognito integrated authenticator is free.
Update after almost 1yr
I moved away from API Gateway custom authenticator, mainly because I've not been able to automate its deployment with cloudformation scripts. My solution is now to have authentication directly within the API caching tokens for some time, like the Authenticator does, so to avoid excessive validations.