Loading...
Loading...
Backend interview guide
Authentication, authorization, JWT, password hashing and securing endpoints. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
A 403 confirms that the order exists, so an attacker could list which ids are real and how many orders the shop has. A 404 with the same body as a missing order reveals nothing. 403 is for things a user knows exist but may not do, like a customer trying to ship.
Lesson: Owner checks: a customer sees only their own ordersThey are URLs for the signed-in user's own data, like /api/v1/me/orders. The server takes the customer id from the token, so there is no id to change. The request records have no customerId field, and unknown fields are rejected, so a client cannot place orders for someone else.
No. They make ids harder to guess, but ids leak through links, logs, emails and shared screenshots. They can be an extra layer, but the owner check on every request is what actually protects the data.
Lesson: Owner checks: a customer sees only their own ordersThe AuthenticationEntryPoint produces 401 and the AccessDeniedHandler produces 403. ExceptionTranslationFilter catches AccessDeniedException and calls the entry point for anonymous callers and the handler for signed-in ones; the bearer token filter calls the entry point directly for a rejected token.
RFC 6750's Bearer challenge, and I let Spring write it: BearerTokenAuthenticationEntryPoint sends Bearer (with resource_metadata) for no token and error="invalid_token" with a short reason for a rejected one, and the access-denied handler sends insufficient_scope. My classes only add the Problem Details body; the full reason stays in the log.
A small SecurityProblemWriter builds the same five fields as the global handler, in contract order, with the app's Jackson mapper and application/problem+json. The entry point and the access-denied handler both use it, and both are registered in exceptionHandling and in oauth2ResourceServer.
URL rules are the broad first gate by method and path, kept together in one SecurityFilterChain. @PreAuthorize goes on service methods that matter, or where the rule needs parameters, so the method is safe from every caller. I use both for important writes.
It blocks the access, but it reveals that the order exists. ShopLite answers 404 with the same body as a missing order. 403 stays for actions a known user may not do, like shipping.
Lesson: Practice: roles and permissionsA parameterized test matrix copied from the access table: caller, method, path, expected status, against the real app and test data, with jwt() tokens. Plus focused negative tests for ownership, such as another customer's order giving the same 404 as a missing one.
Not today: an admin has no customer profile, so letting ADMIN pass hasRole("CUSTOMER") would send admins into /me endpoints with no customer id. A hierarchy fits when higher roles really are a superset, such as STAFF and ADMIN in a back office.
An origin is the scheme, host and port of a URL. Two URLs are the same origin only when all three match exactly, with default ports filled in (80 for http, 443 for https). So localhost:3000 and localhost:8080 are different origins, and so are a domain and its sub-domain.
Cross-Origin Resource Sharing is a set of HTTP headers that lets a server relax the same-origin policy for chosen origins. The server answers with Access-Control-Allow-Origin and related headers, and the browser lets the page read the response only if those headers allow its origin.
Usually not. For a simple request the server receives it, runs it and answers; the browser then hides the response from the page. Spring does answer 403 Invalid CORS request when it sees an Origin it does not allow, but the important check is still the browser's. That is why CORS is not a way to protect server data or block writes.
It can reject unknown browser origins, and Spring does that, but it is not a security boundary for data. Any non-browser client can send any Origin value or none at all. Real protection comes from authentication and authorization; CORS only decides which browser pages may read responses on behalf of the user.
For simple requests: GET, HEAD or POST with only safe headers such as Accept, and a Content-Type that is text/plain, application/x-www-form-urlencoded or multipart/form-data. These are the requests an HTML form could send before CORS existed, so servers already had to handle them.
The preflight carries no Authorization header and no cookies; the browser sends those only after the preflight says yes. If the security rules demand authentication for OPTIONS, the preflight gets 401, the browser stops, and the signed-in call never happens. In Spring Security that means CORS must be handled inside the security filter chain, before the authorization rules.
It depends on the request. A simple request reaches the server and runs, and only the response is hidden. A preflighted request that fails its preflight never sends the real request; the server only sees the OPTIONS. That is why the server log is a good way to tell the two cases apart.
@CrossOrigin is handled by Spring MVC, which runs after the security filters. A preflight carries no token, so on a protected URL security rejects it with 401 before MVC can add CORS headers. It also spreads origins over many classes; one bean with origins from configuration is easier to review.
Allowed headers are request headers the page may send, such as Authorization and Content-Type; they answer the preflight. Exposed headers are response headers JavaScript may read beyond the safelisted ones, such as Location after a 201 Created.
I check that the security chain calls .cors(...) and that a CorsConfigurationSource bean exists, because a 401 on OPTIONS means security answered before CORS. Then I repeat the preflight with curl -X OPTIONS and the Origin and Access-Control-Request-* headers until it returns 200 with the right headers.
Access-Control-Allow-Origin: * together with cookies?No. Browsers refuse a wildcard on any request sent with credentials, and Spring's CorsConfiguration throws IllegalArgumentException if you combine * with allowCredentials(true). If cookies are needed, list the exact origins.
That CORS is configured inside the Spring Security chain with http.cors(...). Login has a JSON body, so it is preflighted; if CORS is only in @CrossOrigin or addCorsMappings, security rejects the preflight with 401. Public GETs work because they are simple requests.
Read the exact console message, which names the missing or wrong header. Check the Network tab for a failed OPTIONS. Then reproduce the preflight with curl -X OPTIONS and the three headers, and compare the allowed origin with the page origin letter by letter: scheme, host, port and no trailing slash.
The same-origin policy stops a page from reading responses from another origin, because the browser attaches the user's cookies to every request. CORS lets a server relax that for named origins with Access-Control-Allow-* headers. For non-simple requests the browser first sends an OPTIONS preflight. CORS protects users in browsers; it does not protect the server from curl.
The product list is a simple GET, but login sends JSON, so it is preflighted. Most likely the preflight is rejected, often with 401 because CORS is configured in Spring MVC and not in the security chain, or Content-Type is not in the allowed headers. I would reproduce it with curl -X OPTIONS and the request headers.
Serve the frontend and the API from one origin, for example https://shoplite.example for the pages and https://shoplite.example/api/... routed by a reverse proxy to the API servers. In development a dev-server proxy does the same. Then every call is same-origin and no CORS headers are needed.
No. CORS only decides which browser pages can read responses; an attacker with curl ignores it, and Spring's 403 for unknown origins disappears when the attacker sends no Origin header. The admin endpoints are protected by authentication and the ADMIN role rule, and CORS just keeps other websites from using a signed-in admin's browser to read them.
Anything that grants access: database passwords, JWT signing keys, API keys for payment or e-mail services, private keys and cloud credentials. If someone who has the value can act as the app, it is a secret.
Lesson: Secrets in code and Git historyFirst rotate it: change the password in MySQL, update the servers' environment and restart, so the leaked value stops working. Then check the database logs for use since the push, remove it from history with git filter-repo and force-push, and add a scanner so it does not happen again.
A default means a forgotten variable goes unnoticed, and the app runs in production with a value that is printed in the repository. Failing at startup with a message naming the variable makes the mistake obvious before any user is affected.
Lesson: Secrets in code and Git historyDevelopment secrets are seen by many people and stored on laptops, so they leak more easily. If every environment has its own keys, a leaked development key cannot open production data.
Lesson: Secrets in code and Git historyIt is when the client sets fields it should not, such as role or customerId, because the request body is bound to an object that has them. I use request records with only client-controlled fields, set sensitive values in the service, and turn on fail-on-unknown-properties so extra fields answer 400.
Different messages let anyone test a list of e-mails and learn who has an account, which helps targeted guessing and phishing. Spring hides UsernameNotFoundException by default and runs a dummy BCrypt check, so the message and the timing are the same.
I call every endpoint as a guest, a customer and an admin, save the real responses including errors, and check each field against the contract and against who should see it. Anything named like a password, token or hash, internal flags and other users' data are removed; partial data like card numbers is masked.
Lesson: Leaking data through entities and error messagesIt is broken access control where an endpoint uses an id from the request, such as /orders/14, without checking that the caller owns that object. A signed-in user changes the id and reads or changes someone else's data.
403 confirms that the order exists, which leaks information and helps someone map the data. 404 looks exactly like a missing order. For admin-only actions such as ship I still use 403, because there the URL itself is not allowed.
Lesson: Missing ownership checks (IDOR)No. UUIDs make ids hard to guess, but ids leak through shared links, screenshots and logs, and insiders know them. They are a small extra layer; the owner check is still needed on every endpoint.
Lesson: Missing ownership checks (IDOR)With negative integration tests: @SpringBootTest plus MockMvc and jwt() from Spring Security Test to sign in as customer A, then request customer B's resources and expect 404. For writes I also check the database to prove nothing changed.
Brute force guesses many passwords for an account. Credential stuffing tries e-mail and password pairs leaked from other sites, betting on password reuse. Both are slowed by limiting failed logins per account and per IP.
Lesson: Brute-force login and account lockoutI make the lock temporary, for example 15 minutes, never permanent. I add a per-IP limit so one person cannot lock many accounts cheaply, and I log blocks so operations can see an attack. For high-value accounts I would add a CAPTCHA or second factor instead of a long lock.
Lesson: Brute-force login and account lockoutBoth have a place. A gateway or load balancer can drop floods cheaply before they reach Java. The app knows about accounts, so per-account login limits belong there. With several app instances the counters must be in a shared store such as Redis.
Lesson: Brute-force login and account lockoutWith several instances each counts separately, so the limit is multiplied. Counters vanish on restart. A guesser spraying random e-mails grows the map, so entries must expire. A shared store with time-to-live entries solves all three.
Lesson: Brute-force login and account lockoutBy accident: a record's generated toString() printed in a debug line, a request-logging filter that includes bodies and headers, or an exception message that contains the input. That is why I override toString() on records with secrets and do not log bodies.
Successful and failed logins, lockouts, 401s for missing or invalid tokens and 403 access denials, with a fixed event name, the user id or masked e-mail and the IP. Without them nobody notices a guessing attack or can investigate one later.
Lesson: Logging sensitive dataOperations need to see patterns, such as one e-mail failing from many IPs, and a***@example.com with the IP is usually enough. When the user is known I log the id instead, which is even safer.
Treat it as leaked: if it is still valid, revoke it or rotate the signing key, remove the code that logged it, delete or restrict those log entries, and check the logs for use of that token.
Lesson: Logging sensitive datalog4j-core 2.0-beta9 to 2.14.1 interpreted special lookup text inside logged messages and could load code from a remote server, so logging a user-supplied value let an attacker run code. It was fixed in 2.15.0 and hardened in 2.16 and 2.17, and it showed how one transitive library can expose millions of apps.
Lesson: Keeping dependencies up to datemvn dependency:tree, with -Dincludes to search for one group. For updates, mvn versions:display-dependency-updates and display-parent-updates, ignoring milestones and release candidates.
Not by default. I check whether the vulnerable code is reachable and whether a fixed version exists. Usually the safe answer is to upgrade; if I suppress it, I write down why and set a date to look again.
Lesson: Keeping dependencies up to dateI go through three questions: what goes out (responses and errors), who may touch it (every id and its owner check, login limits), and what is written down (config, Git, logs, pom). Each finding gets a place, a consequence, a fix and a risk score.
Lesson: Practice: security code reviewI score impact times likelihood. An IDOR on a write endpoint is the worst: anyone signed in can change other people's data by changing a number. Something that leaks only if a developer adds a debug line comes later, but still gets fixed.
Lesson: Practice: security code reviewMost bugs here are missing refusals. A test that signs in as customer A and expects 404 for customer B's order fails the moment someone removes the owner check, so the review's result is kept by the build.
Lesson: Practice: security code reviewPinned versions of Spring-managed libraries, new libraries without a clear reason, old versions with known CVEs, and that the dependency scan still runs. Boot's parent version should be recent.
Lesson: Practice: security code review@SecurityRequirement and an empty @SecurityRequirements?@SecurityRequirement(name = "bearer-jwt") adds a requirement to one operation, which you need when there is no default. An empty @SecurityRequirements writes security: [] and makes an operation public, which you need when there is a default.
No. They describe the API for readers and tools. Spring Security's filter chain enforces the rules, so I test that the docs match it: an ApiDocsIT checks that public operations have an empty security list and secured ones list 401 and 403.
For ShopLite, no: the docs are a map of every endpoint and error, which helps attackers more than users. They are on in dev, switched off in prod with springdoc.api-docs.enabled=false, and where a server needs them I would restrict the docs URLs to admins.
GET /api/v1/orders/14 in your API.The bearer token filter reads the header and the JwtDecoder checks the HS256 signature, expiry and issuer; the roles claim becomes ROLE_CUSTOMER. The URL rule for one order allows customers and admins. Then the service loads order 14 and checks that it belongs to the customer id from the token; it is someone else's, so the answer is 404.
Lesson: Practice: Spring Security reviewMy URL rules list every endpoint, most specific first, and end with anyRequest().denyAll(). A new endpoint is refused until I add a rule, and the access-matrix test shows what changed.
A refresh token is a random string stored only as a SHA-256 hash, valid for 7 days and usable once: each refresh gives a new pair. If an old one is used again, I revoke its whole family, so the thief and the real client both have to sign in again.
Lesson: Practice: Spring Security reviewA shared store for the login counters so the limits work on several servers, a second factor for admins, and a short block-list for access tokens so logout ends them at once. They are already listed as accepted risks in SECURITY.md.
Lesson: Practice: Spring Security reviewEach lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson