Zum Inhalt springen
ecdsa.com
← Alle Artikel

OIDC-ID-Token-Validierung Schritt für Schritt

Ein ID-Token ist ein JWT mit eigenen Regeln. Dreizehn nummerierte Schritte der Spezifikation entscheiden darüber, ob du einen Benutzer tatsächlich authentifiziert oder nur einen signierten Blob geparst hast. Hier findest du jeden Schritt, einen Validator, der sie umsetzt, und was er abgefangen hat, als er echte Tokens aus einem Keycloak-Realm bekam.

Kurz gesagt. Für jedes ID-Token von OpenID Connect gilt: Prüfe die Signatur mit einem Schlüssel aus der jwks_uri des Anbieters und nur mit dem Algorithmus, den du registriert hast (standardmäßig RS256); verlange, dass iss exakt dem issuer aus dem Discovery-Dokument entspricht; verlange, dass aud deine client_id enthält und nichts, dem du nicht vertraust; ist azp vorhanden, muss der Wert einen deiner eigenen Clients bezeichnen; verlange, dass exp in der Zukunft liegt; und vergleiche nonce mit dem Wert, den du für diesen Anmeldeversuch gespeichert hast. Prüfe at_hash und c_hash, wenn das Token zusammen mit einem Access Token oder einem Code kommt. Identifiziere den Benutzer dann über das Paar aus iss und sub, nie über email.

Die dreizehn Schritte

OpenID Connect Core 1.0 §3.1.3.7 listet in der aktuellen Fassung mit Errata Set 2 (Dezember 2023) auf, was ein Client an einem ID-Token vom Token-Endpunkt prüft. Kurz zusammengefasst:

SchrittPrüfungWer das meist übernimmt
1Entschlüsseln, falls Verschlüsselung registriert wurdeBibliothek
2iss entspricht exakt dem Aussteller aus dem Discovery-DokumentBibliothek, wenn du den Aussteller übergibst
3aud enthält deine client_id und keine Audience, der du nicht vertraustdie erste Hälfte erledigt die Bibliothek, die zweite oft du
4, 5azp wird, falls vorhanden, validiert (das kann einschließen: Der Wert entspricht deiner client_id)unterschiedlich
6Signatur, mit den Schlüsseln des Ausstellers; optional nur für ein Token, das direkt vom Token-Endpunkt über TLS empfangen wurdeBibliothek
7alg ist RS256 oder der Algorithmus, den du als id_token_signed_response_alg registriert hastBibliothek, wenn du ihn festlegst
8Bei HS256/384/512 ist der Schlüssel dein client_secretBibliothek
9Die aktuelle Zeit liegt vor expBibliothek
10iat liegt nicht zu weit vom aktuellen Zeitpunkt entferntdu
11nonce entspricht dem Wert, der in der Anfrage gesendet wurdedu oder eine Bibliothek, der du die erwartete Nonce übergibst
12acr ist akzeptabel, falls angefordertdu
13auth_time ist aktuell genug, falls angefordert oder max_age gesendet wurdedu

Zwei verwandte Prüfungen stehen in anderen Abschnitten: at_hash bindet das Access Token an das ID-Token (§3.1.3.8, im Code-Flow optional), und c_hash bindet den Autorisierungscode an ein ID-Token, das im hybriden Flow vom Autorisierungsendpunkt zurückkommt (§3.3.2.10). Beide sind die linke Hälfte des Hashes über das Token oder den Code, base64url-kodiert, berechnet mit der Hashfunktion, die zum alg des ID-Tokens gehört: bei RS256 die ersten 128 Bit von SHA-256.

Ein Validator, der sie umsetzt

Der Validator unten nutzt jose für das, was eine JWT-Bibliothek gut kann (Signatur, alg, iss, exp und die Prüfung, ob aud die Client-ID enthält), und ergänzt die Schritte, von denen eine JWT-Bibliothek nichts wissen kann, weil sie vom Anmeldeversuch oder von deiner Client-Registrierung abhängen.

validate-id-token.mjs (jose 6.2.13, Node.js 24.22)
import { createHash } from "node:crypto";
import { jwtVerify } from "jose";

// at_hash and c_hash: the left half of the hash of the ASCII value, base64url-encoded,
// with the hash function of the ID token's alg (OIDC Core 3.1.3.6 and 3.3.2.11).
const HASH = { RS256: "sha256", PS256: "sha256", ES256: "sha256", RS384: "sha384", PS384: "sha384", ES384: "sha384", RS512: "sha512", PS512: "sha512", ES512: "sha512" };
export function halfHash(value, alg) {
  const digest = createHash(HASH[alg]).update(value, "ascii").digest();
  return digest.subarray(0, digest.length / 2).toString("base64url");
}

/**
 * Validates an ID token as OpenID Connect Core 1.0 §3.1.3.7 describes.
 * rp:       { issuer, jwks, clientId, alg, trustedAudiences, trustedPresenters, maxIatAgeSeconds }
 * expected: { nonce, maxAge?, accessToken?, code? } from this login's stored state and the response
 */
export async function validateIdToken(idToken, rp, expected) {
  // Steps 2, 3 (aud contains client_id), 6, 7 and 9: issuer, audience, signature, alg, expiry.
  const { payload: claims, protectedHeader } = await jwtVerify(idToken, rp.jwks, {
    issuer: rp.issuer, // exact string from the discovery document
    audience: rp.clientId,
    algorithms: [rp.alg], // RS256 unless id_token_signed_response_alg says otherwise
    requiredClaims: ["iss", "sub", "aud", "exp", "iat"],
    clockTolerance: 30,
  });
  const now = Math.floor(Date.now() / 1000);
  const fail = (step, why) => {
    throw new Error(`step ${step}: ${why}`);
  };

  // Step 3, second half: reject audiences this client does not trust.
  const untrusted = [claims.aud].flat().filter((a) => a !== rp.clientId && !rp.trustedAudiences.includes(a));
  if (untrusted.length > 0) fail(3, `untrusted audience ${untrusted.join(", ")}`);
  // Steps 4-5: an azp, when present, must name a client of this application.
  if (claims.azp !== undefined && ![rp.clientId, ...rp.trustedPresenters].includes(claims.azp)) {
    fail(5, `azp is ${claims.azp}`);
  }
  // Step 10: iat bounds how long nonces must be remembered.
  if (now - claims.iat > rp.maxIatAgeSeconds) fail(10, `issued ${now - claims.iat} s ago`);
  // Step 11: the nonce stored with this login attempt, compared exactly.
  if (claims.nonce !== expected.nonce) fail(11, `nonce ${claims.nonce} does not match the stored value`);
  // Step 13: when max_age was sent, auth_time is required and bounded.
  if (expected.maxAge !== undefined) {
    if (typeof claims.auth_time !== "number") fail(13, "auth_time missing");
    if (now - claims.auth_time > expected.maxAge + 30) fail(13, "authentication too old");
  }
  // Hashes: tie the access token (at_hash) and the code (c_hash) to this ID token.
  if (expected.accessToken !== undefined && claims.at_hash !== undefined) {
    if (claims.at_hash !== halfHash(expected.accessToken, protectedHeader.alg)) fail("3.1.3.8", "at_hash mismatch");
  }
  if (expected.code !== undefined) {
    if (claims.c_hash !== halfHash(expected.code, protectedHeader.alg)) fail("3.3.2.10", "c_hash missing or mismatch");
  }
  return claims;
}

rp.jwks ist createRemoteJWKSet(new URL(discovery.jwks_uri)) und wird einmal pro Aussteller erzeugt. Schritt 1 fehlt, weil der Testaufbau keine ID-Tokens verschlüsselt, die Schritte 8 und 12, weil er RS256 verwendet und acr nicht anfordert.

Test mit echten Tokens

Um den Validator mit Tokens eines echten Anbieters statt mit handgemachten zu testen, lief im Testaufbau Keycloak 26.8.1 (quay.io/keycloak/keycloak:26.8.1, Dev-Modus) mit einem Realm, der einen Benutzer und zwei vertrauliche Clients enthielt: web-app, die zu schützende Relying Party, und other-app, eine weitere Anwendung desselben Anbieters, die mit einem Audience-Mapper so konfiguriert war, dass auch ihre ID-Tokens web-app aufführen. Ein kleines Testprogramm übernahm die Rolle des Browsers: Es öffnete die Autorisierungs-URL mit neuen Werten für state und nonce und einer neuen PKCE-Challenge, schickte das Anmeldeformular ab, nahm den Code aus der Weiterleitung und tauschte ihn am Token-Endpunkt ein. Anschließend durchlief jedes ID-Token validateIdToken als web-app:

Ausgabe: echte Anmeldungen bei Keycloak 26.8.1, validiert als web-app (node:24.22.0-bookworm-slim)
issuer http://127.0.0.1:8080/realms/demo
code flow callback parameters: state, session_state, iss, code
ID token header: alg RS256, typ JWT, kid present: true
ID token claims: exp, iat, auth_time, jti, iss, aud, sub, typ, azp, nonce, sid, at_hash, acr, email_verified, name, preferred_username, given_name, family_name, email
aud "web-app", azp web-app, at_hash present
code flow ID token                                   valid
hybrid callback parameters: state, session_state, iss, code, id_token
front-channel ID token: c_hash present, at_hash absent
hybrid front-channel ID token, with its code         valid
same ID token, code from the first login             rejected: step 3.3.2.10: c_hash missing or mismatch
other-app ID token: aud ["other-app","web-app"], azp other-app
other-app token, aud-contains check only             valid
other-app token, full validation                     rejected: step 3: untrusted audience other-app
first ID token, nonce of the second login            rejected: step 11: nonce lsamKJG6vC-xAIOzo1LIrw does not match the stored value
first ID token, access token of the second login     rejected: step 3.1.3.8: at_hash mismatch

Was jede Ablehnung verhindert:

  • Ein ID-Token, das für eine andere Anwendung ausgestellt wurde. Das Token von other-app ist vom selben Anbieter signiert und führt web-app in aud auf. Eine Prüfung, die nur fragt: „Enthält aud meine Client-ID?“, wie es die meisten JWT-Bibliotheken tun, hat es akzeptiert. Schritt 3 verlangt, Audiences abzulehnen, denen du nicht vertraust, und auch Schritt 5 hätte das Token abgefangen: Sein azp ist other-app. Fehlen beide Prüfungen, kann jede beim selben Anbieter registrierte Anwendung, auch die eines Angreifers, Tokens erhalten, die Benutzer bei deiner Anwendung anmelden.
  • Ein Token aus einem anderen Anmeldeversuch. Die Nonce-Prüfung hat ein gültiges, nicht abgelaufenes, korrekt signiertes ID-Token abgelehnt, weil es zu einer anderen Autorisierungsanfrage gehörte. Genau diese Prüfung hindert einen Angreifer daran, ein Token oder einen Code aus seiner eigenen Sitzung in die Anmeldung eines Opfers einzuschleusen (Schritt 11), und deshalb muss die Nonce pro Anmeldeversuch gespeichert werden, typischerweise in derselben serverseitigen Sitzung oder demselben verschlüsselten Cookie wie state.
  • Vertauschte Artefakte. at_hash und c_hash binden das Access Token und den Code an das ID-Token. Im hybriden Flow läuft das ID-Token aus dem Front-Channel durch den Browser, und c_hash ist der Beweis dafür, dass der Code daneben derjenige ist, den der Anbieter zusammen mit diesem Token ausgestellt hat.

Beachte den Parameter iss in beiden Callbacks. Keycloak fügt ihn der Autorisierungsantwort hinzu (RFC 9207); so kann ein Client, der mit mehreren Anbietern arbeitet, eine Antwort vom falschen Anbieter erkennen, bevor er den Code einlöst.

Was Bibliotheken dir abnehmen

Eine zertifizierte Client-Bibliothek für OpenID Connect führt die meisten dieser Schritte aus, wenn du ihr die nötigen Werte übergibst. openid-client ist eine solche Bibliothek; bekommt sie die erwartete Nonce, prüft sie diese:

openid-client-flow.mjs (openid-client 6.8.8)
// The library route: openid-client 6.8.8 runs discovery, the code exchange and the ID token checks.
import * as client from "openid-client";
import { browserLogin } from "./login.mjs";

const issuer = new URL("http://127.0.0.1:8080/realms/demo");
// allowInsecureRequests only because the lab issuer is plain http; never in production.
const config = await client.discovery(issuer, "web-app", process.env.WEB, undefined, {
  execute: [client.allowInsecureRequests],
});

async function signIn(expectedNonceOverride) {
  const codeVerifier = client.randomPKCECodeVerifier();
  const state = client.randomState();
  const nonce = client.randomNonce();
  const url = client.buildAuthorizationUrl(config, {
    redirect_uri: "http://127.0.0.1:3000/callback",
    scope: "openid email",
    code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
    code_challenge_method: "S256",
    state,
    nonce,
  });
  const { location } = await browserLogin(url, "alice", process.env.PW);
  const tokens = await client.authorizationCodeGrant(config, new URL(location), {
    pkceCodeVerifier: codeVerifier,
    expectedState: state,
    expectedNonce: expectedNonceOverride ?? nonce,
    idTokenExpected: true,
  });
  return tokens.claims();
}

const claims = await signIn();
console.log(`signed in: sub ${claims.sub.slice(0, 8)}..., email ${claims.email}, nonce checked`);
try {
  await signIn(client.randomNonce());
} catch (e) {
  console.log(`wrong expectedNonce: ${e.name}: ${e.message}; cause: ${e.cause?.message}`);
}
Ausgabe
signed in: sub aa5e60df..., email alice@example.com, nonce checked
wrong expectedNonce: ClientError: unexpected JWT claim value encountered; cause: unexpected ID Token "nonce" claim value

browserLogin ist das Testprogramm des Testaufbaus, das den Browser des Benutzers vertritt; in einer Anwendung ist die Autorisierungs-URL eine Weiterleitung und location die URL der Callback-Anfrage. Die untere Schicht von openid-client, oauth4webapi 3.8.8, wendet für azp außerdem die Regel aus Errata Set 1 an: Hat aud mehr als einen Wert, muss azp vorhanden sein und der Client-ID entsprechen.

go-oidc v3.21.0 ist kein vollständiger Client, sondern ein Verifizierer, und seine Dokumentation sagt unmissverständlich: „Verify validiert die Nonce NICHT, das liegt in der Verantwortung des Aufrufers.“ Das Steuerprogramm des Testaufbaus schrieb zwei frische Tokens aus demselben Realm, die von ihm gesendete Nonce sowie eine Nonce und ein Access Token aus einer anderen Anmeldung in eine Datei, und dieses Programm prüfte sie:

main.go (go-oidc v3.21.0, Go 1.27.3)
package main

// What go-oidc's IDTokenVerifier checks on its own, against ID tokens from the lab Keycloak realm.

import (
	"context"
	"encoding/json"
	"fmt"
	"os"

	"github.com/coreos/go-oidc/v3/oidc"
)

func main() {
	var h struct {
		Issuer, IDToken, AccessToken, NonceSent, NonceOfAnotherLogin, OtherAppIDToken, OtherAccessToken string
	}
	data, _ := os.ReadFile("/node/handoff.json")
	if err := json.Unmarshal(data, &h); err != nil {
		panic(err)
	}
	ctx := context.Background()
	ctx = oidc.InsecureIssuerURLContext(ctx, h.Issuer) // only because the lab issuer is plain http
	provider, err := oidc.NewProvider(ctx, h.Issuer)
	if err != nil {
		panic(err)
	}
	verifier := provider.Verifier(&oidc.Config{ClientID: "web-app"})

	token, err := verifier.Verify(ctx, h.IDToken)
	fmt.Printf("%-44s err=%v\n", "Verify(web-app ID token)", err)
	fmt.Printf("%-44s %v\n", "  nonce equals the nonce we sent", token.Nonce == h.NonceSent)
	fmt.Printf("%-44s %v\n", "  nonce equals another login's nonce", token.Nonce == h.NonceOfAnotherLogin)
	fmt.Printf("%-44s err=%v\n", "  VerifyAccessToken(its access token)", token.VerifyAccessToken(h.AccessToken))
	fmt.Printf("%-44s err=%v\n", "  VerifyAccessToken(another access token)", token.VerifyAccessToken(h.OtherAccessToken))

	other, err := verifier.Verify(ctx, h.OtherAppIDToken)
	if err == nil {
		var c struct {
			Azp string `json:"azp"`
		}
		_ = other.Claims(&c)
		fmt.Printf("%-44s err=<nil>, aud=%v, azp=%s\n", "Verify(other-app ID token)", other.Audience, c.Azp)
	} else {
		fmt.Printf("%-44s err=%v\n", "Verify(other-app ID token)", err)
	}
}
Ausgabe (golang:1.27.3-trixie)
Verify(web-app ID token)                     err=<nil>
  nonce equals the nonce we sent             true
  nonce equals another login's nonce         false
  VerifyAccessToken(its access token)        err=<nil>
  VerifyAccessToken(another access token)    err=access token hash does not match value in ID token
Verify(other-app ID token)                   err=<nil>, aud=[other-app web-app], azp=other-app

Verify hat das erste Token akzeptiert, ohne irgendeine Nonce zu kennen; der Vergleich in den nächsten beiden Zeilen ist Sache des Aufrufers. Auch das für other-app ausgestellte Token wurde akzeptiert, denn die Audience-Prüfung testet nur, ob die Client-ID enthalten ist, und azp wird nicht beachtet. Vergleiche bei go-oidc idToken.Nonce mit deinem gespeicherten Wert, rufe idToken.VerifyAccessToken auf, wenn du das Access Token verwendest, und prüfe die Liste der Audiences und azp selbst, wenn dein Anbieter Tokens mit mehreren Audiences ausstellen kann. Die Fehler, die go-oidc tatsächlich meldet, stehen im Katalog, zum Beispiel expected audience und issuer did not match.

Unterschiede zwischen Anbietern, an denen Validatoren scheitern

Was die Discovery-Dokumente von vier Anbietern am 20. Oktober 2026 angaben:

discovery.py
# Prints issuer, jwks_uri and ID token signing algorithms from public OpenID Connect discovery documents.
import json
import urllib.request

URLS = [
    "https://accounts.google.com/.well-known/openid-configuration",
    "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
    "https://login.microsoftonline.com/common/.well-known/openid-configuration",
    "https://samples.auth0.com/.well-known/openid-configuration",
    "https://appleid.apple.com/.well-known/openid-configuration",
]
for url in URLS:
    with urllib.request.urlopen(url, timeout=20) as response:
        doc = json.load(response)
    print(f"== {url}")
    print(f"   issuer   {doc['issuer']}")
    print(f"   jwks_uri {doc['jwks_uri']}")
    print(f"   id_token_signing_alg_values_supported {doc.get('id_token_signing_alg_values_supported')}")
Ausgabe (python:3.14.8-slim-trixie)
== https://accounts.google.com/.well-known/openid-configuration
   issuer   https://accounts.google.com
   jwks_uri https://www.googleapis.com/oauth2/v3/certs
   id_token_signing_alg_values_supported ['RS256']
== https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
   issuer   https://login.microsoftonline.com/{tenantid}/v2.0
   jwks_uri https://login.microsoftonline.com/common/discovery/v2.0/keys
   id_token_signing_alg_values_supported ['RS256']
== https://login.microsoftonline.com/common/.well-known/openid-configuration
   issuer   https://sts.windows.net/{tenantid}/
   jwks_uri https://login.microsoftonline.com/common/discovery/keys
   id_token_signing_alg_values_supported ['RS256']
== https://samples.auth0.com/.well-known/openid-configuration
   issuer   https://samples.auth0.com/
   jwks_uri https://samples.auth0.com/.well-known/jwks.json
   id_token_signing_alg_values_supported ['HS256', 'RS256', 'PS256']
== https://appleid.apple.com/.well-known/openid-configuration
   issuer   https://appleid.apple.com
   jwks_uri https://appleid.apple.com/auth/keys
   id_token_signing_alg_values_supported ['RS256']

Microsoft Entra ID: zwei Token-Versionen und ein Aussteller als Vorlage

  • Die Version richtet sich nach dem Endpunkt. ID-Tokens von /oauth2/authorize haben die Version v1.0 mit iss https://sts.windows.net/{tid}/, die von /oauth2/v2.0/authorize die Version v2.0 mit iss https://login.microsoftonline.com/{tid}/v2.0 (Referenz der ID-Token-Claims). Welche es ist, sagt der Claim ver. Ein Validator, der mit einem der beiden Ausstellerformate konfiguriert ist, lehnt das andere ab; die Meldung von .NET lautet IDX10205: Issuer validation failed. Bei Access Tokens bestimmt nicht der Endpunkt die Version, sondern requestedAccessTokenVersion der API im App-Manifest.
  • {tenantid} ist kein Wert. Die oben gezeigten mandantenfähigen Discovery-Dokumente liefern einen Aussteller mit dem wörtlichen Platzhalter {tenantid}. Ein exakter Vergleich mit diesem String ergibt nie eine Übereinstimmung. Microsofts Leitfaden zu Access Tokens sagt: Setze den tid des Tokens ein, prüfe, dass tid eine GUID ist und iss exakt der Vorlage mit diesem tid entspricht, und prüfe die Eigenschaft issuer, die Entra jedem Schlüssel in seinem JWKS mitgibt.
  • Schränke die Mandanten selbst ein. Eine mandantenfähige Konfiguration akzeptiert Benutzer aus jedem Entra-Mandanten, auch aus solchen, die ein Angreifer anlegt. Wenn deine Anwendung bestimmte Organisationen bedient, führe eine Liste der zulässigen Werte von tid. Persönliche Microsoft-Konten haben den festen Mandanten 9188040d-6c67-4c5b-b112-36a304b66dad.
  • Identifiziere Benutzer über tid plus oid oder sub. Derselbe sub in zwei Mandanten steht für zwei verschiedene Benutzer.

Google: zwei Schreibweisen des Ausstellers und azp für Android

  • iss ist entweder https://accounts.google.com oder accounts.google.com. Googles Dokumentation zu OpenID Connect führt beide als gültig auf, das Discovery-Dokument enthält dagegen nur die Form mit https://. Ein strenger Validator, der den Aussteller aus dem Discovery-Dokument übernimmt, lehnt Tokens mit der Kurzform ab; akzeptiere genau diese beiden Strings (die Option issuer von jose nimmt ein Array an) und keine lockerere Variante.
  • aud ist deine Web-Client-ID, auch wenn eine Android-App das Token angefordert hat. Fordert eine Android-App ein ID-Token für ihr Backend an, ist aud die Web-Client-ID des Backends und azp die Client-ID der Android-App. Trage deine Android-Client-IDs in die Liste der vertrauenswürdigen Presenter ein; verzichte nicht auf die Prüfung von azp.
  • Mit hd schränkst du auf Workspace-Domains ein. Wenn sich nur Benutzer bestimmter Google-Workspace-Domains anmelden dürfen, prüfe den Claim hd; laut Googles Dokumentation bedeutet sein Fehlen, dass das Konto zu keiner bei Google gehosteten Domain gehört. Die Domain der E-Mail-Adresse allein ist kein Ersatz dafür.

Auth0: ein abschließender Schrägstrich und HS256

  • Der Aussteller endet mit einem Schrägstrich: https://samples.auth0.com/. Konfigurierst du https://your-tenant.auth0.com ohne ihn, schlägt die Prüfung bei jedem Token fehl; in go-oidc heißt dieser Fehler issuer did not match, und das Beispiel dort ist genau ein fehlender abschließender Schrägstrich.
  • ID-Tokens können HS256 verwenden. Das Discovery-Dokument führt HS256 auf, und eine dafür konfigurierte Anwendung erhält ID-Tokens, die mit ihrem Client-Secret signiert sind (Schritt 8). Für einen vertraulichen serverseitigen Client funktioniert das, es bedeutet aber, dass das Secret ein Signaturschlüssel ist und jeder Dienst, der es kennt, ID-Tokens ausstellen kann. Auth0 selbst empfiehlt auf seiner Seite zu Signaturalgorithmen RS256. Wähle RS256 in den Anwendungseinstellungen und lege algorithms: ["RS256"] fest, damit eine Konfigurationsänderung deinen Verifizierer nicht unbemerkt auf HMAC umstellen kann.

Häufige Fehler

  1. Ein ID-Token so validieren, als wäre es ein Access Token, oder umgekehrt. Ein ID-Token sagt deinem Client, wer sich angemeldet hat; es ist kein Credential für Aufrufe deiner API. APIs sollten Access Tokens mit ihrer eigenen Audience akzeptieren (Fehler bei der JWT-Prüfung behandelt typ: at+jwt).
  2. Die Signatur überspringen, weil „es über TLS kam“. Schritt 6 erlaubt das nur für ein Token, das dein Server direkt vom Token-Endpunkt empfangen hat. Ein ID-Token, das eine mobile App oder ein Browser an dein Backend sendet, kommt vom Client, nicht vom Anbieter, und muss vollständig geprüft werden.
  3. Die Nonce nicht speichern. Eine Nonce zu erzeugen und sie nie zu vergleichen, ist dasselbe, wie keine zu senden.
  4. Benutzer über email anmelden. Nur iss plus sub ist stabil und eindeutig (§5.7). Ein Abgleich über die E-Mail-Adresse erlaubt jedem, der bei einem beliebigen von dir akzeptierten Anbieter eine nicht verifizierte Adresse erhalten kann, das Konto mit dieser Adresse zu übernehmen.
  5. Schlüssel fest im Code hinterlegen. Rufe sie von der jwks_uri ab und vollziehe Rotationen mit; siehe JWKS-Schlüsselrotation ohne Ausfallzeit.

Quellen