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:
| Schritt | Prüfung | Wer das meist übernimmt |
|---|---|---|
| 1 | Entschlüsseln, falls Verschlüsselung registriert wurde | Bibliothek |
| 2 | iss entspricht exakt dem Aussteller aus dem Discovery-Dokument | Bibliothek, wenn du den Aussteller übergibst |
| 3 | aud enthält deine client_id und keine Audience, der du nicht vertraust | die erste Hälfte erledigt die Bibliothek, die zweite oft du |
| 4, 5 | azp wird, falls vorhanden, validiert (das kann einschließen: Der Wert entspricht deiner client_id) | unterschiedlich |
| 6 | Signatur, mit den Schlüsseln des Ausstellers; optional nur für ein Token, das direkt vom Token-Endpunkt über TLS empfangen wurde | Bibliothek |
| 7 | alg ist RS256 oder der Algorithmus, den du als id_token_signed_response_alg registriert hast | Bibliothek, wenn du ihn festlegst |
| 8 | Bei HS256/384/512 ist der Schlüssel dein client_secret | Bibliothek |
| 9 | Die aktuelle Zeit liegt vor exp | Bibliothek |
| 10 | iat liegt nicht zu weit vom aktuellen Zeitpunkt entfernt | du |
| 11 | nonce entspricht dem Wert, der in der Anfrage gesendet wurde | du oder eine Bibliothek, der du die erwartete Nonce übergibst |
| 12 | acr ist akzeptabel, falls angefordert | du |
| 13 | auth_time ist aktuell genug, falls angefordert oder max_age gesendet wurde | du |
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.
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:
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-appist vom selben Anbieter signiert und führtweb-appinaudauf. Eine Prüfung, die nur fragt: „Enthältaudmeine 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: Seinazpistother-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_hashundc_hashbinden 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, undc_hashist 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:
// 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}`);
}
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:
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)
}
}
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:
# 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')}")
== 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/authorizehaben die Version v1.0 mitisshttps://sts.windows.net/{tid}/, die von/oauth2/v2.0/authorizedie Version v2.0 mitisshttps://login.microsoftonline.com/{tid}/v2.0(Referenz der ID-Token-Claims). Welche es ist, sagt der Claimver. 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, sondernrequestedAccessTokenVersionder 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 dentiddes Tokens ein, prüfe, dasstideine GUID ist undissexakt der Vorlage mit diesemtidentspricht, und prüfe die Eigenschaftissuer, 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 Mandanten9188040d-6c67-4c5b-b112-36a304b66dad. - Identifiziere Benutzer über
tidplusoidodersub. Derselbesubin zwei Mandanten steht für zwei verschiedene Benutzer.
Google: zwei Schreibweisen des Ausstellers und azp für Android
issist entwederhttps://accounts.google.comoderaccounts.google.com. Googles Dokumentation zu OpenID Connect führt beide als gültig auf, das Discovery-Dokument enthält dagegen nur die Form mithttps://. Ein strenger Validator, der den Aussteller aus dem Discovery-Dokument übernimmt, lehnt Tokens mit der Kurzform ab; akzeptiere genau diese beiden Strings (die Optionissuervon jose nimmt ein Array an) und keine lockerere Variante.audist 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, istauddie Web-Client-ID des Backends undazpdie Client-ID der Android-App. Trage deine Android-Client-IDs in die Liste der vertrauenswürdigen Presenter ein; verzichte nicht auf die Prüfung vonazp.- Mit
hdschränkst du auf Workspace-Domains ein. Wenn sich nur Benutzer bestimmter Google-Workspace-Domains anmelden dürfen, prüfe den Claimhd; 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 duhttps://your-tenant.auth0.comohne 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
HS256auf, 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 legealgorithms: ["RS256"]fest, damit eine Konfigurationsänderung deinen Verifizierer nicht unbemerkt auf HMAC umstellen kann.
Häufige Fehler
- 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). - 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.
- Die Nonce nicht speichern. Eine Nonce zu erzeugen und sie nie zu vergleichen, ist dasselbe, wie keine zu senden.
- Benutzer über
emailanmelden. Nurissplussubist 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. - Schlüssel fest im Code hinterlegen. Rufe sie von der
jwks_uriab und vollziehe Rotationen mit; siehe JWKS-Schlüsselrotation ohne Ausfallzeit.
Quellen
- OpenID Connect Core 1.0 mit Errata Set 2: §2 (ID-Token), §3.1.3.7, §3.1.3.8, §3.3.2.10, §3.3.2.11, §5.7
- OpenID Connect Core 1.0 mit Errata Set 1, für die frühere Regel zu
azp - RFC 9207: Identifikation des Ausstellers von Autorisierungsservern in OAuth 2.0
- Microsoft: Referenz der ID-Token-Claims, Access Tokens und Validierung des Ausstellers
- Google: OpenID Connect
- Auth0: ID-Tokens validieren, Signaturalgorithmen
- Dokumentation von openid-client und go-oidc