JWT stands for JSON Web Token. In a banking project, JWT is used to identify a logged-in customer securely when they access APIs like balance enquiry, fund transfer, statement download, loan details, and profile information.
Real-Time Banking Example
Suppose customer Naresh logs into a banking application. After successful login, the backend generates an Access Token and a Refresh Token.
1. Login Request
The customer enters username and password:
{
"username": "naresh",
"password": "123456"
}
The frontend sends this request to backend:
POST /bank/auth/login
2. Backend Validates User
The backend checks whether the username and password are valid by comparing them with the user details stored in the database.
Username: naresh
Password: 123456
Database check:
User exists? YES
Password valid? YES
Account active? YES
3. Backend Generates Tokens
After successful validation, the backend generates two tokens:
| Token | Purpose | Expiry |
|---|---|---|
| Access Token | Used to access protected APIs | 5 to 15 minutes |
| Refresh Token | Used to generate a new access token | 1 day to 7 days |
4. Login Response
The backend returns tokens to the frontend:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9.access.token",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9.refresh.token"
}
5. JWT Payload Example
A JWT contains user information called claims. In banking, only non-sensitive data should be stored inside JWT.
{
"sub": "naresh",
"customerId": "CUST1001",
"role": "CUSTOMER",
"iat": 1715151000,
"exp": 1715151900
}
Never Store Sensitive Data in JWT
Do not store password, PIN, OTP, CVV, account balance, or full card number inside JWT.
6. Client Stores Token
In normal projects, tokens are often stored in localStorage. But in banking applications, the recommended approach is to store tokens in HttpOnly Secure Cookies.
Set-Cookie: accessToken=jwt_access_token;
HttpOnly;
Secure;
SameSite=Strict
HttpOnly cookies are safer because JavaScript cannot directly read the token.
7. Accessing Protected API
When the customer clicks on Check Balance, the frontend sends the access token to the backend.
GET /bank/accounts/balance
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.access.token
8. JWT Filter Validates Token
Before the request reaches the controller, a JWT filter checks whether the token is valid.
JWT Filter Steps:
1. Read Authorization header
2. Extract Bearer token
3. Validate token signature
4. Check token expiry
5. Extract username and customerId
6. Create Authentication object
7. Store authentication in SecurityContext
8. Allow request to controller
9. Signature Validation
JWT has three parts:
HEADER.PAYLOAD.SIGNATURE
If someone changes the payload, for example changing role from CUSTOMER to ADMIN, the signature becomes invalid and backend rejects the token.
{
"role": "CUSTOMER"
}
If changed illegally:
{
"role": "ADMIN"
}
The backend detects tampering and returns:
401 Unauthorized
10. Expiry Validation
Access tokens should have short expiry in banking applications.
Access token expiry: 10 minutes
If the token is expired, backend rejects the request.
401 Unauthorized - Token Expired
11. Controller Receives Authenticated User
After token validation, Spring Security stores user details in SecurityContext. Then the controller can access the logged-in user.
@GetMapping("/accounts/balance")
public BalanceResponse getBalance(Authentication authentication) {
BankingUserPrincipal user =
(BankingUserPrincipal) authentication.getPrincipal();
return accountService.getBalance(user.getCustomerId());
}
12. Account Ownership Validation
JWT proves that the user is logged in. But the backend must still verify whether the requested account belongs to that customer.
Customer ID from JWT: CUST1001
Requested account: ACC1001
Database check:
Does ACC1001 belong to CUST1001? YES
If customer tries another account:
Customer ID from JWT: CUST1001
Requested account: ACC9999
Database check:
Does ACC9999 belong to CUST1001? NO
Response:
403 Forbidden
13. Fund Transfer Example
Customer sends a fund transfer request:
POST /bank/transfer
Authorization: Bearer access_token
{
"fromAccount": "ACC1001",
"toAccount": "ACC2001",
"amount": 5000
}
Backend validations:
1. JWT valid?
2. Token not expired?
3. Customer owns fromAccount?
4. Account active?
5. Balance available?
6. Daily transfer limit valid?
7. OTP verified?
8. Process transfer
14. OTP Verification
For sensitive banking operations like fund transfer, JWT alone is not enough. OTP verification is required.
Transfer Flow:
1. Customer enters transfer details
2. Backend generates OTP
3. OTP sent to mobile/email
4. Customer enters OTP
5. Backend validates OTP
6. Transfer completed
15. Transaction Processing
Money transfer should be done inside a database transaction. If debit succeeds but credit fails, the transaction must rollback.
@Transactional
public TransferResponse transferMoney(TransferRequest request) {
debitFromSenderAccount();
creditToReceiverAccount();
saveTransactionHistory();
return new TransferResponse("SUCCESS", "Transfer completed");
}
16. Refresh Token Flow
Access tokens expire quickly. So, when the access token expires, the frontend uses the refresh token to get a new access token without asking the user to login again.
Access Token Expired
|
v
Frontend calls /auth/refresh
|
v
Backend validates refresh token
|
v
Backend generates new access token
|
v
Frontend continues API usage
17. Refresh Token API Example
POST /bank/auth/refresh
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9.refresh.token"
}
Backend response:
{
"accessToken": "new_access_token_here"
}
18. Refresh Token Backend Logic
public TokenResponse refreshToken(String refreshToken) {
if (!jwtService.isValid(refreshToken)) {
throw new RuntimeException("Invalid refresh token");
}
String username = jwtService.extractUsername(refreshToken);
User user = userRepository.findByUsername(username)
.orElseThrow();
String newAccessToken = jwtService.generateAccessToken(user);
return new TokenResponse(newAccessToken);
}
19. Access Token vs Refresh Token
| Point | Access Token | Refresh Token |
|---|---|---|
| Purpose | Access protected APIs | Generate new access token |
| Expiry | Short | Longer |
| Sent with every request? | Yes | No |
| Risk if stolen | Medium | High |
| Storage | HttpOnly Cookie | HttpOnly Cookie or secure DB-backed token |
20. Logout Flow
JWT is stateless. So logout should clear tokens and invalidate refresh tokens.
Logout Flow:
1. User clicks logout
2. Backend deletes refresh token from DB
3. Access token can be blacklisted in Redis
4. Browser cookies are cleared
5. User redirected to login page
21. Refresh Token Storage in Database
In production banking applications, refresh tokens are usually stored in database or Redis so they can be revoked.
refresh_tokens table
id | user_id | token_hash | expiry_date | revoked
1 | 101 | hash_value | 2026-05-18 | false
22. Full Banking JWT Flow
1. Customer logs in
2. Backend validates username and password
3. Backend generates access token and refresh token
4. Frontend stores tokens securely
5. Customer calls balance or transfer API
6. Access token is sent with request
7. JWT filter validates token
8. Backend extracts customerId and role
9. Service checks account ownership
10. OTP validation happens for transfer
11. Transaction is processed
12. Audit log is saved
13. Response returned to customer
14. If access token expires, refresh token generates new access token
15. On logout, refresh token is revoked
23. Banking Security Best Practices
- Use HTTPS only.
- Keep access token expiry short.
- Store tokens in HttpOnly Secure cookies.
- Never store password, PIN, OTP, or card details in JWT.
- Validate account ownership in service layer.
- Use OTP or MFA for fund transfers.
- Store refresh tokens in DB or Redis.
- Revoke refresh token on logout.
- Use Redis blacklist for high-security logout handling.
- Log all sensitive banking actions.
24. Simple Interview Explanation
In my banking project, after successful login, the backend generates an access token and a refresh token. The access token contains basic claims like username, customerId, and role. The frontend sends the access token with every protected API request. A JWT filter validates the token, extracts the customer details, and stores authentication in SecurityContext. For banking operations like balance enquiry and fund transfer, the backend also verifies account ownership from the database. For fund transfer, OTP validation, balance check, transaction limit check, and audit logging are also performed. When the access token expires, the refresh token is used to generate a new access token. On logout, the refresh token is revoked.