AWS Partner Network (APN) Blog

Building a Secure SaaS Application with Amazon API Gateway and Auth0 by Okta

By Humberto Somensi, Partner Solutions Architect – AWS

Most applications require a form of identity service to manage, authenticate, and authorize users. In software-as-a-service (SaaS) applications, multi-tenancy adds specific challenges to this task that are important aspects to consider when designing a multi-tenant identity management service:

In order to meet these needs, SaaS builders must consider integrating with an identity service provider. AWS services such as Amazon Cognito or AWS Partner services like Auth0 provide deep expertise in the field and allow you to focus on your SaaS application’s value proposition while relying on a secure, feature-rich identity provider.

While adopting such services will accelerate the SaaS journey, SaaS builders still need to make design choices when integrating with an identity service.

In this post, I will dive deep into the Auth0 identity platform by describing how to leverage Auth0 Organizations to enable multi-tenant identity in SaaS solutions, and how to integrate it with Amazon API Gateway.

Auth0 Essential Building Blocks

Before we get into the specific design for multi-tenant identity in Auth0, let’s review the basic Auth0 constructs:

  • Auth0 Tenant represents your SaaS provider’s Auth0 account, and it contains all the objects and implementation for your solution.
  • Auth0 Applications represent the application clients that have access to the services configured in Auth0. Your SaaS frontend or single-page application and your APIs will each be configured as Auth0 Applications.
  • Auth0 APIs configure the permissions that grant access to the resources your application API implements.
  • Auth0 Connections define the relationship between Auth0 and a source of identities: a native Auth0 database, social identity providers like Google, and enterprise identity providers such as Microsoft’s Active Directory Federation Services (ADFS).
  • Auth0 Organizations represent the tenants of your SaaS application.

These basic building blocks allow you the flexibility to design your solution to meet your application requirements. There is not a single right design; it all depends on what customer needs are and how you want to present the application to your users.

Auth0 also provides other non-essential constructs, which will further help in building your solution, but these are not the object of this post. If you are interested in learning more, please visit the Auth0 documentation.

Auth0 Organizations: Your Tenants in a Nutshell

Auth0 Organizations represent your tenants within Auth0. It allows you to model the tenant construct separately from any user attribute or group. This distinction makes it simpler for you to build your SaaS Identity, and to build workflows to manage tenants and tenant users.

It also simplifies tenant onboarding, and enables many options when designing your solution. For example, you may need to allow, for a single tenant, some users to authenticate using credentials while others are authenticated via single sign-on (SSO) federation. With Auth0 Organizations, you can allow multiple identity providers for the same tenant; these are displayed on the tenant login page.

The key takeaway is that by encapsulating your tenants within a first-class construct, Auth0 has created a structure that enables SaaS providers to build for diverse multi-tenant use cases without needing complex solutions.

Multi-Tenant Setup with Auth0 Organizations

Let’s look into a concrete example: in the simplest of forms, you can design your application to have two Auth0 Application objects, one Auth0 API object, one Auth0 Connection, plus multiple Auth0 Organizations, one for each tenant.

In the following sections, I will describe these objects in more detail. The figure below represents the high-level view of the solution.

SaaS-Auth0-API-Gateway-1

Figure 1 – Basic multi-tenant setup with Auth0 Organizations.

The first Auth0 Application (1) is used to allow users to authenticate to the SaaS application. The Auth0 API (2) object holds permissions for the API resources implemented on the SaaS API, and is used to grant access to these resources.

The second Auth0 Application (3) configures backend access to manage Auth0 resources through the Auth0 Management API (4) to onboard new tenants and invite tenant users.

Auth0 Organizations (5) are mapped 1:1 with the tenants of your service, and users stored in the pooled Auth0 Connection (6) belong to a given tenant by being members of that tenant’s Auth0 Organization.

Onboarding New Tenants

Using the example above, onboarding new tenants becomes trivial. As shown in Figure 2 below, your application will request the necessary information to onboard a new tenant using a registration form. Your registration service (1) will orchestrate calls to a tenant microservice (2), which will create a new tenant entry in your backend database and will use the Auth0 Management API to create a new Auth0 Organization associated with the Auth0 Connections object shared among all tenants (3).

It’s important to highlight the TenantID that is generated by your tenant microservice is stored as metadata on the tenant’s Auth0 Organization object. This will be used to create the SaaS Identity object mentioned above. Finally, the User microservice will be invoked (4) to invite the user to the tenant’s Auth0 Organization (5).

SaaS-Auth0-API-Gateway-2.1

Figure 2 – Basic onboarding flow with Auth0 Organizations.

Looking at the code needed to execute these steps, all you need to do is create a new Auth0 Organizations object, using the Auth0 Node.js client library, and enable the default connection object to authenticate users for this organization:

    //create Tenant organization
    auth0.organizations.create({ name: tenantName.toLowerCase().trim().replace(/\s/g, "-"), //sanitize(no-spaces)
                display_name: tenantName,  
                metadata: {"tenantId": tenantId})
    .then(org => {  
        //enable connection to organization
        auth0.organizations.addEnabledConnection({id: org.id}, {
            connection_id:defaultAuth0Connection, 
            assign_membership_on_login: false
        })    
    })

Then, you will use the Auth0 Management API to add the user to the newly-created tenant organization:

    //test if user exists
    auth0.users.getByEmail({email: userEmail})
    .then(users => {
        if(users.length > 0) {
            //add existing connection user to Tenant Auth0 Organization
           auth0.organizations.addMembers({id: orgId}, {members:[users[0].id]})
        }
        else {
            //invite new user to Tenant Auth0 Organization
            auth0.organizations.inviteMembers({id: orgId}, {inviter: {name: "Tenant onboarding"},
                                                                invitee: {email: userEmail}})            
        }
    })

The code above tests if the user email is already stored in the Connection database before deciding which workflow to initiate. It either invites the user, for an email address that is not yet stored in the Auth0 Connection database object, or adds an already existing user to the new tenant.

You can manage this flow as your requirements dictate; users may be unique to a tenant, in which case you can return a message that tenant users must be unique. This code can also be used to execute workflows where a tenant admin user invites a new user to its tenant.

Login Flow

Now users can navigate to your SaaS application login page, type in the name of the tenant they belong to, and log in to the application (Figure 3). Auth0 will authenticate the user against the user’s identity object stored in the Connection database, but also validate the user belongs to the selected tenant by checking if the user is a member of the Auth0 Organization that matches the tenant’s name entered in the form. Tenant resolution flow and branding are simplified by Auth0 Universal Login page.

SaaS-Auth0-API-Gateway-3

Figure 3 – Tenant resolution on Auth0 Universal Login page.

If your application has other methods to resolve the tenant name, it’s possible to set the Auth0 Organization Id in runtime so that users don’t need to type the tenant name in the login flow.

One way to achieve this is by hosting each tenant in their own sub-domain, or by passing the tenant name in the application path. In this case, a mapping structure between tenant and Auth0 Organization Id would need to be maintained. The AWS SaaS Serverless reference architecture provides an example of this strategy.

Authorization Structures in Auth0

In order to authorize users to access API resources in your application, you need to be able to assign permission to resources and grant these permissions to users, which are called privileges. For example, if your API implements an action to create items in a repository, your API permission can be called create:item (Figure 4).

SaaS-Auth0-API-Gateway-4

Figure 4 – Assigning permissions to an API in Auth0.

After that, you can assign permissions to users directly or create roles that contain multiple permissions a given type of user is granted. Users in an Auth0 Organization are assigned roles, which grant them the privileges listed in the role’s permissions. When your application requests an access token, it should inform the scopes needed for this action to complete. If the user has the appropriate privileges, Auth0 will return an access token with the requested scopes in the scope claim.

Below is an example implementation using the Auth0 Angular SDK on how to set scopes and audiences based on the resource’s location, and the resulting decoded access token.

    AuthModule.forRoot({ 
      domain: environment.auth0_domain,
      clientId: environment.auth0_client_id,      
      httpInterceptor: {
        allowedList: [{
            uri: '/api/item',
            httpMethod: HttpMethod.Post,
            tokenOptions: {
              audience: 'api://default',
              scope: 'create:item',
            },
          }]
      },
    })

    //resulting decoded access token
    {
      "https://tenantId": "tenantId",
      "iss": "https://[auth0-tenant].eu.auth0.com/",
      "sub": "auth0|XXXXXXXXXXXXXXXXX",
      "aud": [
        "api://default",
        "https://[auth0-tenant].eu.auth0.com/userinfo"
      ],
      "iat": 1655901996,
      "exp": 1655988396,
      "azp": "XXXXXXXXXXXXXXXXXXXXXXX",
      "scope": "openid create:item",
      "org_id": "org_XXXXXXXXXXXX"
    }

The workflow to authenticate users and request access tokens implements the Authorization Code Flow with Proof Key for Code Exchange (PKCE), which is the Authorization Code Flow (defined in OAuth 2.0 RFC 6749, section 4.1) for single-page applications.

Auth0 provides SDK libraries for various languages and frameworks, which abstract away the complexities of the OAuth 2.0 protocol, and help simplify the development of your application.

Securing Your Application with Amazon API Gateway

Now that you have the ability to create tenants, authenticate users’ tenants, and assign privileges to your tenant users, let’s review how you can secure your API.

Amazon API Gateway HTTP APIs JWT authorizers allow you to configure parameters for authorization and will, by default, check the validity of the access token: the issuer, audience, and if the token is valid. This process ensures the request to the API is being made by a user that has authenticated with your identity provider.

You may also want to authorize API routes based on which privileges are granted to the user. In this case, you need to configure the route in Amazon API Gateway to use your JWT authorizer, but also to validate the token carries the scope needed to perform the action in the route.

For example, if the API route is POST /item, then the authorization scope for this action can be create:item. Access tokens that contain this scope in the scope claim will be authorized to execute the action. Figure 5 shows such configuration in Amazon API Gateway authorization console.

SaaS-Auth0-API-Gateway-5

Figure 5 – Configuring the Amazon API Gateway JWT authorizer.

Another option is to use Amazon API Gateway Lambda authorizers. In this case, the authorizer will be executed in the form of an AWS Lambda function, which performs similar steps as described above: validate the token issuer, audience and dates, and authorize the action based on claims in the access token. Here’s a sample implementation of this pattern.

It’s important to note that with Lambda authorizers, you have the freedom to extend and modify this function to fit it to your requirements. For example, you may want to rely on a user role instead of relying on the scope claim. This may simplify your implementation, and can be achieved by injecting the role claim into the token using Auth0 Actions.

The key takeaway is that either by relying on the scope claim, role permissions, or in static roles, by adding application privileges to the JWT access token, you eliminate the need to look up privileges in every authorization flow. You also harden the security of your application by managing privileges within your identity service domain and away from your application code.

Using SaaS Identity to harden your tenant isolation posture

There is one final step to harden the tenant isolation posture of the application, and we’ll use the JWT tokens to flow tenant context through the application and drive tenant isolation.

The first step is to create a SaaS Identity object, where we add tenant context to the user context. One way to achieve this is to set tenantId to the Auth0 Organization metadata. Whenever a user logs in to a tenant (through that specific Auth0 Organization), you can use an onExecutePostLogin Auth0 Action to take the tenantId set on the organization and add it to the JWT tokens:

exports.onExecutePostLogin = async (event, api) => {      
  if (!event.organization) return;
  var tenantId = event.organization.metadata["tenantId"];
  if (event.authorization) {
    api.idToken.setCustomClaim("https://tenantId", tenantId);  
    api.accessToken.setCustomClaim("https://tenantId", tenantId);  
  }
};

Now, when your application requests a token from Auth0, the snippet above will execute and add the tenantId custom claim to the token. With this information, there are many patterns to implement tenant isolation. It depends on what type of resources your services use and your tenancy model.

You can refer to the AWS SaaS Factory Serverless Reference Solution for more information on patterns for tenant isolation in serverless applications. The AWS SaaS Tenant Isolation Strategies whitepaper analyzes tenant isolation in depth.

Exploring More Complex Use Cases

More advanced scenarios can be dealt with by creating Auth0 Connections per tenant. If you need to allow your tenants to configure their own password policies, or if you expect to federate identity to your tenant’s enterprise identity providers, then you’ll need to create a new Auth0 Connection per tenant.

It’s interesting to note that one organization can have multiple connections, which allows for use cases where some tenant users authenticate from an enterprise identity provider and others use email/password, for example. It also enables the root user use case, where tenant root users are stored in a single database connection, with strict security rules associated with it, and standard tenant users are stored in a separate connection per tenant.

Auth0 Connections can be created using the Auth0 Management API, so you can implement these flows as part of your onboarding process, like we did for Auth0 Organizations. The good news is that, from an application standpoint, none of the changes above change how you implement SaaS Identity, authentication, and authorization: the application relies on Auth0 Organizations to hold tenant context, and to manage tenant user access.

Conclusion

Identity is an important and complex subject in any context. When analyzed from a multi-tenant perspective, some new challenges are imposed. Like with anything we do at Amazon, start by understanding what your customers require. Then, select the appropriate identity provider and design your application to meet your customer needs.

With the introduction of Auth0 Organizations, AWS Partner Auth0 positions itself as the one of the leading providers of identity services for multi-tenant applications. By creating a structure that represents the tenants of your service, Auth0 simplifies the implementation required to build simple and complex multi-tenant identity use cases.

To get started, learn more about building your SaaS identity service with Auth0.