Skip to main content
This guide covers OAuth authentication flows for Shopify apps using the Shopify API library.

OAuth Flow

The library provides two main authentication methods:
  1. OAuth flow - For public apps
  2. Token exchange - For embedded apps (recommended)

Standard OAuth Flow

The OAuth flow involves two steps: beginning the OAuth process and handling the callback.
1

Begin OAuth

Redirect the user to Shopify’s OAuth authorization page.
app.get('/auth', async (req, res) => {
  await shopify.auth.begin({
    shop: req.query.shop,
    callbackPath: '/auth/callback',
    isOnline: false,
    rawRequest: req,
    rawResponse: res,
  });
});
Parameters:
  • shop - The shop domain (e.g., example.myshopify.com)
  • callbackPath - Where Shopify redirects after authorization
  • isOnline - true for online tokens, false for offline tokens
The begin function creates a state cookie and redirects to Shopify’s OAuth authorization endpoint.Source: lib/auth/oauth/oauth.ts:60-127
2

Handle OAuth Callback

Process the callback from Shopify and create a session.
app.get('/auth/callback', async (req, res) => {
  try {
    const {session, headers} = await shopify.auth.callback({
      rawRequest: req,
      rawResponse: res,
    });

    // Store the session
    await shopify.config.sessionStorage.storeSession(session);

    // Redirect to app
    res.redirect(`/?shop=${session.shop}`);
  } catch (error) {
    console.error('Auth error:', error);
    res.status(500).send('Authentication failed');
  }
});
The callback validates the HMAC, verifies the state cookie, and exchanges the authorization code for an access token.Source: lib/auth/oauth/oauth.ts:129-236
Token exchange allows you to obtain access tokens using session tokens without redirecting users through OAuth.
import {RequestedTokenType} from '@shopify/shopify-api';

app.post('/api/auth/token-exchange', async (req, res) => {
  const sessionToken = req.headers['authorization']?.replace('Bearer ', '');
  
  const {session} = await shopify.auth.tokenExchange({
    shop: req.body.shop,
    sessionToken,
    requestedTokenType: RequestedTokenType.OfflineAccessToken,
  });

  await shopify.config.sessionStorage.storeSession(session);
  
  res.json({success: true});
});
Token Types:
  • RequestedTokenType.OnlineAccessToken - User-specific access (expires)
  • RequestedTokenType.OfflineAccessToken - Shop-level access (long-lived)
Source: lib/auth/oauth/token-exchange.ts:32-79

Online vs Offline Access Tokens

Offline access tokens are tied to the shop and persist until uninstalled.
await shopify.auth.begin({
  shop: 'example.myshopify.com',
  callbackPath: '/auth/callback',
  isOnline: false, // Offline token
  rawRequest: req,
  rawResponse: res,
});
Use offline tokens for:
  • Background jobs
  • Webhooks
  • Operations that don’t require a specific user

Session Management

After authentication, the library creates a Session object containing the access token.
interface Session {
  id: string;                    // Unique session identifier
  shop: string;                  // Shop domain
  state: string;                 // OAuth state
  isOnline: boolean;            // Token type
  scope?: string;               // Granted scopes
  accessToken?: string;         // Access token
  expires?: Date;               // Token expiration
  onlineAccessInfo?: OnlineAccessInfo; // User info (online tokens only)
}

Validating Sessions

const session = await shopify.config.sessionStorage.loadSession(sessionId);

if (!session.isActive(shopify.config.scopes)) {
  // Session is expired or scopes have changed - re-authenticate
  return res.redirect('/auth');
}
Source: lib/session/session.ts:198-206

Error Handling

The library throws specific errors during authentication:
import {
  BotActivityDetected,
  CookieNotFound,
  InvalidOAuthError,
} from '@shopify/shopify-api';

try {
  const {session} = await shopify.auth.callback({...});
} catch (error) {
  if (error instanceof BotActivityDetected) {
    // Bot detected, return 410 Gone
    return res.status(410).send('Bot activity detected');
  }
  
  if (error instanceof CookieNotFound) {
    // OAuth state cookie missing
    return res.redirect('/auth');
  }
  
  if (error instanceof InvalidOAuthError) {
    // Invalid callback parameters or HMAC
    return res.status(400).send('Invalid OAuth callback');
  }
}
Source: lib/error.ts:94-96
The OAuth flow includes bot detection using the isbot library. Bot requests receive a 410 Gone status.Source: lib/auth/oauth/oauth.ts:82-86

HMAC Validation

All OAuth callbacks are automatically validated using HMAC signatures:
// The library automatically validates:
// 1. HMAC signature from Shopify
// 2. State parameter matches cookie
// 3. Shop domain is valid
Source: lib/auth/oauth/oauth.ts:238-251

Complete Example

import express from 'express';
import {shopifyApi, RequestedTokenType} from '@shopify/shopify-api';

const shopify = shopifyApi({...});
const app = express();

// Begin OAuth
app.get('/auth', async (req, res) => {
  await shopify.auth.begin({
    shop: req.query.shop,
    callbackPath: '/auth/callback',
    isOnline: false,
    rawRequest: req,
    rawResponse: res,
  });
});

// OAuth callback
app.get('/auth/callback', async (req, res) => {
  const {session} = await shopify.auth.callback({
    rawRequest: req,
    rawResponse: res,
  });

  await shopify.config.sessionStorage.storeSession(session);
  res.redirect(`/?shop=${session.shop}`);
});
Never use OAuth authentication for private/custom apps. Private apps should use the adminApiAccessToken configuration instead.Source: lib/auth/oauth/oauth.ts:253-260

Build docs developers (and LLMs) love