Skip to content
Saleor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Authentication and Authorization

This document describes different ways in which clients can authenticate with the API.

Saleor recognizes two types of actors:

  • Users: either customers or staff members (humans)
  • Apps: third-party integrations (programs)

User tokens are short-lived and are used to access resources related to the user, such as their cart or orders.

App tokens are long-lived and are used to access resources related to the app, such as products or orders.

See permissions for more information on permissions.

Choosing the Authentication Method

When integrating with Saleor, one of the first decisions is which authentication mechanism to use.

  • Public access (no token): Some GraphQL operations are public and require no authentication at all. This is the appropriate choice when your integration needs no permissions, such as retrieving public product data for a simple storefront.

  • User-based authentication: When a user is present (e.g., a storefront with customer login or dashboard staff user session), you typically use:

    • OIDC – the recommended method for user login
    • tokenCreate – based on JWT credentials
  • App-based authentication: If you are building machine-to-machine integrations, you use App Tokens, issued to a Saleor App.

    However, apps cannot install or manage other Apps. Those actions require a human user.

  • Dashboard extension authentication: Dashboard extensions receive a special dashboard extension JWT passed automatically via the App SDK.

Authentication with Auth SDK

Saleor Auth SDK is a TypeScript library for adding auth functionality to your TypeScript and JavaScript applications. We also provide tooling to make it easier to use our SDK with React.js and Next.js applications.

Next.js App Router

Next.js 13+ App Router is the recommended way to use the Saleor Auth SDK. It is the easiest to set up and provides the best user experience.

In order to use Saleor Auth SDK in React Server Components, the client needs to be created in the following way:

import { createSaleorAuthClient } from "@saleor/auth-sdk";
import { getNextServerCookiesStorage } from "@saleor/auth-sdk/next/server";

export const getServerAuthClient = () => {
  const nextServerCookiesStorage = getNextServerCookiesStorage();
  return createSaleorAuthClient({
    saleorApiUrl,
    refreshTokenStorage: nextServerCookiesStorage,
    accessTokenStorage: nextServerCookiesStorage,
  });
};

Logging in can be implemented via Server Actions:

<form
  className="bg-white shadow-md rounded p-8"
  action={async (formData) => {
    "use server";

    await getServerAuthClient().signIn(
      {
        email: formData.get("email").toString(),
        password: formData.get("password").toString(),
      },
      { cache: "no-store" },
    );
  }}
>
  {/* … rest of the form … */}
</form>

Then, you can use getServerAuthClient().fetchWithAuth for any queries and mutations.

For a full working example, see the Saleor Auth SDK example.

Next.js (Pages Router) with Apollo Client

Saleor Auth SDK helps you integrate auth functionality into your Next.js application thanks to SaleorAuthProvider that injects the Saleor Auth client into the component tree:

import { AppProps } from "next/app";
import {
  ApolloProvider,
  ApolloClient,
  InMemoryCache,
  createHttpLink,
} from "@apollo/client";
import { createSaleorAuthClient } from "@saleor/auth-sdk";
import { SaleorAuthProvider, useAuthChange } from "@saleor/auth-sdk/react";

const saleorApiUrl = "<your Saleor API URL>";

// Saleor Auth Client
const saleorAuthClient = createSaleorAuthClient({ saleorApiUrl });

// Apollo Client
const httpLink = createHttpLink({
  uri: saleorApiUrl,
  fetch: saleorAuthClient.fetchWithAuth,
});

export const apolloClient = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
});

export default function App({ Component, pageProps }: AppProps) {
  useAuthChange({
    saleorApiUrl,
    onSignedOut: () => apolloClient.resetStore(),
    onSignedIn: () => {
      apolloClient.refetchQueries({ include: "all" });
    },
  });

  return (
    <SaleorAuthProvider client={saleorAuthClient}>
      <ApolloProvider client={apolloClient}>
        <Component {...pageProps} />
      </ApolloProvider>
    </SaleorAuthProvider>
  );
}

and the useSaleorAuthContext hook for accessing the signIn and signOut functions:

import React, { FormEvent } from "react";
import { useSaleorAuthContext } from "@saleor/auth-sdk/react";
import { gql, useQuery } from "@apollo/client";

const CurrentUserDocument = gql`
  query CurrentUser {
    me {
      id
      email
      firstName
      lastName
      avatar {
        url
        alt
      }
    }
  }
`;

export default function LoginPage() {
  const { signIn, signOut } = useSaleorAuthContext();

  const { data: currentUser, loading } = useQuery(CurrentUserDocument);

  const submitHandler = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const result = await signIn({
      email: "admin@example.com",
      password: "admin",
    });

    if (result.data.tokenCreate.errors.length) {
      // handle errors
    }
  };

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <main>
      {currentUser?.me ? (
        <>
          <div>Display user {JSON.stringify(currentUser)}</div>
          <button className="button" onClick={() => signOut()}>
            Log Out
          </button>
        </>
      ) : (
        <div>
          <form onSubmit={submitHandler}>
            {/* You must connect your inputs to state or use a form library such as react-hook-form */}
            <input type="email" name="email" placeholder="Email" />
            <input type="password" name="password" placeholder="Password" />
            <button className="button" type="submit">
              Log In
            </button>
          </form>
        </div>
      )}
    </main>
  );
}

Check the Saleor Auth SDK docs for more details and examples.

You can also check Saleor Auth examples live at saleor-auth-examples.vercel.app along with the source code.

Follow a complete video tutorial on YouTube.

Authentication with GraphQL

Email and Password

This is the most common way of authenticating users. It is used in the Saleor Storefront and Dashboard.

To authenticate a user, you need to use the tokenCreate mutation. It takes an email and password as input and returns an access token that can be used to access resources related to the user and a refresh token that can be used to generate future access tokens.

mutation {
  tokenCreate(email: "hello@example.com", password: "password") {
    token
    refreshToken
    errors {
      field
      message
    }
  }
}

A valid email and password will return a token that can be passed to the authorization header of subsequent HTTP requests:

POST /graphql/ HTTP/1.1
...
Authorization: Bearer <token>

Alternatively, if you already use that header for proxy authentication, you can pass the token in the authorization-bearer header:

POST /graphql/ HTTP/1.1
...
Authorization: ...
Authorization-Bearer: <token>

Access token structure

Saleor’s access tokens are JSON Web Tokens signed with RS256.

A decoded token has the following structure:

{
  "iat": 1671039993,
  "owner": "saleor",
  "iss": "https://example.saleor.cloud/graphql/",
  "exp": 1671040293,
  "token": "K3IJEFFZgshc",
  "email": "admin@example.com",
  "type": "access",
  "user_id": "VXNlcjo1OQ==",
  "is_staff": true
}

Refresh token structure

Saleor’s refresh tokens are JSON Web Tokens signed with RS256.

A decoded token has the following structure:

{
  "iat": 1671039993,
  "owner": "saleor",
  "iss": "https://example.saleor.cloud/graphql/",
  "exp": 1673631993,
  "token": "K3IJEFFZgshc",
  "email": "admin@example.com",
  "type": "refresh",
  "user_id": "VXNlcjo1OQ==",
  "is_staff": true,
  "csrf_token": "cAgshsjNr9AnsMKsAkpq4Q3fb4qBBPQT5mIyDMxShQEyO1fVpwCVzVyNQAJAZfKi"
}

Customer vs. staff tokens

tokenCreate returns the same token shape for both customers and staff users. The decoded token includes an is_staff claim that reflects the role of the underlying User, but permissions themselves are not embedded in the token — they are resolved server-side on each request from the user’s permission groups and direct permissions.

Treat is_staff as informational only. Do not use it on the client to make authorization decisions; rely on the response of the actual operation and on the me query (which exposes userPermissions and permissionGroups) when you need to drive UI by role.

Fetching information about the currently authenticated user

You can use the me query to retrieve information about the currently authenticated user:

query {
  me {
    id
    email
  }
}

This query will verify the token and return the current user’s data or an error if the token is invalid.

Verifying tokens

There are two ways to verify a token: by using the JWKS file or via a GraphQL mutation.

Verifying tokens with a JWKS file

You can verify tokens in your code by checking that it’s signed with a trusted key. You can find the necessary JSON Web Key Set (JWKS) by visiting https://<your-saleor-domain>/.well-known/jwks.json.

Here is an example implemented in TypeScript with the use of the jose library:

const token = `<token>`;

import * as jose from "jose";

const JWKS = jose.createRemoteJWKSet(
  new URL("https://<your-saleor-domain>/.well-known/jwks.json"),
);

const result = await jose.jwtVerify(token, JWKS);

console.log(result);
Verifying tokens with a mutation

You can verify tokens by calling the tokenVerify mutation:

mutation {
  tokenVerify(token: "<token>") {
    isValid
    errors {
      field
      code
    }
  }
}

Refreshing the access token

Refresh tokens have a longer lifetime than access tokens. You can generate a new access token by using the tokenRefresh mutation. It takes a refresh token as input and returns a new access token.

mutation {
  tokenRefresh(refreshToken: "<refresh-token>") {
    token
  }
}

In the future, tokenRefresh mutation will also generate a new refresh token and refresh tokens will be one-time use only.

Deactivating all tokens of a particular user

The tokensDeactivateAll mutation will invalidate all tokens (access and refresh, including the token used to invoke the mutation) that belong to the invoking user.

mutation {
  tokensDeactivateAll {
    errors {
      field
      message
      code
    }
  }
}

Typical authentication flow for a storefront

In the most common scenario, you’ll use the tokenCreate mutation with the user’s email and password when they try logging in inside a storefront.

The mutation will return an access token and a refresh token. Depending on your use case, you might consider storing refresh token in localStorage so that the token is persisted between page reloads and browser sessions.

You should make sure to refresh your access token (generate a new one using refresh token) whenever it’s close to its expiration date or expired.

If you make a request with an expired access token, you will receive an ExpiredSignatureError (“Signature has expired”). You can implement a flow where your app automatically tries to refresh the access token in such cases, and then repeat the same request with the newly generated access token.

When refreshing the access token fails, you should remove the refresh token from storage and ask the user to log in again.

Login throttling

To prevent accounts from credential-guessing attacks, Saleor uses login throttling. First, it blocks concurrent login requests originating from the same IP address for the duration of the authentication process. Next, if the user fails to log in due to an invalid password, the block time will be extended by a power of 2 seconds. In case of an unknown email address, the policy is loosened and the delay of the next login attempt will be extended after every 10 attempts. The maximum time for a user’s IP can be blocked is set to 60 minutes. The throttling mechanism will be reset after a successful login attempt.

Password reset throttling

To prevent abuse of the password reset functionality, Saleor uses password reset throttling. You can only request single password reset link per user per 15 minutes. If you self-host Saleor, you can modify this limit by setting the RESET_PASSWORD_LOCK_TIME environment variable.

Password Login Mode

Saleor allows administrators to control whether password-based authentication is available. This is useful for organizations transitioning staff to OIDC/SSO — you can gradually restrict password login for staff while optionally keeping it available for customers.

The passwordLoginMode setting on Shop supports three modes:

Modes

Mode Customers Staff API-observable behavior
ENABLED (default) Normal login Normal login tokenCreate issues tokens for both roles as usual
CUSTOMERS_ONLY Normal login Staff can still log in, but they get no permissions tokenCreate succeeds for staff, but the issued token resolves no staff permissions — operations requiring MANAGE_* permissions will fail as if the caller were a customer
DISABLED Blocked Blocked tokenCreate and tokenRefresh return DISABLED_AUTHENTICATION_METHOD; password-related mutations are blocked

Configuring the Mode

Use the shopSettingsUpdate mutation to change the mode:

mutation ShopSettingsUpdate($input: ShopSettingsInput!) {
  shopSettingsUpdate(input: $input) {
    shop {
      passwordLoginMode
    }
    errors {
      field
      code
      message
    }
  }
}
{
  "input": {
    "passwordLoginMode": "CUSTOMERS_ONLY"
  }
}
{
  "data": {
    "shopSettingsUpdate": {
      "shop": {
        "passwordLoginMode": "CUSTOMERS_ONLY"
      },
      "errors": []
    }
  }
}

Affected Mutations

  • tokenCreate — returns an error when DISABLED; in CUSTOMERS_ONLY, staff can still log in, but the token has no staff permissions
  • tokenRefresh — returns an error when DISABLED; in CUSTOMERS_ONLY, staff can still log in, but the token has no staff permissions
  • passwordChange — blocked when DISABLED
  • setPassword — blocked when DISABLED
  • requestPasswordReset — blocked when DISABLED

Error Codes

Code Type Returned when
DISABLED_AUTHENTICATION_METHOD AccountErrorCode A password mutation is called when mode is DISABLED
PASSWORD_AUTH_RESTRICTION ShopErrorCode A password-authenticated admin tries to restrict the mode

Blocking storefront traffic

Added in Saleor 3.23.

By default the GraphQL API is public — anonymous clients and logged-in customers can call it directly. Setting Shop.allowStorefrontTraffic to false turns that off: only apps (app tokens) and staff users can reach the API.

This is meant for deployments that put a proxy or a backend-for-frontend in front of the whole API, where that layer holds app or staff credentials and is the only client talking to Saleor directly.

Example use case

Advanced, non-standard setups where customer flows must be controlled by your own backend logic. Some operations can be proxied to Saleor as-is, while others need enrichment or extra protection to keep business rules valid.

For example, a multi-tenant storefront where each customer may only shop in certain sub-shops. Saleor alone cannot express that constraint, so the backend must resolve which channels a given customer is entitled to and rewrite or reject the request accordingly. If customers could also call the API directly, they would be able to bypass that logic and order from a sub-shop they have no access to. Blocking storefront traffic makes the backend the only way in, so the rule always applies.

Caller allowStorefrontTraffic: true (default) allowStorefrontTraffic: false
App token allowed allowed
Staff user allowed allowed
Customer (non-staff) allowed HTTP 401
Anonymous or invalid token allowed HTTP 401

Rejected requests are never executed. The response is:

{
  "errors": [
    {
      "message": "Storefront traffic is not allowed.",
      "extensions": { "code": "STOREFRONT_TRAFFIC_NOT_ALLOWED" }
    }
  ]
}

A batched request is rejected as a whole: the response is a single 401 carrying one such error object per batch entry.

Configuring the setting

Use the shopSettingsUpdate mutation. It requires the MANAGE_SETTINGS permission:

mutation ShopSettingsUpdate($input: ShopSettingsInput!) {
  shopSettingsUpdate(input: $input) {
    shop {
      allowStorefrontTraffic
    }
    errors {
      field
      code
      message
    }
  }
}
{
  "input": {
    "allowStorefrontTraffic": false
  }
}

What else gets blocked

The check runs before the GraphQL document is inspected, so it rejects every non-privileged request regardless of the operation. That includes:

This is by design: exempting operations would require classifying the document before the check and maintaining an allowlist. Do not enable this setting if you need public login or public introspection.

User authentication using OpenID Connect

OpenID Connect (OIDC) is an OAuth 2.0 extension allowing the transfer of identity information between the client and the authorization server.

Saleor supports OIDC authentication in two modes:

  • Single Sign-On (SSO): the client receives the access token directly from the authorization server and uses it to authenticate with Saleor. Implicit flow is the most common way of authenticating users in Single Page Applications (SPAs).

  • Saleor as an OIDC client: the client uses Saleor as a proxy to the authorization server.

To enable OIDC authentication, you must enable and configure the OpenID Connect plugin using the Saleor dashboard. See OIDC Plugin docs.

OAuth scopes

The implicit flow requires the following scopes to operate:

  • openid: required by the OIDC specification
  • profile: required to retrieve the user’s information
  • email: required to retrieve the user’s email address
  • saleor:<permission>: optional, used to determine user permissions

Permissions

If Use OAuth scope permissions is enabled, any permissions granted to a user on the authentication provider side will be used as the effective permissions on the Saleor side.

If the plugin is configured to manage user permissions, you need to create OAuth scopes corresponding to Saleor permissions on the identity provider side. The plugin will request lowercase scope names based on the PermissionEnum. For example, the MANAGE_PRODUCTS corresponds to the saleor:manage_products scope.

Saleor will look for supported permissions in the scope field of the access token. If the scope doesn’t contain any Saleor permissions, Saleor will look for permissions in the token’s permissions field.

Please see the documentation of your authentication server to see how to manage permissions and configure role-based access control (RBAC).

See also:

OIDC Single Sign-On (SSO) flow

In this mode, Saleor’s API only verifies if the token included in the request is valid.

Operations like login, logout, or token refresh have to be handled by your code and are best handled using the identity provider’s native SDK.

Authentication flow

Setup

To use this flow, you need to configure the OIDC plugin providing values for JSON Web Key Set URL, User info URL. If you use OAuth permissions, you will also need to provide Audience value.

Once you obtain the access token, include it in the HTTP authorization header when making requests to the API:

POST /graphql/ HTTP/1.1
...
Authorization: Bearer <access-token>

Saleor as an OIDC client (legacy) flow

In this mode, Saleor acts as a proxy between the client and the authorization server. It handles the login, logout, and token refresh operations.

Authentication flow

Setup

To use this flow, you need to configure the OIDC plugin by providing values for JSON Web Key Set URL, OAuth Token URL, and OAuth Authorization URL.

Once you obtain the access token, include it in the HTTP authorization header when making requests to the API:

POST /graphql/ HTTP/1.1
...
Authorization: Bearer <access-token>

Initiating the authentication flow

To authenticate the user, you will need to redirect them to the identity provider’s login page.

Start by calling the externalAuthenticationUrl mutation passing "mirumee.authentication.openidconnect" as the pluginId argument and include a redirectUri parameter in the input argument:

mutation {
  externalAuthenticationUrl(
    pluginId: "mirumee.authentication.openidconnect"
    input: "{\"redirectUri\":\"https://example.com/callback\"}"
  ) {
    authenticationData
    accountErrors {
      field
      message
    }
  }
}

The resulting authenticationData field will contain the authorization URL:

{
  "data": {
    "externalAuthenticationUrl": {
      "authenticationData": "{\"authorizationUrl\": \"https://saleor-test.eu.auth0.com/authorize?response_type=code&client_id=RUgv72Cvzd5xjlMtgOEQLJ8QF4eQ3e1U&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&scope=openid+profile+email+offline_access&state=eyJyZWRpcmVjdFVyaSI6Imh0dHA6Ly8xMjcuMC4wLjE6MzAwMC9jYWxsYmFjayJ9%3A1l1W9H%3AFsxnhejCQKB4JdFL-t0BqNPrHtODh9T0mG2E3KzT-bQ\"}",
      "accountErrors": []
    }
  }
}

Next, redirect the user to the identity provider’s login page. Once the user is authenticated, the identity provider will redirect them to the redirectUri you provided in the externalAuthenticationUrl mutation.

Obtaining an access token

The externalObtainAccessTokens mutation will allow you to exchange the token returned by the identity provider for a Saleor access token.

Once again, you will need to pass "mirumee.authentication.openidconnect" as the pluginId argument, this time including the code and state received in the query parameters:

mutation {
  externalObtainAccessTokens(
    pluginId: "mirumee.authentication.openidconnect"
    input: "{\"code\": \"uLB...LJ3\", \"state\": \"eyJ...c-E\"}"
  ) {
    token
    refreshToken
    user {
      id
      email
    }
    accountErrors {
      field
      code
      message
    }
  }
}

Refreshing tokens

The externalRefresh mutation will generate a new access token when given a valid refresh token.

As with the previous calls, you will need to pass "mirumee.authentication.openidconnect" as the pluginId argument, this time including the refreshToken:

mutation {
  externalRefresh(
    pluginId: "mirumee.authentication.openidconnect"
    input: "{\"refreshToken\": \"xJc...Uas\"}"
  ) {
    token
    refreshToken
    accountErrors {
      field
      message
      code
    }
  }
}

Verifying tokens

To verify the token, use the following externalVerify mutation.

Once again, you will need to pass "mirumee.authentication.openidconnect" as the pluginId argument, this time including the token:

mutation {
  externalVerify(
    pluginId: "mirumee.authentication.openidconnect"
    input: "{\"token\": \"eyJ0eXAiOiJK...J1M7tqSzP0\"}"
  ) {
    isValid
    user {
      userPermissions {
        code
        name
      }
    }
  }
}

The user field will contain the user’s details if the token is valid.

Logging out

Logging out requires the user to be redirected to the identity provider’s logout page.

You can prepare the logout URL by calling the externalLogout mutation. Any parameters you pass in the input argument will be included in the logout URL.

mutation ($input: JSONString!) {
  externalLogout(
    pluginId: "mirumee.authentication.openidconnect"
    input: "{\"returnTo\": \"https://example.com\"}"
  ) {
    logoutData
    accountErrors {
      field
      message
      code
    }
  }
}

The logoutData field will contain the logout URL:

{
  "data": {
    "externalLogout": {
      "logoutData": "{\"logoutUrl\": \"https://saleor-test.eu.auth0.com/v2/logout?returnTo=https%3A%2F%2Fexample.com\"}",
      "accountErrors": []
    }
  }
}

Next, redirect the user to the identity provider’s logout page.

App authentication

Saleor Apps are a primary method for extending Saleor’s core functionalities.

Unlike regular users, Saleor Apps have long-living tokens they can use to communicate with the API.
These tokens are generated when an app is installed or created and must be stored securely. To obtain an auth token, you must either install a third-party app or create a local app.

The authorization header for Apps is similar to the one used for users:

POST /graphql/ HTTP/1.1
...
Authorization: Bearer <app-token>

When calling graphQL, you may see AUTHENTICATED_APP error - if this happens, ensure you have attached valid token to the request header.

Apart of the authentication, app must also authorize with valid permissions, that are applied once app is created.

Dashboard extensions

Saleor Apps extending the dashboard will be passed a special JSON Web Token (JWT) with the effective permission set equal to the intersection of user and application permissions. This token allows your frontend part to access the API on behalf of the user.

You can also use this token to authenticate browser calls to your application’s API by validating the token in your backend. See the user authentication section for more details on obtaining the necessary JWKS.

See the Saleor App SDK documentation for more details on how to receive the token.

Authentication with Playground

If you are using Playground to access the API, you can authorize your requests by providing user or app token in the HTTP HEADERS tab. To do that, paste the following JSON structure into the tab, replacing the token with your real token:

{
  "Authorization": "Bearer <your-access-token>"
}

Last updated on August 14, 2026

Was this page helpful?