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 difference between authentication and authorization in the API context. Describe two common authentication methods (JWT bearer tokens and OAuth2 Authorization Code flow) and two authorization models (role-based access control RBAC and attribute-based access control ABAC). For each, give a short example of when it is appropriate.
Sample Answer
Authentication answers "who is calling?" and happens once, at the start of a request. Authorization answers "what is this caller allowed to do?" and gets checked on every action the caller attempts, even after authentication succeeds. In an API, a request must clear both gates, in that order, before it returns data.
Two ways to authenticate
JSON Web Token (JWT) bearer tokens. The server issues a signed token containing claims (who the user is, when it expires, sometimes their role). The client sends it as Authorization: Bearer <token>, and the server verifies the signature and expiry locally, without a database lookup. This is appropriate for stateless APIs and microservices where low latency and easy horizontal scaling matter more than instant revocation, for example a mobile app calling your own backend.
OAuth 2.0 Authorization Code flow. A standardized way for one application to get delegated access to a user's resources on another service, without ever seeing the user's password. The user is redirected to the authorization server's login page, authenticates there, and the calling app receives a short-lived code it exchanges (server-side) for an access token. This is appropriate when a third-party app needs to act on a user's behalf, for example a scheduling tool asking to read someone's calendar on a different platform.
Two ways to authorize
Role-Based Access Control (RBAC). Users are assigned roles, and roles carry a fixed set of permissions. It is simple to reason about and easy to audit ("who has the admin role?"). Appropriate for systems where permissions map cleanly onto job functions, for example an internal content tool with admin, editor, and viewer roles.
Attribute-Based Access Control (ABAC). The decision is computed from attributes of the user, the resource, the action, and the environment, evaluated against a policy at request time. Appropriate when access depends on runtime context a role alone can't express, for example "an employee may view a customer record only if that customer is in their assigned region."
Worked example
Request: GET /api/invoices/482 with a bearer JWT.
- Authentication: the API verifies the JWT's signature against the issuer's public key and checks
exp(the expiry timestamp). If either check fails, the response is401 Unauthorizedand processing stops here. If both pass, the caller's identity is established, saysub: "user_9fa2",role: "billing_viewer". - Authorization under RBAC: role
billing_viewercarries the permissioninvoice:read. A naive RBAC check only asks "does this role haveinvoice:read?" and answers yes, it never asks whether invoice 482 actually belongs touser_9fa2. - Authorization under ABAC: the policy instead evaluates
action == "read" AND resource.owner_id == subject.customer_id. If invoice 482 belongs to a different customer,resource.owner_id != subject.customer_id, and the policy returns deny,403 Forbidden, even though the same user'sbilling_viewerrole would pass a plain RBAC check.
This is the concrete difference between "can this role read invoices in general" (RBAC) and "can this specific caller read this specific invoice, right now" (ABAC).
Trade-offs and pitfalls
A common mistake is treating a passed authentication check as if it were also an authorization decision, that gap is exactly how broken-authorization bugs happen. RBAC is cheap to evaluate and simple to audit, but it is coarse: it cannot express "only your own records" without an added object-level check. ABAC is expressive enough to capture exactly that, but policies are harder to test exhaustively and can hide the real rule set inside a policy engine instead of a readable role list. Many production systems use both: RBAC for coarse feature access, ABAC (or a simpler ownership check) layered on top for record-level ownership.
How would you handle authentication token lifecycle and rotation for long-lived API clients such as IoT devices? Include token issuance, refresh, rotation, revocation, offline device handling, heartbeat strategies, secure storage on device, and methods for detecting and responding to token compromise.
Sample Answer
Direct answer
Treat an Internet of Things (IoT) device like a machine-to-machine client with a materially worse threat model than a server: it can be physically accessed, it cannot always phone home, and it typically has limited secure-storage hardware. The design centers on a strong, device-bound identity established once at provisioning, short-lived operational tokens refreshed opportunistically rather than on a fixed clock, and a revocation path that still works for a device that happens to be offline right now.
Structured elaboration
Issuance
At manufacturing or provisioning time, each device gets a unique, device-bound credential, ideally a private key generated on a hardware security element or secure enclave on the device itself, so the private key material never exists outside that chip, plus a certificate or registration record tying that key to a specific device ID. This is the root of trust everything else is refreshed from; it is not the day-to-day operational token.
Refresh
The device uses its long-lived provisioning credential to periodically obtain short-lived operational access tokens, brief, signed JSON Web Tokens (JWTs) that a receiving service can verify locally against a cached signing key rather than a long-lived static credential, refreshed well before expiry during normal connectivity so a brief network blip does not strand the device without a valid token.
Rotation
Operational tokens rotate frequently, short time-to-live values. The underlying device credential itself should also be rotatable on a much slower cadence, a scheduled re-provisioning or over-the-air credential rotation, so that even the root device identity is not a forever-secret, though this is the hardest piece operationally for a fleet with unreliable connectivity.
Revocation
Maintain a device-identity-level revocation list keyed on device ID, not on individual tokens, since a compromised device should lose all access, not just one token, checked at the point a device requests a new operational token. Because operational tokens are already short-lived, revocation does not need to reach an already-issued token instantly; it only has to stop the next refresh from succeeding, which bounds the compromise window to that token's remaining lifetime.
Offline device handling
A device disconnected for an extended period will have an expired operational token by the time it reconnects. Design the re-provisioning flow to work from the device's still-valid root credential, not requiring a human to physically re-touch the device, while still checking that root credential against the revocation list on every reconnect, so an offline period never becomes a loophole that skips the revocation check.
Heartbeat strategies
Devices send periodic, low-cost heartbeat or check-in calls, distinct from full operational traffic, that double as both a liveness signal for fleet management and a natural, low-frequency point to check the device's credential against the revocation list and pull a fresh operational token, without requiring constant high-frequency calls that would drain a battery-powered device.
Secure storage on device
The root credential's private key should live in hardware-backed secure storage, a Trusted Platform Module (TPM), secure enclave, or dedicated secure element chip, that resists extraction even with physical access, since an IoT device, unlike a typical server, can plausibly end up physically in an attacker's hands. If the hardware cannot support that, at minimum encrypt credentials at rest with a key derived from device-specific hardware characteristics, understanding that is a materially weaker guarantee than true hardware-backed storage.
Detecting and responding to compromise
Baseline expected behavior per device or device class, call frequency, data volume, which endpoints it normally calls, and alert on deviation. A sensor that normally reports once an hour suddenly polling every second, or calling an endpoint it has never called before, is a strong compromise signal. On a confirmed or suspected compromise, revoke at the device-identity level immediately, and for a fleet-wide vulnerability, a shared firmware bug rather than one stolen device, be prepared to force re-provisioning of the entire affected device class.
Worked example
A fleet of smart thermostats, each provisioned with a device-bound key at the factory, sends a heartbeat every fifteen minutes and refreshes its operational token alongside it. A firmware vulnerability is discovered that allows key extraction from a physically accessed unit. The response is to revoke at the affected batch or device-ID range level, force those specific devices through re-provisioning on their next heartbeat, and treat the vulnerable firmware version itself as a compromise indicator to actively hunt for across fleet telemetry, since any device still running it is a live exposure regardless of whether it has been physically tampered with yet.
Trade-offs & pitfalls
The most common and costly mistake is baking one long-lived, static API key into every device's firmware image: cheap to build, catastrophic to rotate, since it lives inside every unit ever shipped, and revoking it revokes the entire fleet at once. Heartbeat frequency is a genuine trade-off against a battery or bandwidth-constrained device's power budget, so the revocation-check cadence is coupled to a real cost, not a free design choice. Hardware-backed secure storage adds real bill-of-materials cost per unit, so cheaper device tiers sometimes skip it and inherit a materially worse compromise story; that trade-off should be made explicitly by the product and security teams together, not defaulted into silently.
Explain OAuth 2.0 concepts relevant to APIs: scope-driven authorization, access tokens (JWT vs opaque), refresh tokens, and token revocation. Compare the trade-offs of short-lived JWTs with no introspection versus opaque tokens with centralized introspection for internal and external APIs.
Sample Answer
OAuth 2.0 access tokens carry scopes that declare exactly what the token is allowed to do, and whether the token itself is a self-contained JSON Web Token (JWT) or an opaque reference determines how fast you can shut it down early. For high-throughput internal services, prefer short-lived JWTs validated locally; for external clients or anything needing instant revocation, prefer opaque tokens backed by a centralized introspection endpoint.
Scopes: least-privilege authorization
A scope is a string like orders:read or orders:write that the client requests, the resource owner (or an admin, for machine clients) grants, and the resource server checks on every call. Keep scopes small and specific rather than one broad full_access scope, this is what limits the damage if a token leaks.
Access tokens: JWT vs opaque
| JWT (self-contained) | Opaque token | |
|---|---|---|
| Validation | Local signature check, no network call | Requires an introspection call to the authorization server |
| Revocation | Hard, the server can't unsign a token others still hold | Immediate, the authorization server just marks it invalid |
| Latency | Fast, no round trip | Adds one round trip (mitigated with a short-TTL local cache) |
| Best fit | Internal, high-throughput, latency-sensitive calls | External clients, anything needing fast, auditable revocation |
Refresh tokens
A refresh token is a longer-lived credential used to obtain a new access token without forcing the user to log in again. Issue it only to clients that can store it securely, and rotate it on every use: each refresh call returns a brand-new refresh token and invalidates the one just used. If that now-invalid refresh token is ever presented again, treat it as evidence of theft and revoke the whole token family, not just the one request.
Revocation
Two standard endpoints matter here: a revocation endpoint (RFC 7009) that lets a client or admin explicitly kill a token, and an introspection endpoint (RFC 7662) that lets a resource server ask "is this token still valid?" For self-contained JWTs, since you can't recall a token that's already out in the world, revocation in practice means short lifetimes plus an optional deny-list check for high-risk operations.
Worked example
Access token TTL is 600 seconds (10 minutes). Suppose it leaks at the 120-second mark. For a bare JWT with no introspection, there is no revocation path, so the exposure window is bounded only by the remaining lifetime: 600 - 120 = 480 seconds. For an opaque token with introspection, an admin can call the revocation endpoint the moment theft is confirmed, so the exposure window is however long detection and response take, which can be far shorter than 480 seconds, or, if detection is slow, could exceed it. The trade-off is not "which is always safer" but "which failure mode you're designing for": a bounded-but-fixed window (JWT) versus a variable-but-potentially-instant one (opaque + introspection).
Trade-offs and pitfalls
For internal microservices, short-lived signed JWTs with automated key rotation avoid the fan-out load that calling introspection on every internal request would create. For external or regulated APIs, opaque tokens with a scalable, cached introspection layer buy immediate revocation and centralized auditing. A caching layer in front of introspection helps performance but reintroduces a small revocation-lag window of its own, so keep that cache's TTL well under the access token's TTL, not equal to it. A common wrong turn is assuming "JWTs are stateless, so we don't need any server-side state", that's true for routine reads, but you still need somewhere to record and check revocation for anything sensitive enough to outlive its own trust.
Walk through the API key management lifecycle you'd put in place: issuance workflow, key metadata (owner, scopes, creation time), secure storage (hashed keys vs plaintext), rotation policies (automatic/manual), revocation propagation, and tying rate limits/auditing to keys. Describe how to discover keys across services and how to handle emergency revocation.
Sample Answer
Direct answer
An API key management lifecycle covers the key from birth to death: issue it through a
controlled workflow that captures ownership and scope up front, store only a hash of it (never
the plaintext) so a database leak does not directly hand out usable keys, rotate and revoke it
through processes that do not require redeploying the calling service, and tie every request
back to the key that made it so rate limits and audit trails are meaningful per key rather than
anonymous.
Structured elaboration
Issuance workflow. Generate keys with a cryptographically random, sufficiently long value
(not a predictable or sequential identifier), and require the requester to specify an owner (a
team or service, not just an individual, since individuals leave) and the intended scopes before
a key is minted. Return the plaintext key exactly once, at creation time, and never again,
setting the expectation from day one that "I lost my key" means "generate a new one," not
"look up the old one."
Key metadata. Persist, separately from the key material itself, an owner, a creation
timestamp, the granted scopes, a human-readable description of what the key is for, and a
last-used timestamp. This metadata is what makes the rest of the lifecycle possible: without a
recorded owner, revocation has nobody to notify; without last-used tracking, you cannot tell a
stale, unused key from an active one when deciding what is safe to clean up.
Secure storage: hashed keys, not plaintext. Store a one-way hash of the key (a fast
cryptographic hash is appropriate here, unlike password storage, since API keys are already
high-entropy random values rather than low-entropy human-chosen secrets, so the slow,
work-factor-heavy hashing used for passwords is not the load-bearing defense here the way key
length and randomness are); at request time, hash the presented key and compare against the
stored hash. This means a database compromise reveals hashes, not usable keys, mirroring the
same principle password storage uses, applied to a different kind of secret.
Rotation policies. Support both automatic rotation on a schedule (appropriate for
machine-to-machine keys with no human in the loop to notice a problem) and manual, on-demand
rotation (for a suspected leak). Rotation should support a brief overlap window where both the
old and new key remain valid, so a caller can pick up the new key without a hard cutover causing
a synchronized outage the moment the old key stops working.
Revocation propagation. Revoking a key must take effect everywhere that validates it, quickly
and consistently; a design where each service caches key-validity locally for an hour means a
revoked key can still work in some parts of the system for up to an hour after the security team
believed it was dead. Either centralize validation (every service checks a shared, low-latency
key-validation service or cache) or keep local cache TTLs short enough that the propagation delay
is an accepted, explicit part of the incident-response plan rather than a surprise.
Tying rate limits and auditing to keys. Once a request is authenticated by a specific key,
apply rate limits and log the key's identifier (not the raw key) on every request. This is what
turns "someone is abusing our API" into "key k_a1b2... (owned by Acme Corp, integration
service) is abusing our API," which is the difference between a vague alert and an actionable
one.
Discovering keys across services. In any organization beyond a handful of services, keys
proliferate faster than any single team tracks manually; maintain a central inventory (the
metadata store described above, exposed through a dashboard or API) that every service's
key-issuance path writes to, so "list every key that has ever been granted the billing:write
scope" is a query, not an archaeology project during an incident.
Emergency revocation. For a confirmed leak, the process needs to go faster than the routine
rotation path: revoke the specific key immediately (propagating per the point above), and, if the
leaked key's blast radius is unclear, treat the associated owner's other keys and any resources
the leaked key could reach as suspect until proven otherwise, since attackers who obtain one
credential frequently pivot to look for others nearby.
Worked example
A key k_live_9f8e... is issued to a partner integration with scope orders:read. Its stored
record: {owner: "Acme Corp integration", scopes: ["orders:read"], created: "2026-01-15", last_used: "2026-08-09", hash: "<stored hash, not the key>"}. Only the SHA-256 hash of the key
is stored server-side; the plaintext was shown once, at creation. On 2026-08-09, this key is
found committed to a public repository. Emergency revocation: the key is immediately marked
revoked in the central store; every validating service, whether it checks a shared low-latency
cache or the store directly, rejects it on the next request within the accepted propagation
window; a new key is issued to the same owner with the same scope so the partner integration can
resume without a scope-approval round trip; and the access log for k_live_9f8e... is pulled to
determine whether any orders:read calls occurred from an unexpected source between when the
leak likely occurred (the commit's timestamp) and when it was revoked, which is the concrete
answer to "was this actually exploited."
Trade-offs and pitfalls
- Fast hashing is the right choice for API keys specifically because the key itself is already
high-entropy, unlike a human-chosen password; applying slow, password-style hashing here adds
latency to every authenticated request for no real security benefit, since the attack this
defends against (an attacker guessing the key by brute force) is already infeasible given the
key's own randomness. - A short cache TTL for key validation trades a small amount of extra load on the central
validation path for a much shorter revocation-propagation window; a design that instead
caches key validity for hours to reduce load quietly turns every emergency revocation into a
slower, less certain process than the team believes it to be. - Common wrong turn: treating "the key still works" as proof it has not leaked. Attackers
frequently use a leaked credential quietly rather than immediately and obviously, so
last-used and usage-pattern monitoring (a sudden change in call volume, source IP, or accessed
scopes for a given key) matters as much as reacting to an explicit leak report. - Common wrong turn: no supported rotation path, only revocation. If the only lifecycle event
a system supports is "kill the key," every rotation becomes an emergency-shaped event (an
outage while the new key propagates to the caller) instead of a routine, low-drama one with a
planned overlap window.
Describe a secure API error-handling policy: what to return to clients vs what to log internally, how to classify errors (client vs server), how to avoid exposing stack traces or sensitive info, and strategies for redaction, correlation IDs, and enabling safe debug modes in non-production environments.
Sample Answer
Direct answer
Return the client a minimal, stable error envelope (an error code, a short human message, and a correlation ID) and send everything else, the stack trace, request internals, and any sensitive fields, only to your own logs, redacted before they are written. Tie the two together with a correlation ID that appears in both places, so a client can report an error in seconds without you ever having to hand them internal detail.
Structured elaboration
Classifying errors: client vs server
- 4xx (client-caused): the caller sent something invalid, unauthenticated, not found, or too fast for a rate limit. It is generally safe to describe the shape of the problem precisely (which field failed validation and why), since that helps a legitimate integrator fix their own request, but never echo back internal identifiers or system state as part of that description.
- 5xx (server-caused): the failure is on your side, a bug, a downstream dependency failing, an unhandled edge case. The client gets a generic message ("something went wrong, we're looking into it") plus the correlation ID; anything more specific than that helps an attacker map your internals rather than helping a legitimate caller, who cannot fix a server bug anyway.
What to return vs what to log
- Return: a stable error code drawn from a documented catalog (e.g.
VALIDATION_FAILED,RATE_LIMITED), a short message, the correlation ID, and for 4xx errors only, field-level detail about the caller's own input. - Log: the full stack trace, the request (with sensitive fields redacted), the query or downstream call that failed, the authenticated user or service identity, and the same correlation ID.
Redaction
- Scrub sensitive fields (passwords, tokens, card numbers, national ID numbers) at the logging middleware boundary using a field-level allowlist or denylist applied centrally, not by trusting every individual call site to remember to redact. A single missed
log.info(request.body)call anywhere in the codebase defeats redaction everywhere else if redaction is not enforced structurally.
Correlation IDs
- Generate one at the edge (the gateway) if the caller did not already supply one, propagate it through every downstream call via a header, put it in every log line for that request, and put it in the client-facing error body. A client reporting "I got an error at 3:14pm" becomes a five-second log lookup instead of a guessing game about which of thousands of requests they meant.
Safe debug modes in non-production
- Verbose error output (stack traces, internal variable dumps) is off by default everywhere. If a non-production environment needs it, gate it with an environment flag read from a separate configuration source that cannot be copy-pasted into production by accident, and never enable it in any environment that carries real customer data, since "non-production" and "no real PII" (Personally Identifiable Information) are not the same guarantee unless you actively enforce it.
Worked example
A database constraint violation on POST /users because of a duplicate email:
Bad response (what NOT to send a client):
{
"error": "duplicate key value violates unique constraint \"users_email_idx\" on table \"users\": Error at db/pool.js:142 in insertUser(), stack: ..."
}
This leaks the table name, the constraint name, an internal file path, and confirms the exact database engine, none of which the client needs to fix their request.
Good response (what the client actually gets):
{
"error_code": "EMAIL_ALREADY_REGISTERED",
"message": "An account with this email already exists.",
"correlation_id": "b7f2-9a41-4e08-c113"
}
The server log for that same request, keyed by the identical correlation ID b7f2-9a41-4e08-c113, carries the full constraint name, the query, and the stack trace, available to an engineer investigating, never to the caller.
Trade-offs & pitfalls
Being too generic frustrates legitimate integrators who cannot tell why their request failed; the fix is a rich, publicly documented error-code catalog, not leaking internals. A recurring pitfall is returning the raw database error text verbatim (as in the bad example above), which leaks schema and table names for free. Another is including the full request body in an error response "for debugging convenience" when that body contains a password or token field. A subtler pitfall directly relevant to authentication: if a login endpoint returns a different error message, status code, or even response timing for "user not found" versus "wrong password," that difference lets an attacker enumerate valid accounts one guess at a time; both cases should return the same generic message and take a comparable amount of time.
Unlock Full Question Bank
Get access to all 27 API Security, Authentication and Authorization interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.