DEV Community

Yan Cui for AWS Heroes

Posted on • Originally published at theburningmonk.com on

Group-based auth with AppSync Lambda authoriser

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"])
  ...
}
Enter fullscreen mode Exit fullscreen mode

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.

Group-based auth with Lambda authorizer

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>
}
Enter fullscreen mode Exit fullscreen mode

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"])
}
Enter fullscreen mode Exit fullscreen mode

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"
]
Enter fullscreen mode Exit fullscreen mode

These lists contain the actions that only users in those groups can access. We can use these to build up the deniedFields array by:

  1. concatenate all the actions into one list
  2. 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 identify provider. I have written about the case for and against Cognito here.

The pricing model for many identity services are 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 – and save 30% while we’re still in early access!

Top comments (0)