Guides & tutorials

SaaS paywall integration with Auth0 & Stripe

Table of contents

Building a SaaS or a membership site sometimes requires coding logic and multiple integrations for any of these scenarios:

  1. The trial is over and you want your users to enrol in a paid subscription.
  2. The card of a paid user expires before renewal.
  3. Requiring payment details before allowing to access an application.

In this tutorial, we will cover how to add a paywall to dynamically prompt the users for credit card details based on their Auth0 meta information.

Previous requirements

  • A Stripe account to process the payments.
  • An Auth0 account to handle your users’ identity.
  • An Arengu account to build the payment screen and handle the flow logic.

Overview of the flow

An overview of the user flow would be:

  1. The user logs in with Auth0.
  2. Auth0 processes rules and redirects to a custom screen (Paywall) with a signed JWT if the user has no “isPaidUser” metadata with true value.
  3. The user provides payment details in Arengu.
  4. Arengu performs custom logic or validations with Stripe, and redirects the user back to Auth0 passing a signed JWT with the user's information.
  5. Auth0 verifies the signed JWT, updates the user or app metadata and resumes the authentication flow.

1. Add & configure the Auth0 rule

Using Auth0 rules, you can redirect users before a login transaction is completed. Go to the Auth0 dashboard and create one under Auth Pipeline > Rules menu.

Name it as you want and use the following code: 

async function arenguRequirePayment(user, context, callback) {
  if (
    !configuration.SESSION_TOKEN_SECRET ||
    !configuration.ARENGU_PAYMENT_FORM_URL
  ) {
    console.log('Missing required configuration. Skipping.');
    return callback(null, user, context);
  }

  const {
    Auth0RedirectRuleUtilities,
    Auth0UserUpdateUtilities
  } = require('@auth0/rule-utilities@0.2.0');

  const ruleUtils = new Auth0RedirectRuleUtilities(
    user,
    context,
    configuration
  );

  const userUtils = new Auth0UserUpdateUtilities(user, auth0);

  function validateSessionToken() {
    try {
      return ruleUtils.validateSessionToken();
    } catch (error) {
      return callback(error);
    }
  }

  // Modify your login criteria to your needs
  function isLogin() {
    const loginCount = configuration.ARENGU_PAYMENT_LOGIN_COUNT || 1;
    return context.stats.loginsCount > parseInt(loginCount, 10);
  }

  function isPaidUser() {
    // Modify your meta key to your needs
    return userUtils.getUserMeta('isPaidUser') === true;
  }

  function generateSessionToken() {
    const additionalClaims = {
      stripeCustomerId : userUtils.getUserMeta('stripeCustomerId'),
    };
    return ruleUtils.createSessionToken(additionalClaims);
  }

  if (ruleUtils.isRedirectCallback && ruleUtils.queryParams.session_token) {
    const decodedToken = validateSessionToken();
    const customClaims = decodedToken.other;

    for (const [key, value] of Object.entries(customClaims)) {
      userUtils.setUserMeta(key, value);
    }

    try {
      await userUtils.updateUserMeta();

      return callback(null, user, context);
    } catch (error) {
      return callback(error);
    }
  }

  if (isLogin() && !isPaidUser()) {
    const redirectOptions = {
      sessionToken: generateSessionToken(),
    };
    ruleUtils.doRedirect(configuration.ARENGU_PAYMENT_FORM_URL, redirectOptions);
  }

  return callback(null, user, context);
}

We will need to configure two variables:

Notice the following about this rule:

  • It assumes you are storing a Stripe customer ID in a metadata property. This value is read and passed to Arengu to perform the payment flow linked to that user.
  • It assumes you are using a “isPaidUser” boolean to decide if you want to prompt the user for payment details or not. Modify it according to your needs.

2. Build your payment form in Arengu

Add a payment field with the following settings:

  • Payment type: choose “Subscription”.
  • Payment action: choose “Create”.
  • Price ID: you will find it in your Stripe dashboard under the products menu and after selecting your plan.
  • Customer action: choose “Update customer” as you’ve already created one when they signed up.
  • Customer ID: add {{state.customerId}} to reference a variable we will get from a flow in the next section.

When Auth0 redirects the user to Arengu, it will send a “session_token” and “state” params via URL. Create two hidden fields with the same keys to automatically prefill the form with the provided information:

Now, we will create and link a flow that will be executed before processing the payment:

3. Verify users’ information and resume the authentication flow

We will build a flow that verifies the received “session_token” information from Auth0 and that resumes the authentication flow once the payment flow is completed.

3.1 Verify the session_token JWT

First, add a “Verify JSON web token” action to verify the received “sesion_token”. This action will have the following settings:

  • ID: change it to “verifyJwt”.
  • Token: add {{input.body.session_token}} to reference the hidden fields received from a form.
  • Secret or public key: add the same “SESSION_TOKEN_SECRET” value you have configured in Auth0.
  • Issuer: add the host from where you will receive the requests.

This action will return a payload with the following structure: 

{
  "valid": true,
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "ip": "81.32...",
    "iss": "https://arengu.eu.auth0.com/",
    "sub": "auth0|5e57068d0767ca...",
    "stripeCustomerId": "cus_JPkx4J52t6yAET",
    "iat": 1620052475,
    "exp": 1620311675
  },
  "signature": "AdzX0o_F4RxmQMV8UjyoMKFU..."
}

So we will need to create an If/then condition to handle whether the valid property is true or false.

3.2. Check if the JWT is valid or not

Add an If/then condition with the following settings:

  • Condition: choose “is true” and add {{verifyJwt.valid}} to the first input.

You can display the user a custom error message by adding a “Show error message” action to the false branch.

3.3 Use the Stripe customer ID in your payment settings

We’ve already decoded the JWT that we are generating in Auth0, so we will expose the stripeCustomerId property to be used in our payment form settings. Add a “Store state variable” with the following settings:

  • Data fields: add “customerId” to the first input and {{verifyJwt.payload.stripeCustomerId}} to the second one.

Once you store a state variable, you can reference it in the whole user journey using {{state.field_id}} syntax. Notice we’ve already configured {{state.customerId}} in our payment field settings.

3.4 Sign a JWT to update Auth0 user’s metadata

As we need to update Auth0 users’ metadata in a secure way, we will sign a JWT to pass it back to Auth0 with the properties we want to update. Add a “Sign JSON web token” action with the following settings:

  • ID: change it to “signToken”.
  • Algorithm: choose “HS256”.
  • Secret or private key: again, add your Auth0 “SESSION_TOKEN_SECRET”.
  • Issuer: add the host from where you will receive the requests.
  • Subject: add {{verifyJwt.payload.sub}} to reference the verified sub of the Verify JSON web token output.
  • Expires in: add a short expiration time (eg. 15-30 seconds) to avoid replay attacks.
  • Payload: we will add our custom claims inside the other property:
{
  "other": {
    "isPaidUser": true
  }
}

3.5 Resume the authentication flow

Close this flow by adding a “Submit the form” action with the following settings:

  • Redirect to URL: enable it and add https://YOUR_TENANT_SUBDOMAIN.auth0.com/continue?state={{input.body.state}}&session_token={{signToken.token}} to redirect using the state parameter we've received in the form, and the new JWT we've generated in the previous flow action.

You’re done! Now you have an authentication flow that will prompt your users with payment details based on their metadata.


You might like to read

Getting started with Arengu

Arengu allows you to build all your user flows connected to your current stack, and avoids coding all the UI, complex integrations, validations or logic from scratch. Try it for free and start building faster and scaling your application needs as they grow.