Yan Cui
I help clients go faster for less using serverless technologies.
This article is brought to you by
Don’t reinvent the patterns. Catalyst gives you consistent APIs for messaging, data, and workflow with key microservice patterns like circuit-breakers and retries for free.
AWS AppSync added support for Lambda authorizers on 30th July 2021 and it made it much easier to implement group-based authorization with 3rd party identity services.
Group-based auth with AppSync and Cognito
I previously wrote about how you can secure multi-tenant applications with AppSync and Cognito. Where you can use custom attributes to capture the tenant ID and use Cognito groups to model the different access levels.
This is a simple and effective solution partly because AppSync supports group-based authorization with Cognito out-of-the-box. You can decorate your GraphQL schema with the @aws_auth
directive to limit access to those GraphQL operations to users from those groups.
type Mutation { addUser(name: String!, email: AWSEmail!, role: Role!): User @aws_auth(cognito_groups: ["Admin", "SuperUser"]) ... }
However, this support doesn’t extend to 3rd party identity services (such Auth0 or Okta) if you connect AppSync to them via AppSync’s OpenID Contact (OPENID_CONNECT
) authorization mode.
On a recent client project, I opted to use Auth0 as the identity provider because it matched our requirements better. But I still used Cognito with AppSync because we needed group-based authorization and I wanted to leverage the built-in support AppSync has with Cognito.
I was able to connect the Cognito User Pool to Auth0 via SAML federation. The user authentication is therefore handled by Auth0. I used a Lambda function to inject custom attributes (e.g. tenant ID) and groups information into the JWT token on the PreTokenGeneration trigger.
This gave me the best of both worlds:
- Using Auth0 to manage user authentication and leverage its built-in MFA support and other advanced features.
- Using Cognito to secure the AppSync API and leverage the built-in group-based authorization.
This set-up works, except for the annoying “2nd login screen” when you use Cognito with SAML federation.
I could connect AppSync to Auth0 with OPENID_CONNECT
authorization mode, but then I’d have to implement the group-based authorization logic myself. I could use pipeline resolvers for that, and that’s what pipeline resolvers are designed for to some extend. This sounds great on paper, or when you have only a handful of resolvers. But when you have a non-trivial AppSync API with 100+ resolvers, it takes A LOT of grunt work to rewrite them all to pipeline resolvers. The only sane way to do it (and this is what we did on another project) is to automate the rewrite through a Serverless framework plugin, assuming you’re using the Serverless framework.
AppSync’s Lambda authorizer works a little differently from API Gateway’s Lambda authorizer. In my opinion, it’s simpler.
An AppSync Lambda authorizer has to return a payload like this to AppSync.
{ "isAuthorized": <true|false>, "resolverContext": {<JSON object, optional>}, "deniedFields": [ "<list of denied fields (ARNs or short names)>" ], "ttlOverride": <optional value in seconds> }
You can include custom attributes such as tenant ID, etc. in the resolverContext
and access these via the resolver as $context.identity.resolverContext
.
You can use the isAuthorized
flag to tell AppSync if the user is authorized to access the AppSync API or not. But this is not an all or nothing decision. You can use the deniedFields
array to specify which operations the user is not allowed to access.
To implement group-based authorization, you need to maintain a list of the GraphQL operations that each group can access. For example, if this was your GraphQL schema (using AppSync with Cognito):
type Query { getSomething(id: ID!): Something getUser(id: ID!): User @aws_auth(cognito_groups: ["Admin", "SuperUser"]) getTenantConfig: TenantConfig @aws_auth(cognito_groups: ["Admin"]) } type Mutation { doSomething(input: DoSomethingInput!): Something addUser(name: String!, email: AWSEmail!, role: Role!): User @aws_auth(cognito_groups: ["Admin", "SuperUser"]) removeUser(id: ID!): User @aws_auth(cognito_groups: ["Admin", "SuperUser"]) configureTenant(input: ConfigureTenantInput!): TenantConfig @aws_auth(cognito_groups: ["Admin"]) }
Here you have three groups of users – Admin
, SuperUser
and everyone else.
In this case, you need to have two arrays, one for Admin
and one for SuperUser
.
const AdminActions = [ "Query.getUser", "Query.getTenant", "Mutation.addUser", "Mutation.removeUser", "Mutation.configureTenant" ] const SuperUserActions = [ "Query.getUser", "Mutation.addUser", "Mutation.removeUser" ]
These lists contain the actions that only users in those groups can access. We can use these to build up the deniedFields
array by:
- concatenate all the actions into one list
- for each group the user belongs to, remove the associated actions from the list from step 1.
After iterating through all the groups a user belongs to, whatever’s left is what you should return in the deniedFields
.
The authorizer function’s response can be cached and you can even override the default TTL setting on a per-request basis.
This is a simple solution to implement and is easy to maintain. And, you don’t have to rewrite all your resolvers as pipeline functions!
When should you use this approach instead of Cognito?
I think the most pertinent decision here is whether you want to use Cognito or another identity provider. I have written about the case for and against Cognito here.
The pricing model for many identity services is not designed for B2C businesses where you have many non-paying, transient users. This is where Cognito’s pricing really shines through. But Cognito lacks many of the features that other identity providers offer out-of-the-box – for example, MFA, CAPTCHA, passwordless login flow, etc. Sure, you can build these custom flows yourself using Lambda triggers, but these are very much undifferentiated heavy-lifting that I want the identity provider to handle for me.
Luckily, implementing group-based authorization with 3rd party identity providers have become a lot simpler with the new AppSync Lambda authorizers.
And if you want to learn more about AppSync and GraphQL, then check out my video course – the AppSync Masterclass.
Related Posts
Whenever you’re ready, here are 3 ways I can help you:
- Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
- I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
- Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.