Comment ajouter la connexion avec GitHub (Sign in with GitHub)
Ajoutez la connexion avec GitHub à Node et Express. Créez le flux OAuth avec state et PKCE, échangez les codes en sécurité, récupérez l’e-mail vérifié et actualisez les jetons.
Pour ajouter la connexion avec GitHub, vous redirigez l’utilisateur vers https://github.com/login/oauth/authorize, vous recevez un code temporaire sur votre URL de callback, puis vous échangez ce code contre un jeton d’accès en envoyant une requête POST à https://github.com/login/oauth/access_token depuis votre serveur.
Vous avez les paramètres de l’application OAuth ouverts dans un onglet et votre gestionnaire de route dans un autre, et la plupart des tutoriels délèguent tout le travail à un SDK. Ce guide construit le flux côté serveur en Node avec fetch et Express, sans aucune bibliothèque OAuth. Pour le modèle sous-jacent, consultez All about OAuth 2.0. Il aborde state et PKCE, le problème de l’e-mail null, les scopes, ainsi que les changements concernant les URI de redirection et le renouvellement des jetons publiés par GitHub le 14 août 2026.
Points clés
- Le code d’autorisation de GitHub expire 10 minutes après son émission, et l’échange du code doit s’exécuter sur votre serveur, car il nécessite le secret client.
- Générez
statecôté serveur, stockez-le dans un cookie httpOnly et interrompez le callback si la valeur renvoyée diffère. Cela bloque les attaques CSRF sur la connexion (login CSRF). GET https://api.github.com/userrenvoieemail: nulllorsque l’utilisateur n’a pas d’e-mail public : demandez donc le scopeuser:emailet utilisez l’entrée principale et vérifiée renvoyée parGET /user/emails.- Depuis la mise à jour de GitHub du 14 août 2026, une application OAuth peut enregistrer jusqu’à 10 URI de redirection, et les nouvelles applications émettent par défaut des jetons d’accès valables 8 heures, accompagnés de jetons de rafraîchissement valables 6 mois.
Enregistrer l’application OAuth GitHub
Créez une application OAuth via Settings → Developer settings → OAuth Apps, puis copiez le client ID et générez un secret client. Le guide d’enregistrement de GitHub détaille chaque champ.
Depuis la version du 14 août 2026, une même application OAuth peut contenir jusqu’à 10 URI de redirection, que vous ajoutez avec le bouton Add callback URL. Une seule application suffit ainsi pour couvrir la production, les déploiements de prévisualisation et le développement local. Pour le travail en local, enregistrez http://127.0.0.1:3000/auth/github/callback. localhost fonctionne, mais les recommandations de GitHub sur les adresses de loopback rappellent que la RFC OAuth préconise l’adresse littérale 127.0.0.1.
La correspondance par caractères génériques (wildcard) est désormais un paramètre propre à chaque URI de redirection. Si votre application ne comptait qu’une seule URL de callback avant le 3 août 2026, GitHub y a activé la correspondance wildcard afin de conserver l’ancien comportement de redirection. Désactivez-la, sauf si vous contrôlez chaque sous-domaine et chaque chemin situés sous cette URL.
Rediriger vers l’endpoint d’autorisation de GitHub
Le bouton de connexion pointe vers une route serveur. Cette route génère state et un vérificateur PKCE, les stocke tous deux dans des cookies de courte durée, puis redirige vers GET https://github.com/login/oauth/authorize.
import crypto from 'node:crypto';
import express from 'express';
import cookieParser from 'cookie-parser';
const app = express();
app.use(cookieParser());
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_REDIRECT_URI } = process.env;
// GITHUB_CLIENT_SECRET is read only here, on the server. Never ship it to the browser.
const isProd = process.env.NODE_ENV === 'production';
const tempCookie = {
httpOnly: true,
secure: isProd,
sameSite: 'lax',
maxAge: 10 * 60 * 1000, // matches the 10-minute code lifetime
path: '/auth/github',
};
app.get('/auth/github', (req, res) => {
const state = crypto.randomBytes(32).toString('base64url');
const verifier = crypto.randomBytes(32).toString('base64url'); // 43 chars
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
res.cookie('gh_state', state, tempCookie);
res.cookie('gh_verifier', verifier, tempCookie);
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: GITHUB_REDIRECT_URI, // one value per environment
scope: 'read:user user:email',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});
Les paramètres d’autorisation de GitHub ne prennent en charge qu’une seule méthode PKCE, S256, et refusent plain. Le challenge doit être une valeur SHA-256 de 43 caractères, ce qui correspond exactement à un condensé SHA-256 encodé en base64url. Si vos utilisateurs possèdent souvent plusieurs comptes GitHub, ajoutez prompt=select_account pour que GitHub affiche systématiquement le sélecteur de compte.
Valider state pour bloquer les attaques CSRF sur la connexion
Le paramètre state est une valeur aléatoire que personne ne peut deviner. Votre serveur la crée et la stocke dans la session ou dans un cookie httpOnly. Lorsque GitHub vous redirige, comparez-la au state reçu sur le callback. Si les deux diffèrent, interrompez la connexion et n’échangez pas le code.
Sans cette vérification, votre application est exposée au login CSRF, une attaque qui connecte la victime au compte de l’attaquant :
- L’attaquant lance votre flux de connexion GitHub avec son propre compte GitHub.
- Il approuve l’accès, mais s’arrête avant l’exécution du callback et conserve son
codevalide. - Il amène le navigateur de la victime à charger
https://yourapp.com/auth/github/callback?code=ATTACKER_CODE, par exemple via une image masquée ou un lien qui se soumet automatiquement. - Votre serveur échange le code et connecte la victime en tant qu’attaquant. Pire encore, si la victime était déjà connectée, il associe l’identité GitHub de l’attaquant au compte de la victime.
- Tout ce que la victime enregistre ensuite atterrit dans un compte contrôlé par l’attaquant. Une identité associée permet en outre à l’attaquant de se connecter plus tard en tant que victime.
Avec state, l’étape 4 échoue. Le navigateur de la victime ne possède soit aucun cookie gh_state, soit un cookie qui ne correspond pas à la valeur de l’attaquant : le serveur rejette donc la requête. PKCE ajoute un second lien : le code ne fonctionne qu’avec le code_verifier stocké dans le navigateur qui a initié le flux. La RFC 6749 définit state à cette fin, et l’OAuth 2.0 Security Best Current Practice recommande PKCE pour tous les clients. Les recommandations sur state et PKCE dans Securing OAuth best practices approfondissent le sujet.
function statesMatch(received, stored) {
if (typeof received !== 'string' || typeof stored !== 'string') return false;
const a = Buffer.from(received);
const b = Buffer.from(stored);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Les attributs des cookies ont également leur importance. HttpOnly rend la valeur inaccessible aux scripts, et Secure la limite à HTTPS. SameSite=Lax permet au cookie de survivre à la redirection de premier niveau de GitHub vers votre site. Avec un seul cookie gh_state, un second onglet de connexion écrase le premier : affichez donc un message clair invitant à réessayer plutôt qu’une erreur vide.
Gérer le callback et échanger le code
Le callback vérifie state, supprime les cookies temporaires et envoie le code en POST à https://github.com/login/oauth/access_token avec l’en-tête Accept: application/json. Sans cet en-tête, l’endpoint de jeton de GitHub répond avec un corps encodé en formulaire. Le code expire 10 minutes après son émission : échangez-le donc immédiatement.
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;
const { gh_state: savedState, gh_verifier: verifier } = req.cookies;
res.clearCookie('gh_state', { path: '/auth/github' });
res.clearCookie('gh_verifier', { path: '/auth/github' });
if (typeof code !== 'string' || !verifier || !statesMatch(state, savedState)) {
return res.status(400).send('Sign-in failed. Please try again.');
}
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: GITHUB_REDIRECT_URI, // must match the authorize request
code_verifier: verifier,
}),
});
const token = await tokenRes.json();
if (token.error || !token.access_token) {
return res.status(400).send('Sign-in failed. Please try again.');
}
const profile = await getGitHubProfile(token.access_token);
const user = await upsertUserByGitHubId(profile); // key on profile.id
await saveGitHubTokens(user.id, token); // server-side only
await createSession(res, user.id); // your own session cookie
res.redirect('/');
});
Gardez cet échange côté serveur, car il nécessite le secret client, qui ne doit jamais atteindre le navigateur. Une fois l’échange réussi, émettez votre propre cookie de session et conservez le jeton GitHub côté serveur. Cookies vs localStorage for JWT auth détaille les compromis en matière de stockage. Le res.redirect('/') final retire code et state de la barre d’adresse, de sorte qu’un retour arrière ne puisse pas rejouer un code déjà utilisé.
Les enregistrements de session (session replays) des connexions GitHub échouées montrent la séquence complète : clic, GitHub, callback, page d’erreur. Cette séquence permet de distinguer les incohérences de state des codes expirés. Configurez tout outil de session replay, d’analytique ou de journalisation pour masquer les paramètres de requête code et state.
Récupérer l’utilisateur et un e-mail vérifié
Appelez GET https://api.github.com/user avec le jeton pour identifier l’utilisateur. L’endpoint de l’utilisateur authentifié renvoie email: null lorsque l’utilisateur n’a pas défini d’e-mail public. Les jetons OAuth ont également besoin du scope read:user pour obtenir la réponse privée de l’utilisateur.
const ghHeaders = (accessToken) => ({
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github+json',
'User-Agent': 'your-app-name',
});
async function getGitHubProfile(accessToken) {
const userRes = await fetch('https://api.github.com/user', { headers: ghHeaders(accessToken) });
if (!userRes.ok) throw new Error(`GET /user failed: ${userRes.status}`);
const user = await userRes.json();
let email = user.email;
if (!email) {
const emailsRes = await fetch('https://api.github.com/user/emails', { headers: ghHeaders(accessToken) });
if (emailsRes.ok) {
const emails = await emailsRes.json();
email = emails.find((e) => e.primary && e.verified)?.email ?? null;
}
}
return { id: user.id, login: user.login, name: user.name, email };
}
Avec le scope user:email, GET /user/emails renvoie toutes les adresses accompagnées des indicateurs primary et verified. Choisissez l’entrée pour laquelle les deux valent true, et n’associez jamais de comptes sur la base d’une adresse non vérifiée. Identifiez les utilisateurs par leur id numérique, car les logins et les e-mails peuvent changer. L’API REST de GitHub refuse toute requête dépourvue d’en-tête User-Agent : définissez-le donc vous-même. GitHub recommande également de vérifier l’identité de l’utilisateur à chaque obtention d’un nouveau jeton, car l’utilisateur a pu changer de compte sur la page d’autorisation.
Demander le minimum de scopes nécessaires
La connexion avec GitHub ne nécessite que deux scopes OAuth : read:user et user:email. La référence des scopes de GitHub définit chacun d’eux, et les utilisateurs voient tous les scopes que vous demandez sur l’écran de consentement de GitHub.
| Besoin | Scope |
|---|---|
| Profil : id, login, nom, avatar | read:user |
| Adresses e-mail | user:email |
| Contenu des dépôts | repo (accès complet, y compris aux dépôts privés) |
Demander repo pour la connexion revient à exiger des utilisateurs un accès en lecture/écriture à leur code privé, et beaucoup abandonneront à ce stade. Si vous avez besoin d’accéder aux dépôts pour une fonctionnalité ultérieure, lancez un flux distinct à ce moment-là. GitHub autorise au maximum dix jetons par combinaison utilisateur, application et scope, et pas plus de dix nouveaux jetons par heure. Si une boucle de callback atteint cette limite horaire, GitHub s’arrête et affiche à l’utilisateur une demande de réautorisation.
Renouveler les jetons qui expirent
Pour renouveler un jeton GitHub, envoyez grant_type=refresh_token en POST à l’endpoint de jeton. L’utilisation d’un jeton de rafraîchissement le révoque, ainsi que l’ancien jeton d’accès : enregistrez donc la nouvelle paire avant d’effectuer un autre appel. Lorsque l’expiration des jetons est activée, les jetons d’accès expirent au bout de huit heures, et un jeton de rafraîchissement inutilisé expire au bout de six mois. Depuis le changement du 14 août 2026, les nouvelles applications adoptent ce comportement par défaut. Les applications existantes peuvent l’activer pour une connexion donnée en ajoutant le scope offline_access, qui n’ajoute aucune demande de consentement, ou l’activer dans les paramètres de l’application.
async function refreshGitHubToken(refreshToken) {
// No refresh token means a non-expiring token: nothing to refresh.
if (!refreshToken) return null;
const res = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
});
const data = await res.json();
if (data.error === 'bad_refresh_token') return { reauth: true };
if (data.error) throw new Error(data.error);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
}
Vous ne pouvez pas modifier les scopes lors d’un renouvellement. bad_refresh_token signifie que l’utilisateur doit se reconnecter. Ne partez pas du principe qu’un refresh_token sera systématiquement renvoyé : certaines versions de GitHub Enterprise Server ignorent offline_access et émettent un jeton sans expiration.
Application OAuth ou GitHub App ?
Pour la seule connexion avec GitHub, une application OAuth demandant read:user user:email suffit, même si GitHub recommande de manière générale les GitHub Apps plutôt que les applications OAuth. La page comparative de GitHub détaille l’ensemble des différences.
| Application OAuth | GitHub App | |
|---|---|---|
| Modèle de permissions | Scopes étendus | Permissions granulaires |
| Sélection des dépôts | Tous les dépôts couverts par le scope | L’utilisateur choisit les dépôts |
| Durée de vie des jetons | Expiration à 8 heures par défaut pour les nouvelles applications ; longue durée pour les applications plus anciennes, sauf activation | Courte durée |
| Recommandation de GitHub | Prise en charge | À privilégier |
Optez pour une GitHub App lorsque votre produit agit sur des dépôts. Pour l’identité, le flux d’application OAuth présenté ci-dessus suffit.
Checklist de sécurité
- Envoyez
stateet PKCES256à chaque requête, et vérifiez-les tous deux sur le callback. - Envoyez le même
redirect_uridans la requête d’autorisation et dans la requête de jeton. - Désactivez la correspondance wildcard dont vous n’avez pas besoin.
- Conservez le secret client et les jetons GitHub sur le serveur. Ne transmettez au navigateur que votre propre cookie de session.
- Redirigez hors du callback après l’échange.
- Gérez le cas d’une réponse de jeton sans
refresh_token.
La connexion avec GitHub se résume à trois requêtes : autoriser, échanger, récupérer l’utilisateur. La sécurité réside dans les détails qui les entourent : un state conservé côté serveur, un vérificateur PKCE, un secret qui ne quitte jamais votre backend et un e-mail dont vous avez confirmé qu’il est vérifié. Étape suivante : ouvrez les paramètres de votre application OAuth, ajoutez vos URL de callback de prévisualisation et 127.0.0.1, désactivez la correspondance wildcard et déployez les trois routes ci-dessus.
FAQ
Pourquoi l'endpoint de jeton de GitHub renvoie-t-il bad_verification_code ?
GitHub renvoie bad_verification_code lorsque le code que vous envoyez est incorrect, expiré ou ne correspond pas à celui émis lors de la requête d'autorisation. Les causes habituelles sont un code datant de plus de 10 minutes, ou un code déjà échangé, par exemple après un retour arrière ou une seconde requête vers le callback. Ce code est irrécupérable. Relancez le flux de connexion pour en obtenir un nouveau.
Qu'est-ce qui provoque l'erreur redirect_uri_mismatch avec GitHub OAuth ?
GitHub renvoie redirect_uri_mismatch lorsque le redirect_uri que vous envoyez ne correspond à aucune URL de callback enregistrée sur l'application OAuth. Envoyez exactement une URL enregistrée, ou omettez le paramètre pour que GitHub utilise l'URL par défaut de l'application. Envoyez la même valeur dans la requête d'autorisation et dans la requête de jeton. GitHub considère 127.0.0.1 et localhost comme des hôtes distincts : enregistrez donc celui qu'utilise votre serveur local.
La déconnexion de mon application révoque-t-elle le jeton GitHub de l'utilisateur ?
Non. Mettre fin à votre propre session laisse le jeton GitHub et l'autorisation valides. Pour révoquer un jeton, appelez DELETE /applications/{client_id}/token. Pour supprimer tous les jetons ainsi que l'autorisation de l'application pour cet utilisateur, appelez DELETE /applications/{client_id}/grant. Ces deux endpoints utilisent l'authentification Basic avec le client_id et le client_secret, ainsi que l'access_token dans le corps de la requête. Les utilisateurs peuvent également révoquer l'accès depuis les paramètres de leur compte GitHub.
Un jeton OAuth GitHub sans expiration peut-il quand même cesser de fonctionner ?
Oui. GitHub révoque automatiquement un jeton OAuth inutilisé pendant un an. Un jeton cesse également de fonctionner lorsque l'utilisateur révoque l'autorisation de votre application, ou lorsque le jeton est publié dans un dépôt ou un gist public. Si une même combinaison utilisateur, application et scope accumule plus de dix jetons, GitHub révoque l'un des plus anciens. Lorsqu'un jeton stocké cesse de fonctionner, faites repasser l'utilisateur par le processus de connexion.