JWT & OAuth2 Interview Questions & Answers
Stateless authentication in Spring Boot — JWT structure and validation, the OAuth2 resource server, oauth2Login, OAuth2 vs OIDC, access vs refresh tokens, scopes to authorities, token revocation, and JWT security pitfalls.
A JWT (JSON Web Token) is a compact, self-contained token of three base64url parts separated by dots: header, payload (claims), and signature. The signature lets the server verify the token wasn't tampered with — without a database lookup.
eyJhbGciOiJSUzI1NiJ9 . eyJzdWIiOiJhbGljZSIsInJvbGUiOiJBRE1JTiJ9 . <signature>
header (alg) payload (sub, role, exp, ...) signature
Standard claims include sub (subject), exp (expiry), iat, iss (issuer), and aud (audience).
The payload is encoded, not encrypted — anyone can read it — so never put secrets in it.
Rule of thumb: A JWT is signed, self-describing claims (header.payload.signature); it proves integrity without server state, but it's readable, so it carries no secrets.
Because a JWT carries its own proof. The server validates the signature and expiry on each request and trusts the claims inside — no session store, no per-request DB lookup. Any node with the verification key can authenticate the request.
// each request: verify signature with issuer's public key → read claims → authorize
This enables horizontal scaling and clean microservice auth: services share the issuer's public key and validate independently. The trade-off is revocation — a self-contained token stays valid until it expires, so you keep lifetimes short.
Rule of thumb: JWTs scale because validation is local (signature + expiry, no shared session); the price is hard revocation, so keep access tokens short-lived.
Add spring-boot-starter-oauth2-resource-server, point it at the issuer, and enable
oauth2ResourceServer().jwt(). Boot fetches the issuer's public keys (JWKS) and validates the
Authorization: Bearer <jwt> on every request.
# application.yml
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://issuer.example.com/
http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(withDefaults()))
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS));
You write no token-parsing code — Spring validates signature, expiry, and issuer, and exposes the
claims as a JwtAuthenticationToken. This is the standard way to protect a REST API.
Rule of thumb: For a JWT-protected API, use the resource-server starter with an issuer-uri and
oauth2ResourceServer().jwt() — Spring does validation; you just declare authorization rules.
The resource server checks the signature against the issuer's published key (JWKS, fetched and
cached from jwks-uri/issuer-uri), the exp (not expired) and nbf/iat timing, and the
iss issuer. You can add audience or custom validators.
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer(issuer),
new JwtClaimValidator<List<String>>("aud", aud -> aud.contains("my-api"))));
return decoder;
}
Validating the signature is what makes the claims trustworthy; skipping it (or accepting alg: none) is a critical vulnerability. Validate aud so a token minted for another service can't be
replayed against yours.
Rule of thumb: Always verify signature + expiry + issuer (and ideally audience); the signature is the whole basis of trust — never accept an unsigned token.
OAuth2 is an authorization framework — it issues access tokens that grant a client permission to call APIs on a user's behalf. OpenID Connect (OIDC) is a thin authentication layer on top of OAuth2 that adds an ID token (a JWT describing who the user is).
OAuth2 → access token → "this client may call the API" (authorization)
OIDC → id token → "this is the authenticated user" (authentication)
"Log in with Google" is OIDC; calling an API with a delegated scope is OAuth2. Spring
Security's oauth2Login speaks OIDC for login, while the resource server consumes OAuth2 access
tokens.
Rule of thumb: OAuth2 = delegated authorization (access token); OIDC = authentication on top of it (ID token) — "login with X" is OIDC.
Add spring-boot-starter-oauth2-client, register the provider's client id/secret, and enable
oauth2Login(). Spring runs the full authorization-code flow — redirect to the provider, exchange
the code, fetch user info — and creates a session.
spring.security.oauth2.client.registration.google:
client-id: ${GOOGLE_ID}
client-secret: ${GOOGLE_SECRET}
scope: openid,profile,email
http.oauth2Login(withDefaults()); // adds /oauth2/authorization/google etc.
This makes the app an OAuth2 client (it logs users in via an external IdP), which is different from a resource server (it validates tokens on its own API). An app can be both.
Rule of thumb: Use oauth2Login + the client starter to delegate login to Google/GitHub/etc.; it
runs the authorization-code flow and gives you a logged-in session.
An access token is short-lived (minutes) and sent on every API call to prove authorization. A refresh token is long-lived, stored securely, and used only against the authorization server to mint new access tokens — so the user isn't forced to re-login when the access token expires.
access token → API calls, short TTL (e.g. 5–15 min), exposed often
refresh token → token endpoint only, long TTL, kept secret, revocable
Short access-token lifetimes limit the damage of a leak; the refresh token lives in a safer place (HttpOnly cookie / secure storage) and can be revoked server-side, which restores the revocation that bare JWTs lack.
Rule of thumb: Keep access tokens short and widely used; keep refresh tokens long, secret, and revocable at the token endpoint — that pairing balances scale with control.
A self-contained JWT can't be "deleted," so you add state back selectively: keep access-token TTLs
short, revoke the refresh token at the authorization server, and for immediate access-token
cut-off, check a deny-list (by token id jti) on each request.
// resource server: reject revoked tokens by jti
if (revokedTokenStore.contains(jwt.getId()))
throw new OAuth2AuthenticationException("revoked");
The deny-list only needs to hold ids until their natural expiry, so it stays small. This trades a bit of statefulness for the ability to log someone out immediately. Most apps rely on short TTLs plus refresh-token revocation and skip the deny-list.
Rule of thumb: You can't unsign a JWT — control it with short access TTLs, refresh-token revocation,
and an optional short-lived jti deny-list for instant logout.
Avoid localStorage — it's readable by any JavaScript, so an XSS flaw leaks the token. Prefer a
HttpOnly, Secure, SameSite cookie, which JS can't read; if you use cookies you reintroduce CSRF
concerns and must add CSRF protection.
localStorage → easy, but XSS-readable ✗ for sensitive tokens
HttpOnly cookie → not JS-readable (XSS-safe) ✓, but needs CSRF defense
For SPAs a common pattern is a short-lived access token held in memory plus a refresh token in an HttpOnly cookie. There's no perfectly free option — pick based on your XSS vs CSRF posture and keep lifetimes short.
Rule of thumb: Don't park long-lived tokens in localStorage; use HttpOnly cookies (with CSRF
defense) or in-memory access tokens with an HttpOnly refresh cookie.
Symmetric (HMAC, HS256) uses one shared secret to sign and verify — fine when the same service does both. Asymmetric (RSA/EC, RS256/ES256) signs with a private key and verifies with a public one, so many resource servers can validate without ever holding the signing key.
HS256 → shared secret (issuer == verifier) simple, but secret must be shared
RS256 → private signs / public (JWKS) verifies ideal for multi-service / third-party
In an OAuth2 setup the IdP signs with RS256 and publishes its public keys at a JWKS endpoint; resource servers fetch them automatically. Asymmetric is the norm for distributed systems because no secret ever leaves the issuer.
Rule of thumb: Use asymmetric (RS256 + JWKS) when more than one service or party verifies tokens; symmetric HMAC only when a single service both issues and verifies.
Choose JWTs for stateless, horizontally scaled, multi-service, or mobile/SPA scenarios where you don't want a shared session store and any node must authenticate any request. Choose sessions for classic server-rendered apps where easy server-side logout and simplicity matter more than statelessness.
JWT → scale-out APIs, microservices, mobile, no sticky sessions; weak revocation
Session → server-rendered apps, instant logout, simpler; needs shared store to scale
JWTs aren't automatically "better" — they move state to the client and complicate revocation. If you have one app and a session store, sessions are often simpler and safer.
Rule of thumb: JWTs for stateless, distributed APIs; sessions for single server-rendered apps that value simple, immediate logout — match the tool to the topology.
The classic mistakes:
- Accepting
alg: noneor letting the token dictate the algorithm — pin the expectedalg. - Not verifying the signature (treating claims as trusted because they "look right").
- Long-lived access tokens with no revocation path.
- Putting secrets/PII in the readable payload.
- Skipping
aud/isschecks, letting a token for another service be replayed. - Storing tokens in
localStorage, exposing them to XSS.
// Spring's resource server avoids most of these by validating signature, exp, and issuer for you.
Letting the framework (resource-server starter) do validation, keeping TTLs short, and checking issuer/audience eliminate the majority of real-world JWT bugs.
Rule of thumb: Pin the algorithm, always verify the signature, check iss/aud/exp, keep
payloads secret-free and tokens short-lived — and let Spring's resource server enforce it.
More Security interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.