API Security, Authentication and Authorization Questions
Controlling who can call an API, what they may do, and defending it against abuse. Covers the access-control mechanics: API keys, OAuth 2.0 flows, OpenID Connect, JWT issuance/validation, session vs. token auth, scopes/roles for fine-grained authorization, token lifetime and refresh, mutual TLS, and machine-to-machine vs. user-delegated access. Also covers the adversarial hardening view: input validation, injection and deserialization risks, broken object-level authorization (BOLA), mass assignment, secrets handling, and the OWASP API Security Top 10, plus securing data in transit, preventing enumeration/scraping, and testing APIs for vulnerabilities.
Explain the core OAuth 2.0 roles (resource owner, client, authorization server, resource server) and the common flows. For each actor below map the role and justify the flow choice:
- mobile app
- backend API
- third-party web app
- end user
Also explain when to use Authorization Code (with PKCE), Client Credentials, and when to avoid the Implicit flow.
Sample Answer
Direct answer
OAuth 2.0 defines four roles: the resource owner (the user who owns the data), the client (the
application requesting access), the authorization server (issues tokens after authenticating the
resource owner and getting their consent), and the resource server (the API that holds the data
and accepts the token). Which flow (grant type) a client uses follows directly from what kind of
client it is: a mobile app or single-page web application (SPA) is a "public" client that cannot
keep a secret, so it uses Authorization Code with PKCE (Proof Key for Code Exchange); a backend
service calling another API with no end user involved uses Client Credentials; a third-party web
app acting on behalf of a signed-in user uses Authorization Code (also with PKCE, since PKCE is
now recommended for every client type, not only public ones); and the end user is always the
resource owner, never a role that itself "chooses a flow."
Structured elaboration
Mapping each actor to its role and flow:
- Mobile app: a public client (it ships to end-user devices, so it cannot embed a secret that
stays confidential). It acts as the client role, uses Authorization Code with PKCE: the app
redirects the user to the authorization server to authenticate and consent, receives a
one-time authorization code back, then exchanges that code (plus a locally generated proof
value) for tokens directly with the authorization server. - Backend API: typically plays the resource server role (the earlier three actors send it
requests carrying a token) or the client role when it needs to call another service on its
own behalf, with no end user present, in which case it uses Client Credentials: it
authenticates directly to the authorization server with its own credential and receives an
access token representing itself, not any user. - Third-party web app: the client role, acting on behalf of a signed-in user. If it has a
confidential backend component that can hold a secret safely (a traditional server-rendered
web app), it uses Authorization Code (still layering PKCE on top, as current guidance
recommends for all clients); if it is a pure browser-side SPA with no confidential backend, it
is treated as a public client, same as the mobile app case. - End user: the resource owner. The end user does not "pick a flow"; they authenticate to the
authorization server and grant (or deny) consent, and the flow the client used determines what
they see during that step.
When to use each core flow:
- Authorization Code (with PKCE): the default choice whenever a human resource owner needs
to authenticate and grant consent, for both public and confidential clients under current
guidance. PKCE adds a locally generated secret (the code verifier) and its derived hash (the
code challenge) so that even if the authorization code itself is intercepted in transit, an
attacker cannot redeem it without also having generated the matching verifier. - Client Credentials: the machine-to-machine case, no end user in the loop at all; the client
authenticates as itself and receives a token scoped to what that client (not a user) is allowed
to do. - Implicit flow: avoid it. It returned access tokens directly in the URL fragment with no
authorization-code exchange step, which meant no client secret and no proof-of-possession check
were ever required, making tokens easier to leak (browser history, referrer headers, logs) and
easier to intercept than the Authorization Code flow's exchange step. It has been formally
deprecated in OAuth 2.1 guidance in favor of Authorization Code with PKCE, which now covers the
public-client use case the Implicit flow was originally created for, with none of its exposure.
Where to store tokens securely, since it differs meaningfully by client type. For an SPA,
there is no fully safe place to persist a token long-term in the browser: localStorage is
readable by any script on the page, which makes it directly exposed to a cross-site scripting
(XSS) vulnerability anywhere on the site; an in-memory-only access token (held in JavaScript
variables, gone on page refresh) paired with a refresh mechanism handled by a
backend-for-frontend (a thin server-side component the SPA talks to, which holds the actual
refresh token in an HttpOnly cookie the browser's own JavaScript cannot read) is the safer
current pattern. For a confidential backend client, refresh and access tokens are held
server-side, never sent to a browser at all, encrypted at rest, and scoped to that specific
client's own identity, which is a fundamentally different trust environment than a public
client's browser sandbox.
Worked example
sequenceDiagram
participant U as User
participant App as Client App
participant AS as Authorization Server
participant RS as Resource Server
App->>App: generate code_verifier, derive code_challenge
App->>AS: GET /authorize with code_challenge, client_id, redirect_uri
AS->>U: Login and consent prompt
U->>AS: Approves
AS->>App: redirect with authorization code
App->>AS: POST /token with code and code_verifier
AS->>AS: verify code_verifier matches stored code_challenge
AS->>App: access_token and refresh_token
App->>RS: API call with access_token
RS->>App: protected resource
Walking the mobile-app case through this diagram: the app (client) generates code_verifier
locally and derives code_challenge from it before the user ever sees a login screen. The
/authorize request carries only the challenge, never the verifier. After the user (resource
owner) authenticates and consents at the authorization server, the redirect back to the app
carries a short-lived authorization code, not a token. Only the app's own /token exchange,
which must present the original code_verifier, can turn that code into real tokens; an attacker
who intercepted the redirect and captured the code alone cannot complete this exchange without
also having captured the verifier, which never left the app's memory. The resource server then
accepts the resulting access token exactly like it would for a confidential client's token, since
from the resource server's point of view, a validly issued token is a validly issued token
regardless of which flow produced it.
Trade-offs and pitfalls
- PKCE protects the authorization-code exchange step specifically; it does not solve where a
public client stores the resulting tokens afterward. Those are two separate problems, which
is exactly why the token-storage question above (in-memory plus a backend-for-frontend for an
SPA) matters as its own decision, not something PKCE already covers. - Per-flow mitigations worth naming explicitly: Authorization Code with PKCE mitigates
code-interception attacks; Client Credentials should always run over a channel that itself
authenticates the client strongly (mutual TLS or a signed JWT assertion rather than a static
shared secret sent as plaintext, where the deployment's risk profile warrants it); and any flow
should always use short-lived access tokens with a separate, more tightly controlled refresh
token, so a leaked access token expires quickly on its own even if revocation is delayed. - Common wrong turn: treating "we use OAuth" as equivalent to "we made the right flow choice
for this client." A backend service using Authorization Code as if a human were involved when
no human is present adds unnecessary complexity and a dependency on an interactive login step
that has no one to complete it; Client Credentials is the correct, simpler fit. - Common wrong turn: storing an SPA's tokens in
localStoragefor developer convenience. It
works in testing and is the single most common real-world OAuth implementation mistake for
browser-based clients, precisely because it is the easiest thing to write and the failure mode
(any injected script can read every token) only shows up once there is an actual XSS
vulnerability elsewhere on the site to exploit it.
How would you design a secure refresh-token strategy for a Single Page Application (SPA) with a backend API serving 1M users? Requirements: mitigate refresh-token theft, enable revocation, minimize user friction, support refresh-token rotation and offline access. Describe storage, rotation, revocation lists or introspection, and trade-offs between stateless and stateful approaches.
Sample Answer
Direct answer
Store the refresh token in an HttpOnly, Secure, SameSite cookie, never in localStorage, where any Cross-Site Scripting (XSS) vulnerability on the page could read and exfiltrate it. Rotate the refresh token on every use, issuing a new one and invalidating the old one atomically, and treat the reuse of an already-rotated-away token as a theft signal that revokes the whole token family, not just that one token.
Structured elaboration
Storage
The refresh token lives in an HttpOnly, Secure, SameSite=Strict (or Lax, if legitimate cross-site navigation into the app is a real requirement) cookie, scoped to the specific refresh endpoint path so it is not sent on every request, shrinking its exposure to only the one call that actually needs it. The short-lived access token can live in memory (a JavaScript variable, never persisted storage) since it is replaced constantly anyway.
Rotation
Every call to the refresh endpoint issues a brand-new refresh token and invalidates the previous one, atomically, on the server. This bounds the value of a stolen refresh token to a single use before it is dead.
Revocation and theft detection
Keep a lightweight server-side record per refresh-token "family," a chain created at login where each rotation produces a new generation of the same family. If a token that has already been rotated away is presented again, that is a strong signal an attacker copied it and is now racing the legitimate user, or that the legitimate user's old token leaked somehow. The response is to revoke the entire family immediately, forcing full reauthentication, rather than trying to guess which of the two callers is the real one.
Minimizing user friction
Refresh silently in the background, using the HttpOnly cookie, proactively before the access token expires rather than only reacting to a 401 response, so the user never sees a login prompt during a normal active session. Rotation itself stays invisible to the user; only a detected theft forces a real reauthentication.
Offline access
For a browser-based SPA, "offline" mainly means the cookie persists across tab close and reopen for a defined session lifetime, with a sliding absolute cap (re-require login after some maximum session age regardless of activity), so a stolen-but-undetected token cannot live forever. This differs from a native or mobile offline-access story, which would rely on device-bound secure storage and a longer-lived credential instead.
Stateless vs stateful trade-off
A pure stateless JWT (JSON Web Token) refresh token, with no server-side record at all, cannot support real-time revocation or reuse detection, since there is nothing to check against. This design deliberately keeps some server-side state, the token-family record, specifically to make revocation and theft detection possible. The token-family record described above is effectively a lightweight, purpose-built revocation list, one row per active family rather than one row per token. The alternative pattern is introspection, where every refresh call is validated by a live call to a central token-issuing service instead of a local record lookup; introspection centralizes the decision even further and makes revocation instantaneous everywhere at once, but adds a network round trip to every single refresh and makes the introspection service itself a shared, latency-sensitive dependency at 1M-user scale. The token-family approach above is the better fit here because refreshes are already infrequent relative to access-token use, so the extra state stays small, cheap, and does not need the always-on network dependency introspection would add.
Worked example (token-family rotation and theft detection)
sequenceDiagram
participant User as Legit browser
participant Attacker
participant API as Backend API
User->>API: Login
API-->>User: Refresh token (family F1, gen 0)
User->>API: Refresh using gen 0
API-->>User: New refresh token (gen 1)
Note over API: gen 0 marked used, invalid
Attacker->>API: Refresh using stolen gen 0
API-->>Attacker: Reject: gen 0 already used
Note over API: Reuse of a rotated-away token detected, revoke entire family F1
API-->>User: Next refresh attempt also fails, forces full re-login
Whichever side, the legitimate user or the attacker, presents the already-rotated-away token second is treated identically: the whole family is revoked, and the legitimate user is forced to re-authenticate. This is a deliberate trade-off (a real user occasionally gets logged out due to a race, see pitfalls below) in exchange for reliably catching theft without guessing.
Trade-offs & pitfalls
Rotating on every call sounds airtight but can break under legitimate concurrency, two open tabs from the same real user issuing near-simultaneous refresh calls can look identical to an attack. Handle this with a short grace window that accepts the immediately-prior token once, rather than an absolute single-use rule that locks out a legitimate second tab. Storing the refresh token anywhere JavaScript can read it, localStorage or sessionStorage, defeats this entire design regardless of how good the rotation logic is, since an XSS vulnerability bypasses all of it in one step. At 1M users, the token-family store needs to be a fast, horizontally scalable keyed cache, not a heavy relational table scanned on every refresh call.
That is every published API Security, Authentication and Authorization question for Mobile Developer so far. Browse the other topics in this category, or practice this one interactively.