Overview
TechStore implements a robust authentication system using RSA-encrypted JWT tokens with email verification and password recovery flows. All passwords are encrypted using BCrypt, and user accounts require email confirmation before login.
The authentication system uses Spring Security with OAuth2 Resource Server and stateless session management.
Security Architecture
RSA Key Pair Generation
The system generates a 2048-bit RSA key pair on startup for signing and verifying JWT tokens:
private static KeyPair generateRsaKey () {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator . getInstance ( "RSA" );
keyPairGenerator . initialize ( 2048 );
return keyPairGenerator . generateKeyPair ();
} catch ( Exception ex ) {
throw new IllegalStateException (ex);
}
}
JWT Token Structure
Tokens are generated with the following claims:
Issuer : self
Subject : User’s email address
Expiry : 10 hours (36000 seconds)
Scope : User roles (e.g., ROLE_USER, ROLE_ADMIN)
JwtClaimsSet claims = JwtClaimsSet . builder ()
. issuer ( "self" )
. issuedAt (now)
. expiresAt ( now . plusSeconds ( 36000L ))
. subject (username)
. claim ( "scope" , scope)
. build ();
User Registration Flow
User submits registration
POST request to /api/auth/register with user details
Password encryption
Password is encrypted using BCrypt before storage
Generate verification token
A unique UUID token is generated and stored with the user
Send verification email
HTML email sent with verification link containing the token
Account created (disabled)
User account created with enabled=false status
Registration Endpoint
@ PostMapping ( "/api/auth/register" )
public ResponseEntity < ? > registrar (@ RequestBody Usuario usuario) {
try {
Usuario usuarioGuardado = usuarioService . save (usuario);
emailService . enviarCorreoVerificacion (
usuarioGuardado . getEmail (),
usuarioGuardado . getTokenVerificacion ()
);
return ResponseEntity . ok ( Map . of (
"message" , "Usuario registrado. Revisa tu correo para activar la cuenta."
));
} catch ( Exception e ) {
return ResponseEntity . status ( HttpStatus . BAD_REQUEST )
. body ( Map . of ( "error" , "Error al registrar: " + e . getMessage ()));
}
}
Users cannot login until they verify their email address. The system checks enabled status during authentication.
Email Verification
When users click the verification link in their email:
@ GetMapping ( "/api/auth/verificar" )
public ResponseEntity < String > verificarCuenta (@ RequestParam ( "token" ) String token) {
boolean verificado = usuarioService . verificarToken (token);
if (verificado) {
// Returns HTML success page
return ResponseEntity . ok ( "<html>...Account activated...</html>" );
} else {
return ResponseEntity . status ( HttpStatus . BAD_REQUEST )
. body ( "<html>...Invalid or expired token...</html>" );
}
}
Verification Process
System looks up user by verification token
Sets enabled = true
Clears the tokenVerificacion field
Displays success HTML page
Login Flow
Submit credentials
POST to /api/auth/login with email and password
Validate account status
Check if user exists and account is enabled
Authenticate
Spring Security authenticates using BCrypt password matching
Generate JWT token
RSA-signed JWT token created with user email and roles
Return token
Token and user details returned to client
Login Endpoint
@ PostMapping ( "/api/auth/login" )
public Map < String, String > login (@ RequestBody Map < String, String > request) {
String email = request . get ( "email" );
String password = request . get ( "password" );
Usuario usuario = usuarioService . findUserByEmail (email)
. orElseThrow (() -> new ResponseStatusException (
HttpStatus . NOT_FOUND , "Usuario no encontrado"
));
// Check if account is enabled
if ( usuario . getEnabled () == null || ! usuario . getEnabled ()) {
throw new ResponseStatusException (
HttpStatus . FORBIDDEN ,
"Tu cuenta no está activa. Revisa tu correo."
);
}
Authentication auth = authManager . authenticate (
new UsernamePasswordAuthenticationToken (email, password)
);
String token = jwtUtil . generateToken (
auth . getName (),
auth . getAuthorities ()
);
return Map . of (
"id" , String . valueOf ( usuario . getId ()),
"access_token" , token,
"email" , email,
"nombre" , usuario . getNombre (),
"rol" , usuario . getRoles (). get ( 0 ). getNombre ()
);
}
Response Example
{
"id" : "5" ,
"access_token" : "eyJhbGciOiJSUzI1NiJ9..." ,
"email" : "user@example.com" ,
"nombre" : "John Doe" ,
"rol" : "ROLE_USER"
}
Store the access_token in your client application and include it in the Authorization header as Bearer <token> for subsequent requests.
Password Recovery
Users can reset their password through a secure token-based flow:
Request Password Reset
@ PostMapping ( "/api/auth/forgot-password" )
public ResponseEntity < ? > solicitarRecuperacion (@ RequestBody Map < String, String > request) {
String email = request . get ( "email" );
return usuarioService . findUserByEmail (email). map (user -> {
String token = UUID . randomUUID (). toString ();
user . setTokenVerificacion (token);
usuarioService . save (user);
emailService . enviarCorreoRecuperacion (email, token);
return ResponseEntity . ok ( Map . of (
"message" , "Correo de recuperación enviado exitosamente."
));
}). orElse ( ResponseEntity . status ( HttpStatus . NOT_FOUND )
. body ( Map . of ( "error" , "Email no encontrado." )));
}
Reset Password with Token
@ PostMapping ( "/api/auth/reset-password" )
public ResponseEntity < ? > restablecerPassword (@ RequestBody Map < String, String > request) {
String token = request . get ( "token" );
String nuevaPassword = request . get ( "nuevaPassword" );
boolean exito = usuarioService . cambiarPasswordConToken (
token,
passwordEncoder . encode (nuevaPassword)
);
if (exito) {
return ResponseEntity . ok ( Map . of (
"message" , "Tu contraseña ha sido actualizada correctamente."
));
}
return ResponseEntity . status ( HttpStatus . BAD_REQUEST )
. body ( Map . of ( "error" , "El enlace ha expirado o es inválido." ));
}
Tokens are single-use only. After password reset, the token is cleared from the database.
Authorization Rules
The security configuration defines role-based access control:
Public Routes
/api/auth/** - All authentication endpoints
GET /api/productos/** - View products
GET /api/categorias/** - View categories
Static resources: /index.html, /css/**, /js/**
Authenticated User Routes
GET/PUT/PATCH/DELETE /api/usuarios/{id} - Own profile management
POST /api/pedidos/** - Create orders
GET /api/pedidos/usuario/** - View own orders
Admin-Only Routes
GET /api/pedidos - View all orders
PUT/DELETE /api/pedidos/** - Manage all orders
POST/PUT/DELETE /api/productos/** - Manage products
/api/usuarios/** - User management
. authorizeHttpRequests (auth -> auth
. requestMatchers ( "/api/auth/**" ). permitAll ()
. requestMatchers ( HttpMethod . GET , "/api/productos/**" ). permitAll ()
. requestMatchers ( HttpMethod . POST , "/api/pedidos/**" ). authenticated ()
. requestMatchers ( "/api/usuarios/**" ). hasAuthority ( "ROLE_ADMIN" )
. anyRequest (). authenticated ()
)
CORS Configuration
The system allows cross-origin requests from development environments:
. cors (cors -> cors . configurationSource (request -> {
var cfg = new CorsConfiguration ();
cfg . setAllowedOrigins ( List . of (
"http://localhost:63342" ,
"http://127.0.0.1:5500" ,
"http://localhost:8080"
));
cfg . setAllowedMethods ( List . of ( "GET" , "POST" , "PUT" , "PATCH" , "DELETE" , "OPTIONS" ));
cfg . setAllowedHeaders ( List . of ( "*" ));
cfg . setAllowCredentials ( true );
return cfg;
}))
Best Practices
Token Security Tokens expire after 10 hours. Implement refresh token logic for production use.
Password Policy BCrypt automatically handles salting and hashing with industry-standard security.
Email Verification Always verify emails before enabling accounts to prevent spam and fake registrations.
Error Handling Never expose internal error details. Return generic messages to prevent information leakage.
Method Endpoint Description Auth Required POST /api/auth/registerRegister new user No GET /api/auth/verificarVerify email with token No POST /api/auth/loginLogin and get JWT No POST /api/auth/forgot-passwordRequest password reset No POST /api/auth/reset-passwordReset password with token No
For testing, you can check token contents at jwt.io to debug authentication issues.