diff --git a/JWT_AUTH_README.md b/JWT_AUTH_README.md
new file mode 100644
index 0000000..aeeb629
--- /dev/null
+++ b/JWT_AUTH_README.md
@@ -0,0 +1,93 @@
+# JWT Authentication Implementation
+
+This Spring Boot application now includes JWT-based authentication with user registration and login functionality.
+
+## Features Added
+
+- ✅ JWT token generation and validation
+- ✅ User registration endpoint
+- ✅ User login endpoint
+- ✅ Protected endpoints requiring authentication
+- ✅ Public endpoints accessible without authentication
+- ✅ Password encryption using BCrypt
+- ✅ User entity with Spring Security integration
+
+## API Endpoints
+
+### Public Endpoints (No Authentication Required)
+- `GET /api/test/public` - Test public endpoint
+- `POST /auth/register` - User registration
+- `POST /auth/login` - User login
+- `GET /auth/test` - Test authentication endpoints
+
+### Protected Endpoints (Authentication Required)
+- `GET /api/test/protected` - Test protected endpoint
+- All other endpoints in the application
+
+## Testing the Authentication
+
+### 1. Register a New User
+```bash
+curl -X POST http://localhost:6950/auth/register \
+ -H "Content-Type: application/json" \
+ -d '{
+ "username": "testuser",
+ "password": "password123",
+ "email": "test@example.com",
+ "phone": "+263771234567",
+ "firstName": "Test",
+ "lastName": "User"
+ }'
+```
+
+### 2. Login with the User
+```bash
+curl -X POST http://localhost:6950/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{
+ "username": "testuser",
+ "password": "password123"
+ }'
+```
+
+### 3. Access Protected Endpoint
+```bash
+curl -X GET http://localhost:6950/api/test/protected \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
+```
+
+### 4. Test Public Endpoint
+```bash
+curl -X GET http://localhost:6950/api/test/public
+```
+
+## Configuration
+
+The JWT configuration is in `application.properties`:
+- `jwt.secret`: Secret key for signing JWT tokens
+- `jwt.expiration`: Token expiration time in milliseconds (default: 24 hours)
+
+## Security Features
+
+- **Password Encryption**: All passwords are encrypted using BCrypt
+- **JWT Tokens**: Stateless authentication using JSON Web Tokens
+- **CORS Support**: Cross-origin requests are enabled for testing
+- **Input Validation**: Request validation using Bean Validation annotations
+- **Stateless Sessions**: No server-side session storage
+- **Direct Password Validation**: Login uses direct password comparison for simplicity
+
+## Database
+
+The application will automatically create the `users` table when it starts up. The table includes:
+- User credentials (username, password, email)
+- Personal information (first name, last name)
+- Account status flags
+- Timestamps for creation and updates
+
+## Notes
+
+- In production, change the `jwt.secret` to a secure, randomly generated key
+- The default JWT expiration is set to 24 hours
+- All passwords must be at least 6 characters long
+- Usernames and emails must be unique
+- The application uses PostgreSQL as the database
diff --git a/pom.xml b/pom.xml
index e2ad341..66f906d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,6 +108,31 @@
specification-arg-resolver
3.1.0
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.3
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.3
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.3
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
diff --git a/src/main/java/zw/qantra/tm/configs/JwtAuthenticationFilter.java b/src/main/java/zw/qantra/tm/configs/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..515ffd4
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/configs/JwtAuthenticationFilter.java
@@ -0,0 +1,62 @@
+package zw.qantra.tm.configs;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import zw.qantra.tm.domain.repositories.UserRepository;
+import zw.qantra.tm.utils.JwtUtils;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtUtils jwtUtils;
+ private final UserRepository userRepository;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+
+ final String authHeader = request.getHeader("Authorization");
+ final String jwt;
+ final String username;
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ jwt = authHeader.substring(7);
+ try {
+ username = jwtUtils.extractUsername(jwt);
+
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails = userRepository.findByUsername(username)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
+
+ if (jwtUtils.validateToken(jwt, userDetails)) {
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.getAuthorities());
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+ }
+ } catch (Exception e) {
+ // Token is invalid, continue without authentication
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
new file mode 100644
index 0000000..799417b
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
@@ -0,0 +1,62 @@
+package zw.qantra.tm.configs;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import zw.qantra.tm.domain.services.UserService;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtAuthenticationFilter jwtAuthFilter;
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http, UserService userService) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/auth/**").permitAll()
+ .requestMatchers("/public/**").permitAll()
+ .anyRequest().authenticated()
+ )
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+ .authenticationProvider(authenticationProvider(userService))
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ @Bean
+ public AuthenticationProvider authenticationProvider(UserService userService) {
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userService);
+ authProvider.setPasswordEncoder(passwordEncoder());
+ return authProvider;
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java b/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java
new file mode 100644
index 0000000..238c02c
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/controllers/AuthController.java
@@ -0,0 +1,36 @@
+package zw.qantra.tm.domain.controllers;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import zw.qantra.tm.domain.dtos.AuthResponse;
+import zw.qantra.tm.domain.dtos.LoginRequest;
+import zw.qantra.tm.domain.dtos.RegisterRequest;
+import zw.qantra.tm.domain.services.UserService;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+@CrossOrigin(origins = "*")
+public class AuthController {
+
+ private final UserService userService;
+
+ @PostMapping("/register")
+ public ResponseEntity register(@Valid @RequestBody RegisterRequest request) {
+ AuthResponse response = userService.register(request);
+ return ResponseEntity.ok(response);
+ }
+
+ @PostMapping("/login")
+ public ResponseEntity login(@Valid @RequestBody LoginRequest request) {
+ AuthResponse response = userService.login(request);
+ return ResponseEntity.ok(response);
+ }
+
+ @GetMapping("/test")
+ public ResponseEntity test() {
+ return ResponseEntity.ok("Authentication endpoints are working!");
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/controllers/TestController.java b/src/main/java/zw/qantra/tm/domain/controllers/TestController.java
new file mode 100644
index 0000000..140e6af
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/controllers/TestController.java
@@ -0,0 +1,27 @@
+package zw.qantra.tm.domain.controllers;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/test")
+@CrossOrigin(origins = "*")
+public class TestController {
+
+ @GetMapping("/protected")
+ public ResponseEntity protectedEndpoint() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ String username = authentication.getName();
+ return ResponseEntity.ok("Hello " + username + "! This is a protected endpoint.");
+ }
+
+ @GetMapping("/public")
+ public ResponseEntity publicEndpoint() {
+ return ResponseEntity.ok("This is a public endpoint - no authentication required!");
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java b/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java
new file mode 100644
index 0000000..04779c1
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/dtos/AuthResponse.java
@@ -0,0 +1,16 @@
+package zw.qantra.tm.domain.dtos;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class AuthResponse {
+ private String token;
+ private String type = "Bearer";
+ private String username;
+ private String email;
+ private String phone;
+ private String firstName;
+ private String lastName;
+}
diff --git a/src/main/java/zw/qantra/tm/domain/dtos/LoginRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/LoginRequest.java
new file mode 100644
index 0000000..5a97f7c
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/dtos/LoginRequest.java
@@ -0,0 +1,13 @@
+package zw.qantra.tm.domain.dtos;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class LoginRequest {
+ @NotBlank(message = "Username is required")
+ private String username;
+
+ @NotBlank(message = "Password is required")
+ private String password;
+}
diff --git a/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java
new file mode 100644
index 0000000..7096cd1
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/dtos/RegisterRequest.java
@@ -0,0 +1,30 @@
+package zw.qantra.tm.domain.dtos;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class RegisterRequest {
+ @NotBlank(message = "Username is required")
+ @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
+ private String username;
+
+ @NotBlank(message = "Password is required")
+ @Size(min = 6, message = "Password must be at least 6 characters")
+ private String password;
+
+ @NotBlank(message = "Email is required")
+ @Email(message = "Email should be valid")
+ private String email;
+
+ @NotBlank(message = "First name is required")
+ private String firstName;
+
+ @NotBlank(message = "Last name is required")
+ private String lastName;
+
+ @NotBlank(message = "Phone is required")
+ private String phone;
+}
diff --git a/src/main/java/zw/qantra/tm/domain/models/User.java b/src/main/java/zw/qantra/tm/domain/models/User.java
new file mode 100644
index 0000000..b84f34a
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/models/User.java
@@ -0,0 +1,71 @@
+package zw.qantra.tm.domain.models;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Entity
+@Table(name = "users")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class User extends BaseEntity implements UserDetails {
+
+ @Column(unique = true, nullable = false)
+ private String username;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Column(unique = true, nullable = false)
+ private String email;
+ private String phone;
+
+ @Column(name = "first_name")
+ private String firstName;
+
+ @Column(name = "last_name")
+ private String lastName;
+
+ @Column(name = "is_enabled")
+ private boolean enabled = true;
+
+ @Column(name = "is_account_non_expired")
+ private boolean accountNonExpired = true;
+
+ @Column(name = "is_account_non_locked")
+ private boolean accountNonLocked = true;
+
+ @Column(name = "is_credentials_non_expired")
+ private boolean credentialsNonExpired = true;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(new SimpleGrantedAuthority("ROLE_USER"));
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return accountNonExpired;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return accountNonLocked;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return credentialsNonExpired;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java
new file mode 100644
index 0000000..7fca923
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java
@@ -0,0 +1,16 @@
+package zw.qantra.tm.domain.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import zw.qantra.tm.domain.models.User;
+
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+ Optional findByUsername(String username);
+ Optional findByEmail(String email);
+ Optional findByPhone(String phone);
+ boolean existsByUsername(String username);
+ boolean existsByEmail(String email);
+}
diff --git a/src/main/java/zw/qantra/tm/domain/services/UserService.java b/src/main/java/zw/qantra/tm/domain/services/UserService.java
new file mode 100644
index 0000000..0c6ed3e
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/services/UserService.java
@@ -0,0 +1,67 @@
+package zw.qantra.tm.domain.services;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import zw.qantra.tm.domain.dtos.AuthResponse;
+import zw.qantra.tm.domain.dtos.LoginRequest;
+import zw.qantra.tm.domain.dtos.RegisterRequest;
+import zw.qantra.tm.domain.models.User;
+import zw.qantra.tm.domain.repositories.UserRepository;
+import zw.qantra.tm.utils.JwtUtils;
+
+@Service
+@RequiredArgsConstructor
+public class UserService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtUtils jwtUtils;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userRepository.findByUsername(username)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
+ }
+
+ public AuthResponse register(RegisterRequest request) {
+ if (userRepository.existsByUsername(request.getUsername())) {
+ throw new RuntimeException("Username already exists");
+ }
+
+ if (userRepository.existsByEmail(request.getEmail())) {
+ throw new RuntimeException("Email already exists");
+ }
+
+ User user = new User();
+ user.setUsername(request.getUsername());
+ user.setPassword(passwordEncoder.encode(request.getPassword()));
+ user.setEmail(request.getEmail());
+ user.setFirstName(request.getFirstName());
+ user.setLastName(request.getLastName());
+ user.setPhone(request.getPhone());
+
+ User savedUser = userRepository.save(user);
+ String token = jwtUtils.generateToken(savedUser);
+
+ return new AuthResponse(token, "Bearer", savedUser.getUsername(),
+ savedUser.getEmail(), savedUser.getPhone(), savedUser.getFirstName(), savedUser.getLastName());
+ }
+
+ public AuthResponse login(LoginRequest request) {
+ User user = userRepository.findByUsername(request.getUsername())
+ .orElseThrow(() -> new RuntimeException("Invalid username or password"));
+
+ if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
+ throw new RuntimeException("Invalid username or password");
+ }
+
+ String token = jwtUtils.generateToken(user);
+
+ return new AuthResponse(token, "Bearer", user.getUsername(),
+ user.getEmail(), user.getPhone(), user.getFirstName(), user.getLastName());
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/utils/JwtUtils.java b/src/main/java/zw/qantra/tm/utils/JwtUtils.java
new file mode 100644
index 0000000..0edf53b
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/utils/JwtUtils.java
@@ -0,0 +1,72 @@
+package zw.qantra.tm.utils;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+public class JwtUtils {
+
+ @Value("${jwt.secret:defaultSecretKey123456789012345678901234567890}")
+ private String secret;
+
+ @Value("${jwt.expiration:86400000}")
+ private Long expiration;
+
+ private SecretKey getSigningKey() {
+ return Keys.hmacShaKeyFor(secret.getBytes());
+ }
+
+ public String extractUsername(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ public Date extractExpiration(String token) {
+ return extractClaim(token, Claims::getExpiration);
+ }
+
+ public T extractClaim(String token, Function claimsResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
+
+ private Claims extractAllClaims(String token) {
+ return Jwts.parser()
+ .verifyWith(getSigningKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+
+ private Boolean isTokenExpired(String token) {
+ return extractExpiration(token).before(new Date());
+ }
+
+ public String generateToken(UserDetails userDetails) {
+ Map claims = new HashMap<>();
+ return createToken(claims, userDetails.getUsername());
+ }
+
+ private String createToken(Map claims, String subject) {
+ return Jwts.builder()
+ .setClaims(claims)
+ .setSubject(subject)
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
+ .signWith(getSigningKey(), SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ public Boolean validateToken(String token, UserDetails userDetails) {
+ final String username = extractUsername(token);
+ return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 2fb4c30..f196340 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -41,3 +41,7 @@ erpnext.income.account=5111 - Sales Income - QPAY
erpnext.cost.center=Main - QPAY
erpnext.tax.account=VAT - QPAY
erpnext.tax.rate=15.0
+
+# JWT Configuration
+jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-production
+jwt.expiration=86400000