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.
You're building a secure webhook receiver for third-party partners. Requirements: authenticate payloads, prevent replay attacks, support retries while ensuring idempotency, scale to high volume, and allow secret rotation. Describe signing schemes (HMAC vs asymmetric), replay defenses (timestamps, unique IDs), idempotency handling and operational patterns for secret rotation.
Sample Answer
Direct answer
Sign every webhook payload so the receiver can prove it really came from the partner (a shared-secret HMAC, or an asymmetric signature if there is no shared secret), bind a timestamp and a unique event ID into that signature so a captured request cannot be replayed later or against a different event, and record processed event IDs so retried deliveries are absorbed as no-ops instead of double-processed. Handle secret rotation by accepting two valid secrets during a short overlap window instead of a hard cutover.
Structured elaboration
Signing schemes: HMAC vs asymmetric
- HMAC (Hash-based Message Authentication Code) is the common default: both sides share one secret, the sender computes a keyed hash (typically HMAC-SHA256) over the request, and the receiver recomputes the same hash and compares it. Fast, simple, and what most webhook providers (Stripe, GitHub) use.
- Asymmetric signing (the sender signs with a private key, the receiver verifies with the sender's public key) removes the need to ever share a secret at all, which matters when you cannot trust a channel to distribute a shared secret safely, or when many receivers need to verify the same sender's signature without each holding a copy of a secret that could leak. It costs more CPU per verification and adds public key distribution/rotation as its own problem.
Replay defenses
- A timestamp header, signed as part of the payload (not sent alongside unsigned), so an attacker who captures a valid request cannot replay it after altering the timestamp without invalidating the signature.
- The receiver rejects any request whose timestamp is outside a short tolerance window (a few minutes), which bounds how long a captured request stays useful even if replayed unmodified.
- A unique event ID per webhook delivery, checked against a short-lived dedup store (a cache keyed by event ID with a TTL slightly longer than the timestamp tolerance), so an identical request replayed within the tolerance window is still caught.
Idempotency for retries
- Partners retry on timeout or a 5xx response, which means the receiver will see the same event ID more than once by design, not by attack. The fix is the same dedup store used for replay defense: before doing any side effect, check whether this event ID has already been processed; if so, return success without repeating the work.
- The "already processed" check and the "mark as processed" write need to be atomic (a unique constraint in the datastore, or an atomic check-and-set in the cache) so two near-simultaneous deliveries of the same event cannot both pass the check before either marks it done.
- Respond fast: verify signature, check the dedup store, enqueue the actual business work, and return 200 immediately. Doing the real processing synchronously inside the request is what makes retries expensive and dedup races more likely under high volume.
Secret rotation
- Never a hard cutover. Accept signatures computed with either the current secret or the previous secret for a defined overlap window, and include a key identifier in the request headers so the receiver knows which secret to try first instead of testing both on every request.
- Communicate the rotation window to partners, retire the old secret only after the window closes, and treat "a partner is still signing with the retired secret past the deadline" as an operational alert, not a silent failure.
Worked example
A concrete HMAC-SHA256 signature, computed and verified exactly as shown (copy, run, and you will get the same digest since every input is pinned):
import hmac, hashlib
secret = b"whsec_5f8a3c9e2b7d4a1f6e0c8b2d9a4f7c1e"
timestamp = "1735689600"
body = b'{"event":"payment.succeeded","id":"evt_8f2a","amount":4999}'
signed_payload = timestamp.encode() + b"." + body
signature = hmac.new(secret, signed_payload, hashlib.sha256).hexdigest()
print(signature)
Output:
2e702838565a081c906544b3c8d22f6ce61ab8cedeb9babb9f7ae1245e596b14
On the receiving side, verification recomputes the same digest and compares in constant time, then applies the replay and idempotency checks:
def verify_and_should_process(headers, body, secret, seen_event_ids, now, tolerance_s=300):
timestamp = headers["X-Timestamp"]
if abs(now - int(timestamp)) > tolerance_s:
return False, "timestamp outside tolerance"
expected = hmac.new(secret, (timestamp + ".").encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, headers["X-Signature"]):
return False, "bad signature"
event_id = headers["X-Event-Id"]
if event_id in seen_event_ids:
return True, "already processed, ack without reprocessing"
seen_event_ids.add(event_id)
return True, "process"
This is not run in this answer since it depends on request-time state (now, a shared dedup store); the HMAC computation above is the part with fully pinned inputs and a verifiable output.
Trade-offs & pitfalls
Using a plain string comparison (==) instead of a constant-time comparison (hmac.compare_digest) leaks timing information an attacker can use to guess the signature byte by byte. Trusting a client-supplied timestamp that is not itself signed into the HMAC input lets an attacker pair an old, still-valid signature with a fresh timestamp. If the idempotency store is local to one server instance instead of shared, two receiver replicas behind a load balancer can each independently "not have seen" the same event and both process it. Rotating a secret without an overlap window causes a hard outage for every in-flight or slightly-delayed delivery signed with the old secret.
Compare API keys, JSON Web Tokens (JWTs), and OAuth 2.0 access tokens: describe typical use cases, security properties (revocation, statelessness, signature verification), storage considerations, and common attack vectors (theft, replay, misuse). When would you choose each approach in a modern API platform?
Sample Answer
Direct answer
These three are not fully parallel choices: an API key identifies which application is calling, a JSON Web Token (JWT) is a signed credential format, and an OAuth 2.0 access token is the output of a delegated-authorization protocol, often implemented as a JWT but not always. In practice: API keys suit low-risk, non-user-specific calls like internal tooling or simple third-party integrations; OAuth 2.0 suits anything involving delegated or user-specific access; and whether that OAuth token happens to be a JWT is a separate implementation decision about revocation versus validation speed.
Structured elaboration
| API key | JWT (used standalone) | OAuth 2.0 access token | |
|---|---|---|---|
| Typical use case | Identifying a calling app or project, simple server-to-server calls | Any self-contained signed credential, often issued by OAuth itself | Delegated access: a user or service authorizing a client to act on its behalf |
| Revocation | Instant (a lookup against the key's status), since it's just an opaque reference the server checks | Hard: valid until it expires, no built-in server-side kill switch | Depends on format chosen: opaque token is instantly revocable, JWT-formatted token inherits the JWT revocation problem |
| Statelessness | Stateful: server must look up the key on every call | Stateless: signature verification alone confirms validity, no lookup needed | Depends on format chosen |
| Signature verification | None inherently, it's just a shared secret string compared or looked up | Cryptographic signature (HMAC or asymmetric) verified against a known key | Same as JWT if JWT-formatted; introspection call if opaque |
| Storage | Often long-lived, must be stored securely by the caller (never in client-side code) | Wherever the client keeps credentials; short lifetime reduces the blast radius of poor storage | Same guidance as JWT, plus a refresh token which needs stronger protection since it's longer-lived |
| Common attack vectors | Theft (leaked in a repo or client bundle) then long-lived misuse, since keys rarely expire on their own | Theft and replay within the token's validity window; algorithm-confusion attacks against poorly implemented verifiers | Theft of the access or refresh token; misuse if scopes are too broad for what the client actually needed |
Worked example
A small SaaS platform choosing per client type: an internal nightly cron job pulling its own data uses a simple API key, since there is no user to delegate on behalf of and low value in the extra protocol overhead. A mobile app's end user logging in goes through OAuth 2.0's authorization code flow with PKCE (Proof Key for Code Exchange, a mechanism that protects clients unable to hold a secret safely), producing a short-lived JWT access token the mobile app never has to store for long. A partner's backend server integrating directly, with no end user involved, uses OAuth 2.0's client credentials grant, a machine-to-machine (M2M) flow that issues a scoped, short-lived token without any human ever logging in.
Trade-offs & pitfalls
Treating an API key as if it carries fine-grained scopes is a common mistake; most API keys are all-or-nothing per project, not per action, so a leaked key usually grants everything that key can do. Assuming "JWT" automatically means "secure" ignores real implementation failures: accepting an alg: none header, or skipping expiry validation, turns a JWT into a forgeable credential. The single most common reflex to correct in an interview is "JWT is always better than a session or an API key": stateless tokens genuinely cannot be revoked instantly without adding extra server-side state, which is exactly the property a plain API key or a stateful session gives you for free.
Explain the differences between input validation, schema/contract validation (OpenAPI/JSON Schema), and output encoding. Give concrete examples of how each prevents different attack classes such as SQL injection, XSS, and parameter pollution, and list common developer mistakes that lead to validation bypasses.
Sample Answer
Direct answer
Input validation checks that a value is well-formed for the field it fills (a phone number
looks like a phone number); schema or contract validation (commonly using OpenAPI, a
specification format for describing an API's shape, or JSON Schema, a specification for
describing the structure of JSON data) checks that an entire request or response matches the
API's declared shape (required fields present, correct types, no unexpected extra fields); output
encoding transforms data on the way out so that a value which is safe as data cannot be
reinterpreted as code by whatever consumes the output (a browser rendering HTML, a database
executing a query). They are complementary layers, not substitutes for each other, and each
blocks a different attack class.
Structured elaboration
Input validation. Confirms an individual value conforms to expected format, range, and type
before it is used: a string that should be an email address actually looks like one, a quantity
field is a positive integer, a date is a real calendar date. This is the first line of defense
against malformed or unexpected data reaching business logic, but on its own it does not
guarantee an entire request is well-formed (a required field could simply be missing) or that
data is safe wherever it eventually ends up being used.
Schema and contract validation. Validates the request or response as a whole against a
formal contract: every required field present, every field the correct type, no unrecognized
fields silently accepted. Enforcing this at the API boundary (often automatically, since an
OpenAPI or JSON Schema definition can generate a validator) catches an entire category of bug
before it reaches handler code at all, and it directly prevents mass assignment style problems,
where an unexpected extra field in a request body ("isAdmin": true tacked onto a normal
profile-update payload) gets silently accepted and bound onto an internal object because nothing
rejected the unexpected field in the first place.
Output encoding. Transforms data based on where it is being written to, so that untrusted
data embedded in an output cannot be reinterpreted as a command by whatever parses that output.
The same string needs different encoding depending on its destination: HTML-encoding before
writing into an HTML page (so <script> in a stored value renders as visible text, not as an
executed tag), parameterization (not encoding) before using a value in a SQL query, so the
database engine never treats the value as part of the query's syntax.
How each prevents a different attack class:
- SQL injection is fundamentally an output-encoding problem, even though it is often
discussed as if it were an input-validation problem: a value that passes every reasonable
input-validation rule (a legitimate-looking last name likeO'Brien) can still break a
hand-built SQL string if it is concatenated directly into the query rather than passed as a
parameter. Parameterized queries (or an ORM that parameterizes for you) are the actual fix,
since they keep the value as data and never let it become part of the query's syntax,
regardless of what characters it contains. - Cross-site scripting (XSS) is an output-encoding problem specifically for the HTML/JS
context: a stored value that was correctly input-validated as "a non-empty string under 500
characters" can still contain<script>alert(1)</script>, and only encoding at render time
(or a strict content-security policy as defense in depth) stops that string from executing as
code when rendered in a browser. - Parameter pollution (sending the same parameter name multiple times, or in an unexpected
location, to see which value a poorly-specified handler actually uses) is primarily a
schema/contract validation problem: a strict schema that specifies exactly one value is
expected per field, and rejects a request that violates that shape, closes the ambiguity that
parameter pollution exploits.
Common developer mistakes that lead to validation bypasses:
- Validating on the client side only (in JavaScript in a browser, for example) and trusting that
client-side check server-side; any client-side validation is a request the server never sees
if the caller bypasses the browser entirely. - Validating a nested or optional field's presence but not revalidating its contents once it
is inside a larger, already-validated object. - Doing input validation and then building output by string concatenation anyway (
f"SELECT * FROM users WHERE name = '{name}'"), which discards everything input validation bought you the
moment it hits the output boundary, since input validation was never the layer responsible for
safe output construction in the first place. - Allowlisting only some fields in a schema (
additionalProperties: truein JSON Schema, or the
API framework's default of silently accepting unknown fields) rather than explicitly rejecting
unrecognized fields, which reopens the mass-assignment gap schema validation is supposed to
close.
Worked example
A profile-update endpoint expects {"displayName": string, "bio": string}. An attacker sends
{"displayName": "Jane", "bio": "hi", "isAdmin": true}.
- Input validation alone (checking
displayNameandbioare non-empty strings under some
length) says nothing about theisAdminfield, since input validation as commonly implemented
checks the fields it knows about, not the absence of fields it does not. - Schema validation with
additionalProperties: falserejects the whole request outright,
becauseisAdminis not a field the schema declares, closing the mass-assignment path before
it ever reaches the handler. - Separately, if
displayNameis later rendered on a public profile page as
<h1>{displayName}</h1>with no output encoding, an attacker who sets
displayName = "<img src=x onerror=alert(1)>"(a value that legitimately passes "non-empty
string under some length") gets that payload executed in every visitor's browser; only
HTML-encoding at render time (or an equivalent templating engine that encodes by default)
closes that specific gap, and it closes it regardless of what input validation rule was or was
not applied at write time.
Trade-offs and pitfalls
- These three layers fail independently, so skipping any one leaves a real gap, not a
redundant one: a request can pass schema validation perfectly (every field the right shape)
and still carry a payload that is dangerous purely because of where it later gets rendered or
interpreted, which is exactly why output encoding cannot be replaced by "we already validated
the input." - Overly strict schema validation has a real cost. Rejecting anything not explicitly listed
in the schema (additionalProperties: false) is the right default for security, but it also
means any new field a client starts sending, even a harmless one, breaks until the schema is
updated; this is a deliberate trade of forward-compatibility for safety, worth stating rather
than treating as a free win. - Common wrong turn: encoding once, at input time, and trusting it downstream. Encoding for
the wrong destination (or encoding too early, before the value passes through another layer
that expects raw data) is a frequent source of both broken functionality and residual
vulnerability; encode as close as possible to the point of output, for the destination that
specific output is going to.
Explain how to use JSON Schema and OpenAPI validation to mitigate parameter pollution, type confusion, and injection attacks. Provide concrete schema constraints (pattern, enum, maxLength, additionalProperties:false) and explain where contract validation should run (gateway vs service) and how to handle legitimate unknown fields gracefully.
Sample Answer
Approach
Define a strict JSON Schema (or its OpenAPI equivalent) for every request body and enforce it as a validation gate before the request reaches business logic. Strict typing (type, pattern, enum) closes off type confusion, additionalProperties: false closes off parameter pollution and unexpected-field injection, and length or pattern constraints shrink the injection surface by rejecting malformed input outright instead of relying on the handler to sanitize it correctly every single time.
from jsonschema import Draft202012Validator
schema = {
"type": "object",
"properties": {
"sku": {"type": "string", "pattern": "^[A-Z]{3}-[0-9]{4}$"},
"quantity": {"type": "integer", "minimum": 1, "maximum": 100},
"shipping_method": {"type": "string", "enum": ["standard", "express"]},
"notes": {"type": "string", "maxLength": 200}
},
"required": ["sku", "quantity", "shipping_method"],
"additionalProperties": False
}
payload = {
"sku": "ABC-1234",
"quantity": 3,
"shipping_method": "teleport",
"unit_price": 0.01
}
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(payload), key=lambda e: e.path)
for e in errors:
print(f"{list(e.path)}: {e.message}")
Output (run exactly as shown, using the jsonschema package):
[]: Additional properties are not allowed ('unit_price' was unexpected)
['shipping_method']: 'teleport' is not one of ['standard', 'express']
This one validation pass caught both an attempted mass-assignment field (unit_price, which the caller has no business setting) and an invalid enum value, before the payload ever reached business logic.
Key points
patterncloses type confusion in string fields that are actually structured data. A SKU field constrained to^[A-Z]{3}-[0-9]{4}$cannot accept an object like{"$ne": null}serialized into a string position, which is the classic shape of a NoSQL-operator-injection attempt where a client sends an object where a scalar was expected.enumcloses off out-of-range or injected values in any field with a known, fixed vocabulary, so business logic never has to defensively handle an unexpected branch it did not plan for.maxLengthbounds how much attacker-controlled data can flow into a downstream operation, a log line, a query clause, a shell argument, through a single field, and helps limit a class of resource-exhaustion abuse.additionalProperties: falseis the parameter-pollution and mass-assignment control: it rejects any field the schema did not explicitly declare, so a client cannot smuggle anis_admin: trueorunit_pricefield alongside a legitimate payload and have a naive model-binder pick it up.
Where contract validation runs
The gateway performs structural validation, does this payload match the declared OpenAPI shape at all, correct types, required fields present, no undeclared properties, because that check is cheap and identical for every caller. The service still re-validates on entry as defense in depth, since not every caller necessarily transits the gateway, and the service performs the semantic validation the schema cannot express on its own, such as whether a given SKU actually exists or whether there is enough stock, which requires business-logic context a schema alone does not have.
Handling legitimate unknown fields
Do not solve "clients keep sending an extra field we did not ask for" by loosening additionalProperties to true; that reopens exactly the hole this constraint exists to close. Instead, version the schema and communicate deprecations explicitly, or if forward-compatible metadata is a genuine need, model it as its own bounded, typed sub-object (for example a metadata field with its own nested schema and its own additionalProperties: false) rather than opening the entire payload.
Complexity
Schema validation is roughly linear in payload size, since type, pattern, enum, and length checks are each a constant-time or fixed-regex operation per field, negligible next to a database round trip. It is cheap defense-in-depth, not a meaningful performance cost.
Edge cases
additionalProperties: false needs to be applied recursively at every nested level, not just the top, a strict top-level schema with a loosely typed nested object still leaks. A field like quantity sent as the string "3" instead of the number 3 needs an explicit type decision rather than accepting both, since loosening the type reopens the same type-confusion problem this design set out to close. Some validator libraries silently coerce types by default; verify your specific validator's coercion behavior, since a lenient validator can quietly defeat the protection the schema was written to provide.
Trade-offs & pitfalls
Schema validation cannot express relational or business rules, such as "quantity must not exceed available stock," so it is necessary but never sufficient on its own. Over-strict schemas without a versioning plan break legitimate API evolution the moment a client needs to add a genuinely new field.
Unlock Full Question Bank
Get access to all 20 API Security, Authentication and Authorization interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.