Modern JWT auth building blocks for Spring Boot apps.
You get:
- ⚡ Access token generation + validation
- 🔁 Refresh token rotation with reuse-attack detection
- 🛡️ Permission-based authorization helpers
- 📱 Optional passwordless OTP flow (SMS)
- 🧩 Clear extension ports for your own persistence and providers
This library is intentionally composable: it gives you core security components, while your app owns storage, transports, and HTTP endpoints.
- ✨ What this library does
- 🧱 Core components
- 🔄 Token lifecycle
- ✅ Requirements
- 📦 Installation
- ⚙️ Configuration
- 🛠️ Integration guide
- 🔑 PasswordEncoder override
- 🚀 Usage examples
- 🔐 Permissions and authorization
- 🧩 Contracts you must implement
- 🗃️ Data model recommendations
- 🧪 Testing checklist
- 🧯 Troubleshooting
- 🔒 Security best practices
This library centralizes JWT auth logic so each service does not reinvent it.
- Signs and validates JWT access/refresh tokens.
- Extracts claims:
email,userId,permissions, token type. - Rotates refresh tokens by family + generation.
- Detects refresh-token replay and revokes the full token family.
- Provides a request filter that authenticates Bearer access tokens.
- Supports permission checks from JWT claims.
- Supports OTP send/verify via pluggable store + SMS ports.
| Component | Purpose |
|---|---|
JwtTokenProvider |
Build/verify JWTs and read claims |
RefreshTokenService |
Create/rotate/revoke refresh tokens |
AuthService |
Login, OTP login, refresh, logout orchestration |
JwtAuthenticationFilter |
Reads Bearer token and populates SecurityContextHolder |
CustomPermissionEvaluator |
Evaluates permission checks against SecurityUser.permissions |
SecurityAutoConfiguration |
Registers core beans and method-security wiring |
JwtProperties |
Typed config under javloom.jwt.* |
AuthServiceresolves user viaSecurityUserService.JwtTokenProvidercreates access token.RefreshTokenServicecreates refresh token and persists only a hash.- API returns
AuthResponsewith aTokenPair.
JwtAuthenticationFilterreadsAuthorization: Bearer <token>.- Validates token and enforces
TokenType.ACCESS. - Extracts claims (
userId,email,permissions). - Builds
SecurityUserand sets Spring Security authentication context.
- Client sends current refresh token.
- Service hashes token and loads stored record.
- If token is already revoked => reuse attack => revoke whole family.
- If token is active => revoke current token + issue next generation.
- Java 21
- Spring Boot 3.4+
- Spring Security 6+
Add the dependency to your app:
<dependency>
<groupId>io.javloom</groupId>
<artifactId>spring-security-jwt-lib</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>application.yml:
javloom:
jwt:
secret: "replace-with-a-long-random-secret-at-least-32-chars"
access-token-expiry: 900000
refresh-token-expiry: 604800000
issuer: "your-service-name"| Property | Default | Description |
|---|---|---|
javloom.jwt.secret |
none | HMAC signing secret |
javloom.jwt.access-token-expiry |
900000 |
Access token TTL in ms (15 min) |
javloom.jwt.refresh-token-expiry |
604800000 |
Refresh token TTL in ms (7 days) |
javloom.jwt.issuer |
javloom |
JWT issuer (iss) |
SecurityAutoConfiguration is auto-registered via Spring Boot's auto-configuration imports file:
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
So in a Spring Boot app, you usually do not need any manual @Import.
If you are wiring components manually (non-standard setup), you can still import it explicitly:
@Configuration
@Import(io.javloom.security.config.SecurityAutoConfiguration.class)
public class JwtLibConfig {
}Required in your app:
SecurityUserServiceRefreshTokenStore
Optional (passwordless OTP):
OtpStoreSmsPort
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}This library does not impose controllers. You define your REST endpoints and delegate to AuthService.
The library auto-registers a default PasswordEncoder only when your app does not provide one.
If your app uses a different hash strategy, define your own bean and it will override the default safely.
@Configuration
public class PasswordConfig {
@Bean
PasswordEncoder passwordEncoder() {
// Example: Argon2 for new hashes in this app.
return new org.springframework.security.crypto.argon2.Argon2PasswordEncoder();
}
}For mixed/legacy environments, prefer a delegating encoder with explicit ids (for example {bcrypt}, {argon2}, {pbkdf2} prefixes) so multiple formats can be verified during migration.
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public AuthResponse login(@Valid @RequestBody LoginRequest request) {
return authService.login(request.getEmail(), request.getPassword());
}
@PostMapping("/otp/send")
public void sendOtp(@Valid @RequestBody OtpRequest request) {
authService.initiateOtpLogin(request.getPhone());
}
@PostMapping("/otp/verify")
public AuthResponse verifyOtp(@Valid @RequestBody OtpVerifyRequest request) {
return authService.verifyOtpLogin(request.getPhone(), request.getCode());
}
@PostMapping("/refresh")
public AuthResponse refresh(@RequestHeader("Authorization") String bearer) {
String refreshToken = bearer.substring("Bearer ".length());
return authService.refresh(refreshToken);
}
@PostMapping("/logout")
public void logout(@AuthenticationPrincipal SecurityUser user) {
authService.logout(user.getUserId());
}
}Access tokens carry a permissions claim. The filter maps this to granted authorities.
Use standard authority checks:
@PreAuthorize("hasAuthority('ORDER_READ')")
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable String id) {
return service.getById(id);
}Or use evaluator-style checks:
@PreAuthorize("hasPermission(null, 'ORDER_READ')")
public void readOrder() {
}@HasPermission("ORDER_READ") is also available when your Spring Security setup supports annotation template resolution.
public interface SecurityUserService {
Optional<SecurityUser> findByEmail(String email);
Optional<SecurityUser> findByPhone(String phone);
}Notes:
- Put app permissions in
SecurityUser.permissions. findByPhoneis required for OTP flow.findByEmailis required for login/refresh user resolution.
public interface RefreshTokenStore {
RefreshToken save(RefreshToken token);
Optional<RefreshToken> findActiveByHash(String tokenHash);
Optional<RefreshToken> findByHash(String tokenHash);
void revokeById(String id);
void revokeAllByFamilyId(String familyId);
void revokeAllByUserId(String userId);
void deleteAllExpired();
}Notes:
- Store
tokenHash, never raw refresh token. findActiveByHashmust enforcerevoked = falseandexpiresAt > now.- Run
deleteAllExpired()on a schedule.
public interface OtpStore {
void save(String phone, String code, Duration ttl);
Optional<String> find(String phone);
void delete(String phone);
}
public interface SmsPort {
void send(String phone, String message);
}For refresh token persistence, index at least:
token_hash(unique)family_iduser_idexpires_atrevoked
Suggested columns:
id(string/UUID)token_hash(string)user_id(string)email(string)family_id(string)generation(int)revoked(boolean)expires_at(timestamp)created_at(timestamp)
- Login/OTP verify returns access + refresh tokens.
- Protected endpoints accept valid access token.
- Invalid/expired access token is rejected.
- Refresh rotates token and revokes prior token.
- Reused refresh token revokes full family.
- Permission-protected endpoints allow/deny correctly.
Run this library tests:
./mvnw test- Verify
javloom.jwt.secretis consistent in issuer + validator. - Verify Bearer header format.
- Ensure access endpoints are not using refresh tokens.
- Validate
RefreshTokenStore.findActiveByHashlogic. - Validate UTC expiration checks.
- Validate SHA-256 hashing of raw refresh token.
- Check
permissionsclaim exists in access token. - Check
SecurityUser.permissionscontains expected values. - Check JWT filter order in
SecurityFilterChain.
- Check OTP TTL + delete-on-success behavior in
OtpStore. - Check E.164 phone normalization consistency.
- Use high-entropy secrets (32+ random bytes).
- Keep short access-token TTL.
- Store hashed refresh tokens only.
- Revoke token families on reuse detection.
- Enforce HTTPS everywhere.
- Rate-limit login/OTP/refresh endpoints.
- Add audit logging and anomaly detection.
- Keep dependencies patched and scan regularly.
Built for teams that want secure defaults and clean extension points 😎