Aperçus

ArrowDown

Le paysage du e-commerce est en constante évolution, exigeant de nouvelles stratégies et une optimisation continue. Notre équipe vous apporte des conseils d'experts, des conseils pratiques et les dernières tendances pour aider votre entreprise à se développer et à garder une longueur d'avance.

Comment migrer vers les nouveaux jetons d'accès expirants de Shopify

Comment migrer vers les nouveaux jetons d’accès expirants de Shopify

Si vous avez déjà développé ou maintenu une app Shopify, vous connaissez le jeton d’accès offline comme cette chose qu’on récupère une fois à l’installation et qu’on ne revoit plus jamais. Il dort dans votre base de données, fonctionne indéfiniment, et authentifie tranquillement chaque tâche de fond, chaque webhook et chaque cron job de votre app — jusqu’à la désinstallation.

Ça change. Shopify fait passer les jetons d’accès offline de non-expirants à expirants, appuyés par un refresh token — exactement le même modèle qu’utilisent presque toutes les autres API OAuth2 sur la planète. Si votre app communique avec l’API Admin de Shopify en dehors d’une session utilisateur active, ça vous concerne directement, et il y a une échéance ferme.

Pourquoi un jeton qui expire est en fait un meilleur design

Ça peut sembler être un recul — une chose de plus à gérer plutôt que « configurer une fois et oublier ». En pratique, c’est une amélioration de sécurité concrète, et ça ne coûte rien à vos marchands.

  • Un rayon d’impact réduit en cas de fuite. Un jeton non-expirant qui se retrouve dans un fichier de log, une vieille sauvegarde ou un serveur compromis reste valide indéfiniment, pour quiconque le détient. Un jeton expirant, c’est une horloge qui tourne — même un jeton divulgué devient inutile en moins d’une heure.
  • Une rotation forcée, par conception. Chaque rafraîchissement émet un tout nouvel access token et un tout nouveau refresh token. Vous ne comptez plus sur votre équipe pour penser à faire tourner manuellement un secret longue durée ; le protocole le fait pour vous à chaque utilisation.
  • Aucune friction pour l’utilisateur final. Ça reste un jeton offline — aucune invite de réauthentification pour le marchand, aucune interruption dans l’expérience de votre app. Seul ce que fait votre serveur en coulisses change.
  • Une hygiène OAuth2 standard. Shopify aligne simplement ses jetons offline sur le modèle access-token/refresh-token qu’utilisent déjà Google, Microsoft et pratiquement toutes les API modernes. Si votre équipe a déjà intégré l’une d’elles, ça vous sera familier.

Comment fonctionnent réellement les nouveaux jetons

Il existe maintenant deux variantes du jeton offline :

Ancien modèle (non-expirant)Nouveau modèle (expirant)
Durée de vie de l’access tokenIndéfinie (jusqu’à la désinstallation ou la révocation du secret)1 heure (expires_in: 3600)
Durée de vie du refresh tokenN/A90 jours (refresh_token_expires_in: 7776000)
RotationAucuneNouvelle paire access + refresh token à chaque rafraîchissement

Pour demander un jeton expirant, ajoutez expiring=1 à votre flux OAuth existant :

  • Token exchange (le flux recommandé si votre app utilise les session tokens d’App Bridge) : POST https://{shop}.myshopify.com/admin/oauth/access_token avec client_id, client_secret, grant_type: urn:ietf:params:oauth:grant-type:token-exchange, subject_token, subject_token_type, requested_token_type: urn:shopify:params:oauth:token-type:offline-access-token, et expiring=1.
  • Authorization code grant (le flux d’installation OAuth classique) : le même endpoint, en remplaçant subject_token par le code reçu lors de la redirection, et en ajoutant expiring=1.

Une fois votre access token expiré, vous le rafraîchissez sur le même endpoint avec grant_type: refresh_token et votre refresh_token courant. La réponse vous remet un nouvel access token et un nouveau refresh token — celui que vous venez d’utiliser est immédiatement invalidé.

Un détail à intégrer dans votre logique de retry : Shopify rejoue exactement la même réponse pendant jusqu’à une heure après un rafraîchissement réussi, donc si votre première requête expire côté réseau mais a en fait réussi, réessayer est sans risque. Traitez les timeouts, les 5xx et les 429 comme transitoires et sécuritaires à réessayer — mais un 401 avec invalid_request signifie que la relation du jeton est rompue et qu’il faut réacquérir l’accès via token exchange depuis le début.

L’échéance

Ce n’est pas facultatif indéfiniment :

  • Les apps publiques créées après le 1er avril 2026 doivent utiliser les jetons expirants dès leur création.
  • Les apps publiques créées avant cette date ont jusqu’au 1er janvier 2027 pour migrer — après quoi les requêtes utilisant l’ancien flux non-expirant commenceront à être rejetées.
  • Les apps personnalisées et les apps créées par les marchands sont exemptées de cette obligation, même s’il n’y a aucun inconvénient à adopter le nouveau modèle malgré tout.

Si vous maintenez une app publique, c’est un vrai projet de migration avec une vraie échéance — pas un élément « un jour peut-être » dans votre backlog.

Un 403 qu’on a rencontré nous-mêmes : GraphQL Client: Forbidden sur une app toute neuve

On est tombés là-dessus en testant cette migration : on a créé une nouvelle app de test dans le Partner Dashboard — bien après le 1er avril 2026 — en réutilisant le code d’une app qu’on avait construite avant cette date charnière. Chaque appel Admin GraphQL sur la nouvelle app revenait avec un 403 générique, que le SDK affiche comme GraphQL Client: Forbidden, sans plus de détail dans le corps de la réponse.

La cause était exactement la règle d’application vue plus haut : notre code demandait encore l’ancien flux de jeton offline non-expirant — ce qui fonctionnait très bien pour l’app d’origine, puisqu’elle a été créée avant le 1er avril 2026. Mais la nouvelle app qu’on venait de créer tombe pile dans la règle « créée après le 1er avril 2026 », qui exige des jetons expirants dès la création. Shopify applique ça immédiatement pour les nouvelles apps — ça n’attend pas la période de grâce du 1er janvier 2027, qui ne s’applique qu’aux apps déjà existantes avant la date charnière. Notre ancien flux OAuth non-expirant a été rejeté d’emblée, et ça s’est manifesté par un 403 sur le client GraphQL plutôt que par une erreur claire à l’étape OAuth.

La correction, c’est exactement l’étape de migration ci-dessous : activer expiringOfflineAccessTokens: true (ou ajouter expiring=1 si vous appelez directement les endpoints de token exchange/OAuth) pour que l’app demande un jeton expirant conforme dès l’installation. Une fois ça fait, le même code s’est authentifié sans problème sur la nouvelle app.

Si vous êtes arrivé ici en cherchant exactement cette erreur — un simple GraphQL Client: Forbidden / 403 sur des appels Admin GraphQL d’une app que vous venez de créer — vérifiez d’abord la date de création de votre app par rapport au 1er avril 2026. C’est facile à manquer si vous réutilisez un code plus ancien pour créer de nouvelles apps de test ou de nouveaux client IDs, exactement comme on l’a fait.

Comment migrer une app existante

  1. Étendez votre stockage de session. Ajoutez expires_at, refresh_token et refresh_token_expires_at en plus de l’access token que vous stockez déjà par boutique.
  2. Ajoutez une logique de rafraîchissement préventif. Avant qu’une tâche de fond, un webhook ou une tâche planifiée n’appelle l’API Admin, vérifiez si l’access token est expiré (ou sur le point de l’être) et rafraîchissez-le de manière proactive plutôt que de réagir à un 401 en plein milieu d’une requête.
  3. Basculez les nouvelles installations vers les jetons expirants. Ajoutez expiring=1 à votre flux d’installation pour que chaque nouveau marchand parte directement sur le nouveau modèle.
  4. Migrez les installations existantes. Pour les marchands installés avant ce changement, faites un token exchange de leur ancien jeton non-expirant en tant que subject_token pour recevoir une nouvelle paire expirante. Attention ici : cela révoque immédiatement l’ancien jeton non-expirant, et c’est irréversible. Ne lancez pas cette migration avant que votre logique de rafraîchissement soit en production et testée — il n’y a pas de retour en arrière une fois une boutique basculée.

En code : avec le SDK Node

Si votre app tourne sur @shopify/shopify-api (que shopify-app-express et shopify-app-remix encapsulent tous les deux), vous n’avez pas besoin d’implémenter les appels HTTP ci-dessus à la main — les quatre étapes de migration se traduisent en trois méthodes du SDK.

Nouvelles installations — demandez un jeton expirant dès le token exchange :

import { RequestedTokenType } from "@shopify/shopify-api";

const { session } = await shopify.auth.tokenExchange({
  sessionToken, // depuis l'en-tête Authorization ou le paramètre `id_token`
  shop,
  requestedTokenType: RequestedTokenType.OfflineAccessToken,
  expiring: true,
});

await sessionStorage.storeSession(session);

Installations existantes — migrez l’ancien jeton non-expirant, une seule fois :

const { session } = await shopify.auth.migrateToExpiringToken({
  shop,
  nonExpiringOfflineAccessToken: existingToken,
});

await sessionStorage.storeSession(session);

C’est l’appel du SDK qui révoque immédiatement existingToken — ne l’exécutez pour une boutique qu’une fois la logique de rafraîchissement ci-dessous déployée et testée.

Avant tout appel de fond — vérifiez l’expiration et rafraîchissez de manière proactive :

async function getFreshOfflineSession(shop: string) {
  const session = await sessionStorage.loadOfflineSession(shop);

  const expiringSoon =
    session.expires &&
    session.expires.getTime() - Date.now() < 5 * 60 * 1000; // marge de 5 min

  if (expiringSoon && session.refreshToken) {
    const { session: refreshed } = await shopify.auth.refreshToken({
      shop,
      refreshToken: session.refreshToken,
    });
    await sessionStorage.storeSession(refreshed);
    return refreshed;
  }

  return session;
}

Appelez getFreshOfflineSession partout où vos webhooks et tâches de fond chargent actuellement une session, et la durée de vie d’une heure de l’access token devient un détail d’implémentation dont le reste de votre code n’a plus à se soucier.

Si vous utilisez shopify-app-react-router

Si vous avez généré votre app avec shopify app init récemment, ou migré depuis shopify-app-remix, vous utilisez @shopify/shopify-app-react-router — le package framework qui encapsule tout ce qui précède. Il utilise le token exchange comme stratégie d’authentification par défaut, donc il n’y a pas de flux OAuth classique à maintenir. Vous n’appelez généralement pas non plus shopify.auth.tokenExchange ou shopify.auth.refreshToken vous-même — vous obtenez plutôt une session valide via l’un des trois points d’entrée suivants, selon l’endroit où votre code s’exécute.

Un point à préciser : les jetons expirants ne sont pas activés par défaut ici pour l’instant. Le support est arrivé derrière un flag future nommé expiringOfflineAccessTokens — introduit dans @shopify/shopify-app-remix@4.1.0 et, pour le package React Router, dans @shopify/shopify-app-react-router@1.1.0. Au moment d’écrire ces lignes, dans les dernières versions publiées (shopify-app-remix@4.2.1, shopify-app-react-router@1.2.1), il reste optionnel et vaut false par défaut — la librairie ne l’a pas encore activé pour vous, donc sur toute version actuelle il faut l’activer explicitement :

// app/shopify.server.ts
export const shopify = shopifyApp({
  // ...votre config existante : api, appUrl, etc.
  future: {
    expiringOfflineAccessTokens: true,
  },
});

Une fois ce flag activé, authenticate.admin, authenticate.webhook et unauthenticated.admin se mettent tous à vérifier l’expiration et à rafraîchir les sessions offline pour vous en toute transparence — c’est ce sur quoi reposent les trois exemples ci-dessous. Vu les échéances de migration mentionnées plus haut, n’attendez pas qu’une future version en fasse le défaut : activez-le maintenant et testez le flux avant d’y être forcé.

Dans le loader ou l’action d’une route UI :

import type { LoaderFunctionArgs } from "react-router";
import { authenticate } from "../shopify.server";

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { admin, session } = await authenticate.admin(request);

  const response = await admin.graphql(`#graphql
    query {
      shop { name }
    }
  `);

  return response.json();
};

authenticate.admin vérifie d’abord le stockage de session pour une session valide ; si elle est absente ou expirée, il effectue le token exchange (ou le rafraîchissement) pour vous avant de vous remettre admin et session.

Dans un gestionnaire de webhook :

import type { ActionFunctionArgs } from "react-router";
import { authenticate } from "../shopify.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, admin, session, payload } = await authenticate.webhook(request);

  if (!session) {
    // L'app a peut-être été désinstallée — aucune session offline à utiliser.
    throw new Response();
  }

  if (topic === "PRODUCTS_UPDATE") {
    await admin.graphql(`#graphql
      mutation touchProduct($id: ID!, $time: String!) {
        metafieldsSet(metafields: [{
          ownerId: $id, namespace: "my-app", key: "webhook_at", value: $time, type: "string"
        }]) { metafields { key } }
      }
    `, { variables: { id: payload.admin_graphql_api_id, time: new Date().toISOString() } });
  }

  return new Response();
};

Dans une tâche de fond ou un worker — en dehors de tout contexte de requête (un consommateur de queue, une tâche cron) —, il n’y a pas de session token en direct pour relancer un token exchange, donc le package se rabat sur le chargement (et, au besoin, le rafraîchissement) de la session offline déjà présente dans votre adaptateur de stockage :

import { unauthenticated } from "../shopify.server";

export async function syncInventory(shop: string) {
  const { admin } = await unauthenticated.admin(shop);
  await admin.graphql(`#graphql
    query { shop { name } }
  `);
}

Le détail qui piège le plus souvent : ne passez que le domaine shop dans le payload de votre queue, jamais une session ou un jeton. Appelez unauthenticated.admin(shop) à neuf à l’intérieur de chaque exécution de tâche plutôt que de réutiliser un client mis en cache au moment où la tâche a été mise en file — c’est ce qui garantit que vous travaillez avec l’access token courant, éventuellement tout juste rafraîchi, plutôt qu’avec un jeton expiré pendant que la tâche patientait dans la queue.

Un piège à anticiper

La durée de vie de 90 jours du refresh token est une limite ferme, pas un simple avertissement. Si la boutique d’un marchand reste inactive — aucune tâche en cours, aucun webhook déclenché — pendant plus de 90 jours, votre refresh token expire en même temps que tout le reste. À ce moment-là, un appel de rafraîchissement ne fonctionnera plus ; il faut réacquérir l’accès via un token exchange, comme s’il s’agissait d’une nouvelle installation. Si votre app a des boutiques peu actives ou dormantes, prévoyez une vérification pour ce cas — pour éviter de vous retrouver silencieusement bloqué hors d’un compte que vous croyiez toujours connecté.

Ne laissez pas ça passer entre les mailles du filet

C’est exactement le genre de changement de plateforme facile à manquer jusqu’à ce qu’il casse quelque chose en production — une échéance annoncée dans un changelog que personne ne relit. Si vous n’êtes pas certain que la gestion des jetons de votre app est prête pour ce changement, ou si vous préférez qu’une équipe surveille les changements de plateforme Shopify à votre place plutôt que de les découvrir à la dure, c’est exactement à ça que sert un Dev Retainer. Contactez-nous, on va vous aider à prendre les devants.

Si vous préférez qu’on s’occupe de cette migration pour vous, ou si vous rencontrez un problème dans votre propre implémentation, contactez notre équipe — on est là pour vous aider.

Concevons ensemble votre prochain projet

Réservez une séance de découverte avec nos ingénieurs en chef. Lors de cet appel stratégique initial, nous discuterons de vos défis spécifiques, examinerons votre architecture e-commerce actuelle et identifierons les opportunités les plus prometteuses en matière de croissance et d'automatisation basée sur l'IA.

Vadim Côte Bonjour, je suis Vadim Côte Co Fondateur & Consultant en affaires
Contactez nous maintenant !
Remplir le formulaire ->

Nous aimerions connaître votre avis. Ensemble, créons quelque chose d'exceptionnel.

Réserver un appel ->