Skip to content

How to Set Up OAuth with a Stripe App

Stripe Apps are a great way to extend Stripe dashboard functionality using third-party integrations. But when using these integrations, developers must prioritize security. The best way to do this is by using OAuth with the third-party product with which you would like to integrate.

However, there are some constraints, and we cannot use cookies to set up a cookie based authentication front-end only. In this article, we would like to show you how to do it with a NestJS back-end.

Stripe signatures

The best way to secure your API is by making sure that every request comes from a verified Stripe App instance. The @stripe/ui-extension-sdk package provides a way to generate a signature on the front-end side. This signature is valid for 5 minutes, and you can send it as a header for every request you make. For this to work, you need to have @stripe/ui-extension-sdk installed in your repository.

import fetchStripeSignature from "@stripe/ui-extension-sdk/signature";
// fetchStripeSignature returns with a Promise<string>

In order to properly validate this signature on your API, you will need some additional information to be sent in the request headers as well. That information is the Stripe user's ID, and the Stripe account's ID. We found that the best way is to implement a global context with this information.

import { createContext } from "react";
import { ExtensionContextValue } from "@stripe/ui-extension-sdk/context";
export const GlobalContext = createContext<{
  userContext: ExtensionContextValue["userContext"] | null;
  environment: ExtensionContextValue["environment"] | null;
}>({ userContext: null, environment: null });

The above context stores the ExtensionContextValue that gets passed from the Stripe dashboard to the app when it opens in the view. For example, if you are on a payment detail page, the userContext will contain information about your Stripe user, while the environment will provide you access to the object that you are viewing. In the above example, that would be the payment's ID as the objectContext.id property.

Let's set up the view with this global context.

import type { ExtensionContextValue } from "@stripe/ui-extension-sdk/context";
import { ContextView } from "@stripe/ui-extension-sdk/ui";
import { GlobalContext } from "./common/global-context";
const PaymentDetailView = ({
  userContext,
  environment,
}: ExtensionContextValue) => (
  <ContextView title={title || " "} description={description}>
    <GlobalContext.Provider value={{ userContext, environment }}>
      TODO: navigation and login will come here.
    </GlobalContext.Provider>
  </ContextView>
);
export default PaymentDetailView;

Now, we can set up a hook to provide a proper fetch method that always appends a Stripe signature, and the other required fields to the headers.

useFetchWithCredentials hook

In order to make our future job easier, we need to set up a hook that creates a proper wrapper around fetch. That wrapper will handle setting the headers for us. It needs to have access to our GlobalContext, so we can get the Stripe user's, and their account's, IDs.

import { ExtensionContextValue } from "@stripe/ui-extension-sdk/context";
import fetchStripeSignature from "@stripe/ui-extension-sdk/signature";
import { useCallback, useContext } from "react";
import { GlobalContext } from "../common/global-context";
export function useFetchWithCredentials() {
  const globalContext = useContext(GlobalContext);
  return useCallback(
    async (uri: string, { headers, ...options }: RequestInit = {}) => {
      const stripeSignature = await fetchStripeSignature();
      const headersObject = new Headers(headers);
      headersObject.append("stripe-signature", stripeSignature);
      headersObject.append("Content-Type", "application/json");
      headersObject.append(
        "stripe-user-id",
        globalContext.userContext?.id ?? ""
      );
      headersObject.append(
        "stripe-account-id",
        globalContext.userContext?.account.id ?? ""
      );
      return fetch(uri, {
        ...options,
        headers: headersObject,
      });
    },
    [globalContext]
  );
}

Let's set up a very basic component for demonstrating the use of the useFetchWithCredentials hook. This component will be the default route for our app's navigation wrapper. It is going to handle more later. But for now, let's just implement a basic use for our hook. The AUTH_INIT_URL constant will point at our back-end's /api/oauth/userinfo endpoint.

Please note that, for this to work, you are going to need to install react-router-dom.

import { Box, Spinner } from "@stripe/ui-extension-sdk/ui";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { AUTH_INIT_URL } from "../constants/auth.urls";
import { setUser } from "../auth/state";
import { useFetchWithCredentials } from "../hooks/useFetchWithCredentials";
export const AuthInit: React.FC<{ redirectPath: string }> = ({
  redirectPath,
}) => {
  // Please note, that we are going to set the whole routing up in a later article.
  const navigate = useNavigate();
  const fetchWithCredentials = useFetchWithCredentials();
  useEffect(() => {
    fetchWithCredentials(AUTH_INIT_URL)
      .then((user) => {
        // Make sure you set the user in your chosen global state management tool.
        setUser(user);
        // For now if the response is a success response (status 200), we are going to navigate to the first authenticated route.
        navigate(redirectPath);
      })
      .catch((e) => {
        console.error(e.message);
        // If the request returns with an error (status 401), it means that the user is unauthenticated, so we send them to the login page
        navigate("/login");
      });
  }, [redirectPath]);
  // Let's display a spinner while we wait for our initial request to return.
  return (
    <Box
      css={{
        layout: "column",
        alignX: "center",
        alignY: "center",
      }}
    >
      <Spinner size="large" />
      Loading...
    </Box>
  );
};

As we can see from the above implementation, this component will be the initial component that gets rendered inside of the application. It will send out a request to determine if the user is logged in. If they are logged in, we are going to send them to a route that is the first page of our application. If they are not signed in, we are going to redirect them to our login page. This initial call, just as every other API call, must be verified and always have a Stripe signature.

Let's visualise how routing looks like right now:

import type { ExtensionContextValue } from "@stripe/ui-extension-sdk/context";
import { ContextView } from "@stripe/ui-extension-sdk/ui";
import { GlobalContext } from "./common/global-context";
import { Route, Routes } from 'react-router-dom';
import { PaymentInfo } from './components/PaymentInfo;
const PaymentDetailView = ({
  userContext,
  environment,
}: ExtensionContextValue) => (
  <ContextView title={title || " "} description={description}>
    <GlobalContext.Provider value={{ userContext, environment }}>
      <Router basename="/" initialEntries={['/init']}>
        <Routes>
          <Route
            path="/init"
            element={<AuthInit redirectPath={'/payment-details'} />}
          />
          <Route path="/login" element={<Login />} />
          <Route path="/payment-details" element={<PaymentInfo />}>
        </Routes>
      </Router>
    </GlobalContext.Provider>
  </ContextView>
);
export default PaymentDetailView;

Stripe secrets and the Stripe API

In order to be able to use the Stripe NodeJS Api, you will need two secrets from Stripe. One is your Stripe account's API key, and the other one is your Stripe-app's secret. You need to set up your .env file as the following.

STRIPE_API_KEY='your api key goes here'
STRIPE_APP_SECRET='your app secret goes here'

Stripe API key

You can find your Stripe API key at https://dashboard.stripe.com/apikeys, under the Standard keys section. The key you are looking for is called Secret key, and you need to reveal it by clicking the button that hides it.

Stripe App Secret

For this key, you are going to need to upload your stripe-app using the stripe apps upload command. Make sure that you set a development app ID in your app manifest (stripe-app.json). After you uploaded your app, visit https://dashboard.stripe.com/apps. Under My Apps, you should see your uploaded application. Open it and search for the Signing secret. Reveal it and copy it into your .env file.

Stripe NodeJS API

Please make sure you have installed the stripe nmp package for your server code. In this example series, we use NestJS as our framework for our API. We need the above two secret keys to be able to start up our Stripe API.

// verify-signature.ts
import { BadRequestException, Logger } from "@nestjs/common";
import { Stripe } from "stripe";
const STRIPE_API = new Stripe(process.env.STRIPE_API_KEY, {
  apiVersion: "2020-08-27",
});
const APP_SECRET = process.env.STRIPE_APP_SECRET;
export function verifySignature(
  userId: string,
  accountId: string,
  signature: string,
  logger: Logger
): void {
  // Signature verification will come here
}

NestJS VerifySignatureInterceptor implementation

In NestJS, we can use interceptors to abstract away repetitive logic that needs to be done on multiple requests. In our case, we need to verify almost every API for a valid Stripe signature. We have access to the proper secret keys, and we have a Stripe NodeJS API set up. Let's create our VerifySignatureInterceptor.

import {
  BadRequestException,
  CallHandler,
  ExecutionContext,
  Injectable,
  Logger,
  NestInterceptor,
} from "@nestjs/common";
import { Request } from "express";
import { Observable } from "rxjs";
import { Stripe } from "stripe";
const STRIPE_API = new Stripe(process.env.STRIPE_API_KEY, {
  apiVersion: "2020-08-27",
});
const APP_SECRET = process.env.STRIPE_APP_SECRET;
@Injectable()
export class VerifySignatureInterceptor implements NestInterceptor {
  // We set up a logger instance, so we can properly log errors
  private readonly logger = new Logger(VerifySignatureInterceptor.name);
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req: Request = context.switchToHttp().getRequest();
    const signature = req.headers["stripe-signature"] as string;
    const userId = req.headers["stripe-user-id"] as string;
    const accountId = req.headers["stripe-account-id"] as string;
    verifySignature(userId, accountId, signature, this.logger);
    return next.handle();
  }
}

Every interceptor must implement the intercept() method. We extract the Request object from the execution context, and we get the headers that we previously set in our useFetchWithCredentials hook. We call our verifySignature function which will throw errors if the signature is invalid. We also pass the Logger instance, so we can determine when an error comes from this interceptor in our logs.

Please be aware that there are several reasons signature verification can go wrong, like if we provide the wrong Stripe account keys or app secrets. In order for you to be able to easily debug these issues, proper logging is a must. That is why we set up a Logger instance in our interceptor.

// ...
export function verifySignature(
  userId: string,
  accountId: string,
  signature: string,
  logger: Logger
): void {
  if (!(user_id && account_id && signature)) {
    throw new BadRequestException("Missing user identifiers");
  }
  try {
    stripe.webhooks.signature.verifyHeader(
      JSON.stringify({ user_id, account_id }),
      signature,
      APP_SECRET
    );
  } catch (e: any) {
    logger.error(`Could not verify signature due to: ${e.message}`);
    throw new BadRequestException("Could not verify signature");
  }
}

If the user_id, account_id, or the signature are missing, that could mean that the request came from outside a stripe application, or the useFetchWithCredentials hook was not used. We throw a BadRequestException that will result in the request sending back a status: 400 HTTP response.

If the signature verification fails, that could mean that a not valid signature was used in the request, or that the API environment variables might have the wrong keys.

Set up the userinfo endpoint

Let's quickly set up our /api/oauth/userinfo endpoint. For that, we are going to create the OauthModule and the OauthController.

// oauth.module.ts
import { Module } from "@nestjs/common";
import { OauthController } from "./oauth.controller";
@Module({
  controllers: [OauthController],
})
export class OauthModule {}

In our controller, we decorate our getUserInfo() method, with the @Get() decorator, so we set up the route. We also decorate the method with the @UseInterceptors() decorator, where we pass our VerifySignatureInterceptor.

// oauth.controller.ts
import {
  Controller,
  Get,
  Headers,
  Logger,
  UnauthorizedException,
  UseInterceptors,
} from "@nestjs/common";
import { VerifySignatureInterceptor } from "./interceptors/verify-signature.interceptor";
import { UserInfo } from "../interfaces/user";
@Controller("oauth")
export class OauthController {
  // We set up our Logger instance here as well
  private readonly logger = new Logger(OauthController.name);
  @Get("userinfo")
  @UseInterceptors(VerifySignatureInterceptor)
  async getUserInfo(
    @Headers("stripe-account-id") accountId: string,
    @Headers("stripe-user-id") userId: string
  ): Promise<UserInfo> {
    // For now, we always throw an unauthorised exception, so our front-end will load the login page
    throw new UnauthorizedException();
  }
}

This setup will enable us to call the /api/oauth/userinfo endpoint which will, in-turn, check if we have a valid signature present in the headers. If the request is invalid, it will throw a 400 Bad Request exception. If the signature is valid, for now, we will throw a 401 Unauthorized exception just to make our front-end navigate to the login page.

The Login flow

Just to keep this example simple, our login page will only have a button in the center that will start our login flow with our API.

import fetchStripeSignature from '@stripe/ui-extension-sdk/signature';
import { LOGIN_URI } from '../../constants/url.constants';
import { Box, Button } from '@stripe/ui-extension-sdk/ui';
import { GlobalContext } from '../../common/global-context';
export const Login: React.FC = () => {
  const globalContext = useContext(GlobalContext);
  const [stateKey, setStateKey] = useState<string | null>(null);
  // setting the stateKey will be implemented here
  const queryParams = new URLSearchParams({
    state: stateKey as string,
  })
  // LOGIN_URI points to our API at the '/api/oauth/login' endpoint
  const loginUrl = `${LOGIN_URI}?${queryParams}`
  return (
      <Box css={{
            height: 'fill',
            width: 'fill,
            layout: 'row',
            alignX: 'center',
            alignY: 'center'
      }}>
        <Button
            type="primary"
            css={{
                width: 'fill',
                alignX: 'center',
            }}
            disabled={!stateKey}
            href={loginUrl}
        >
          Sign in
        </Button>
      </Box>
  )
}

We need to create a state key, that can be validated before we fetch the token. This state key will first be sent to our third-party oauth client, and it will be returned to us when the authentication is finished. This key is passed securely and over https. Therefore, it can be a stringified object. While the key is not set, we disable the button.

import fetchStripeSignature from "@stripe/ui-extension-sdk/signature";
import { LOGIN_URI } from "../../constants/url.constants";
import { Box, Button } from "@stripe/ui-extension-sdk/ui";
import { GlobalContext } from "../../common/global-context";
export const Login: React.FC = () => {
  const globalContext = useContext(GlobalContext);
  const [stateKey, setStateKey] = useState<string | null>(null);
  useEffect(() => {
    // we create an async function which will get all the necessary data and then call JSON.stringify() on it
    const getStateKey = async () => {
      const signature = await fetchStripeSignature();
      const userId = globalContext.userContext?.id;
      const accountId = globalContext.userContext?.account.id;
      return JSON.stringify({ userId, accountId, signature });
    };
    getStateKey().then(setStateKey);
  }, [globalContext.userContext]);
  const queryParams = new URLSearchParams({
    state: stateKey as string,
  });
  // LOGIN_URI points to our API at the '/api/oauth/login' endpoint
  const loginUrl = `${LOGIN_URI}?${queryParams}`;
  return {
    /* ... */
  };
};

Pressing the Sign in button will call our API that will redirect us to our third-party login screen. When the login happens, it will redirect us to our API, where we can fetch a valid token and redirect again to the Stripe dashboard. Let's extend our environment variables.

STRIPE_API_KEY='your api key goes here'
STRIPE_APP_SECRET='your app secret goes here'
STRIPE_REDIRECT_URL='https://dashboard.stripe.com/test/apps-oauth/your.app.id'
## The above url is set to the development preview redirect url. In production, this url will look like the following:
## STRIPE_REDIRECT_URL='https://dashboard.stripe.com/apps-oauth/your.app.id'
## Don't forget that your app-id in development and in production should be different
THIRD_PARTY_CLIENT_ID='your client id goes here'
THIRD_PARTY_CLIENT_SECRET='your client secret goes here'
THIRD_PARTY_URL_BASE='https://third-party.com'
API_HOST_URL='http://localhost:3333'
## In production, the above url should point to your deployed back-end

Now that we have every environment variable set up, let's implement our api/oauth/login and api/oauth/authorise endpoints in our OauthController.

// ...
const THIRD_PARTY_OAUTH_URL = `${process.env.THIRD_PARTY_URL_BASE}/oauth`;
const API_AUTH_CALLBACK_URL = `${process.env.API_HOST_URL}/api/oauth/authorise`;
@Controller("oauth")
export class OauthController {
  // ...
  @Get("login")
  @Redirect(THIRD_PARTY_OAUTH_URL, 303)
  // the HasValuePipe checks if the state is not an empty string or undefined.
  login(@Query("state", HasValuePipe) state: string): { url: string } {
    const queryParams = new URLSearchParams({
      response_type: "code",
      client_id: process.env.THIRD_PARTY_CLIENT_ID,
      redirect_uri: API_AUTH_CALLBACK_URL,
      state,
    });
    // with the query parameters set, we redirect to our third-party oauth page
    return {
      url: `${THIRD_PARTY_OAUTH_URL}?${queryParams.toString()}`,
    };
  }
}

The login endpoint, if everything is correct, redirects us to the login page where the user should be able to log in. Make sure that if you oauth client needs to have configured redirect urls, you configure them. For example, for development, the http://localhost:3333/api/oauth/authorise endpoint should be in the allowed redirect url list.

@Controller("oauth")
export class OauthController {
  // ...
  @Get("authorize")
  @Redirect(process.env.STRIPE_REDIRECT_URL, 303)
  async loggedIn(
    @Query("state", HasValuePipe) state: string,
    @Query("code", HasValuePipe) code: string
  ) {
    // If either the state or the code is missing, we return with a 400 Bad Request response
    if (!state || !code) {
      throw new BadRequestException();
    }
    let userId: string;
    let accountId: string;
    let signature: string;
    try {
      const _ = ({ userId, accountId, signature } = JSON.parse(state));
    } catch (error: any) {
      // If the state is not a valid JSON we can be sure that we won't be able to
      this.logger.error(`Invalid state returned to the authorise endpoint`);
      throw new BadRequestException();
    }
    // We verify the signature to make sure it comes from our Stripe app.
    verifySignature(userId, accountId, signature, this.logger);
    // see implementation below
    await this.fetchToken(accountId, userId, code).catch(
      (error: AxiosError) => {
        if (error.response) {
          throw new UnauthorizedException();
        }
        throw new InternalServerErrorException(
          "Cannot contact authentication server"
        );
      }
    );
  }
}

We validate everything to be sure that this endpoint was called from our third-party OAuth page. With the information available to us, we can fetch the access token and store it in the Stripe Secret Storage. In this example, we use axios in our bakc-end to send requests to our third-party API.

// ...
const THIRD_PARTY_TOKEN_URI = `${process.env.THIRD_PARTY_URL_BASE}/token`;
@Controller("oauth")
export class OauthController {
  // ...
  constructor(private secretService: SecretService) {}
  // ...
  private async fetchToken(
    accountId: string,
    userId: string,
    code: string
  ): Promise<void> {
    const data = {
      grant_type: "authorization_code",
      client_id: process.env.THIRD_PARTY_CLIENT_ID,
      client_secret: process.env.THIRD_PARTY_CLIENT_SECRET,
      code,
    };
    const response = await axios.post(THIRD_PARTY_TOKEN_URI, data, {
      responseType: "json",
    });
    // Please note that the third-party API you integrate with might have a different response structure.
    const { access_token } = response.data;
    // We set the token in the secret store. See the implementation below. The Secret Store only accepts strings as values.
    // The accountId is needed so we can scope the request to the requestor user's Stripe account.
    // The userId is needed so we can use the requestor user's secrets
    // We set the secret name to 'access_token', and we pass the token as its value.
    await this.secretService.addSecret(
      accountId,
      userId,
      "access_token",
      access_token
    );
  }
}

We exchange our code returned from our OAuth client to a valid access token, and then store it in the Stripe Secret Store. That logic got extracted into a SecretService class, because the logic implemented in it can be reused later for other API calls. Please make sure you set up a NestJS module that exports this service.

Stripe Secret Store

Stripe's Secret Store API enables your app to securely store and retrieve strings that can be authentication credentials, tokens, etc. This API enables users to stay logged in to third party services even when they log out of their Stripe dashboard. Let's set up a service that handles access to the Secret Store on our back-end.

import { Stripe } from "stripe";
interface Secret {
  id: string;
  name: string;
  payload: string;
}

// With the below setup, we can communicate with the Stripe Secret Store.
const SecretResource = Stripe.StripeResource.extend({
  find: Stripe.StripeResource.method({
    method: "GET",
    path: "apps/secrets/find",
  }) as (...args: any[]) => Promise<Secret>,
  set: Stripe.StripeResource.method({
    method: "POST",
    path: "apps/secrets",
  }) as (...args: any[]) => Promise<Secret>,
  delete: Stripe.StripeResource.method({
    method: "POST",
    path: "apps/secrets/delete",
  }) as (...args: any[]) => Promise<Secret>,
});
@Injectable()
export class SecretService {
  // To make calls for connected accounts, we should create a new Stripe API instance every time.
  private getSecretResource(stripeAccount: string): Stripe {
    const client = new Stripe(process.env.STRIPE_API_KEY, {
      apiVersion: "2020-08-27",
      stripeAccount,
    });
    return new SecretResource(client);
  }
}

Adding secrets

As we can see above, the Secret Storage needs some preliminary setup, which we do in our SecretService. The StripeResource sets up the find, set, and delete methods on the Stripe Api, and interacts with the Secret Store. Let's implement the addSecret method, so we can actually store our returned token.

import { Stripe } from "stripe";
interface Secret {
  id: string;
  name: string;
  payload: string;
}
@Injectable()
export class SecretService {
  // ...
  async addSecret(
    accountId: string,
    userId: string,
    secretName: string,
    value: string
  ): Promise<Secret | null> {
    return this.getSecretResource(accountId)
      .set({
        "scope[type]": "user",
        "scope[user]": userId,
        name: secretName,
        payload: value,
      })
      .catch((error: any) => {
        this.logger.error(
          `Could not set secret to Stripe Secret Store: ${error.statusCode} - ${error.message}`
        );
        return null;
      });
  }
}

With the above, we can finally store our token with which we can make authenticated requests.

Getting secrets

Let's implement the getSecret so we can retrieve secrets. The principles are the same. We will need the accountId, the userId, and the secret's name for it.

import { Stripe } from "stripe";
interface Secret {
  id: string;
  name: string;
  payload: string;
}
@Injectable()
export class SecretService {
  // ...
  async getSecret(
    accountId: string,
    userId: string,
    secretName: string
  ): Promise<Secret | null> {
    const secret = await this.getSecretResource(accountId)
      .find({
        "scope[type]": "user",
        "scope[user]": userId,
        name: secretName,
        "expand[]": "payload",
      })
      .catch((error: any) => {
        if (error.statusCode === 404) {
          throw new UnauthorizedException();
        } else {
          this.logger.error(
            `Could not get secret from Stripe Secret Store: ${error.statusCode} - ${error.message}`
          );
          return null;
        }
      });
    if (!secret) {
      throw new UnauthorizedException();
    }
    return secret.payload;
  }
}

Let's close the login flow, and implement the final version of the api/oauth/userinfo endpoint.

// ...
const THIRD_PARTY_WHO_AM_I_URL = `${process.env.THIRD_PARTY_URL_BASE}/oauth/whoami`;
@Controller("oauth")
export class OauthController {
  // ...
  constructor(private secretService: SecretService) {}
  @Get("userinfo")
  @UseInterceptors(VerifySignatureInterceptor)
  async getUserInfo(
    @Headers("stripe-account-id") accountId: string,
    @Headers("stripe-user-id") userId: string
  ): Promise<UserInfo> {
    const token = await this.secretService.getSecret(
      accountId,
      userId,
      "access_token"
    );

  const user = await axios
      .get<UserInfo>(THIRD_PARTY_WHO_AM_I_URL, {
        responseType: "json",
        headers: {
          authorization: `Bearer ${token}`,
        },
      })
      .catach(() => {
        throw new UnauthorizedException();
      });
    // We add an extra layer of security by requiring the user to verify their e-mail.
    if (!user.email_verified) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Deleting secrets

We want our users to have ability to log out from our third-party API as well. That can be achieved by deleting their access_token from the Secret store.

import { Stripe } from "stripe";
// ...
@Injectable()
export class SecretService {
  // ...
  async deleteSecret(
    accountId: string,
    userId: string,
    secretName: string
  ): Promise<boolean> {
    return this.getSecretResource(accountId)
      .delete({
        "scope[type]": "user",
        "scope[user]": userId,
        name: secretName,
      })
      .then(() => true)
      .catch((error: any) => {
        this.logger.error(
          `Could not delete secret from Stripe Secret Store: ${error.statusCode} - ${error.message}`
        );
        return false;
      });
  }
}

The /api/oauth/logout endpoint is going to be a GET request, that will delete the token from the Secret Store.

// ...
@Controller("oauth")
export class OauthController {
  // ...
  @Get("logout")
  @HttpCode(204)
  @UseInterceptors(VerifySignatureInterceptor)
  async deleteToken(
    @Headers("stripe-account-id") accountId: string,
    @Headers("stripe-user-id") userId: string
  ): Promise<boolean> {
    return this.secretService.deleteToken(accountId, userId, "access_token");
  }
}

We can create a SignOutLink that will send the request to our back-end and navigates to the /login page. You can put this component into the footerContent property of your ContextView.

import { LOGOUT_URI } from "../../constants/url.constants";
import { useFetchWithCredentials } from "../../hooks/useFetchWithCredentials";
import { Link } from "@stripe/ui-extension-sdk/ui";
import { useNavigate } from "react-router-dom";
export const SignOutLink = () => {
  const navigate = useNavigate();
  const fetchWithCredentials = useFetchWithCredentials();
  return (
    <Link
      onPress={() => {
        fetchWithCredentials(LOGOUT_URI).then(() => {
          navigate("/logout");
        });
      }}
    >
      Sign out
    </Link>
  );
};

And now we are ready with our authentication setup. When the user opens our app, it will call the /api/oauth/userinfo endpoint. Initially, it will return with a 401 error, and our front-end will navigate to the /login route. When the user presses the Sign in button, it will redirect them to the third-party OAuth page. After they log in, our back-end also redirects them back to their Stripe dashboars where the application will open. The app will call the /api/oauth/userinfo endpoint again. But this time, it will return an actual user information and it routes to the protected route.

To help visualize the whole flow, you can also use the following sequence diagram for reference:

sequence-diagram

Conclusion

As you can see, there are many steps involved in setting up a proper OAuth flow. However, it's necessary to make it right, since this is the most critical part of the app. We hope blog post article will help you to set up some good foundations when implementing your own Stripe app.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

How to Style Using SCSS in Nuxt cover image

How to Style Using SCSS in Nuxt

Introduction SASS makes working on large projects more organized. It allows you to use variables, nested rules, mixins, and functions. The preferred styling method in Nuxt is component file styling, and integrating SASS into your project can make your component file styling appear more understandable. How to Import SASS in Nuxt To add SASS after setting up your Nuxt application, we will first install SASS and sass-loader. Let's run either of these commands depending on our package manager. `shell $ yarn add sass sass-loader --dev or $ npm install sass sass-loader --save-dev ` Component File Styling With SASS and sass-loader installed in our project, we can now write SCSS in our component file. Lets see an example: `html .app-button { position: relative; display: inline-flex; cursor: pointer; text-align: center; white-space: nowrap; align-items: center; justify-content: center; vertical-align: top; text-decoration: none; outline: none; // variant &--primary { background-color: #0e34cd; color: #ffffff; } } ` In the example above, all we need to specify is lang="scss" on the style tag, and we can now write scss in that component. Global File Import and Variables To import SCSS files that are global, like variables and mixins files, we need to install style-resources: `shell $ yarn add @nuxtjs/style-resources --dev or $ npm install @nuxtjs/style-resources --save-dev ` Next, we can update our nuxt.config.js file by adding the module we just installed to the buildModules. `ts // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules buildModules: [ '@nuxtjs/style-resources', ], ` Next, let's create a global variables.scss file in our assets/style folder. Add a single variable inside: `scss // assets/style/variables.scss $primary: #010933; $white: #fff; ` Next, we need to import this file inside the nuxt.config file: `ts styleResources: { scss: [ '/assets/style/variables.scss', ], }, ` Now we have the variables in our variables.scss available in all our components for use. Next, let's test it out by updating our button component. `html .app-button { position: relative; display: inline-flex; cursor: pointer; text-align: center; white-space: nowrap; align-items: center; justify-content: center; vertical-align: top; text-decoration: none; outline: none; // variant &--primary { background-color: $primary; color: $white; } } ` We have updated our button variant color to be our global SCSS variable ($primary and $white). Mixins Mixins in SASS are used to write styles that can be reused in other parts of the code. Let's create a sample mixin to center an item. `scss // assets/style/mixins.scss @mixin center-item { text-align: center; align-items: center; justify-content: center; } ` Next, we need to import our mixin in Nuxt config: `ts styleResources: { scss: [ '/assets/style/variables.scss', '/assets/style/mixins.scss' ], }, ` Now, let's update our button component with our mixin: `html .app-button { position: relative; display: inline-flex; cursor: pointer; white-space: nowrap; vertical-align: top; text-decoration: none; outline: none; @include center-item; // variant &--primary { background-color: $primary; color: $white; } } ` Functions Functions in SASS are used to write complex operations or behaviours that can be reused in other parts of the code. Let's create a function to handle a media query for our application. `scss // assets/style/functions.scss // Get the difference between 2 numbers. @function minus($param1, $param2) { @return $param1 - $param2; } // Get the sum of 2 numbers. @function add($param1, $param2) { @return $param1 + $param2; } ` This is a basic example of what a function can be used for. More complex cases, like calculating percentage, can also be done. Let's import our mixin in Nuxt config: `ts styleResources: { scss: [ '/assets/style/variables.scss', '/assets/style/mixins.scss', '/assets/style/functions.scss' ], }, ` Let's update our button component with our function: `html .app-button { position: relative; display: inline-flex; cursor: pointer; white-space: nowrap; vertical-align: top; text-decoration: none; outline: none; @include center-item; width: minus(30px, 100%); // variant &--primary { background-color: $primary; color: $white; } } ` We have been able to add SASS to our Nuxt project, and also looked at some ways in which SASS can make our codebase look cleaner. I hope this article has been helpful to you. If you encounter any issues, you can reach out to me on Twitter or Github....

State Management with Apollo Client and Vue using Reactive Variable cover image

State Management with Apollo Client and Vue using Reactive Variable

State Management is an integral part of Software development with tools like VueX, Pinia which is like VueX 5, Redux, Context API, and others. In our example, we will be using Vue3 composition API, a bit of TypeScript. This article will assume that you already have a project setup with Apollo. State Managemnet Before we dive into what state management is and what it offers, lets understand few thing which are integral to it. What is State? State is a part of an application, such as user details, usernames, login information, and website themes(dark or light). In simple terms, state is like a warehouse that's contents you can access when you need to from wherever you are. What is State Management? State Management is just simply a design pattern to help synchronize the state of the application throughout all of the components in the application. This also prevents us from passing too many props accross the application. When to implement State Management: - When the application contains large number of components. - To prevent redundant data, knowing well some other components might need that data. - Prevent passing props across the application, making it messy. For more explainations, you can check the links below: - What does state-management even mean and why does it matter in Front End Web Development with frameworks like React or Vue? by Sean Grogg - What does state-management even mean and why does it matter in Front End Web Development with frameworks like React or Vue? by Robert Polevoi - State management by Tom Nolle How to implement State management? There are so many ways to do it. But in this discussion, we will be implementing it with Vue Apollo. Apollo Client It is the client for Vue Apollo. It helps to easily integrate GraphQL queries and mutation in your application which, in our case, is Vue. Reactive Variables This is a new feature from Apollo Client 3. > Reactive variables are a useful mechanism for representing local state outside of the Apollo Client cache. How to create a Reactive variable in Apollo? This is quite simple since Apollo client provides us with makeVar`. `js import { makeVar } from '@apollo/client'; const counts = makeVar(0); console.log(counts()); // Output: 0 / Update the value of counts */ counts(1); console.log(counts()); // Output: 1 const cartItemsVar = makeVar([]); console.log(cartItemsVar()); // Output: [] / Update the value of cartItemsVar */ cartItemsVar(['Vue', 'Apollo', 'State Management']); console.log(cartItemsVar()); // Output: ['Vue', 'Apollo', 'State Management'] const isLoggedIn = makeVar(false); console.log(isLoggedIn()); // Output: false / Update the value of isLoggedIn */ isLoggedIn(true); console.log(isLoggedIn()); // Output: true ` What the above code means is we make use of makeVar to create a variable of any type be it boolean`, `number`, `array`, or `object`. Also, by calling the variable that makeVar is assigned to without passing a parameter will only output its current value, but passing a parameter will update its value. You can learn more about makeVar here. Setup of the Project We will be creating a state for a counter that does increment, decrement, or reset. But first, let's set up our enviroment. Setting up the Reactive variable Lets create our reactive variable in ./variables/counts.js`: `js import { makeVar } from '@apollo/client'; export const counts = makeVar(0); ` Configuring the cache Lets also update our cache config to something like this: `js // Need to import the variable we reated import { counts } from '../variables/counts'; // Cache implementation const cache = new InMemoryCache({ typePolicies: { Query: { fields: { count: { //The name we will be querying read() { return counts(); //Returns the updated value of counts }, }, }, }, }, }); ` Creating a component Let's create ./components/Counter.vue` component: `html Count: {{ count }} Increment Decrement Reset import { computed, defineComponent } from 'vue'; export default defineComponent({ name: 'NumberCounter', }); import { counts } from 'src/variables/counts'; import { useQuery } from '@vue/apollo-composable'; import gql from 'graphql-tag'; // This is to fetch the count value using the query const COUNTERSQUERY = gql` query Counter { count @client } ; const { result, loading } = useQuery(COUNTERSQUERY); const count = computed(() => (result.value ? result.value.count : 0)); const increment = () => { let cnts = counts(); cnts++; counts(cnts); }; const decrement = () => { let cnts = counts(); cnts--; counts(cnts); }; const reset = () => { cnts= 0; counts(cnts); }; ` Please note that you can always get the counts value even without the GraphQL query. All you need to do is call count()` like the example about _Reactive variables_, and force a re-render using loading as a ref. Like this: `html Count: {{ counts() }} Increment Decrement Reset import { ref, defineComponent } from 'vue'; export default defineComponent({ name: 'NumberCounter', }); import { counts } from 'src/variables/counts'; // loading ref to help re-render when there is a change so as to get the latest value of counts. const loading = ref(false); const increment = () => { let cnts = counts(); loading.value = true; cnts++; counts(cnts); loading.value = false; }; const decrement = () => { let cnts = counts(); loading.value = true; cnts--; counts(cnts); loading.value = false; }; const reset = () => { cnts = 0; loading.value = true; counts(cnts); loading.value = false; }; ` loading ref to help re-render when there is a change so as to get the latest value of counts. The manner you will prefer depends on what you want to achieve. But I will go with the example with the query as it still preserves the GraphQL feel. Conclusion State Management with Apollo Client saves you the stress of installing VueX, or Redux (for the React users), as long as the application makes use of Apollo. In this quick training, we learned about state and why state management is important to building and maintaining applications. Further, we learned how to manage state by using a reactive variable in Apollo. If you need any help understanding how to use this training, or additional clarification, please feel free to reach out to me....

BullMQ with ExpressJS cover image

BullMQ with ExpressJS

Node.js uses an event loop to process asynchronous tasks. The event loop is responsible for handling the execution of asynchronous tasks, but it does it in a single thread. If there is some CPU-intensive or long-running (blocking) logic that needs to be executed, it should not be run on the main thread. This is where BullMQ can help us. In this blog post, we are going to set up a basic queue, using BullMQ. If you'd like to jump right into developing with a queue, check out our starter.dev kit for ExpressJS, where a fully functioning queue is already provided for you. Why and when to use a queue? BullMQ is a library that can be used to implement message queues in Node.js applications. A message queue allows different parts of an application, or different applications, to communicate with each other asynchronously by sending and receiving messages. This can be useful in a variety of situations, such as when one part of the application needs to perform a task that could take a long time, or when different parts of the application need to be decoupled from each other for flexibility and scalability. If you need to process large numbers of messages in a distributed environment, it can help you solve your problems. One such scenario to use a queue is when you need to deal with webhooks. Webhook handler endpoints usually need to respond with 2xx` quickly, therefore, you cannot put long running tasks inside that handler, but you also need to process the incoming data. A good way of mitigating this is to put the incoming data into the queue, and respond quickly. The processing gets taken care of with the BullMQ worker, and if it is set up correctly, it will run inside a child process not blocking the main thread. Prerequisites BullMQ utilizes Redis to handle its message queue. In development mode, we are using docker-compose` to start up a redis instance. We expose the redis docker container's `6379` port to be reachable on the host machine. We also mount the `/misc/data` and the `misc/conf` folders to preserve data for our local development environments. `yaml version: '3' services: ## Other docker containers ... redis: image: 'redis:alpine' command: redis-server /usr/local/etc/redis/redis.conf ports: - '6379:6379' volumes: - ./misc/data:/var/lib/redis - ./misc/conf:/usr/local/etc/redis/ environment: - REDISREPLICATION_MODE=master ` We can start up our infrastructure with the docker-compose up -d` command and we can stop it with the `docker-compose stop` command. We also need to set up connection information for BullMQ. We make it configurable by using environment variables. `typescript // config.constants.ts export const REDISQUEUE_HOST = process.env.REDIS_QUEUE_HOST || 'localhost'; export const REDISQUEUE_PORT = process.env.REDIS_QUEUE_PORT ? parseInt(process.env.REDISQUEUE_PORT) : 6479; ` To start working with BullMQ, we also need to install it to our node project: `bash npm install bullmq ` Setting up a queue Creating a queue is pretty straightforward, we need to pass the queue name as a string and the connection information. `typescript // queue.ts import { Queue } from 'bullmq'; import { REDISQUEUE_HOST, REDISQUEUE_PORT, } from './config.constants'; export const myQueue = new Queue('my-queue', { connection: { host: REDISQUEUE_HOST, port: REDISQUEUE_PORT, }, }); ` We also create a function that can be used to add jobs to the queue from an endpoint handler. I suggest setting up a rule that removes completed and failed jobs in a timely manner. In this example we remove completed jobs after an hour from redis, and we leave failed jobs for a day. These values depend on what you want to achieve, It can very well happen that in a production app, you would keep the jobs for weeks. `typescript // queue.ts import { Queue, Job } from 'bullmq'; // ... const DEFAULTREMOVE_CONFIG = { removeOnComplete: { age: 3600, }, removeOnFail: { age: 24 3600, }, }; export async function addJobToQueue(data: T): Promise> { return myQueue.add('job', data, DEFAULTREMOVE_CONFIG); } ` It would be called when a specific endpoint gets called. `typescript // main.ts app.post('/', async (req: Request, res: Response, next: NextFunction) => { const job = await addJobToQueue(req.body); res.json({ jobId: job.id }); return next(); }); ` The queue now can store jobs, but in order for us to be able to process those jobs, we need to set up a worker. Put the processing into a thread We set up a worker with an async function at the beginning. The worker needs the same name as the queue to start consuming jobs in that queue. It also needs the same connection information as the queue we set up before. `typescript // worker.ts import { Job, Worker } from 'bullmq'; import { REDISQUEUE_HOST, REDISQUEUE_PORT, } from './config.constants'; let worker: Worker export function setUpWorker(): void { worker = new Worker('my-queue', async () => {/ ... */}, { connection: { host: REDISQUEUE_HOST, port: REDISQUEUE_PORT, }, autorun: true, }); defaultWorker.on('completed', (job: Job, returnvalue: 'DONE') => { console.debug(Completed job with id ${job.id}`, returnvalue); }); defaultWorker.on('active', (job: Job) => { console.debug(Completed job with id ${job.id}`); }); defaultWorker.on('error', (failedReason: Error) => { console.error(Job encountered an error`, failedReason); }); } // we call the method after we set up the queue in queue.ts import { Queue } from 'bullmq'; import { setUpWorker } from './worker'; // ... setUpWorker(); ` After we create the worker and set up event listeners, we call the setUpWorker()` method in the `queue.ts` file after the `Queue` gets created. Let's set up the job processor function. `typescript // job-processor.ts import { Job } from 'bullmq'; module.exports = async function jobProcessor(job: Job): Promise { await job.log(Started processing job with id ${job.id}`); console.log(Job with id ${job.id}`, job.data); // TODO: do your CPU intense logic here await job.updateProgress(100); return 'DONE'; }; ` This example processor function doesn't do much, but if we had a long running job, like complex database update operations, or sending data towards a third-party API, we would do it here. Let's make sure our worker will run these jobs on a separate thread. `typescript // worker.ts // ... let worker: Worker const processorPath = path.join(dirname, 'job-processor.js'); export function setUpWorker(): void { worker = new Worker('my-queue', processorPath, { connection: { host: REDISQUEUE_HOST, port: REDISQUEUE_PORT, }, autorun: true, }); // ... } ` If you provide a file path to the worker as the second parameter, BullMQ will run the function exported from the file in a separate thread. That way, the main thread is not used for the CPU intense work the processor does. The above example works if you run the TypeScript compiler on your back-end code (tsc`), but if you prefer keeping your code in TypeScript and run the logic with `ts-node`, then you should use the TypeScript file as your processor path. `typescript const processorPath = path.join(dirname, 'job-processor.ts'); ` The problem with bundlers Sometimes, your back-end code gets bundled with webpack. For example, if you use NX to keep your front-end and back-end code together in one repository, you will notice that your back-end code gets bundled with webpack. In order to be able to run your processors in a separate thread, you need to tweak your project.json` configuration: `json { "targets": { "build": { "options": { "outputPath": "dist/apps/api", "main": "apps/api/src/main.ts", "tsConfig": "apps/api/tsconfig.app.json", "assets": [], "additionalEntryPoints": [ { "entryName": "sync.processor", "entryPath": "apps/api/src/app/queue/job-processor.ts" } ] } } } } ` Conclusion In an ExpressJS application, running CPU intense tasks on the main thread could cause your endpoints to turn unresponsive and/or slow. Moving these tasks into a different thread can help alleviate performance issues, and using BullMQ can help you out greatly. If you want to learn more about NodeJS, check out node.framework.dev for a curated list of libraries and resources. If you are looking to start a new ExpressJS project, check out our starter kit resources at starter.dev...

Being a CTO at Any Level: A Discussion with Kathy Keating, Co-Founder of CTO Levels cover image

Being a CTO at Any Level: A Discussion with Kathy Keating, Co-Founder of CTO Levels

In this episode of the engineering leadership series, Kathy Keating, co-founder of CTO Levels and CTO Advisor, shares her insights on the role of a CTO and the challenges they face. She begins by discussing her own journey as a technologist and her experience in technology leadership roles, including founding companies and having a recent exit. According to Kathy, the primary responsibility of a CTO is to deliver the technology that aligns with the company's business needs. However, she highlights a concerning statistic that 50% of CTOs have a tenure of less than two years, often due to a lack of understanding and mismatched expectations. She emphasizes the importance of building trust quickly in order to succeed in this role. One of the main challenges CTOs face is transitioning from being a technologist to a leader. Kathy stresses the significance of developing effective communication habits to bridge this gap. She suggests that CTOs create a playbook of best practices to enhance their communication skills and join communities of other CTOs to learn from their experiences. Matching the right CTO to the stage of a company is another crucial aspect discussed in the episode. Kathy explains that different stages of a company require different types of CTOs, and it is essential to find the right fit. To navigate these challenges, Kathy advises CTOs to build a support system of advisors and coaches who can provide guidance and help them overcome obstacles. Additionally, she encourages CTOs to be aware of their own preferences and strengths, as self-awareness can greatly contribute to their success. In conclusion, this podcast episode sheds light on the technical aspects of being a CTO and the challenges they face. Kathy Keating's insights provide valuable guidance for CTOs to build trust, develop effective communication habits, match their skills to the company's stage, and create a support system for their professional growth. By understanding these key technical aspects, CTOs can enhance their leadership skills and contribute to the success of their organizations....