OAuth and OIDC
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 22 min · 6 figures

    OAuth 2.0 and OpenID Connect

    How OAuth 2.0 gives apps limited API access without your password, how OpenID Connect adds sign-in with ID tokens, and how to explain the authorization code flow with PKCE, token types, other grants and the RFC 9700 security rules in an interview.

    OAuth 2.0 is a protocol for delegated authorisation: it lets an app act for you, with limited and revocable permissions, without ever seeing your password. OpenID Connect (OIDC) is a thin identity layer on top that also tells the app who just signed in, through a signed ID token addressed to that app. Use OAuth when an app calls an API for a user and OIDC when it logs the user in; most flows do both.

    Picture it first: a gate pass, not your house keys

    You live in flat B-402 of a gated society, and your cook, Ravi, must get in at 9 am while you are at work. Your house keys would let him in at any hour, into every cupboard, until you changed the locks.

    Instead, you approve a visit in the society’s app: “Ravi, B-402, 9 to 11 am, kitchen only”. The security desk confirms it is really you and gives Ravi a gate pass. The tower guard never asks who Ravi is; he checks the pass: right flat, right hours, not expired. Whoever finds a dropped pass can use it until 11 am, unless it carries Ravi’s photo.

    Now the clubhouse wants to know which resident just walked in. Someone waves Ravi’s pass: “It says B-402, so I’m the resident.” It proves nothing of the sort: the pass was written for the guard and says what its holder may do, not who they are. The clubhouse needs its own slip from the desk: “resident of B-402, checked at 9:02, for the clubhouse”.

    Map it back:

    • You are the resource owner; your keys are your password.
    • Ravi is the client app; the desk is the authorization server; the guard is the resource server (the API).
    • The pass is an access token, limited by audience (one flat), scope (the kitchen) and expiry. A plain pass is a bearer token; a photo pass is sender-constrained.
    • The clubhouse slip is the OIDC ID token: who you are, addressed to one app.

    The problem OAuth solves

    Before OAuth, a budgeting app that wanted your bank transactions asked for your net-banking password and logged in as you. It could then do anything you could, including transfers, and you could not cut it off without changing your password everywhere.

    RFC 6749 (October 2012) replaced the password with a token issued to that one app, carrying only the permissions you approved (scopes such as transactions.read), for a limited time, revocable on its own. You type your password only on the bank’s login page.

    What OAuth does not say is who you are. An access token is a permission slip for an API; the app presents it but is not its audience. Treating “I received a token, so the user logged in” as sign-in proved exploitable; OpenID Connect fixes that with a second token aimed at the app. OAuth answers “what may this app do?”; OIDC answers “who just signed in?”.

    In the interview

    Say it in one line: “The access token is for the API; the ID token is for my app. I never use an access token as proof of login.”

    Four roles and two channels

    In the running example you let a budgeting app read 90 days of your bank transactions, and sign in to it with your bank identity. RFC 6749 names four roles:

    • Resource owner: you.
    • Client: the budgeting app, wherever it runs.
    • Authorization server (AS): the bank’s login and consent service, which issues tokens.
    • Resource server (RS): the bank’s transactions API, which accepts them.

    A confidential client runs on a server and can hold a secret or private key. A public client, such as a mobile app or single-page app (SPA), runs on the user’s device, where anything shipped can be extracted, so it relies on its redirect URI and PKCE.

    The front channel is browser redirects with parameters in the URL, which can leak into history, logs and Referer headers, or be tampered with. The back channel is direct server-to-server HTTPS the browser never sees. The rule: only short-lived, single-use values go through the front channel; tokens go through the back channel.

    The four OAuth roles in the budgeting-app example. Redirects pass through the browser; the code-for-token exchange and API calls go directly between servers.Notice The browser only ever carries the short-lived code. Tokens move only on the server-to-server back channel.

    The authorization code flow with PKCE

    Use this flow whenever a user is present. Figure 2 shows the ten steps; the HTTP follows.

    The authorization code flow with PKCE as a sequence diagram. The user signs in to the budgeting app and grants it read access to transactions in one pass.Notice The tokens appear only in steps 8 to 10, on calls the browser never sees.

    Steps 1–3: the authorization request

    On “Connect bank”, the backend stores three fresh random values in your session: state (ties the response to this browser), nonce (ties the ID token to this login) and the PKCE code_verifier. Then it redirects you:

    GET /authorize?response_type=code
        &client_id=budget-web
        &redirect_uri=https%3A%2F%2Fbudget.example%2Fcallback
        &scope=openid%20profile%20transactions.read
        &state=r7Kq2vXw9pLm
        &nonce=Hc4tY8sNq1Ze
        &code_challenge=Tziyt3BFlwsXJTMthIGVizuFeRGmlNN8nKys6B9br2c
        &code_challenge_method=S256 HTTP/1.1
    Host: login.bank.example

    The AS compares redirect_uri character for character with a pre-registered URI. RFC 9700 requires exact matching (except the port of a native app’s localhost redirect), because wildcard and prefix rules have repeatedly let attackers steer codes to their own pages. scope asks for the least you need: openid makes this a login; transactions.read is the API permission.

    The AS authenticates you (password, OTP, passkey), unseen by the client, then shows the scopes in plain words: “Read your transactions for the last 90 days”. Users may untick some, so check which scopes were actually granted.

    Steps 5–6: the code returns through the browser

    HTTP/1.1 302 Found
    Location: https://budget.example/callback?code=c_7Gm2Qx9RtLw4&state=r7Kq2vXw9pLm&iss=https%3A%2F%2Flogin.bank.example

    The app checks state against your session and discards it; a mismatch means someone else started this flow (login CSRF). A client that uses several authorization servers also checks iss (RFC 9207) to defeat mix-up attacks, where a malicious AS tricks it into handing over another server’s code. The code is single-use, and RFC 6749 recommends a 10-minute maximum lifetime.

    Steps 7–8: the back channel swaps the code for tokens

    POST /token HTTP/1.1
    Host: login.bank.example
    Content-Type: application/x-www-form-urlencoded
    Authorization: Basic <base64 of client_id:client_secret>
    
    grant_type=authorization_code
    &code=c_7Gm2Qx9RtLw4
    &redirect_uri=https%3A%2F%2Fbudget.example%2Fcallback
    &code_verifier=LEQtmyQH-fVCsamvFJqHXjuSYvV9vD0eqObXVRJD0AM

    The AS checks the client’s credentials (RFC 9700 prefers a signed JWT or mTLS to a shared secret), that the code is unused and was issued to this client, and that BASE64URL(SHA256(code_verifier)) equals the stored challenge. (The OAuth 2.1 draft drops redirect_uri here; PKCE does its job.) The response:

    {
      "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3Mi…",
      "token_type": "Bearer",
      "expires_in": 600,
      "refresh_token": "rt_9QwXk2…",
      "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMTAifQ.eyJpc3Mi…",
      "scope": "openid profile transactions.read"
    }

    The app validates the ID token, starts its own session and keeps the tokens server-side.

    Steps 9–10: calling the API

    GET /v1/accounts/acc_12/transactions?from=2026-07-29 HTTP/1.1
    Host: api.bank.example
    Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3Mi…

    Tokens go in the Authorization header, never in a URL, where logs and history would keep them. The API verifies the token, then its issuer, expiry, audience (is it meant for me?) and scope.

    PKCE: making a stolen code worthless

    PKCE (RFC 7636, 2015, said “pixy”) was built for mobile apps, where the code returns through a custom scheme such as budgetapp://callback that a malicious app can also register. With no client secret, a stolen code used to be enough.

    PKCE binds the code to the client instance that asked for it. The client generates a random code_verifier of 43 to 128 characters. RFC 7636 suggests 32 random bytes, which is 256 bits; base64url carries 6 bits per character, so 256 ÷ 6 = 42.7, rounded up to 43 characters. Only the code_challenge, BASE64URL(SHA256(verifier)), travels through the browser; the verifier goes straight to the token endpoint, which hashes and compares.

    import base64, hashlib, secrets
    
    def b64url(raw: bytes) -> str:
        return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
    
    verifier = b64url(secrets.token_bytes(32))                      # 43 characters
    challenge = b64url(hashlib.sha256(verifier.encode()).digest())  # sent in the redirect

    A thief with the code also needs the verifier, and the hash does not reveal it. The plain method (challenge equals verifier) leaks it to anyone who reads the request, which is why RFC 9700 favours S256 and the OAuth 2.1 draft drops plain.

    The same intercepted code, with and without PKCE. The attacker sees everything in the front channel but nothing that stayed on the client.Notice The attacker holds the same code in both panels. Only the verifier, which never travelled, decides whether it turns into tokens.

    Why PKCE if you have a client secret? The secret stops an attacker redeeming a stolen code themselves, but not code injection: the attacker starts a login with your app in their own browser and pastes the victim’s code into the callback, and your server redeems it with your secret, linking the attacker’s session to the victim’s account. With PKCE, your server sends the verifier from the attacker’s session, which does not match, so the exchange fails. Hence RFC 9700 recommends PKCE for confidential clients too, and requires the AS to block downgrade: accept a verifier only if the authorization request carried a challenge.

    Use a library, then check its settings

    Most OAuth bugs are missing checks, not broken cryptography. Use a well-maintained OAuth and OIDC library, then confirm exact redirect URIs, S256 PKCE, one-time state and nonce, the expected issuer, and audience checks on every token.

    Three tokens, three audiences

    For each token, ask: who is it for, where does it go, how long does it live?

    • Access token: for the resource server. Attached to API calls, opaque to the client, short-lived.
    • Refresh token: for the authorization server only. Swapped for new access tokens without the user, so it lives far longer and is the prize for thieves.
    • ID token: for the client. A signed JWT checked once at login, then replaced by the app’s own session.
    Who each artefact is for, and how long it lives. Lifetimes beyond the 10-minute code limit are example policy you choose, drawn on a log scale.Notice The longest-lived token, the refresh token, never goes to an API. It only ever returns to the authorization server that issued it.

    Opaque or JWT access tokens?

    An opaque token is a random reference that only the AS understands, so the API asks the AS through introspection (RFC 7662) and gets back fields such as active, scope, sub and exp. A JWT access token (RFC 9068) is signed and self-contained (iss, sub, aud, exp, client_id, header type at+jwt), so the API validates it locally with the AS’s public keys.

    Opaque + introspection JWT, validated locally
    Cost per request A call to the AS, or a cache hit A signature check, no network hop
    Revocation Seen at the next introspection Waits for exp, unless you keep a denylist
    AS outage API calls fail or use stale cache Issued tokens work until expiry
    A leaked token reveals Nothing Its claims
    Size A short random string Hundreds of bytes; a 2048-bit RS256 signature alone is 342 base64url characters
    Opaque tokens with introspection vs JWT access tokens validated locally

    Caching introspection answers trades AS load for staleness. A common hybrid: the gateway introspects opaque external tokens and forwards a short-lived signed JWT internally.

    Refresh tokens: rotate or bind

    RFC 9700 says a public client’s refresh tokens must be rotated or sender-constrained. With rotation, each use returns a new refresh token and invalidates the old; if an old one ever comes back, two parties hold copies, so the AS revokes the whole chain and the real user signs in again. Sender-constraining binds the token to a key only the client holds. Confidential clients already authenticate when refreshing. Idle refresh tokens should expire.

    Bearer vs sender-constrained access tokens

    A plain bearer token works for whoever holds it, like cash, so RFC 9700 recommends binding it. With DPoP (RFC 9449, 2023) the client signs a small JWT for every request, covering the method, URL, time, a unique ID and the token’s hash, with a key whose thumbprint the token is bound to; it works in SPAs and mobile apps. mTLS-bound tokens (RFC 8705) tie the token to a client certificate, natural between servers (see service-to-service auth and mTLS). You gain tokens that are useless when copied from a log; you pay with client key management and a signature check per request.

    OpenID Connect: who just signed in

    Why an access token is not a login

    A Swiggy-style food-delivery app adds “Sign in with Provider X”. Its mobile app gets an access token and sends it to the backend, which calls X’s profile API and logs in whoever it names. But the token was never addressed to the food app. A malicious quiz app that the victim once authorised with X holds tokens for the same user, and can replay one at the food app’s login endpoint to become the victim. Nothing in an access token says “issued to the food app, just now, for this login”.

    An ID token does: it is signed by the provider, its aud is the food app’s client_id, and its nonce matches the one stored in this session. The quiz app’s token fails the audience check; a replay fails the nonce check.

    The ID token and how to check it

    Add openid to the scope (plus profile or email) and the token response includes an ID token:

    {
      "iss": "https://login.bank.example",
      "sub": "u_8b41c2e7",
      "aud": "budget-web",
      "exp": 1793095500,
      "iat": 1793095200,
      "auth_time": 1793095185,
      "nonce": "Hc4tY8sNq1Ze",
      "name": "Ananya Rao",
      "email": "ananya@example.com"
    }

    OIDC Core has the client check that iss exactly matches the expected issuer, aud contains its client_id and no untrusted audience, the signature verifies under the issuer’s keys (a code-flow client may rely on TLS to the token endpoint instead), exp has not passed, and nonce matches the stored value (plus auth_time, if you sent max_age). Then key the user on iss plus sub, the only identifier the spec guarantees is stable, never on email, which it warns is not unique. JWT internals and attacks such as alg: none are covered in authentication: sessions, tokens and JWT.

    Validate the audience every time

    An ID token with a good signature but someone else’s aud is a valid token for another app; accepting it is the same bug as logging users in with access tokens. APIs follow the same rule: reject an access token with the wrong aud.

    Discovery and signing keys

    Providers publish a document at /.well-known/openid-configuration under the issuer URL, so libraries configure themselves:

    {
      "issuer": "https://login.bank.example",
      "authorization_endpoint": "https://login.bank.example/authorize",
      "token_endpoint": "https://login.bank.example/token",
      "userinfo_endpoint": "https://login.bank.example/userinfo",
      "jwks_uri": "https://login.bank.example/jwks.json",
      "code_challenge_methods_supported": ["S256"]
    }

    jwks_uri serves the provider’s public signing keys, each with a key ID (kid) that token headers name. To rotate, the provider publishes the new key beside the old one, switches signing, and keeps the old key listed for a while. Verifiers cache the set and refetch only on an unfamiliar kid, which is what lets local validation scale.

    UserInfo, single sign-on and logout

    The UserInfo endpoint returns profile claims for an access token; its sub must match the ID token’s. Single sign-on comes from the provider’s own session: a second app’s redirect lands on an AS that already knows you, though each app gets its own ID token. That makes logout harder: RP-initiated logout sends the user to the provider to end its session, and back-channel logout has the provider post a signed logout token to each app’s server, even after the tab is closed.

    Other grants, and the ones to retire

    Client credentials. A nightly reconciliation job has no user and no browser. It authenticates as itself (grant_type=client_credentials, with a secret, a signed JWT or mTLS) and gets a token for its own permissions. RFC 6749 allows this only for confidential clients and issues no refresh token; the job simply asks again (see service-to-service auth and mTLS).

    Device authorization grant. A smart TV has no usable keyboard, so RFC 8628 (2019) moves the login to your phone. The TV asks for a device code and gets:

    {
      "device_code": "Gk3xT0vQe8…",
      "user_code": "BKTF-QPLM",
      "verification_uri": "https://stream.example/tv",
      "verification_uri_complete": "https://stream.example/tv?user_code=BKTFQPLM",
      "expires_in": 900,
      "interval": 5
    }

    The TV shows the URL and code, often as a QR code. You approve on your phone while the TV polls the token endpoint every interval seconds (5 by default), hearing authorization_pending until tokens arrive and backing off 5 seconds on slow_down. A typeable code is a guessable one: eight letters from RFC 8628’s 20-consonant alphabet give 20⁸ ≈ 25.6 billion codes, about 34.5 bits, so allowing 5 guesses per code leaves odds of 5 ÷ 25.6 billion, about 1 in 5 billion. Rate-limit code entry, and have the approval screen say plainly that a device is being connected, to blunt phishing.

    Refresh and retired grants. The refresh grant swaps a refresh token for new tokens with the same or narrower scope. The implicit grant (response_type=token) put the access token in the URL fragment, where it leaks through history and Referer and cannot be sender-constrained; RFC 9700 says not to use it. The password grant had apps collect your password, the habit OAuth exists to break, and was never designed for MFA or passkeys; RFC 9700 forbids it.

    Grant Who uses it User present? Tokens issued OAuth 2.1
    Authorization code + PKCE Web, mobile, SPAs via a backend Yes, in a browser Access, refresh, ID Core; PKCE required
    Client credentials Services and jobs No Access only Kept
    Device (RFC 8628) TVs, consoles, CLIs Yes, on another device Access, refresh Extension grant
    Refresh token Any client holding one No Access, maybe a new refresh Kept; public clients rotate or bind
    Implicit Old SPAs Yes Access, in the URL Removed
    Password Legacy first-party apps Yes, typing it into the app Access, refresh Removed
    OAuth grant types and their status in the OAuth 2.1 draft (draft-16, September 2026)

    OAuth 2.1 folds these fixes into one document that will replace RFC 6749 and RFC 6750; as of draft-16 (3 September 2026) it is still a draft. It requires S256 PKCE (a confidential client may skip it only if the AS trusts its OIDC nonce handling), exact redirect matching, and sender-constrained or one-time refresh tokens for public clients; removes the implicit and password grants and query-string tokens; and drops redirect_uri from the token request. Follow RFC 9700 today and you are essentially building OAuth 2.1.

    Browsers and phones

    Single-page apps: keep tokens out of JavaScript

    An SPA shares its JavaScript context with every script on the page, so one XSS bug or poisoned npm package can read its tokens and send them away. Keeping tokens in memory is not enough: RFC 10017, the browser-apps Best Current Practice published in August 2026, shows malicious script running a silent authorization flow in a hidden iframe to get a brand-new set of tokens, which neither storage tricks nor DPoP can stop.

    RFC 10017 ranks three patterns by security, strongly recommending the first for business apps, sensitive apps and anything handling personal data:

    1. Backend for Frontend (BFF): a server-side component on your site is a confidential client. It keeps the tokens and gives the browser only a Secure, HttpOnly cookie, preferably SameSite=Strict with a __Host-Http- prefix. The SPA calls the BFF, which attaches the access token and forwards the call.
    2. Token-mediating backend: the backend keeps the refresh token but hands access tokens to the SPA.
    3. Browser-only client with PKCE: for low-risk apps, with short-lived tokens and rotation.
    The same XSS bug against two architectures. On the left the single-page app holds the tokens; on the right a Backend for Frontend holds them.Notice The injected script can still act through the open tab on the right, but it leaves with nothing it can reuse elsewhere.

    You gain an app with nothing reusable to steal. You pay with another server, every API call proxied through it, and CSRF protection on its endpoints. The BFF belongs to the frontend; your API gateway still validates tokens.

    Native and mobile apps

    RFC 8252 requires native apps to authorize in the system browser or an in-app browser tab, never an embedded webview, which would let the app read your password and cookies and would break single sign-on. The code returns through a claimed HTTPS link, a private-use URI scheme or a loopback address, protected by PKCE.

    Going deeper: attacks and defences

    RFC 9700 (January 2025), the OAuth 2.0 Security Best Current Practice, distils a decade of real attacks. These mistakes keep recurring:

    Mistake or attack What goes wrong Defence
    Loose redirect URI matching A crafted redirect sends the code to an attacker’s page Exact matching; no open redirectors
    No PKCE on a public client Another app on the phone intercepts the code and redeems it S256 PKCE, enforced by the AS
    Missing state Login CSRF: the attacker’s code lands in the victim’s callback One-time state, or PKCE
    Codes or tokens in URLs Leaks via Referer, history and logs Referrer-Policy: no-referrer, single-use codes, header-only tokens
    Wrong audience accepted App A’s token logs in to app B Check aud on every token
    Mix-up across servers A malicious AS collects another AS’s code Check iss (RFC 9207)
    Scope creep “Just in case” write scopes make any leak a disaster Least privilege, incremental consent
    Long-lived bearer tokens A copied token works for days, anywhere Short lifetimes, rotation, DPoP or mTLS

    Two more rules: authorization responses never travel over plain HTTP (only a native app’s loopback redirect may use http), and servers should publish metadata so libraries enable new protections automatically.

    Going deeper: OAuth at scale

    The authorization server becomes tier 0: if it is down nobody signs in, and as access tokens expire nobody can refresh.

    Where the authorization server sits relative to the hot path, at 30,000 API requests per second. Local JWT validation with a cached key set keeps it off the path; introspection puts it on every request.Notice With local validation the authorization server is not on the request path at all. It serves keys, logins and refreshes, not API calls.

    Validate locally. At 30,000 requests per second, introspecting every token means 30,000 AS calls/s, an extra hop on every request, and an AS outage that becomes an API outage. JWTs plus a cached key set need 0 AS calls per request. Rate-limit refetches on unknown kid values so forged key IDs cannot flood the AS, and keep introspection or a jti denylist for operations, such as moving money, where revocation lag is unacceptable.

    Choose token lifetimes deliberately. With local validation the access-token lifetime is your revocation lag, while refresh load runs the other way. For 1,000,000 active users:

    • 5-minute tokens: 1,000,000 ÷ 300 s ≈ 3,333 refreshes/s, and at most 5 minutes of exposure.
    • 60-minute tokens: 1,000,000 ÷ 3,600 s ≈ 278 refreshes/s, and up to an hour of exposure.

    Twelve times the load buys a window twelve times shorter. Short tokens also surface AS outages sooner: with 5-minute tokens, a 20-minute outage cuts off nearly every active user within 5 minutes.

    Protect the keys. Run the AS across zones, cache the key set with a long TTL and keep using it if jwks_uri is unreachable, and hold signing keys in a KMS or HSM with scheduled, overlapping rotation (encryption and key management).

    In multi-tenant SaaS, tenants bring their own identity providers, so validate iss per tenant (multi-tenancy and isolation, design a multi-tenant SaaS platform). AI agents acting for users are OAuth clients too, and need narrow, short-lived tokens per run (design an AI agent platform).

    Cheat sheet

    Term Remember it as
    OAuth 2.0 Delegated authorisation: limited, revocable API access without a password
    OpenID Connect Identity layer: the ID token tells the client who signed in
    Four roles Resource owner, client, authorization server, resource server
    Client types Confidential (server, keeps a secret) vs public (phone or browser)
    Default flow Code + S256 PKCE, exact redirect URI, one-time state and nonce
    Access token For the API; short-lived; introspect if opaque, validate locally if JWT
    Refresh token For the AS only; rotate it or bind it
    ID token For the client; check iss, aud, signature, exp, nonce; key on iss + sub
    No user / no keyboard Client credentials / device grant
    Gone in OAuth 2.1 Implicit, password grant, plain PKCE, query-string tokens
    Browser apps BFF with an HttpOnly cookie; tokens stay on the server
    Native apps System browser, never an embedded webview
    Sender-constrained DPoP or mTLS: copied tokens are useless

    How to say it in the interview

    “OAuth 2.0 is delegated authorisation: the user lets my app call an API with limited scopes, and my app never sees their password. With a user present I use the authorization code flow with PKCE: the browser carries only a single-use code, which my backend redeems with the verifier and its own credentials. Access tokens are short-lived, audience-restricted JWTs validated at the gateway against cached keys; refresh tokens rotate. For login I add OpenID Connect, check the ID token’s issuer, audience, expiry and nonce, and key users on issuer plus subject. SPAs go through a Backend-for-Frontend; services use client credentials.”

    What’s the difference between OAuth and OpenID Connect? OAuth gives an app an access token meant for an API, which tells the app nothing reliable about the user. OIDC adds an ID token addressed to the app, with issuer, stable subject, audience, expiry and nonce. OAuth is for API access, OIDC for sign-in.

    Why use PKCE if my client already has a secret? The secret stops an attacker redeeming a stolen code, not injecting it into their own session with your app, where your server redeems it with your secret. PKCE ties the code to the session that started the flow. RFC 9700 recommends it for every client; OAuth 2.1 requires it.

    Should access tokens be JWTs or opaque? JWTs validate locally with no AS on the hot path, but revocation waits for expiry, so keep them short. Opaque tokens revoke instantly and reveal nothing, but cost an AS call per request or cache miss. Use JWTs by default, introspection for high-risk operations.

    How does a smart TV log in? With the device grant: the TV shows a URL and a short code, you approve on your phone, and the TV polls the token endpoint every 5 seconds until tokens arrive. The AS rate-limits guesses because the code is short.

    Where should a single-page app keep its tokens? Ideally nowhere: a Backend-for-Frontend keeps them server-side and gives the browser an HttpOnly, Secure, SameSite cookie. Otherwise keep them in memory, short-lived, with rotating refresh tokens.

    Where to go next

    References and further reading

    1. RFC 6749 — The OAuth 2.0 Authorization Framework (October 2012)rfc-editor.orgThe four roles, confidential and public clients, the grant types, the 10-minute maximum code lifetime and the no-refresh-token rule for client credentials.
    2. RFC 7636 — Proof Key for Code Exchange (PKCE)rfc-editor.orgThe code interception attack, the 43–128 character verifier, the S256 transformation and the 32-byte entropy recommendation.
    3. RFC 9700 — Best Current Practice for OAuth 2.0 Security (January 2025)rfc-editor.orgExact redirect matching, PKCE for all clients, code injection, mix-up and Referer attacks, the end of implicit and password grants, sender-constrained and audience-restricted tokens, refresh token rotation.
    4. The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-16 (3 September 2026)datatracker.ietf.orgThe list of changes from OAuth 2.0, including mandatory PKCE, the removal of the plain method and of redirect_uri from the token request.
    5. OpenID Connect Core 1.0 (incorporating errata set 2)openid.netID token claims, the client’s validation steps, iss plus sub as the only stable user key, email not being unique, and signing-key rotation by kid.
    6. OpenID Connect Discovery 1.0openid.netThe /.well-known/openid-configuration document and its jwks_uri and userinfo_endpoint fields.
    7. RFC 8628 — OAuth 2.0 Device Authorization Grant (August 2019)rfc-editor.orgDevice and user codes, the 5-second default polling interval, slow_down, the base-20 user-code alphabet and the brute-force arithmetic.
    8. RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)rfc-editor.orgHow a per-request signed proof binds access and refresh tokens to a client key pair, and why it suits browser and mobile apps.
    9. RFC 10017 — OAuth 2.0 for Browser-Based Applications (BCP 212, August 2026)rfc-editor.orgThe malicious-JavaScript threat model, including silent new-token acquisition, and the BFF, token-mediating backend and browser-only patterns with BFF cookie rules.
    10. RFC 7662 — OAuth 2.0 Token Introspectionrfc-editor.orgHow a resource server asks the authorization server about an opaque token, and the freshness-versus-load trade-off of caching the answer.