Implement RBAC (Role-Based Access Control) with metadata

Authentication (authN), i.e. who a user is, and authorization (authZ), i.e. what the user can access, both play critical roles in identity and access management.

Although Stytch's platform is focused on best-in-class authentication solutions, Stytch's flexibility can also be leveraged to implement authorization logic in your app in order to control what each user has access to, also known as Role-Based Access Control (RBAC).

In this guide, we'll demonstrate how you can use metadata to assign different roles to your Stytch Users and how to use those roles when determining which content to grant access to.

Note: If you're using Stytch's B2B product suite, we offer off-the-shelf RBAC! See our B2B RBAC Guides for more details.

Before you start

  • Create a Stytch Consumer project via the Stytch Dashboard if you don't have one already. To do so, click on your existing project name in top left corner of the Dashboard, click Create a new project, and then select Consumer authentication.
  • Copy your project_id and secret from the Test environment tab in the API keys section of the Stytch Dashboard. You'll need to include these values in every backend Stytch API call.

Step 1: Assign a role to a Stytch User

First, create a new Stytch User by calling our Create User endpoint and include a role value in the User's trusted_metadata object (see our Metadata resource for additional information about User metadata). For the purposes of this guide, we'll set this user's role value to admin, but you can specify any role values that make sense for your application.

curl --request POST \
  --url https://test.stytch.com/v1/users \
  -u 'PROJECT_ID:SECRET' \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "USER_EMAIL",
    "trusted_metadata": {
        "role": "admin"
    }
  }'

You can also add or modify a role value in the trusted_metadata of an existing Stytch User at any time by calling our Update User endpoint.

curl --request PUT \
	--url https://test.stytch.com/v1/users/USER_ID \
	-u 'PROJECT_ID:SECRET' \
	-H 'Content-Type: application/json' \
	-d '{
	    "trusted_metadata": {
          "role": "admin"
        }  
	  }'

Step 2: Create a custom claims template [optional]

The role value that we specified in Step 1 will now be present on this user's Stytch user object. The user object will be returned in many Stytch API responses, including responses to our Authenticate session endpoint when a session_token is included in the request. If your application uses Stytch session tokens exclusively and does not use JWTs, you can skip to the next step, given that the User's custom_claims will already be present in all responses to your Authenticate session calls.

If your application does use JWTs, the user object will not be included in session authentication calls when JWT validation occurs locally. However, you can set up a custom claims template to ensure that the role value is automatically included in all JWTs created for your users so that you can make authorization decisions without making an additional call to the Stytch API.

If you're still deciding whether to use session tokens or JWTs (or a combination of both) in your application, check out our JWTs vs. Sessions blog post, which explains the key differences to take into consideration.

In order to set up a custom claims template, navigate to the Custom Claims Template tab in the Stytch Dashboard and add the following template content:

{
  "role": {{ user.trusted_metadata.role }}
}

After doing so, the custom_claims object included in your Stytch Sessions and JWTs will have a claim called role that will reflect the value specified in your user's trusted_metadata.

Step 3: Start a new session

Now, let's start a new session for the User created in Step 1. In practice, a new session will be started when the user logs into your application, but for the purposes of this guide, we'll start a session by completing the Email one-time passcode flow. First, send a one-time passcode by calling our Send one-time passcode by email endpoint.

curl --request POST \
  --url https://test.stytch.com/v1/otps/email/send \
  -u 'PROJECT_ID:SECRET' \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "USER_EMAIL_FROM_STEP_1"
  }'

Save the email_id from the Send one-time passcode by email response to include in the next API call.

Next, authenticate the one-time passcode by calling our Authenticate one-time passcode endpoint. Be sure to specify the session_duration_minutes parameter so that a session is created upon successful authentication. For the purposes of this guide, we'll use a 30 minute session duration.

curl --request POST \
  --url https://test.stytch.com/v1/otps/authenticate \
  -u 'PROJECT_ID:SECRET' \
  -H 'Content-Type: application/json' \
  -d '{
    "method_id": "EMAIL_ID_FROM_SEND_OTP_RESPONSE",
    "code": "CODE_FROM_EMAIL",
    "session_duration_minutes": 30
  }'

Save the session_jwt from the Authenticate one-time passcode response for use in Step 4.

Step 4: Authenticate the session and check the user's role

At this point, you're ready to decide whether or not the user should be granted access to protected content. First, you'll need to make sure that the user's session is valid by calling our Authenticate session endpoint. You can include either a session_token or session_jwt in your Authenticate session call. For the purposes of this guide, we'll include the session_jwt from Step 3.

curl --request POST \
	--url https://test.stytch.com/v1/sessions/authenticate \
	-u 'PROJECT_ID:SECRET' \
	-H 'Content-Type: application/json' \
	-d '{
	    "session_jwt": "SESSION_JWT_FROM_STEP_3"
	}'

If you set up a custom claims template, you should always receive a session object in the Authenticate session response that looks like this:

{
    "session": {
        "attributes": {
            "ip_address": "",
            "user_agent": ""
        },
        "authentication_factors": [
            {
                "created_at": "2023-09-28T15:29:52Z",
                "delivery_method": "email",
                "email_factor": {
                    "email_address": "example@stytch.com",
                    "email_id": "email-test-..."
                },
                "last_authenticated_at": "2023-09-28T15:29:52Z",
                "type": "otp",
                "updated_at": "2023-09-28T15:29:52Z"
            }
        ],
        // session.custom_claims.role will contain the user's role value
        "custom_claims": {
            "role": "admin"
        },
        "expires_at": "2023-09-28T15:59:52Z",
        "last_accessed_at": "2023-09-28T15:29:52Z",
        "session_id": "session-test-...",
        "started_at": "2023-09-28T15:29:52Z",
        "user_id": "user-test-..."
    }
}

If you're using session tokens exclusively and did not set up a custom claims template, you should always receive a user object in the Authenticate session response that looks like this:

"user": {
    "biometric_registrations": [],
    "created_at": "2023-09-28T15:29:09Z",
    "crypto_wallets": [],
    "emails": [
        {
            "email": "example@stytch.com",
            "email_id": "email-test-...",
            "verified": true
        }
    ],
    "name": {
        "first_name": "",
        "last_name": "",
        "middle_name": ""
    },
    "password": null,
    "phone_numbers": [],
    "providers": [],
    "status": "active",
    "totps": [],
    // user.trusted_metadata.role will contain the user's role value
    "trusted_metadata": {
        "role": "admin"
    },
    "untrusted_metadata": {},
    "user_id": "user-test-...",
    "webauthn_registrations": []
},

Now that you've confirmed the user's session is valid, you can use the role value in order to determine which content to serve the user. For example, given that the role value in this case is admin, you could return admin-only content or allow admin-only actions.

Here's a simplified example function that accepts an action and the user's role value and returns an authorization decision. In this case, we're gating the INVITE_NEW_USER action to admin users only:

function actionIsAuthorized(action, role) {
  switch(action) {
    case INVITE_NEW_USER:
      return role === 'admin';
    ...
  }
}

What's next

You now have a working authorization implementation that will allow you to make access decisions based on a user's role.

For more complex role and access management, we'd recommend checking out an access control solution such as Cerbos or Oso, which can be used in combination with the logic in this guide. Cerbos's team also created a similar guide that demonstrates how to use Cerbos in tandem with Stytch.

See also

For more information about the differences between authentication and authorization, check out our blog post: Authentication vs. Authorization: What You Need to Know.