What it is
OAuth 2.0 is an authorisation protocol — it grants an application access to resources on a user's behalf. OpenID Connect (OIDC) is an authentication layer on top of OAuth that answers "who is this person". Most vulnerabilities come from using OAuth (authorisation) as if it were authentication, and from omitting checks the protocol requires but cannot enforce.
Why you should care
OAuth is hard not because of cryptography — because it is a multi-party, multi-step protocol where each step has a check that, if omitted, reports nothing. The flow still works, users still sign in, and the vulnerability only surfaces when someone attacks.
The four most common flaws, all omissions:
- Not validating
state.stateis the OAuth flow's CSRF token. Without it, an attacker grafts THEIR authorization code onto the victim's session — the victim signs into the attacker's account (login CSRF), then enters data into it. - Not using PKCE. An authorization code intercepted in transit (the redirect goes through the browser) can be exchanged for a token. PKCE binds the code to the client that started the flow, so a stolen code is useless.
- Loose
redirect_urimatching. If the server accepts a redirect_uri matched by prefix or by subdomain, the attacker diverts the authorization code to their own server. - Using the
access_tokento authenticate. An access token says "may do what", not "is who". Using it to sign in is the "confused deputy" flaw — a token issued for another app accepted as identity.
And one architectural point: the implicit flow is dead. OAuth 2.1 and RFC 9700 drop it entirely because it returns the token in the URL fragment (leaking via history, referer, logs). If your code still uses response_type=token, the task is to move to authorization code + PKCE, not to patch.
How the attack works
The authorization code flow is four steps, each with a check. The diagram marks where each check lives.
sequenceDiagram autonumber actor U as User participant A as Your app participant I as Identity Provider A->>U: Redirect to I with state + code_challenge (PKCE) U->>I: Sign in and consent I->>A: Redirect to redirect_uri?code=...&state=... Note over A: CHECK 1: does state match the stored value? (anti-CSRF) A->>I: Exchange code for tokens, with code_verifier (PKCE) Note over I: CHECK 2: does code_verifier match code_challenge? I->>A: access_token + id_token Note over A: CHECK 3: verify id_token (signature, aud, iss, nonce)<br/>CHECK 4: use id_token for identity,<br/>NOT the access_tokenThe key point: a code intercepted in transit (it passes through the browser) is useless WITH PKCE — because exchanging it needs the code_verifier only the initiating client has. Without PKCE, an intercepted code exchanges for a token immediately.
The check table and the consequence of omitting each:
| Check | Consequence of omitting |
|---|---|
state matches | Login CSRF — the victim signs into the attacker's account |
| PKCE | An intercepted authorization code exchanges for a token |
redirect_uri matched EXACTLY | The authorization code is diverted to the attacker's server |
verify id_token (signature, aud, iss) | A forged token, or another app's token, is accepted |
nonce | Replay of an old id_token |
use id_token for identity | Confused deputy — another app's access token accepted as identity |
The last row deserves explaining: some "sign in with Google" apps accept an access_token from the client and call Google's /userinfo to get identity. But that access token may have been obtained by ANOTHER app — and Google returns the victim's real identity, so the app mistakenly believes the attacker is the victim. This is the "confused deputy", and the fix is to use the id_token (whose aud is your app).
Diagram description: A sequence diagram of the authorization code flow with four checks marked. The app redirects the user to the identity provider with state and the PKCE code_challenge. The user signs in and consents, the provider redirects to redirect_uri with a code and state. Check one: state matches the stored value to prevent CSRF. The app exchanges the code for tokens with the code_verifier; check two at the provider: code_verifier matches code_challenge. The provider returns an access_token and an id_token. Check three: verify the id_token including signature, aud, iss and nonce. Check four: use the id_token for identity, not the access_token.
Concrete example
Three attack paths, matching three omitted checks from block 3.
# ① Login CSRF from not checking state. The attacker gets THEIR code and forces the victim to use it.# The attacker starts an OAuth flow, stops at the code step, then sends the victim a link:GET https://app.example/oauth/callback?code=<the_ATTACKER's_code> HTTP/1.1 # The victim clicks → the app exchanges the code → the victim is now signed into the attacker's account,# and everything they enter (card, documents) goes into that account.# ② Loose redirect_uri matching. The server accepts anything starting with the registered URL.GET https://id.provider/authorize ?client_id=app &redirect_uri=https://app.example.evil.com/callback ← prefix-matches "https://app.example" &response_type=code # The code is delivered to app.example.evil.com — the attacker's server.# ③ Confused deputy. The app accepts an access_token from the client and trusts /userinfo.POST /api/auth/google HTTP/1.1{"access_token":"<a token the attacker obtained for ANOTHER APP>"} # The app calls Google /userinfo with that token → Google returns the token owner's real identity,# but the token may have been obtained by a phishing app, and the user never intended to sign into app.example.# After the fix: state checked, PKCE required, redirect_uri matched EXACTLY, id_token used.# An intercepted code → useless without the code_verifier.# Another app's access_token → the id_token has the wrong aud → rejected.[HttpGet("/oauth/callback")]public async Task<IActionResult> Callback([FromQuery] string code, [FromQuery] string state){ // ❌ 1 — không kiểm state. state là token chống CSRF của luồng. Thiếu nó, kẻ tấn // công ghép code CỦA HỌ vào phiên nạn nhân → nạn nhân đăng nhập vào tài khoản // kẻ tấn công (login CSRF). // // ❌ 2 — không PKCE. Một code bị chặn trên đường (nó đi qua trình duyệt) đổi // được token ngay, vì không có code_verifier để đòi. var http = new HttpClient(); var tokenResponse = await http.PostAsync("https://id.provider/token", new FormUrlEncodedContent( new Dictionary<string, string> { ["grant_type"] = "authorization_code", ["code"] = code, ["client_id"] = "app", ["client_secret"] = _secret, // ❌ 3 — redirect_uri không cần khớp chính xác vì server tự viết cũng // khớp lỏng. Covert Redirect ở khối 5 là chính xác lỗi này. })); var tokens = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(); // ❌ 4 — dùng ACCESS_TOKEN để lấy danh tính. access_token nói "được phép làm gì", // không nói "là ai". Một token cấp cho APP KHÁC vẫn gọi được /userinfo và // Google trả về danh tính đúng của chủ token → confused deputy. var userinfo = await http.GetFromJsonAsync<UserInfo>( $"https://id.provider/userinfo?access_token={tokens.AccessToken}"); await SignInAsync(userinfo.Email); // đăng nhập bằng danh tính chưa verify đúng cách // ❌ 5 — và token trả về client để lưu localStorage. return Ok(new { accessToken = tokens.AccessToken, refreshToken = tokens.RefreshToken });}// ❌ Implicit flow: response_type=token. OAuth 2.1/RFC 9700 đã BỎ nó vì token trả// trong URL fragment — rò qua lịch sử trình duyệt, referer, và log.function login() { const params = new URLSearchParams({ client_id: "app", redirect_uri: "https://app.example/callback", response_type: "token", // ❌ implicit scope: "openid profile email", // ❌ không có state, không có code_challenge (PKCE) }); window.location.href = "https://id.provider/authorize?" + params;} function handleCallback() { // ❌ Token đọc từ URL fragment và lưu localStorage. XSS đọc được bằng một dòng. const token = new URLSearchParams(location.hash.slice(1)).get("access_token"); localStorage.setItem("access_token", token); // XSS đọc được — xem topic xss}What happened in the wild
"Covert Redirect" (Wang Jing, 2014). A family of loose-redirect_uri flaws across many OAuth providers, letting an authorization code or token be diverted to an attacker domain via an open redirect on the registered domain itself. Memorable because it shows prefix or domain matching is not enough — redirect_uri must match EXACTLY.
Facebook "Login with Facebook" confused deputy (many reports 2012–2018). The recurring pattern: an app accepts an access_token from the client and calls the Graph API for identity. The token was obtained for another app, so the attacker signs into the victim's account at a third app. The fix is validating the token's aud — exactly the last row of the block 3 table.
And the standards' decision: OAuth 2.1 (draft) and RFC 9700 (BCP, 2025). They drop the implicit flow, drop the password grant, and make PKCE mandatory for every authorization code flow — confidential clients included. This is not a "should" recommendation; it is the standard saying "the old ways are not safe". If your code still uses the implicit flow or lacks PKCE, that is technical debt the standard has flagged.
How to defend
Authorization code + PKCE, and all four checks
mandatoryThe core fix is using the right flow and omitting no check from the block 3 table.
- The authorization code flow, never implicit. OAuth 2.1/RFC 9700 dropped implicit — it returns the token in the URL fragment, leaking via browser history, referer and logs.
- PKCE mandatory, confidential clients included.
code_challenge = SHA256(code_verifier)sent at initiation,code_verifiersent at token exchange. An intercepted authorization code is useless because the interceptor lacks the verifier. staterandomly generated, stored server-side, checked at callback. It is the flow's anti-CSRF token. For a client, store it in anHttpOnlycookie bound to the initiating session (not merely compared to the value in the URL).- A
noncein the id_token to prevent replay.
And do not write your own OAuth client. This matters: use a vetted library — Microsoft.AspNetCore.Authentication.OpenIdConnect for .NET — which already enforces PKCE, state, nonce, and correct id_token verification. Writing your own takes on responsibility for all seven checks, and omitting one is nearly certain.
// KHÔNG tự viết OAuth client. Microsoft.AspNetCore.Authentication.OpenIdConnect// cưỡng chế PKCE, state, nonce, và verify id_token đúng cách — bốn trong bảy phép// kiểm. Ba phép còn lại (redirect_uri chính xác, dùng id_token, chỗ lưu token) là// cấu hình bên dưới.builder.Services .AddAuthentication(o => { o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) // BFF pattern: sau khi đăng nhập, client chỉ giữ một cookie phiên HttpOnly. // Token KHÔNG BAO GIỜ chạm tới JavaScript, nên XSS không đọc được — cách an // toàn nhất, và là mặc định ở đây. .AddCookie(o => { o.Cookie.HttpOnly = true; o.Cookie.SecurePolicy = CookieSecurePolicy.Always; o.Cookie.SameSite = SameSiteMode.Lax; o.Cookie.Name = "__Host-session"; // xem topic csrf }) .AddOpenIdConnect(o => { o.Authority = "https://id.provider"; // JWKS, endpoint lấy từ metadata o.ClientId = "app"; o.ClientSecret = builder.Configuration["Oidc:ClientSecret"]; // authorization code flow, KHÔNG implicit (OAuth 2.1 đã bỏ implicit). o.ResponseType = "code"; // PKCE bắt buộc — kể cả với client bí mật (RFC 9700). Thư viện tự sinh // code_verifier/challenge và kiểm. o.UsePkce = true; // state và nonce do thư viện tự sinh, lưu trong một cookie tương quan // HttpOnly, và kiểm khi callback về. Không có gì cho ta bỏ sót ở đây. o.SaveTokens = true; // lưu ở SERVER (trong cookie đã mã hoá), không trả về client // DÙNG id_token cho danh tính. Thư viện verify chữ ký qua JWKS, kiểm aud = // ClientId, iss = Authority, nonce, exp — bảy phép kiểm của topic jwt. Đây // là bản vá cho confused deputy: một access_token của app khác không có // đường nào trở thành danh tính ở đây. o.GetClaimsFromUserInfoEndpoint = true; // làm giàu claim, KHÔNG dùng để xác thực o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidAudience = "app", ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), // không phải 5 phút mặc định }; // Scope tối thiểu (lớp 2): chỉ những gì app thật sự cần. o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("profile"); o.Scope.Add("email"); // redirect_uri phải khớp CHÍNH XÁC với giá trị đã đăng ký ở provider. // Đăng ký ở provider là danh sách hữu hạn URL đầy đủ — không khớp tiền tố, // không domain con, không regex (Covert Redirect ở khối 5). o.CallbackPath = "/signin-oidc"; });// SPA với authorization code + PKCE. Dùng thư viện đã kiểm (oidc-client-ts),// không tự viết — nhưng phần quan trọng dưới đây minh hoạ các phép kiểm. async function login() { // PKCE: sinh verifier ngẫu nhiên và challenge = SHA256(verifier). Web Crypto, // không Math.random — verifier là một bí mật. const verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); const challenge = base64url(new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)))); // state: token chống CSRF của luồng. Lưu cùng verifier để callback kiểm lại. const state = base64url(crypto.getRandomValues(new Uint8Array(16))); sessionStorage.setItem("oauth", JSON.stringify({ state, verifier })); const params = new URLSearchParams({ client_id: "app", redirect_uri: "https://app.example/callback", // khớp CHÍNH XÁC với đăng ký response_type: "code", // authorization code, không implicit scope: "openid profile email", // scope tối thiểu (lớp 2) state, code_challenge: challenge, code_challenge_method: "S256", }); location.href = "https://id.provider/authorize?" + params;} async function handleCallback() { const url = new URLSearchParams(location.search); const saved = JSON.parse(sessionStorage.getItem("oauth") ?? "{}"); // KIỂM state: so với giá trị ĐÃ LƯU, không với chính giá trị trong URL. if (!url.get("state") || url.get("state") !== saved.state) { throw new Error("state không khớp — có thể là login CSRF"); } // Đổi code lấy token Ở SERVER (BFF), không ở client. Server giữ client_secret, // đặt phiên vào cookie HttpOnly, và token KHÔNG BAO GIỜ trả về JavaScript — // nên XSS không có gì để đọc. const res = await fetch("/api/oauth/exchange", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: url.get("code"), verifier: saved.verifier }), }); sessionStorage.removeItem("oauth"); if (!res.ok) throw new Error("đổi token thất bại"); // Không lưu token nào ở client. Danh tính nằm trong cookie phiên HttpOnly.}Match `redirect_uri` EXACTLY, and use the `id_token` for identity
mandatoryThe two remaining checks, both where real vulnerabilities live.
Match redirect_uri by exact string against the registered list — no prefix match, no subdomain match, no regex. https://app.example/callback matches exactly that string, not https://app.example.evil.com/callback (prefix) or https://app.example/callback/../x (path traversal — see the path-traversal topic). Covert Redirect in block 5 is exactly this loose-matching flaw. Registration on the provider side must be a finite list of full URLs.
Use the id_token for identity, not the access_token. The access token answers "may do what", the id_token answers "is who" — and the id_token has an aud of your own app, so a token issued for another app is rejected. This is the confused-deputy fix (last row of the block 3 table, and the Facebook incident in block 5).
Concretely: do not accept a token from the client and call /userinfo to sign in. The correct flow is the app (server) exchanging the code for an id_token, verifying the signature via the provider's JWKS, checking aud = your client_id, checking iss = your provider (see the jwt topic — the same seven checks).
Tokens in the right place: refresh tokens server-side, no tokens in localStorage
After obtaining tokens correctly, where you store them decides whether an XSS steals the session.
- For a traditional web app (the BFF pattern): tokens live on the SERVER, the client holds only an
HttpOnlysession cookie. This is the safest — the token never touches JavaScript, so XSS cannot read it. It is also what ASP.NET Core'sAddOpenIdConnectdoes by default. - For a SPA: if you must hold tokens client-side, the access token in memory (a JS variable, not
localStorage), the refresh token in anHttpOnly+SameSitecookie.localStorageis readable in one line of JS — see xss layer 3. - Refresh token rotation with reuse detection: see jwt layer 1b. An already-used refresh token appearing a second time means it was stolen — revoke the whole token family.
The principle: OAuth gives you tokens; tokens are credentials; and every lesson about holding credentials in the jwt and xss topics applies here intact.
Minimal scopes and explicit consent
This layer bounds the damage when a token leaks or a consent is tricked (clickjacking the consent page — see the clickjacking topic).
- Request minimal scopes. An app that reads a profile needs no write scope. Excess scope means a stolen token does more than necessary — the same least-privilege principle as the api-security topic.
- The consent page resists clickjacking.
frame-ancestors none, and for a significant grant require non-single-click interaction (see clickjacking layer 1b). A clickjacked OAuth consent grants the attacker's app long-lived access. - Review and limit the third-party apps you permit. If you are the identity provider, the list of registered apps is your trust surface — the same inventory problem as API9 in the api-security topic.
Detection: anomalous callbacks are a low-noise signal
The OAuth flow has a regular shape, so deviations from it are a clean signal.
- A callback with a mismatched or missing
state. A legitimate client always returns the exact storedstate. A callback missing state or with an unknown one is a login-CSRF attempt — alert on the first, not on a total. - A rejected
redirect_uri. If your provider refuses a non-matching redirect_uri, log it: it is a covert-redirect attempt. - An id_token with the wrong
audorissbut a valid signature. The same signal as the jwt topic — it means somebody is trying another app's or another provider's token. - A code exchange failing on a missing/wrong code_verifier. With PKCE on, a code exchange without the correct verifier is an intercepted code being tried.
Layer 3 because the layer-1 checks already block these; but logging them turns "blocked" into "we know who is trying".
Verifying the fix
1. Check PKCE and state are enforced — starting right at the authorize URL:
B=https://app.example# Start the flow and check the redirect to the provider carries BOTH state and code_challenge.L=$(curl -sI "$B/oauth/login" | grep -i '^location:' | tr -d '\r')echo "$L" | grep -q 'code_challenge=' || { echo "MISSING PKCE"; exit 1; }echo "$L" | grep -q 'state=' || { echo "MISSING state"; exit 1; }echo "$L" | grep -q 'response_type=code' || { echo "not using the authorization code flow"; exit 1; }echo "$L" | grep -q 'response_type=token' && { echo "still using the implicit flow — dropped by OAuth 2.1"; exit 1; }2. Test a callback with the WRONG state is rejected — the login-CSRF check:
# A callback with a state not matching the one stored in the session must be 4xx, no sign-in.curl -s -o /dev/null -w '%{http_code}\n' "$B/oauth/callback?code=x&state=wrong-value" \ -b "oauth_session=<a session holding a different state>" # must be 400, not a 302 sign-in3. Test exact redirect_uri matching (if you are the provider) — Covert Redirect from block 5:
for uri in 'https://app.example.evil.com/callback' \ 'https://app.example/callback/../evil' \ 'https://app.example.evil.com' \ 'https://app.example@evil.com/callback'; do code=$(curl -s -o /dev/null -w '%{http_code}' \ "https://id.provider/authorize?client_id=app&response_type=code&redirect_uri=$uri") [ "$code" = "400" ] || echo "loose redirect_uri accepted: $uri ($code)"done4. Check the app uses the id_token for identity, not the access_token (confused deputy). This needs reading code, not fully automatable — find the sign-in path and assert it verifies an id_token (checking aud), rather than accepting an access_token and calling /userinfo:
grep -rnE 'userinfo|access_token' --include='*.cs' src/ | grep -iE 'login|signin|authenticate' \ && echo "WARNING: possibly authenticating with access_token/userinfo — review it"5. Verify the id_token passes the seven JWT checks — see the jwt topic, block 7. An id_token IS a JWT, so aud, iss, signature, exp and nonce must all be checked.
6. Check tokens are not in localStorage (layer 1c):
grep -rnE 'localStorage\.(set|get)Item\([^)]*(token|access|refresh|id_token)' \ --include='*.ts' --include='*.tsx' src/ \ && { echo "tokens in localStorage — XSS can read them"; exit 1; }exit 0public class OAuthFlowTests : IClassFixture<ApiFixture>{ private readonly ApiFixture _fx; public OAuthFlowTests(ApiFixture fx) => _fx = fx; /// <summary> /// URL authorize phải mang CẢ state lẫn code_challenge, và response_type=code. /// Đây là phép kiểm sớm nhất — nó bắt được implicit flow và thiếu PKCE ngay ở /// bước đầu, trước khi có bất kỳ token nào. /// </summary> [Fact] public async Task Authorize_redirect_carries_state_and_pkce() { var res = await _fx.Client.GetAsync("/oauth/login"); var location = res.Headers.Location!.ToString(); Assert.Contains("response_type=code", location); Assert.DoesNotContain("response_type=token", location); // implicit đã bị bỏ Assert.Contains("code_challenge=", location); Assert.Contains("code_challenge_method=S256", location); Assert.Contains("state=", location); Assert.Contains("nonce=", location); } /// <summary> /// Login CSRF: callback với state không khớp giá trị đã lưu phải bị từ chối. /// So sánh phải với giá trị ĐÃ LƯU (cookie tương quan), không với chính giá trị /// trong URL — kẻ tấn công kiểm soát URL. /// </summary> [Fact] public async Task Callback_with_mismatched_state_is_rejected() { var (client, _) = await _fx.StartOAuthFlowAsync(); // lưu state=A trong cookie // Callback mang state=B (của kẻ tấn công) → không khớp A → từ chối. var res = await client.GetAsync("/signin-oidc?code=attacker_code&state=B"); Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode); Assert.False(res.Headers.Contains("Set-Cookie")); // không có phiên nào được tạo } /// <summary> /// Confused deputy: một access_token cấp cho APP KHÁC không được nhận làm danh /// tính. Bản vá là dùng id_token có aud = client của ta, nên một token có aud /// khác bị từ chối ở tầng verify. /// </summary> [Fact] public async Task Access_token_for_another_app_is_not_accepted_as_identity() { // Token này hợp lệ, do đúng provider ký, nhưng aud là một app khác. var foreignIdToken = _fx.IssueIdToken(aud: "some-other-app", sub: "victim"); var res = await _fx.Client.PostAsJsonAsync("/oauth/callback-exchange", new { idToken = foreignIdToken }); Assert.Equal(HttpStatusCode.Unauthorized, res.StatusCode); } /// <summary> /// redirect_uri phải khớp CHÍNH XÁC (nếu ta là provider). Mỗi dòng là một biến /// thể Covert Redirect ở khối 5 — tiền tố, path traversal, userinfo trong host, /// domain con. /// </summary> [Theory] [InlineData("https://app.example.evil.com/callback")] // tiền tố [InlineData("https://app.example/callback/../evil")] // path traversal [InlineData("https://app.example@evil.com/callback")] // userinfo trong host [InlineData("https://evil.com/callback?x=app.example")] // chuỗi con public async Task Provider_rejects_non_exact_redirect_uri(string uri) { var res = await _fx.ProviderClient.GetAsync( $"/authorize?client_id=app&response_type=code&redirect_uri={Uri.EscapeDataString(uri)}"); Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode); } /// <summary>Token không được trả về cho client để lưu localStorage (BFF pattern).</summary> [Fact] public async Task Tokens_are_not_returned_to_the_client() { var (client, _) = await _fx.CompleteOAuthFlowAsync(_fx.Alice); var body = await (await client.GetAsync("/api/me")).Content.ReadAsStringAsync(); Assert.DoesNotContain("access_token", body); Assert.DoesNotContain("refresh_token", body); // Danh tính đến từ cookie phiên HttpOnly, không từ token trong body. }}Common mistakes
| The "fix" | Why it is wrong |
|---|---|
| Use the implicit flow "because it is a SPA" | OAuth 2.1/RFC 9700 dropped it. Authorization code + PKCE is the correct SPA flow |
| Skip PKCE "because it is a confidential client" | RFC 9700 makes PKCE mandatory for EVERY authorization code flow, confidential clients included |
Check state by comparing it to the value in the URL | The attacker controls the URL. State must be compared to the value stored server-side / in an HttpOnly cookie |
| Prefix- or domain-match redirect_uri | app.example.evil.com prefix-matches app.example. Covert Redirect is exactly this. Match EXACTLY |
| Accept an access_token from the client and call /userinfo to sign in | Confused deputy: another app's token accepted as identity. Use the id_token with its aud |
| Not verifying the id_token signature | The id_token is a JWT. Skipping verification accepts a forged token — see the jwt topic |
| Tokens in localStorage | XSS reads them in one line. Use an HttpOnly cookie, or a BFF holding tokens server-side |
| Write your own OAuth client | Seven checks, and omitting one reports nothing. Use a vetted library |
The conceptual mistake, and the root of several rows above: using OAuth (authorisation) as authentication. An access_token says "may do what", not "is who". Signing in with an access_token is the confused deputy. The OIDC id_token is the answer to "is who", and it carries an aud so you know the token was meant for you.
The check-ownership mistake: thinking the library handles everything. A library enforces PKCE, state, nonce if you configure it correctly — but exact redirect_uri matching is on the provider side, using the id_token vs the access_token is your code's decision, and where you store tokens is your architecture. The library closes four checks; the other three are yours.
Comments
Commenting needs an account with at least one completed lesson. That condition is what keeps this thread worth reading: every point belongs to someone who can be asked back, and reputation accrues over time.
You can still read every comment below without an account. Signing in brings you back to this exact spot, not to the top of the page.
Loading comments…