Identity, Authentication, and Access Management Questions
Designing and operating identity and access control systems. Covers authentication protocols and standards (OAuth, SAML, OIDC, MFA), authorization models (RBAC, ABAC), identity lifecycle and privilege management, IAM architecture and automation, and access control across cloud and on-premises environments. The 'who can do what' control plane, distinct from cryptographic key management.
Design a prevention and detection pipeline to avoid hard-coded credentials in source code repositories. Include pre-commit hooks, static analysis rules, CI secret scanning, automated creation and rotation of secrets when leaks are detected, and developer education. Mention specific tools and how you'd integrate the pipeline with GitHub/GitLab.
Sample Answer
Direct answer
Prevent hard-coded credentials with layered defenses at every stage a commit can pass through: a fast local pre-commit hook that catches most leaks before they ever leave the developer's machine, a server-side scan on every push and pull request that catches what the local hook missed or was bypassed, and an automated response (rotate the leaked credential, not just delete the line of code) for the rare leak that gets through both. Developer education matters because the hooks and scanners are the safety net, not the primary control; the primary control is developers reaching for a secrets manager reference instead of a literal string in the first place.
Structured elaboration
Layer 1: pre-commit hooks (local, fastest, first line of defense). A pre-commit hook installed via the pre-commit framework runs a secret-detection tool such as Gitleaks or detect-secrets (originally built at Yelp) against the staged diff before git commit completes. This layer is deliberately fast (only the staged changes, not the full history) and runs entirely on the developer's machine, so it catches a leak at the earliest and cheapest possible point, seconds after it was typed, before it is even committed locally. Its weakness is that it is opt-in per developer unless enforced by tooling: a developer can skip it with --no-verify, so it cannot be the only layer.
Layer 2: static analysis rules and CI secret scanning (server-side, cannot be bypassed by an individual developer). Every push and pull request triggers a server-side scan that runs regardless of whether the local hook ran. Two complementary mechanisms belong here:
- Native platform scanning: GitHub's secret scanning detects known secret formats (cloud provider keys, common API token patterns) automatically once enabled on a repository or organization, and GitHub's push protection goes a step further by blocking the push itself in real time when a recognized secret pattern is detected, before it ever lands in the remote's history at all. GitLab's equivalent is Secret Detection, run as a CI/CD job (via GitLab's built-in
Secret-Detectiontemplate) against each merge request's diff. - Custom static analysis: because native scanners only recognize well-known secret formats, a supplementary CI job running Gitleaks or TruffleHog with a custom rule set (regexes tuned to the organization's own internal token formats, such as an internal service's API key prefix) catches what the native scanner does not know to look for. This is the "static analysis rules" the question asks for: a maintained, versioned rule set specific to the organization, not just the generic patterns a vendor ships by default.
- Both are wired as a required CI status check, so a pull request or merge request cannot merge until the scan passes, which is what makes this layer effective even against a developer who bypassed the local hook.
Layer 3: automated response when a leak is detected. Detection alone is insufficient if the response is "someone deletes the line and force-pushes"; the credential itself is still valid and anyone who cloned the repository before the fix (including in CI logs, forks, or cached pipelines) still has it. The response has to be:
- Immediately revoke or rotate the actual credential at its issuing system (the cloud provider's Identity and Access Management (IAM) console or API, the database, the third-party service), automated wherever the issuing service exposes an API for it, because deleting the leaked line from the repository does nothing to the credential's validity.
- Purge the credential from git history (using a tool such as
git filter-repoor the BFG Repo-Cleaner) as a hygiene follow-up, done after rotation, never as a substitute for it, since a force-pushed history rewrite does not retroactively invalidate a secret anyone already copied. - Open an incident ticket and notify the owning team and security, with the detection tool's finding (file, line, commit, timestamp) attached so the rotation can be scoped precisely to the actual exposed value.
- For public repositories specifically, some cloud providers partner directly with GitHub's secret scanning to receive an automatic notification (and in some cases auto-revoke) when their own token format is detected in a public repo, which is a genuine defense-in-depth layer beyond what the organization's own pipeline controls.
Layer 4: developer education. The hooks and scanners above are a safety net for when the primary practice fails; the primary practice is developers never reaching for a literal credential string to begin with. Onboarding should cover concretely what counts as a secret (not just passwords and API keys, but connection strings, private key files, and webhook signing secrets), the sanctioned alternative (reference a secrets manager or environment variable injected at runtime, never a hard-coded value), and a walkthrough of exactly what happens when the pipeline catches something, so a false sense of "the scanner will catch it anyway" doesn't erode care at the point of typing the code.
Worked example
flowchart TD
Dev["Developer writes code"] --> Pre["Pre-commit hook: Gitleaks / detect-secrets on staged diff"]
Pre -- "clean" --> Push["git push"]
Pre -- "secret found" --> Fix1["Blocked locally, developer fixes before commit"]
Push --> Native["Native scan: GitHub secret scanning + push protection, or GitLab Secret Detection"]
Native -- "known secret pattern" --> Block["Push/merge blocked in real time"]
Native -- "clean" --> CI["CI job: Gitleaks/TruffleHog with custom org rule set"]
CI -- "required status check fails" --> Block2["Merge request blocked until fixed"]
CI -- "passes" --> Merge["Merge allowed"]
Block --> Response["Automated response: rotate credential, purge history, open incident"]
Block2 --> Response
For example, a developer accidentally commits a database connection string containing a plaintext password into a feature branch and opens a pull request on GitHub. The local pre-commit hook was skipped (the developer used --no-verify to save time). GitHub's push protection does not recognize this particular connection-string format (it is not one of the well-known formats it ships detectors for), so the push succeeds, but the required CI status check running Gitleaks with the organization's custom regex for its own connection-string format flags it and fails the check, blocking the merge. The automated response pipeline opens an incident, rotates the database password immediately via the database's own credential-rotation API, and only after rotation does a follow-up task rewrite the branch's history to remove the string, because the branch had not yet merged to a shared history other clones depended on.
Trade-offs and pitfalls
- Relying on a single layer is the most common mistake. A team that only enables native platform scanning will miss any secret format the vendor does not recognize (many internal or third-party token formats), which is exactly why a maintained custom rule set is a required layer, not a nice-to-have.
- History purges without rotation give a false sense of remediation. Removing a line from git history and force-pushing does not invalidate the credential; anyone who already cloned, forked, or has it cached in a CI log still has a working secret. Rotation has to happen first and is the only step that actually closes the exposure.
- Local hooks are trivially bypassable (
--no-verify) and cannot be the sole control, which is exactly why the required server-side CI status check exists: it cannot be skipped by an individual developer's local git configuration. - False positives erode trust in the pipeline. An overly broad custom regex that flags test fixtures or example config files as leaks trains developers to treat scanner failures as noise to work around rather than signal to act on; rule sets need an allowlist mechanism (per-file or per-pattern) for genuine false positives, reviewed periodically rather than left to accumulate silently.
- Public-repository auto-revocation partnerships (where a cloud provider is notified directly by the hosting platform) are a real defense-in-depth layer but are provider-and-platform-specific, so the organization's own rotation automation still has to be the primary mechanism, not a fallback assumption that a third party will always catch it.
Explain why one-way functions matter for password storage and authentication protocols. Contrast the naive approach of storing H(password) with using a proper password-based key derivation function (PBKDF) that includes salts and computational cost (iterations/work factor). What attacks are mitigated by PBKDFs?
Sample Answer
Direct answer
A one-way function is math that is easy to compute in one direction and computationally impractical to reverse: given a password, computing its hash is fast; given only the hash, recovering the original password is not feasible by working backward through the math, the attacker's only real option is to guess candidate passwords and check them, one by one, against the hash. That property is exactly why passwords are never stored as plaintext, but a bare hash, H(password), is not enough on its own: a proper password-based key derivation function (PBKDF) adds two more ingredients, a random salt unique to each user and a deliberately tunable computational cost, that together make the guess-and-check process an attacker is forced into dramatically slower and far less rewarding at scale.
Structured elaboration
Why one-way functions matter, in plain terms. Think of a cryptographic hash function as a one-way blender: you can put a password in and get a fixed-length scrambled result out very easily, but there is no "un-blend" operation. This matters for authentication specifically because the server needs to be able to check a password without ever needing to know it in a recoverable form, if the database is ever stolen, whether through a breach, an insider, or a misconfigured backup, whoever has it should not be able to simply read out everyone's actual password. A one-way function is what makes that possible: the server stores only the blended output, and checking a login attempt means blending the submitted password the same way and comparing the two blended results, never comparing plaintext to plaintext.
Why bare H(password) is still not enough. Suppose the server just computes a single, fast, general-purpose hash (something like a plain SHA-256) of the password and stores that. Two problems appear immediately. First, the same password always produces the exact same hash, so if two users happen to choose the same password, an attacker who has the leaked database instantly knows their hashes match, a real and exploitable signal, and worse, an attacker can precompute a giant table mapping common passwords to their hashes once, then use that same table against every account in every leaked database that used the same bare hashing approach, an attack class called a rainbow table. Second, and more damagingly, general-purpose hash functions are deliberately built to be fast, because they are also used for things like checking file integrity, where speed is a feature. Speed is the opposite of what you want for password storage: a fast hash means an attacker with modern hardware can try billions of candidate passwords per second against a stolen hash, turning "recover the password" back into a fully practical attack even though the function is technically one-way.
What a proper PBKDF adds. A password-based key derivation function (a purpose-built, standardized password-hashing construction such as PBKDF2, bcrypt, scrypt, or the modern default, Argon2) takes the same one-way property and adds two deliberate design choices on top of it. A random, unique salt is generated per user and mixed into the computation, so identical passwords produce completely different stored outputs across different accounts; this alone defeats precomputed rainbow tables, since an attacker would need a separate precomputed table for every possible salt value, which is computationally infeasible at scale. A tunable computational cost (an iteration count, a work factor, or for memory-hard functions like Argon2, a required amount of RAM as well as time) deliberately makes each individual guess slow, on the order of tens to hundreds of milliseconds rather than microseconds, which is imperceptible to a real user logging in once but devastating to an attacker who needs to try billions of guesses.
What attacks a PBKDF actually mitigates. Four specific, named attack classes: (1) rainbow-table attacks, defeated by the per-user salt, since a precomputed table would need to cover every salt value separately; (2) cross-account password-reuse detection by an attacker, also defeated by the salt, since identical passwords no longer produce identical stored hashes; (3) brute-force and dictionary attacks against a stolen hash database, slowed by orders of magnitude by the tunable cost parameter, turning a feasible mass-cracking effort into an impractically slow one; and (4), for memory-hard functions specifically (Argon2, scrypt), attacks that rely on cheap, massively parallel hardware (GPUs and purpose-built ASICs), which are excellent at raw computation but comparatively poor at providing large amounts of fast memory per parallel unit, mitigated by requiring real memory, not just time, per guess.
Worked example
Contrast two concrete scenarios using round, illustrative numbers to make the reasoning traceable, not a claimed benchmark of any specific real system.
Bare H(password) scenario: assume a fast general-purpose hash can be computed at roughly 1 billion hashes per second on commodity GPU hardware, a realistic order of magnitude for such functions. Against a stolen database of unsalted hashes, an attacker checking a dictionary of 100 million common passwords against every account at once (since no salt means one computed hash can be checked against every stored hash simultaneously) finishes that entire dictionary in a small fraction of a second: 100,000,000 / 1,000,000,000 = 0.1 seconds. Every account using one of those 100 million common passwords is recovered essentially immediately.
Proper PBKDF scenario: assume Argon2id is configured so that one hash computation takes 200 milliseconds (0.2 seconds) and requires enough memory that GPU parallelism does not help much, a realistic configuration for a login endpoint's latency budget. First, the salt means the same 100-million-word dictionary attack must now be repeated separately for every single account, since a precomputed guess against one account's salt does not carry over to another account's differently-salted hash. Second, even against one single account, running that same 100-million-word dictionary at 0.2 seconds per guess takes 100,000,000 x 0.2 seconds = 20,000,000 seconds, which is about 231 days of continuous computation for one account alone (20,000,000 / 86,400 seconds per day = 231.5 days). Multiply that by however many accounts the attacker wants to individually target, and the practical difference between "0.1 seconds for the whole database" and "231-plus days per account" is the entire point of using a proper PBKDF instead of a bare one-way hash.
Trade-offs and pitfalls
- The most common misconception is treating "it's a one-way function" as sufficient on its own; one-wayness alone stops direct reversal but does nothing to stop an attacker from simply guessing candidates and checking them quickly, which is exactly the gap salting and cost parameters exist to close.
- A salt does not need to be secret, only unique; it is typically stored right alongside the hash in the same database row, and its security value comes entirely from defeating precomputation and cross-account comparison, not from being hidden.
- Cost parameters that are never revisited become weaker over time in relative terms, since attacker hardware keeps improving; a PBKDF configuration chosen once at launch and never re-benchmarked against current hardware capability quietly loses ground every year.
- A memory-hard function like Argon2 specifically targets GPU/ASIC-based cracking, which a purely CPU-time-based cost parameter (like classic PBKDF2's iteration count) does not resist nearly as well, which is why memory-hardness, not just iteration count, is the more modern design consideration.
- None of this replaces rate limiting on the live login endpoint; a PBKDF makes offline cracking of a stolen database slow, but an attacker simply guessing passwords through the normal login form, one attempt at a time, needs a separate defense (per-account and per-IP throttling), which a strong hashing scheme does not provide by itself.
Design a defense-in-depth system against advanced credential stuffing using rate-limiting, ML-driven risk scoring, device fingerprinting, progressive profiling, and CAPTCHA. Explain which features you'd use for the ML model, data sources, how to serve real-time risk scores (latency and scaling), and strategies to reduce false positives and adversarial manipulation of the ML model.
Sample Answer
Direct answer
A defense-in-depth system against advanced credential stuffing layers five complementary controls rather than relying on any one: rate limiting as the cheap, always-on first line; device fingerprinting and progressive profiling to build a behavioral picture of each login attempt; a machine learning (ML) risk-scoring model that turns that picture into a single actionable number; and CAPTCHA reserved as a costly, user-visible challenge triggered only when the risk score actually warrants it, not shown by default. The design work that separates a working system from a plausible-looking one is in three places: the feature set and data sources the model actually trains on, a serving architecture that can return a risk score inside the login request's latency budget at scale, and explicit safeguards against both false positives (locking out real users) and adversarial manipulation of the model itself.
Structured elaboration
Rate limiting. The first, cheapest layer: per-account and per-source-IP limits on login attempts, with exponential backoff and account-level lockout thresholds distinct from IP-level ones (an attacker who distributes attempts across many IPs against one account, or many accounts from one IP, needs both dimensions covered independently). Rate limiting alone stops unsophisticated, high-volume stuffing but is trivially evaded by an attacker who slows down and distributes across enough source IPs and target accounts, which is exactly why the remaining layers exist.
Device fingerprinting. A composite signal built from client-observable properties (browser and OS characteristics, installed fonts, canvas/WebGL rendering fingerprints, TLS client hello characteristics, screen resolution) that, combined, produce a reasonably stable identifier for a specific browser instance without relying on a cookie the attacker could simply clear. Its value here is detecting reuse: the same fingerprint attempting logins against many different accounts in a short window is a strong stuffing signal, independent of IP address, which credential-stuffing tooling routinely rotates.
Progressive profiling. Rather than making a single go/no-go decision at login, the system accumulates a behavioral profile over time for both accounts and observed devices: typical login times, typical geographic range, typical navigation patterns after login, and how a given device's fingerprint has behaved historically (has this fingerprint been seen attempting logins against dozens of unrelated accounts before). This turns a point-in-time decision into a trend-aware one, which is what lets the system distinguish a genuinely new but legitimate device (a user's new laptop) from a device with a demonstrated pattern of credential-stuffing behavior.
ML-driven risk scoring, feature selection. The model's job is to fuse the signals above into one calibrated risk score per login attempt. A reasonable feature set:
| Feature category | Examples | Why it matters |
|---|---|---|
| Velocity | Login attempts per account/IP/fingerprint in the last 1, 10, 60 minutes | Direct stuffing signal; the single strongest predictor in most deployments |
| Device/fingerprint reputation | Fingerprint's historical distinct-account-attempt count, fingerprint age, fingerprint's prior flagged-attempt rate | Distinguishes a device farm (attacker infrastructure) from ordinary users |
| Network reputation | IP reputation feeds (known proxy/VPN/data-center ranges, prior abuse reports), autonomous system number (ASN) | Data-center and residential-proxy IP ranges disproportionately host stuffing traffic |
| Behavioral consistency | Deviation from the account's own historical login time-of-day, geolocation, and typing/mouse cadence where available | Even a correct password from an unusual behavioral context is a meaningful anomaly signal |
| Credential-specific signal | Whether the submitted username appears in known breach-corpus checks (without storing the password itself), password-reuse-detection style checks | Directly targets the mechanism of credential stuffing: reused, previously-breached credentials |
Data sources. Internal event logs (successful and failed login attempts, with timestamps, IPs, and fingerprints, retained long enough to build the velocity and historical-reputation features above), commercial IP-reputation and breach-corpus feeds (checked as a hash lookup, never storing or transmitting the actual submitted password to a third party), and the account's own historical behavioral baseline computed from its own prior legitimate sessions. A model trained purely on labeled-fraud data from other companies without any of the deployment's own behavioral baseline data tends to generalize poorly, since normal behavior differs meaningfully across products and user bases.
Serving real-time risk scores: latency and scaling. A login endpoint's acceptable added latency budget is typically tens of milliseconds, not seconds, which rules out a heavyweight model call synchronous with a live feature-store lookup spanning multiple slow data sources on every request. The standard architecture: precompute and cache the slower-changing features (fingerprint and account reputation, historical baselines) in a low-latency key-value store (updated asynchronously on a streaming or near-real-time basis, not recomputed per request), and compute only the fast, request-local features (current velocity counters, current IP/fingerprint lookup) synchronously at request time. The model itself should be a lightweight, low-latency inference target (a gradient-boosted tree ensemble or a small neural network served via an optimized runtime), not a large model requiring GPU inference in the login critical path; scaling then becomes a matter of horizontally scaling stateless inference servers behind the feature-serving layer, with the feature store itself being the component that needs the more careful scaling and caching design, since it is the shared, stateful dependency every request touches.
Reducing false positives. A model tuned purely to maximize detection rate without regard to false-positive cost will step up or block real users unacceptably often, which has a direct product and support cost. Concrete strategies: calibrate the score threshold against a labeled validation set specifically balancing detection rate against false-positive rate (not just optimizing a single aggregate accuracy metric), use the risk score to select a graduated response rather than a binary allow/deny (a low-risk score proceeds normally, a medium score triggers step-up MFA or CAPTCHA rather than an outright block, and only a high score blocks outright), and maintain an explicit allowlist/override path informed by account-specific history (a user who has always logged in from the same two devices for years should have a materially higher bar before being challenged than a brand-new account with no history at all).
Preventing adversarial manipulation of the model. Three specific risks and their mitigations: (1) an attacker probing the system to learn which features drive the score (feature-inference attacks via repeated trial logins) is mitigated by never returning the score or any component of it to the client, only the resulting action (allow/challenge/block), and by rate-limiting the number of attempts any single fingerprint or IP can make regardless of score, so probing itself is bounded; (2) an attacker deliberately building up "clean" behavioral history on throwaway accounts or devices before pivoting to stuffing (poisoning the historical-reputation features) is mitigated by weighting recent behavior less heavily than the model's static, harder-to-fake signals (velocity and credential-breach-corpus checks, which a slow-and-clean warmup period does not help an attacker defeat) and by monitoring for the specific pattern of accounts that behave innocuously for an unusually short, uniform period before a coordinated pivot; (3) model or feature drift over time as attackers adapt is mitigated by continuous monitoring of the score distribution and detection-rate metrics in production (not a train-once-and-forget model), with a scheduled retraining cadence and a canary/shadow-deployment step before any updated model fully replaces the current one in the live decision path.
Worked example
A concrete graduated-response policy, given the risk score buckets above: assign a numeric risk score in [0, 1] from the model's output. score < 0.3: allow, no additional friction. 0.3 <= score < 0.7: step up to CAPTCHA or a lightweight MFA challenge (rather than an outright block) specifically because this band represents attempts the model is not confident about either way, and a graduated challenge preserves legitimate access while adding real friction to automated stuffing. score >= 0.7: block outright and log for investigation.
Trace an actual attempt through this: a login request arrives from an IP flagged in a data-center reputation list, with a fingerprint that has attempted logins against 40 distinct accounts in the last 10 minutes (velocity feature strongly elevated), no prior history for this fingerprint (reputation feature at its worst value), and a submitted username that matches a known breach corpus. Each of these four features independently pushes the score up; combined, this attempt lands well above 0.7 and is blocked outright, without the model ever needing to see the password itself to make that call, the decision is entirely based on behavioral and contextual features, not credential correctness. Contrast with a legitimate user logging in from a new phone while traveling: IP reputation is clean, fingerprint is new but the account's own historical baseline shows occasional travel-pattern logins, and velocity is normal (one attempt, this account, this window). This attempt lands in the 0.3 to 0.7 band, a step-up MFA challenge is issued rather than a block, and the user proceeds after passing it, which is the graduated-response design doing its job of adding friction only where it is proportionate.
Trade-offs and pitfalls
- Fingerprinting has a real privacy and regulatory cost that has to be named, not assumed away: composite device fingerprints can be a re-identification vector, and depending on jurisdiction may carry data-protection obligations (retention limits, disclosure requirements) that a design has to account for explicitly, not treat as a free signal.
- A model trained to maximize raw detection rate without an explicit false-positive cost function will degrade legitimate-user experience in exactly the way that erodes trust in the whole system; the graduated-response design above exists specifically to avoid an all-or-nothing outcome that a purely accuracy-optimized model would produce.
- Never exposing the raw score or its component features to the client is a security control, not just an implementation detail; a system that returns anything more granular than allow/challenge/block leaks information an attacker can use to iteratively find the boundary of the model's blind spots.
- A model that is never retrained will decay as attackers adapt their infrastructure and behavior to what the model currently penalizes, so continuous monitoring and a scheduled retraining cadence are not optional operational hygiene, they are part of the security control itself.
- Latency-budget pressure tempts teams to skip the feature-store precomputation step and query slow data sources synchronously, which either blows the login latency budget or forces a smaller, weaker feature set under time pressure; the precompute-slow-features/compute-fast-features-live split exists specifically to avoid that trade-off being forced at the wrong layer.
Explain the principle of least privilege and provide a concrete example applying it to API permission design for an enterprise application. Identify entities involved, the minimal permissions you would grant, how you would enforce validation, and how you'd monitor and detect over-provisioned permissions.
Sample Answer
Direct answer
Least privilege means every identity, human or machine, is granted only the permissions its current task actually requires, nothing broader "just in case," and that grant is re-evaluated as the task changes rather than left in place indefinitely. Applied to API (application programming interface) permission design, it means scoping each credential or token to the exact operations and resources one endpoint needs, never issuing one broad, reusable key that happens to also work everywhere else.
Structured elaboration
Entities involved. Three distinct parties matter in almost any API call: the end user on whose behalf the request is made, the calling client or service application making the request, and any further downstream service the API itself calls to fulfill it. Least privilege has to be applied at each link in that chain, not just the outermost one; an API that carefully scopes what the end user can request but then calls a downstream service with a broad, unscoped internal credential has only solved part of the problem.
Minimal permissions. Take a concrete case: a "get order status" endpoint needs read-only access to the orders data, scoped specifically to the orders belonging to the requesting user, not to all orders and not write access to any of them. Design that as its own explicit permission, for example orders:read:self, rather than reusing a broader orders:* credential that some other, more privileged internal tool happens to already have; reuse for convenience is exactly how an unrelated endpoint quietly ends up more privileged than it needs to be.
Enforcing validation. Authorization has to be checked at the point of use, and it has to check two things, not one: that the token's scope permits this kind of operation at all, and that the specific resource being requested (this particular order ID) actually belongs to the requesting user. Scope alone is not enough, since a valid orders:read:self token still says nothing about whether order 4471 belongs to the user presenting it; a code path that checks only the scope and forgets the object-level ownership check will happily let one user read another user's order. The default should fail closed: deny unless a check explicitly passes, on every request, not only at initial login.
Monitoring and detecting over-provisioning. The useful signal is not what a credential was granted, but what it actually uses. Log which scopes are genuinely invoked over a rolling window, for example ninety days, and flag any credential holding a scope that window never exercised: a granted-but-unused broad scope is precisely the residual risk least privilege exists to eliminate. Periodic, automated comparison of granted versus actually-used permissions, with drift raised for review, turns this from a one-time design exercise into an ongoing discipline.
Worked example
Suppose an internal reporting service was originally granted orders:*, covering read, write, and delete across every order, because that was the fastest way to get it working at the time. A ninety-day audit of the API gateway's access logs for that service's credential shows the following actual call counts:
| Scope exercised | Calls over 90 days |
|---|---|
orders:read (via GET /orders) | 48,213 |
orders:write | 0 |
orders:delete | 0 |
Given that write and delete were invoked zero times across the full window, the credential is narrowed to orders:read alone, and the removed scopes are confirmed with the team that owns the reporting service before the change ships, in case an unexercised capability was intentionally held in reserve for a not-yet-built feature. This table is illustrative of the audit reasoning, not a claim about any specific real system's logs.
Trade-offs and pitfalls
The most common real-world failure is granting a broad scope simply to move faster during initial development, with the intention of "narrowing it later," a step that in practice rarely happens without an explicit process forcing it, which is exactly what the periodic use-versus-grant audit provides. A closely related pitfall is checking authorization only at the coarse scope level and skipping the object-level ownership check, which is how a technically-scoped API still lets one user access another user's specific resource. The underlying trade-off is administrative: fine-grained, per-endpoint scopes are more secure but create more distinct permissions to define, document, and keep track of, while a coarser scope model is easier to administer but concedes more of the least-privilege guarantee than it should.
In a backend service, describe the difference between authentication and authorization. Provide concrete examples showing where they differ in responsibilities (who verifies identity, who enforces permissions), and explain which component(s) in a microservices architecture (API gateway, identity provider, downstream services) should own each responsibility and why.
Sample Answer
Direct answer
Authentication (often shortened to authn) answers "who is this": it verifies a claimed identity against credentials, a token, or a certificate. Authorization (authz) answers "what is this identity allowed to do": it evaluates a policy against an identity that has already been established. In a backend service these must be two distinct steps in the request pipeline: authentication happens once to establish who is calling, and authorization is evaluated against the specific resource and action being requested, often more than once as a single request fans out across services.
Structured elaboration
The reason the two live in different places is that they have different natural owners:
- Identity provider (IdP): owns authentication issuance. It verifies credentials (password plus multi-factor authentication, or a delegated login via OAuth 2.0, OpenID Connect (OIDC), or SAML (Security Assertion Markup Language)) and issues a signed token or assertion carrying identity claims (subject id, roles, tenant id, and so on). This is a single, centralizable competency: one place that is very good at proving identity.
- API gateway: owns authentication enforcement. It is the natural chokepoint for validating a token's signature, expiration, and issuer before the request goes anywhere. A gateway can also do coarse-grained authorization, such as "is this endpoint public" or "does the caller have any valid session," but it should not evaluate business-specific permission rules, because it does not have visibility into every downstream service's domain data.
- Downstream (domain) services: own fine-grained authorization. Only the service that owns an invoice, a document, or a support ticket knows whether this identity is allowed to touch that specific record, because the decision depends on data the gateway never sees (who owns the row, what tenant it belongs to, what state it's in).
This split also explains how the different auth protocols and authorization models connect: OAuth 2.0 access tokens, OIDC ID tokens, and SAML assertions are just transport for identity claims. Those claims then feed either role-based access control (RBAC, where a roles claim maps directly to a permission set) or attribute-based access control (ABAC, where arbitrary claims like department, clearance level, or tenant id feed a policy engine's rules). Which protocol carried the claims does not change who evaluates the authorization decision; it only changes what shape the claims arrive in.
The split gets sharper, not simpler, in a cloud-native multi-tenant product. A SaaS platform typically stamps a tenant_id claim onto the identity at authentication time (issued by the IdP, validated by the gateway), but every downstream service must independently re-check that tenant_id against the resource it is about to return. Assuming the gateway "already handled" tenant isolation is one of the most common root causes behind real cross-tenant data leaks, because the gateway never saw the tenant id of the specific row three services deep.
Worked example
sequenceDiagram
participant C as Client
participant G as API Gateway
participant I as Identity Provider
participant S as Downstream Service
C->>G: Request + Bearer token
G->>I: Validate token signature and expiry
I-->>G: Token valid, claims returned
G->>S: Forward request + verified claims
S->>S: Authorize: does claims.sub own resource X?
S-->>C: 200 OK or 403 Forbidden
Trace a request to view invoice #4821:
- The client sends
GET /invoices/4821with a Bearer token. - The gateway authenticates: it validates the token's signature against the identity provider's public key and checks
exp,iss, andaud. If any check fails, it returns401 Unauthorizedbefore the request reaches any business logic. - The gateway forwards the request to
invoice-service, attaching the verified claims. invoice-serviceauthorizes: it checks thatclaims.tenant_idmatches the invoice's own tenant id, and thatclaims.rolesincludesinvoices:readorclaims.subequals the invoice's owner id. If that check fails, it returns403 Forbidden.
The two failure codes are not interchangeable, and conflating them is a very common bug: an expired or invalid token is a 401 ("we don't know who you are"), while a valid, well-known identity that simply lacks permission is a 403 ("we know exactly who you are, and the answer is no"). The reverse mistake shows up constantly in production: returning 403 for an expired token sends the client toward "ask an admin for access" instead of the actual fix, which is refreshing the token.
Trade-offs and pitfalls
- Pushing fine-grained authorization into the gateway couples shared edge infrastructure to every team's business rules, so a single team's permission change forces a redeploy of code everyone depends on. Coarse gateway-level checks are a fine optimization; they are not a substitute for the domain service's own check.
- Never assume a downstream service's authorization check is redundant because "the gateway already handles auth." Confused-deputy and tenant-isolation incidents in real multi-tenant breach postmortems trace back to exactly this assumption.
- Do not conflate authentication strength with authorization correctness. A login backed by strong multi-factor authentication says nothing about whether that specific user should see this specific record; the two questions are independent and both must be answered on every request.
Unlock Full Question Bank
Get access to all Identity, Authentication, and Access Management interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.