Login system with JWT token and email reset password
Matheus Bernardes Spilari
Posted on November 18, 2024
Introduction
The Spring Login Application is a secure and robust user management system built using Spring Boot. This project demonstrates modern approaches to implementing authentication, authorization, and user account functionalities. Key features include user registration, secure password handling with BCrypt, email-based password reset, and JWT (JSON Web Token) authentication. Designed with extensibility and scalability in mind, this application serves as an excellent foundation for projects requiring user management and role-based access control.
By leveraging Spring's powerful tools such as Spring Security, Spring Data JPA, and JavaMailSender, this project ensures best practices in security, maintainability, and ease of integration. Whether you're building a small web application or a large enterprise system, this project provides a practical, well-structured starting point for managing user accounts securely.
Configuration
Pom.xml dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Docker
To run the PostgreSQL database, create a docker-compose.yaml
file:
services:
postgres:
image: postgres:latest
ports:
- "5432:5432"
environment:
- POSTGRES_DB=database
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Run:
docker compose up -d
application.properties
spring.application.name=login_app
spring.datasource.url=jdbc:postgresql://localhost:5432/database
spring.datasource.username=admin
spring.datasource.password=admin
spring.mail.host=sandbox.smtp.mailtrap.io
spring.mail.port=2525
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.default-encoding=UTF-8
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.config.import=classpath:env.properties
jwt.public.key=classpath:public.key
jwt.private.key=classpath:private.key
env.properties
spring.mail.username=<Get in your mailtrap account>
spring.mail.password=<Get in your mailtrap account>
How to create an asymmetric keys ?
See in this post how to generate asymmetric keys
Project Structure
login_app/
├── .mvn/ # Maven folder (Maven configurations)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── dev/
│ │ │ └── mspilari/
│ │ │ └── login_app/
│ │ │ ├── configs/ # Security, authentication, and other configurations
│ │ │ ├── domains/ # Main application domains
│ │ │ │ ├── email/ # Email-related logic
│ │ │ │ └── user/ # User-related logic
│ │ │ ├── exceptions/ # Custom exceptions and error handling
│ │ │ └── utils/ # Utilities and helpers
│ │ └── resources/ # Resources (e.g., configuration files)
│ └── test/ # Application tests
├── target/ # Build folder generated by Maven
├── .gitattributes # Git attributes configuration
├── .gitignore # Git ignore file
├── docker-compose.yaml # Docker Compose configuration
├── HELP.md # Project help documentation
├── mvnw # Maven Wrapper script for Linux
├── mvnw.cmd # Maven Wrapper script for Windows
└── pom.xml # Maven configuration file
Features
- User registration with email and password validation
- Login with JWT authentication
- Password recovery with email link delivery
- Password reset via link with temporary token
- Field validation and error handling
Code
Config directory
BCryptPasswordConfig.java
package dev.mspilari.login_app.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class BCryptPasswordConfig {
@Bean
public BCryptPasswordEncoder bPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Code Breakdown
-
@Configuration
- This annotation tells Spring that the class contains bean definitions.
- Classes annotated with
@Configuration
are processed during application startup, and any methods annotated with@Bean
will have their return values added to the Spring application context as managed beans.
-
@Bean
- The
@Bean
annotation on thebPasswordEncoder()
method indicates that this method returns an object that should be registered as a bean in the Spring application context. - This allows the BCryptPasswordEncoder object to be injected wherever it's needed in the application.
- The
-
BCryptPasswordEncoder
- This is a utility class provided by Spring Security for encoding passwords.
- It uses the BCrypt hashing algorithm, which is considered a strong and secure way to hash passwords. The algorithm automatically adds a "salt" to the password before hashing, making it resistant to dictionary attacks and rainbow table attacks.
-
Method
bPasswordEncoder()
- When this method is called by the Spring framework, it creates a new instance of
BCryptPasswordEncoder
and makes it available in the application context. - Other classes in the application can then autowire this bean to encode or match passwords.
- When this method is called by the Spring framework, it creates a new instance of
JwtConfig.java
package dev.mspilari.login_app.configs;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
@Configuration
public class JwtConfig {
@Value("${jwt.public.key}")
private RSAPublicKey publicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;
@Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.publicKey).build();
}
}
Code Breakdown
1. Class-Level Annotations
@Configuration
- Indicates this is a Spring configuration class where beans (Spring-managed components) are defined.
- The beans defined here will be available in the Spring Application Context for dependency injection.
2. Injecting RSA Keys from Configuration
@Value("${jwt.public.key}")
private RSAPublicKey publicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;
-
@Value
is used to inject the public key and private key from the application's properties file (e.g.,application.yml
orapplication.properties
). - These keys are expected to be in the properties as:
jwt.public.key=<your-public-key>
jwt.private.key=<your-private-key>
3. JWT Encoder Bean
@Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
- Purpose: Creates a bean for encoding (generating) JWT tokens.
-
Steps:
-
Build RSA Key:
-
RSAKey.Builder
creates a JWK (JSON Web Key) representation of the public/private RSA key pair.
-
-
Create JWK Set:
-
ImmutableJWKSet
stores the key in a set. This set is used by Nimbus JOSE libraries for signing tokens.
-
-
NimbusJwtEncoder:
- This encoder uses the
ImmutableJWKSet
to encode and sign tokens using the private key.
- This encoder uses the
-
Build RSA Key:
4. JWT Decoder Bean
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.publicKey).build();
}
- Purpose: Creates a bean for decoding and verifying JWT tokens.
-
Steps:
-
Public Key Verification:
-
NimbusJwtDecoder.withPublicKey()
is configured with the RSA public key. It verifies the signature of tokens.
-
-
Build Decoder:
- The
build()
method creates the decoder instance.
- The
-
Public Key Verification:
How JWT Encoding and Decoding Work
-
JWT Encoding (Token Generation):
- The
JwtEncoder
bean is used to create a signed JWT token. This token typically contains user information (e.g., username, roles, etc.) as claims and is signed using the RSA private key. - Example:
String token = jwtEncoder.encode(JwtClaimsSet.builder().subject("user").build()).getTokenValue();
- The
-
JWT Decoding (Token Verification):
- The
JwtDecoder
bean is used to decode and verify the token using the RSA public key. This ensures the token:- Was issued by the server (signature verification).
- Has not been tampered with.
- Example:
Jwt jwt = jwtDecoder.decode(token);
- The
SecurityConfig.java
package dev.mspilari.login_app.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private JwtConfig jwtConfig;
public SecurityConfig(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.POST, "/user/register").permitAll()
.requestMatchers(HttpMethod.POST, "/user/login").permitAll()
.requestMatchers(HttpMethod.POST, "/user/redeem-password").permitAll()
.requestMatchers(HttpMethod.POST, "/user/reset-password").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(config -> config.jwt(jwt -> jwt.decoder(jwtConfig.jwtDecoder())));
return http.build();
}
}
1. Class-Level Annotations
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
-
@Configuration
: Marks this class as a Spring configuration that defines beans. -
@EnableWebSecurity
: Enables Spring Security's web security features. -
@EnableMethodSecurity
: Activates method-level security annotations like@PreAuthorize
or@Secured
. This allows you to control access to specific methods in your application based on roles, permissions, or conditions.
2. SecurityFilterChain
Bean
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- Defines the security filter chain for the application. A filter chain is a sequence of security filters applied to incoming HTTP requests.
3. CSRF Protection
.csrf(csrf -> csrf.disable())
-
CSRF (Cross-Site Request Forgery) protection is disabled.
- CSRF protection is often unnecessary for stateless APIs, as tokens (like JWT) already provide a way to prevent unauthorized requests.
- Disabling it simplifies the security configuration for this JWT-based API.
4. Authorization Rules
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/user/register").permitAll()
.requestMatchers(HttpMethod.POST, "/user/login").permitAll()
.requestMatchers(HttpMethod.POST, "/user/redeem-password").permitAll()
.requestMatchers(HttpMethod.POST, "/user/reset-password").permitAll()
.anyRequest().authenticated())
- Configures which endpoints require authentication:
- Permit All:
- POST requests to endpoints like
/user/register
,/user/login
,/user/redeem-password
, and/user/reset-password
are open to everyone (no authentication required). - These endpoints are likely used for user registration, login, and password recovery/reset, which are typically accessible without logging in.
- Authenticate Other Requests:
- All other endpoints (
anyRequest
) require authentication.
5. JWT Validation
.oauth2ResourceServer(config -> config
.jwt(jwt -> jwt.decoder(jwtConfig.jwtDecoder())));
- Configures the application as an OAuth 2.0 resource server that validates requests using JWT tokens.
-
JWT Decoder:
- The
JwtDecoder
bean (provided byJwtConfig
) is used to verify incoming JWT tokens for requests to secure endpoints.
- The
How This Works
- CSRF Disabled: Since this is an API relying on stateless JWT authentication, disabling CSRF is common practice.
-
Authorization Rules:
- Unauthenticated users can only access the explicitly permitted endpoints (e.g.,
/user/register
or/user/login
). - Any other request requires a valid JWT token.
- Unauthenticated users can only access the explicitly permitted endpoints (e.g.,
-
JWT Validation:
- Spring Security automatically extracts the
Authorization
header from incoming requests. - If the header contains a valid JWT token, the request is authenticated, and the user context is established.
- If the token is invalid or missing, the request is rejected.
- Spring Security automatically extracts the
Domains directory
Email directory
Services directory
package dev.mspilari.login_app.domains.email.services;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private JavaMailSender mailSender;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendEmail(String email, String subject, String body) {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(email);
mail.setFrom("app_login@email.com");
mail.setSubject(subject);
mail.setText(body);
mailSender.send(mail);
}
}
User directory
Controllers directory
package dev.mspilari.login_app.domains.user.controllers;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.mspilari.login_app.domains.user.dto.UserDto;
import dev.mspilari.login_app.domains.user.dto.UserRedeemPasswordDto;
import dev.mspilari.login_app.domains.user.dto.UserResetPasswordDto;
import dev.mspilari.login_app.domains.user.services.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("user")
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody @Valid UserDto userDto) {
var token = userService.login(userDto.email(), userDto.password());
return ResponseEntity.ok().body(Map.of("token", token));
}
@PostMapping("/register")
public ResponseEntity<Map<String, String>> register(@RequestBody @Valid UserDto userDto) {
userService.createUser(userDto.email(), userDto.password());
return ResponseEntity.ok().body(Map.of("message", "User created successfully"));
}
@PostMapping("/redeem-password")
public ResponseEntity<Map<String, String>> redeemPassword(@RequestBody @Valid UserRedeemPasswordDto userDto) {
userService.redeemPassword(userDto.email());
return ResponseEntity.ok().body(Map.of("message", "Send the redeem password link to your email"));
}
@PostMapping("/reset-password")
public ResponseEntity<Map<String, String>> resetPassword(@RequestBody @Valid UserResetPasswordDto userDto) {
userService.resetPassword(userDto.token(), userDto.password());
return ResponseEntity.ok().body(Map.of("message", "Credentials updated"));
}
}
DTO directory
UserDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record UserDto(@Email @NotBlank String email, @Min(4) @NotBlank String password) {
}
UserRedeemPasswordDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Email;
public record UserRedeemPasswordDto(@Email String email) {
}
UserResetPasswordDto.java
package dev.mspilari.login_app.domains.user.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record UserResetPasswordDto(@NotBlank String token, @NotBlank @Min(4) String password) {
}
Entity directory
UserEntity.java
package dev.mspilari.login_app.domains.user.entity;
import java.time.Instant;
import java.util.UUID;
import dev.mspilari.login_app.domains.user.enums.Role;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "tb_users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String email;
private String password;
private String resetToken;
private Instant resetTokenExpiration;
@Enumerated(EnumType.STRING)
private Role role;
public UserEntity(String email, String password, Role role) {
this.email = email;
this.password = password;
this.role = role;
}
public UserEntity() {
}
public UserEntity withResetToken(String resetToken, Instant resetTokenAdditionalTime) {
this.resetToken = resetToken;
this.resetTokenExpiration = resetTokenAdditionalTime;
return this;
}
public UUID getId() {
return id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public Instant getResetTokenExpiration() {
return resetTokenExpiration;
}
public void setResetTokenExpiration(Instant resetTokenExpiration) {
this.resetTokenExpiration = resetTokenExpiration;
}
public String getResetToken() {
return resetToken;
}
public void setResetToken(String resetToken) {
this.resetToken = resetToken;
}
}
Enums directory
Role.java
package dev.mspilari.login_app.domains.user.enums;
public enum Role {
ADMIN,
CLIENT
}
Repositories directory
UserRepository.java
package dev.mspilari.login_app.domains.user.repositories;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import dev.mspilari.login_app.domains.user.entity.UserEntity;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByResetToken(String resetToken);
}
Services directory
UserService.java
package dev.mspilari.login_app.domains.user.services;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import dev.mspilari.login_app.domains.email.services.EmailService;
import dev.mspilari.login_app.domains.user.entity.UserEntity;
import dev.mspilari.login_app.domains.user.enums.Role;
import dev.mspilari.login_app.domains.user.repositories.UserRepository;
import dev.mspilari.login_app.utils.JwtActions;
@Service
public class UserService {
@Value("${token.expiration.seconds:300}")
private Long tokenExpirationSeconds;
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtActions jwtActions;
private final EmailService emailService;
public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, JwtActions jwtActions,
EmailService emailService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtActions = jwtActions;
this.emailService = emailService;
}
private Optional<UserEntity> findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
private boolean verifyPassword(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
private void sendPasswordResetEmail(String email, String token) {
String subject = "Password Reset Request";
String resetUrl = "https://seusite.com/reset?token=" + token;
String body = "Click the link to reset your password: " + resetUrl;
// Implemente o serviço de e-mail conforme necessário
emailService.sendEmail(email, subject, body);
}
public void createUser(String email, String password) {
if (findUserByEmail(email).isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email already exists !");
}
var encodedPassword = passwordEncoder.encode(password);
var newUser = new UserEntity(email, encodedPassword, Role.CLIENT);
userRepository.save(newUser);
}
public String login(String email, String password) {
var user = findUserByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid login credentials"));
if (!verifyPassword(password, user.getPassword())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid login credentials");
}
return jwtActions.jwtCreate(user.getEmail(), user.getRole().toString());
}
public void redeemPassword(String email) {
var user = findUserByEmail(email).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid email"));
var token = UUID.randomUUID().toString();
user.withResetToken(token, Instant.now().plusSeconds(this.tokenExpirationSeconds));
userRepository.save(user);
sendPasswordResetEmail(user.getEmail(), token);
}
public void resetPassword(String token, String password) {
var user = userRepository.findByResetToken(token)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
"User not found"));
if (user.getResetTokenExpiration().isBefore(Instant.now())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Token expired");
}
user.setPassword(passwordEncoder.encode(password));
user.withResetToken(null, null);
userRepository.save(user);
}
}
Exceptions directory
GlobalException.java
package dev.mspilari.login_app.exceptions;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> defaultExceptionHandler(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage()));
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, String>> responseStatusExceptionHandler(ResponseStatusException e) {
var error = Map.of("errorMessage", e.getReason(), "errorStatusCode", e.getStatusCode().toString());
return ResponseEntity.status(e.getStatusCode()).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> validationErrorHandler(MethodArgumentNotValidException e) {
var errors = new HashMap<String, String>();
for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.status(e.getStatusCode()).body(errors);
}
}
Utils directory
JwtActions.java
package dev.mspilari.login_app.utils;
import java.time.Instant;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import dev.mspilari.login_app.configs.JwtConfig;
@Service
public class JwtActions {
@Value("${jwt.expiration:300}")
private Long jwtExpiration;
private final JwtConfig jwtConfig;
public JwtActions(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
public String jwtCreate(String email, String role) {
var now = Instant.now();
var claims = JwtClaimsSet.builder()
.issuer("login_app")
.subject(email)
.issuedAt(now)
.expiresAt(now.plusSeconds(jwtExpiration))
.claim("scope", role)
.build();
return jwtConfig.jwtEncoder().encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
Conclusion
In this project, we successfully implemented a secure and feature-rich user authentication system using Spring Boot. Beyond the core functionalities like user registration, login, and JWT-based authentication, the application also incorporates a password recovery system. Users can reset their passwords through an email link, ensuring a smooth and secure recovery process.
To facilitate email-based password recovery, we integrated Spring Email with Mailtrap, a safe and efficient email testing service. This allows the application to send password reset links with temporary tokens while ensuring that emails are sent securely and tested in a controlled environment. This setup demonstrates how to handle sensitive workflows like password recovery without exposing real users to potential issues during development and testing.
The combination of secure authentication practices, robust password management, and seamless email integration makes this application a reliable foundation for any modern web system. Developers can adapt these practices to suit their specific requirements, ensuring both scalability and user trust. By leveraging best practices and tools like Spring Security and Mailtrap, we have demonstrated how to build secure, user-focused applications with ease.
📍 Reference
💻 Project Repository
👋 Talk to me
Posted on November 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.