12k
All articles

So fügen Sie „Sign in with GitHub“ hinzu

Fügen Sie die GitHub-Anmeldung mit Node und Express hinzu. Richten Sie OAuth mit state und PKCE ein, tauschen Sie Codes sicher aus, rufen Sie verifizierte E-Mails ab und erneuern Sie Tokens.

OpenReplay Team
OpenReplay Team
So fügen Sie „Sign in with GitHub“ hinzu

Für „Sign in with GitHub“ leiten Sie den Nutzer zu https://github.com/login/oauth/authorize weiter, empfangen an Ihrer Callback-URL einen temporären code und tauschen diesen Code serverseitig per POST-Request an https://github.com/login/oauth/access_token gegen ein Access Token ein.

Sie haben die Einstellungen Ihrer OAuth-App in einem Tab geöffnet und Ihren Route-Handler in einem anderen, und die meisten Tutorials überlassen die ganze Arbeit einem SDK. Dieser Leitfaden baut den serverseitigen Flow in Node mit fetch und Express auf, ganz ohne OAuth-Bibliothek. Das zugrunde liegende Modell wird in All about OAuth 2.0 erläutert. Der Leitfaden behandelt State und PKCE, das Problem der fehlenden E-Mail-Adresse (null), Scopes sowie die Änderungen an Redirect-URIs und Token-Refresh, die GitHub am 14. August 2026 veröffentlicht hat.

Die wichtigsten Punkte

  • Der Autorisierungscode von GitHub läuft 10 Minuten nach seiner Ausstellung ab, und der Code-Austausch muss auf Ihrem Server stattfinden, da er das Client Secret erfordert.
  • Generieren Sie state auf dem Server, speichern Sie den Wert in einem httpOnly-Cookie und brechen Sie den Callback ab, wenn der zurückgegebene Wert abweicht. Das verhindert Login-CSRF.
  • GET https://api.github.com/user liefert email: null, wenn der Nutzer keine öffentliche E-Mail-Adresse hinterlegt hat. Fordern Sie daher user:email an und verwenden Sie den primären, verifizierten Eintrag aus GET /user/emails.
  • Seit dem GitHub-Update vom 14. August 2026 kann eine OAuth-App bis zu 10 Redirect-URIs registrieren, und neue Apps stellen standardmäßig Access Tokens mit 8 Stunden Gültigkeit und Refresh Tokens mit 6 Monaten Gültigkeit aus.

Die GitHub-OAuth-App registrieren

Legen Sie unter Settings → Developer settings → OAuth Apps eine OAuth-App an, kopieren Sie anschließend die Client-ID und generieren Sie ein Client Secret. GitHubs Registrierungsleitfaden führt alle Felder auf.

Seit dem Release vom 14. August 2026 kann eine einzelne OAuth-App bis zu 10 Redirect-URIs enthalten, die Sie über die Schaltfläche Add callback URL hinzufügen. So decken Sie Produktion, Preview-Deployments und lokale Entwicklung mit einer einzigen App ab. Für die lokale Entwicklung registrieren Sie http://127.0.0.1:3000/auth/github/callback. localhost funktioniert ebenfalls, doch GitHubs Hinweise zu Loopback-Adressen weisen darauf hin, dass der OAuth-RFC das Literal 127.0.0.1 empfiehlt.

Wildcard-Matching ist jetzt eine Einstellung pro Redirect-URI. Wenn Ihre App vor dem 3. August 2026 nur eine einzige Callback-URL hatte, hat GitHub das Wildcard-Matching dafür aktiviert, um das bisherige Weiterleitungsverhalten beizubehalten. Deaktivieren Sie es, sofern Sie nicht jede Subdomain und jeden Pfad unterhalb dieser URL kontrollieren.

Zum Authorize-Endpunkt von GitHub weiterleiten

Der Anmelde-Button verweist auf eine Server-Route. Diese Route generiert state und einen PKCE-Verifier, speichert beide in kurzlebigen Cookies und leitet zu GET https://github.com/login/oauth/authorize weiter.

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}`);
});

GitHubs Authorize-Parameter unterstützen genau eine PKCE-Methode, S256, und lehnen plain ab. Die Challenge muss ein 43 Zeichen langer SHA-256-Wert sein – genau das, was die base64url-Kodierung eines SHA-256-Digests ergibt. Wenn Ihre Nutzer häufig mehr als ein GitHub-Konto haben, ergänzen Sie prompt=select_account, damit GitHub stets die Kontoauswahl anzeigt.

state validieren, um Login-CSRF zu verhindern

Der Parameter state ist ein zufälliger, nicht erratbarer Wert. Ihr Server erzeugt ihn und speichert ihn in der Session oder in einem httpOnly-Cookie. Wenn GitHub zurückleitet, vergleichen Sie ihn mit dem state im Callback. Stimmen die beiden nicht überein, brechen Sie die Anmeldung ab und tauschen den Code nicht ein.

Ohne diese Prüfung ist Ihre App anfällig für Login-CSRF – einen Angriff, bei dem das Opfer im Konto des Angreifers angemeldet wird:

  1. Der Angreifer startet Ihren GitHub-Anmelde-Flow mit seinem eigenen GitHub-Konto.
  2. Er erteilt den Zugriff, stoppt aber, bevor der Callback ausgeführt wird, und behält seinen gültigen code.
  3. Er bringt den Browser des Opfers dazu, https://yourapp.com/auth/github/callback?code=ATTACKER_CODE aufzurufen, etwa über ein verstecktes Bild oder einen automatisch ausgelösten Link.
  4. Ihr Server tauscht den Code ein und meldet das Opfer als Angreifer an. Schlimmer noch: War das Opfer bereits angemeldet, wird die GitHub-Identität des Angreifers mit dem Konto des Opfers verknüpft.
  5. Alles, was das Opfer anschließend speichert, landet in einem Konto, das der Angreifer kontrolliert. Eine verknüpfte Identität ermöglicht es dem Angreifer zudem, sich später als das Opfer anzumelden.

Mit state scheitert Schritt 4. Der Browser des Opfers hat entweder kein gh_state-Cookie oder eines, das nicht zum Wert des Angreifers passt, sodass der Server die Anfrage ablehnt. PKCE fügt eine zweite Bindung hinzu: Der Code funktioniert nur zusammen mit dem code_verifier, der im Browser gespeichert ist, der den Flow gestartet hat. RFC 6749 definiert state genau zu diesem Zweck, und die OAuth 2.0 Security Best Current Practice empfiehlt PKCE für jeden Client. Die Hinweise zu State und PKCE in Securing OAuth best practices gehen weiter in die Tiefe.

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);
}

Auch die Cookie-Attribute sind wichtig. HttpOnly schützt den Wert vor dem Zugriff durch Skripte, und Secure beschränkt ihn auf HTTPS. SameSite=Lax sorgt dafür, dass das Cookie die Top-Level-Weiterleitung von GitHub zurück zu Ihrer App übersteht. Bei einem einzelnen gh_state-Cookie überschreibt ein zweiter Anmelde-Tab den ersten – zeigen Sie daher eine verständliche Aufforderung zum erneuten Versuch statt einer leeren Fehlermeldung an.

Den Callback verarbeiten und den Code eintauschen

Der Callback prüft state, löscht die temporären Cookies und sendet den Code per POST mit Accept: application/json an https://github.com/login/oauth/access_token. Ohne diesen Header antwortet GitHubs Token-Endpunkt mit einem form-kodierten Body. Der Code läuft 10 Minuten nach seiner Ausstellung ab, tauschen Sie ihn also umgehend ein.

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('/');
});

Führen Sie diesen Austausch auf dem Server durch, denn er benötigt das Client Secret, und dieses Secret darf niemals in den Browser gelangen. Stellen Sie nach erfolgreichem Austausch Ihr eigenes Session-Cookie aus und behalten Sie das GitHub-Token serverseitig. Cookies vs localStorage for JWT auth beleuchtet die Vor- und Nachteile der verschiedenen Speicheroptionen. Das abschließende res.redirect('/') entfernt code und state aus der Adressleiste, sodass ein Klick auf „Zurück“ keinen bereits verbrauchten Code erneut senden kann.

Session-Replays fehlgeschlagener GitHub-Anmeldungen zeigen den vollständigen Ablauf: Klick, GitHub, Callback, Fehlerseite. Anhand dieses Ablaufs lassen sich State-Abweichungen von abgelaufenen Codes unterscheiden. Konfigurieren Sie jedes Replay-, Analytics- oder Logging-Tool so, dass die Query-Parameter code und state maskiert werden.

Den Nutzer und eine verifizierte E-Mail-Adresse abrufen

Rufen Sie mit dem Token GET https://api.github.com/user auf, um den Nutzer zu identifizieren. Der Endpunkt für den authentifizierten Nutzer liefert email: null, wenn der Nutzer keine öffentliche E-Mail-Adresse festgelegt hat. OAuth-Tokens benötigen außerdem read:user, um die private Nutzerantwort zu erhalten.

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 };
}

Mit dem Scope user:email liefert GET /user/emails sämtliche Adressen zusammen mit den Flags primary und verified. Wählen Sie den Eintrag, bei dem beide true sind, und verknüpfen Sie Konten niemals über eine nicht verifizierte Adresse. Identifizieren Sie Nutzer über die numerische id, denn Logins und E-Mail-Adressen können sich ändern. GitHubs REST-API lehnt jede Anfrage ohne User-Agent-Header ab, setzen Sie diesen also selbst. GitHub empfiehlt außerdem, bei jedem neuen Token zu prüfen, um welchen Nutzer es sich handelt, da der Nutzer auf der Autorisierungsseite möglicherweise das Konto gewechselt hat.

Nur die minimal nötigen Scopes anfordern

„Sign in with GitHub“ benötigt nur zwei OAuth-Scopes: read:user und user:email. GitHubs Scope-Referenz definiert jeden einzelnen, und Nutzer sehen auf GitHubs Zustimmungsbildschirm jeden Scope, den Sie anfordern.

Sie benötigenScope
Profil: id, login, name, avatarread:user
E-Mail-Adressenuser:email
Repository-Inhalterepo (Vollzugriff, einschließlich privater Repos)

Wenn Sie für die Anmeldung repo anfordern, verlangen Sie von den Nutzern Lese- und Schreibzugriff auf ihren privaten Code – und viele werden an dieser Stelle abbrechen. Falls Sie für ein späteres Feature Repository-Zugriff benötigen, starten Sie zu diesem Zeitpunkt einen separaten Flow. GitHub erlaubt maximal zehn Tokens pro Kombination aus Nutzer, App und Scope und höchstens zehn neue Tokens pro Stunde. Erreicht eine Callback-Schleife dieses stündliche Limit, stoppt GitHub und zeigt dem Nutzer eine Aufforderung zur erneuten Autorisierung an.

Ablaufende Tokens erneuern

Um ein GitHub-Token zu erneuern, senden Sie grant_type=refresh_token per POST an den Token-Endpunkt. Durch die Verwendung eines Refresh Tokens werden dieses und das alte Access Token ungültig, speichern Sie also das neue Paar, bevor Sie einen weiteren Aufruf durchführen. Bei aktivierten ablaufenden Tokens laufen Access Tokens nach acht Stunden ab, und ein ungenutztes Refresh Token läuft nach sechs Monaten ab. Seit der Änderung vom 14. August 2026 erhalten neue Apps dieses Verhalten standardmäßig. Bestehende Apps können es für eine einzelne Anmeldung aktivieren, indem sie den Scope offline_access hinzufügen – dies löst keine zusätzliche Zustimmungsabfrage aus –, oder es in den App-Einstellungen aktivieren.

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,
  };
}

Scopes lassen sich während eines Refresh nicht ändern. bad_refresh_token bedeutet, dass sich der Nutzer erneut anmelden muss. Gehen Sie nicht davon aus, dass überhaupt ein refresh_token zurückgegeben wird: Einige Versionen von GitHub Enterprise Server ignorieren offline_access und stellen ein nicht ablaufendes Token aus.

OAuth-App oder GitHub App?

Für „Sign in with GitHub“ allein genügt eine OAuth-App, die read:user user:email anfordert – auch wenn GitHub generell GitHub Apps gegenüber OAuth-Apps empfiehlt. GitHubs Vergleichsseite erläutert alle Unterschiede.

OAuth-AppGitHub App
BerechtigungsmodellBreite ScopesFeingranulare Berechtigungen
Repository-AuswahlAlle Repos, die der Scope abdecktNutzer wählt die Repositorys aus
Token-LebensdauerBei neuen Apps standardmäßig nach 8 Stunden ablaufend; bei älteren Apps langlebig, sofern nicht aktiviertKurzlebig
Empfehlung von GitHubUnterstütztBevorzugt

Entscheiden Sie sich für eine GitHub App, wenn Ihr Produkt auf Repositorys zugreift. Für die reine Identitätsprüfung genügt der oben beschriebene OAuth-App-Flow.

Sicherheits-Checkliste

  • Senden Sie bei jeder Anfrage state und PKCE S256 mit und prüfen Sie beides im Callback.
  • Senden Sie in der Authorize-Anfrage und in der Token-Anfrage dieselbe redirect_uri.
  • Deaktivieren Sie Wildcard-Matching, das Sie nicht benötigen.
  • Halten Sie das Client Secret und die GitHub-Tokens auf dem Server. Geben Sie dem Browser nur Ihr eigenes Session-Cookie.
  • Leiten Sie nach dem Austausch vom Callback weg.
  • Behandeln Sie Token-Antworten, die kein refresh_token enthalten.

„Sign in with GitHub“ läuft auf drei Requests hinaus: Autorisieren, Eintauschen, Nutzer abrufen. Die Sicherheit steckt in den Details drumherum: ein serverseitig gehaltener state, ein PKCE-Verifier, ein Secret, das Ihr Backend nie verlässt, und eine E-Mail-Adresse, deren Verifizierung Sie bestätigt haben. Öffnen Sie als Nächstes die Einstellungen Ihrer OAuth-App, fügen Sie Ihre Preview- und 127.0.0.1-Callback-URLs hinzu, deaktivieren Sie das Wildcard-Matching und deployen Sie die drei oben gezeigten Routen.

FAQs

Warum gibt der Token-Endpunkt von GitHub bad_verification_code zurück?

GitHub gibt bad_verification_code zurück, wenn der gesendete Code falsch oder abgelaufen ist oder nicht mit dem in der Autorisierungsanfrage ausgestellten Code übereinstimmt. Typische Ursachen sind ein Code, der älter als 10 Minuten ist, oder ein Code, der bereits eingetauscht wurde, etwa nach einem Klick auf „Zurück“ oder einer zweiten Anfrage an den Callback. Dieser Code lässt sich nicht wiederherstellen. Starten Sie den Anmelde-Flow neu, um einen neuen Code zu erhalten.

Was verursacht redirect_uri_mismatch bei GitHub OAuth?

GitHub gibt redirect_uri_mismatch zurück, wenn die gesendete redirect_uri mit keiner der für die OAuth-App registrierten Callback-URLs übereinstimmt. Senden Sie eine registrierte URL exakt so, wie sie hinterlegt ist, oder lassen Sie den Parameter weg, dann verwendet GitHub die Standard-URL der App. Senden Sie in der Authorize-Anfrage und in der Token-Anfrage denselben Wert. GitHub behandelt 127.0.0.1 und localhost als unterschiedliche Hosts, registrieren Sie also den Host, den Ihr lokaler Server verwendet.

Widerruft das Abmelden aus meiner App das GitHub-Token des Nutzers?

Nein. Wenn Sie Ihre eigene Session beenden, bleiben das GitHub-Token und die Autorisierung gültig. Um ein einzelnes Token zu widerrufen, rufen Sie DELETE /applications/{client_id}/token auf. Um alle Tokens sowie die Autorisierung der App für diesen Nutzer zu entfernen, rufen Sie DELETE /applications/{client_id}/grant auf. Beide Endpunkte verwenden Basic-Authentifizierung mit client_id und client_secret sowie das access_token im Request-Body. Nutzer können den Zugriff außerdem in ihren GitHub-Kontoeinstellungen widerrufen.

Kann ein nicht ablaufendes GitHub-OAuth-Token trotzdem ungültig werden?

Ja. GitHub widerruft ein OAuth-Token automatisch, wenn es ein Jahr lang nicht verwendet wurde. Ein Token wird außerdem ungültig, wenn der Nutzer die Autorisierung Ihrer App widerruft oder wenn das Token in ein öffentliches Repository oder einen Gist gepusht wird. Sammeln sich für eine Kombination aus Nutzer, App und Scope mehr als zehn Tokens an, widerruft GitHub eines der älteren. Wenn ein gespeichertes Token nicht mehr funktioniert, schicken Sie den Nutzer erneut durch den Anmeldeprozess.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.