Skip to content

JWT & OAuth2 Interview Questions & Answers

14 questions Updated 2026-06-26 Share:

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.

Read the in-depth guideJWT and OAuth2 in Spring Boot, Explained(opens in new tab)
14 of 14

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.

The standard, most secure flow for web apps. The user is redirected to the authorization server to log in and consent; it returns a short-lived code to the app's redirect URI; the app exchanges that code (server-to-server, with its client secret) for tokens.

1. app → redirect user → /authorize (provider login + consent)
2. provider → redirect back with ?code=...
3. app  → POST /token (code + client_secret)  → access + id (+ refresh) tokens

The code-for-token exchange happens on the back channel, so tokens never pass through the browser URL. Public clients (SPAs/mobile) add PKCE to protect the exchange without a secret. Spring oauth2Login implements all of this.

Rule of thumb: The authorization-code flow keeps tokens off the front channel via a one-time code exchanged server-side — use it (with PKCE for public clients) instead of the deprecated implicit flow.

By default the resource server maps each entry of the scope/scp claim to an authority prefixed with SCOPE_. You then authorize on those, or supply a JwtAuthenticationConverter to map your custom claims (e.g. roles) instead.

auth.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin");

// map a custom "roles" claim to ROLE_ authorities:
@Bean
JwtAuthenticationConverter jwtConverter() {
    var roles = new JwtGrantedAuthoritiesConverter();
    roles.setAuthoritiesClaimName("roles");
    roles.setAuthorityPrefix("ROLE_");
    var conv = new JwtAuthenticationConverter();
    conv.setJwtGrantedAuthoritiesConverter(roles);
    return conv;
}

So a token with scope: "admin" satisfies hasAuthority("SCOPE_admin"). Customize the converter when your IdP encodes permissions under a different claim.

Rule of thumb: Scopes become SCOPE_* authorities automatically; use a JwtAuthenticationConverter to map custom claims like roles to the authorities your rules expect.

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: none or letting the token dictate the algorithm — pin the expected alg.
  • 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/iss checks, 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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel