Back to Miscellaneous Coding
Q7Question 7 of 52

Security: OAuth2, JWT, OWASP

Key Concepts
  • OAuth2 issues a signed JWT via an authorization server; the client carries it as a Bearer token on every request
  • JWT is signed with RS256 using a private key; services verify with the public key from a JWKS endpoint, making the system stateless
  • Access tokens are short-lived (15 minutes); refresh tokens are long-lived and can be revoked server-side
  • SQL injection is prevented by parameterized queries; Spring Data JPA uses them by default
  • IDOR is prevented by scoping every query to the authenticated user's ID at the service layer
  • CSRF protection is unnecessary for stateless JWT APIs because there is no session cookie for the browser to attach
  • BCrypt password hashing is deliberately slow and salted, so a database leak does not expose real passwords

Definition

OAuth2 issues a signed, short-lived JWT the client carries on every request; services verify its signature with a public key; and each OWASP risk has a specific defense applied by default.
  • OAuth2 – an authorization framework where an authorization server issues access tokens to clients for accessing protected resources.
  • JWT (JSON Web Token) – a compact, digitally signed token carrying claims (subject, roles, expiry); signed by the authorization server with RS256 so any service can verify it using the public key.
  • Access token – short-lived (around 15 minutes); authorizes individual API requests.
  • Refresh token – long-lived; obtains new access tokens without re-prompting the user; can be revoked server-side.
  • OWASP Top 10 – the industry-standard list of web application security risks, each with a specific code-level defense.

Rajesh wants to use services inside National Bank of India. He shows his ID once at the security desk (authorization server), and they hand him a tamper-proof wristband (JWT access token) stamped with his name and permissions, valid for fifteen minutes. Every department glances at the wristband's official hologram (signature) and lets him in without calling the security desk again. When the wristband expires, he uses a renewal card (refresh token) to get a fresh one. OWASP is the bank's list of common robbery tricks, and the security team has a specific countermeasure for each.

Understanding the Concepts

What is OAuth2 and how does the token flow work?

OAuth2 is an authorization framework. The client sends credentials to an authorization server (such as Keycloak), which validates them and returns a JWT signed with RS256. The client then sends this token as a Bearer token in the Authorization header on every subsequent API request.

JAVA
package com.bank.security;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

    private final AuthService authService;

    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @PostMapping("/auth/token")
    public TokenResponse login(@RequestBody LoginRequest request) {
        // Validate credentials against BCrypt-hashed password in DB
        // Return signed JWT + refresh token
        return authService.authenticate(
            request.getUsername(), request.getPassword());
    }
}
TEXT
POST /auth/token
{"username": "rajesh", "password": "s3cure!"}

Response:
{
  "accessToken": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJyYWplc2gi...",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
  "expiresIn": 900
}

What is inside a JWT and how is it validated?

A JWT has three base64-encoded parts separated by dots: a header (algorithm), a payload (claims), and a signature. The payload carries the subject, roles, and expiry. The signature is created with the authorization server's private key and verified by any service using the public key from a JWKS endpoint.

JAVA
package com.bank.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.security.PublicKey;

public class JwtValidator {
    private final PublicKey publicKey;  // from JWKS endpoint

    public JwtValidator(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public Claims validate(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(publicKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}
TEXT
Decoded JWT payload:
{
  "sub": "rajesh",
  "roles": ["USER", "ACCOUNT_HOLDER"],
  "iss": "https://auth.nationalbank.in",
  "aud": "banking-api",
  "exp": 1730000900,
  "iat": 1730000000
}

How do access tokens and refresh tokens differ?

Access tokens are short-lived (around 15 minutes) so a stolen token has a limited window. Refresh tokens are long-lived and stored securely; they obtain new access tokens without re-prompting the user. Refresh tokens can be revoked server-side if a device is lost.

JAVA
package com.bank.security;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class TokenService {
    private static final long ACCESS_TTL_MINUTES = 15;
    private static final long REFRESH_TTL_DAYS = 30;

    public TokenPair issueTokens(String userId, String[] roles) {
        Instant now = Instant.now();

        String accessToken = buildJwt(userId, roles,
            now.plus(ACCESS_TTL_MINUTES, ChronoUnit.MINUTES));

        String refreshToken = buildOpaqueToken();
        // Store refresh token in DB with expiry and userId
        refreshTokenRepo.save(new RefreshToken(
            refreshToken, userId,
            now.plus(REFRESH_TTL_DAYS, ChronoUnit.DAYS)));

        return new TokenPair(accessToken, refreshToken);
    }

    public void revokeRefreshToken(String token) {
        refreshTokenRepo.deleteByToken(token);  // server-side revocation
    }
}

How do you prevent SQL injection?

SQL injection is prevented by parameterized queries that bind values rather than concatenating strings. Spring Data JPA uses parameterized queries by default, so repository.findByAccountId(id) is safe.

JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface AccountRepository extends CrudRepository<Account, String> {

    // SAFE: Spring Data binds the parameter, no concatenation
    Account findByAccountId(String accountId);

    // SAFE: named parameter binding
    @Query("SELECT a FROM Account a WHERE a.accountId = :id AND a.ownerId = :owner")
    Account findByIdAndOwner(String id, String owner);
}
JAVA
// WRONG: string concatenation opens SQL injection
String sql = "SELECT * FROM accounts WHERE id = '" + userInput + "'";
// If userInput = "' OR 1=1 --", the query returns ALL accounts

// CORRECT: parameterized query
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM accounts WHERE id = ?");
ps.setString(1, userInput);  // safe binding

How do you prevent IDOR (Insecure Direct Object Reference)?

IDOR happens when Rajesh changes an account ID in the URL and sees Priya's account. The defense is to scope every query to the authenticated user's ID, so a user can never read a record they do not own. This is enforced at the service layer as defense in depth.

JAVA
package com.bank.service;

import java.util.logging.Logger;

public class AccountService {
    private static final Logger LOG = Logger.getLogger(AccountService.class.getName());

    private final AccountRepository repo;

    public AccountService(AccountRepository repo) {
        this.repo = repo;
    }

    public Account getAccount(String accountId, String authenticatedUserId) {
        Account account = repo.findByIdAndOwner(accountId, authenticatedUserId);
        if (account == null) {
            LOG.warning("IDOR attempt: user " + authenticatedUserId
                + " tried to access " + accountId);
            throw new SecurityException("Access denied");
        }
        return account;
    }
}
TEXT
// Rajesh (user123) tries to access Priya's account
GET /api/accounts/ACC2001
Authorization: Bearer <rajesh-jwt>

// Service query: WHERE id='ACC2001' AND ownerId='user123'
// Returns null because Priya owns ACC2001
// Response: 403 Access Denied
// Log: IDOR attempt: user user123 tried to access ACC2001

How do you handle XSS, CSRF, and Content-Security-Policy?

Cross-site scripting (XSS) is prevented by encoding output and setting a Content-Security-Policy header. CSRF protection is unnecessary for stateless JWT APIs because there is no session cookie the browser would attach automatically.

JAVA
package com.bank.security;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;

@EnableWebSecurity
public class SecurityConfig {

    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()  // no session cookie -> CSRF not applicable
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .headers()
                .contentSecurityPolicy(
                    "default-src 'self'; script-src 'self'; style-src 'self'")
                .and()
                .xssProtection().block(true)
            .and()
            .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated();
    }
}

How do you handle password storage with BCrypt?

Passwords are never stored in plain text. BCrypt is a deliberately slow, salted hashing algorithm. Even if the database is leaked, attackers cannot reverse the hashes. The salt is embedded in the hash, so each password has a unique hash even if two users have the same password.

JAVA
package com.bank.security;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PasswordService {
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

    public String hashPassword(String rawPassword) {
        return encoder.encode(rawPassword);  // salted + slow
    }

    public boolean verifyPassword(String rawPassword, String storedHash) {
        return encoder.matches(rawPassword, storedHash);
    }

    public static void main(String[] args) {
        PasswordService svc = new PasswordService();
        String hash = svc.hashPassword("RajeshSecure123!");
        System.out.println("Stored hash: " + hash);
        System.out.println("Verify correct: " + svc.verifyPassword("RajeshSecure123!", hash));
        System.out.println("Verify wrong:   " + svc.verifyPassword("wrongPassword", hash));
    }
}
TEXT
Stored hash: $2a$12$xK7R3eZp...  (60 chars, unique per call)
Verify correct: true
Verify wrong:   false

What are common mistakes in API security?

The most common mistakes are trusting the JWT payload without verifying the signature, not scoping queries to the authenticated user, and storing passwords in plain text or with fast hashes like MD5.

JAVA
// WRONG: trusting JWT claims without signature verification
String payload = new String(Base64.getDecoder().decode(token.split("\\.")[1]));
String userId = parseJson(payload).get("sub");  // attacker can forge this!

// CORRECT: verify signature first, then extract claims
Claims claims = jwtValidator.validate(token);  // throws if tampered
String userId = claims.getSubject();
JAVA
// WRONG: MD5 for passwords (fast, no salt, easily cracked)
String hash = md5(password);  // rainbow table attack

// CORRECT: BCrypt (slow, salted, unique per user)
String hash = bCryptEncoder.encode(password);

What should a senior engineer know?

  • Defense in depth. Validate the token at the gateway and again at the service layer. Scope queries to the caller even if the gateway already authenticated them.
  • Token rotation. Rotate signing keys periodically. The JWKS endpoint publishes the current key, and services cache it with a TTL.
  • Rate limiting. Protect the /auth/token endpoint against brute-force attacks with rate limiting and account lockout after N failures.
  • Audit logging. Log every authentication and authorization decision, including denials, for compliance and forensic analysis.
  • Sensitive operations. For high-value transfers, require step-up authentication (re-enter password or OTP) even if the JWT is valid.
JAVA
// Step-up authentication for high-value transfers
public TransferResponse transfer(TransferRequest req, String jwt) {
    Claims claims = jwtValidator.validate(jwt);
    if (req.getAmount().compareTo(new java.math.BigDecimal("500000")) > 0) {
        if (!claims.get("step_up_verified", Boolean.class)) {
            throw new StepUpRequiredException(
                "Re-authenticate for transfers above 5,00,000");
        }
    }
    return transferService.execute(req);
}

Visual Overview

DIAGRAM
  OAuth2 + JWT Flow:

  Rajesh (Client)         Auth Server (Keycloak)       API Gateway        Account Service
       │                        │                         │                    │
       │── POST /auth/token ───>│                         │                    │
       │   (username, password)  │                         │                    │
       │                        │── Validate credentials  │                    │
       │                        │── Sign JWT (RS256)      │                    │
       │<── JWT + Refresh ──────│                         │                    │
       │                        │                         │                    │
       │── GET /accounts ──────────────────────────────>  │                    │
       │   Authorization: Bearer <jwt>                    │                    │
       │                        │                         │── Verify signature │
       │                        │                         │   (public key)     │
       │                        │                         │── Check expiry     │
       │                        │                         │── Forward ────────>│
       │                        │                         │                    │── Scope to user
       │<── Account data ──────────────────────────────────────────────────────│
DIAGRAM
  OWASP Defenses:

  ┌─────────────────────────┬──────────────────────────────────┐
  │ Risk                    │ Defense                          │
  ├─────────────────────────┼──────────────────────────────────┤
  │ SQL Injection            │ Parameterized queries (JPA)     │
  │ XSS                     │ Output encoding + CSP header    │
  │ CSRF                    │ Not needed (stateless JWT)      │
  │ IDOR                    │ Query scoped to authenticated   │
  │                         │ user at service layer           │
  │ Broken Authentication   │ BCrypt + short-lived tokens     │
  └─────────────────────────┴──────────────────────────────────┘

Key Takeaway

To secure a banking API I use OAuth2 for authorization and JWT for the token format. The flow starts when the client sends credentials to an authorization server like Keycloak, which validates them and returns a JWT signed with RS256. The token carries claims such as the subject, roles, and a short expiry of about fifteen minutes. On every subsequent request, the client sends the token as a Bearer token, and each service validates the signature using the public key from the JWKS endpoint.

The most important security habit is scoping every query to the authenticated user. In my banking work at the National Bank of India, the classic mistake is IDOR, where a user changes an account ID in the URL and sees someone else's account. I defend against it by always adding the owner condition to the query so a user physically cannot read a record they do not own, and I enforce this at the service layer as defense in depth.

For the other OWASP risks, I apply specific defenses. SQL injection is prevented because Spring Data JPA uses parameterized queries by default. XSS is handled with output encoding and Content-Security-Policy headers. CSRF protection is disabled for stateless JWT APIs because there is no session cookie for the browser to attach. Broken authentication is addressed with BCrypt password hashing, which is deliberately slow and salted so even a database leak does not expose real passwords.

The senior-level points I always raise are token rotation, rate limiting on the authentication endpoint, and step-up authentication for high-value operations. A valid JWT is not enough to authorize a large transfer; the user should re-authenticate with a password or OTP for that specific action.

Remember one sentence: sign JWTs with RS256 so any service verifies with the public key, scope every query to the authenticated user to prevent IDOR, and hash passwords with BCrypt so a database leak is not a password leak.

---

Share
Use ← → arrow keys to navigate