From f866bb3e90f26c941030565fa8f5df2dcab3dc2f Mon Sep 17 00:00:00 2001 From: Suweka Date: Wed, 24 Sep 2025 00:38:10 +0530 Subject: [PATCH 01/24] feat(auth): add RoleName enum (ADMIN, EMPLOYEE, CUSTOMER) with descriptions Defines three system roles: ADMIN, EMPLOYEE, CUSTOMER --- .../auth_service/entity/RoleName.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java new file mode 100644 index 0000000..9221107 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java @@ -0,0 +1,23 @@ +package com.techtorque.auth_service.entity; + +/** + * Enum defining the three roles in the system + * ADMIN - Full system access + * EMPLOYEE - Limited access for staff operations + * CUSTOMER - Access to customer-specific features + */ +public enum RoleName { + ADMIN("Administrator - Full system access"), + EMPLOYEE("Employee - Limited system access for staff operations"), + CUSTOMER("Customer - Access to customer-specific features"); + + private final String description; + + RoleName(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} \ No newline at end of file From d1de5cc2ff901b052c1169b6b8d806d92b62a6f8 Mon Sep 17 00:00:00 2001 From: Suweka Date: Wed, 24 Sep 2025 23:43:43 +0530 Subject: [PATCH 02/24] Update pom.xml Dependencies - Add/restore dependencies: * spring-boot-starter-{web,data-jpa,security,validation} * org.postgresql:postgresql (runtime) + com.h2database:h2 (runtime) * io.jsonwebtoken:jjwt-{api,impl,jackson} 0.11.5 * org.projectlombok:lombok 1.18.32 * springdoc-openapi-starter-webmvc-ui 2.8.13 * test deps: spring-boot-starter-test, spring-security-test --- auth-service/pom.xml | 227 +++++++++++++++++++++++++------------------ 1 file changed, 130 insertions(+), 97 deletions(-) diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 295687d..0ec9015 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -1,104 +1,137 @@ - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.6 - - - com.techtorque - auth-service - 0.0.1-SNAPSHOT - auth-service - auth-service of techtorque - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.postgresql - postgresql - runtime - - - org.projectlombok - lombok - true - + + 4.0.0 - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - io.jsonwebtoken - jjwt-api - 0.12.5 - - - io.jsonwebtoken - jjwt-impl - 0.12.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.12.5 - runtime - + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.13 - + com.techtorque + auth-service + 0.0.1-SNAPSHOT + auth-service + Authentication Service with JWT and Role-based Security - + + 21 + UTF-8 + 0.11.5 + 1.18.32 + - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-starter-validation + + + org.postgresql + postgresql + runtime + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.h2database + h2 + runtime + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.projectlombok + lombok + ${lombok.version} + true + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.13 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + From 97e20cc02ca96c383a3fa9967698a1bb43378006 Mon Sep 17 00:00:00 2001 From: Suweka Date: Wed, 24 Sep 2025 23:51:35 +0530 Subject: [PATCH 03/24] config(app): clean application.properties for PostgreSQL - Replace mixed/dev settings with a single, clean Postgres config - Keep server.port=8081 - Use jwt.expiration=86400000 (fix incorrect key 'jwt.expiration.time') - Explicit Postgres driver and Hibernate dialect - Enable SQL logging and pretty formatting - JWT secret pulled from env with secure default placeholder --- .../src/main/resources/application.properties | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties index 054e02c..445b012 100644 --- a/auth-service/src/main/resources/application.properties +++ b/auth-service/src/main/resources/application.properties @@ -1,12 +1,12 @@ +# Application Configuration spring.application.name=auth-service - server.port=8081 # JWT Configuration jwt.secret=${JWT_SECRET:YourSuperSecretKeyForJWTGoesHereAndItMustBeVeryLongForSecurityPurposes} -jwt.expiration.time=86400000 +jwt.expiration=86400000 -# Database Configuration for local testing, it is recommended to make a database, using the following name, and make a user with the given username and password. +# PostgreSQL Database Configuration spring.datasource.url=jdbc:postgresql://localhost:5432/techtorque spring.datasource.username=techtorque spring.datasource.password=techtorque123 @@ -16,4 +16,8 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file +spring.jpa.properties.hibernate.format_sql=true + +# Logging (optional but helpful for debugging) +logging.level.com.techtorque.auth_service=DEBUG +logging.level.org.springframework.security=DEBUG \ No newline at end of file From 9f3ee2d77511bf9c656d8661604d384273d2f75c Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:12:13 +0530 Subject: [PATCH 04/24] Add Role mapping, enforce unique email, and default timestamps --- .../techtorque/auth_service/entity/User.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java index 6a19307..2273d18 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java @@ -7,7 +7,13 @@ import lombok.Builder; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +/** + * User entity representing users in the authentication system + * Contains user credentials and role assignments + */ @Entity @Table(name = "users") @Data @@ -26,7 +32,7 @@ public class User { @Column(nullable = false) private String password; - @Column(nullable = false) + @Column(unique = true, nullable = false) private String email; @Column(nullable = false) @@ -37,11 +43,28 @@ public class User { @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); + // Many-to-Many relationship with Role entity + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + @Builder.Default + private Set roles = new HashSet<>(); + + // Constructor for easy user creation public User(String username, String password, String email) { this.username = username; this.password = password; this.email = email; this.enabled = true; this.createdAt = LocalDateTime.now(); + this.roles = new HashSet<>(); + } + + // Helper method to add roles + public void addRole(Role role) { + this.roles.add(role); } } \ No newline at end of file From 5f1ac539aef0e276530a7b18186a921d96143b9e Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:20:55 +0530 Subject: [PATCH 05/24] Add JPA Role entity with enum-based name and unique constraint --- .../techtorque/auth_service/entity/Role.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java new file mode 100644 index 0000000..8de7d76 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java @@ -0,0 +1,37 @@ +package com.techtorque.auth_service.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * Role entity for managing user roles in the system + * Contains role information and maps to RoleName enum + */ +@Entity +@Table(name = "roles") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(unique = true, nullable = false) + private RoleName name; + + @Column + private String description; + + // Constructor for easy role creation + public Role(RoleName name) { + this.name = name; + this.description = name.getDescription(); + } +} \ No newline at end of file From 81d115a99e88b9b7a141aaf3bbb3e5ef3c75c1fb Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:25:14 +0530 Subject: [PATCH 06/24] Add Role entity and extend UserRepository with role-aware queries --- .../repository/UserRepository.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java b/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java index 3a374e9..6d8d9af 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java @@ -2,16 +2,52 @@ import com.techtorque.auth_service.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; +/** + * Repository interface for User entity operations + * Provides database access methods for user-related queries + */ @Repository public interface UserRepository extends JpaRepository { + /** + * Find user by username + * @param username the username to search for + * @return Optional containing user if found + */ Optional findByUsername(String username); + /** + * Find user by email + * @param email the email to search for + * @return Optional containing user if found + */ + Optional findByEmail(String email); + + /** + * Check if username exists + * @param username the username to check + * @return true if username exists, false otherwise + */ boolean existsByUsername(String username); + /** + * Check if email exists + * @param email the email to check + * @return true if email exists, false otherwise + */ boolean existsByEmail(String email); + + /** + * Find user with roles by username (explicit fetch) + * @param username the username to search for + * @return Optional containing user with roles if found + */ + @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username") + Optional findByUsernameWithRoles(@Param("username") String username); } \ No newline at end of file From 8f8b47543c1595125ccebe72a91b9027559b2193 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:28:36 +0530 Subject: [PATCH 07/24] Add RoleRepository with findByName/existsByName --- .../repository/RoleRepository.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java b/auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java new file mode 100644 index 0000000..2185c65 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java @@ -0,0 +1,30 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * Repository interface for Role entity operations + * Provides database access methods for role-related queries + */ +@Repository +public interface RoleRepository extends JpaRepository { + + /** + * Find role by role name + * @param name the role name to search for + * @return Optional containing role if found + */ + Optional findByName(RoleName name); + + /** + * Check if role exists by name + * @param name the role name to check + * @return true if role exists, false otherwise + */ + boolean existsByName(RoleName name); +} \ No newline at end of file From 0e4d8d3a963fbf6d9999472c85b092a1aaa834e7 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:33:57 +0530 Subject: [PATCH 08/24] Add LoginRequest with validation for username/password --- .../com/techtorque/auth_service/dto/LoginRequest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java index bef262c..040e37a 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java @@ -1,13 +1,21 @@ package com.techtorque.auth_service.dto; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +/** + * DTO for login request containing user credentials + */ @Data @NoArgsConstructor @AllArgsConstructor public class LoginRequest { + + @NotBlank(message = "Username is required") private String username; + + @NotBlank(message = "Password is required") private String password; } \ No newline at end of file From d5d7f560bb67f6aaa79ea138edcfb10a75721cb9 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:38:29 +0530 Subject: [PATCH 09/24] Add LoginResponse with JWT token, user info and roles --- .../auth_service/dto/LoginResponse.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java index 1d3acfc..6ec6644 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java @@ -3,11 +3,31 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +import lombok.Builder; +import java.util.Set; + +/** + * DTO for login response containing JWT token and user information + */ @Data @NoArgsConstructor @AllArgsConstructor +@Builder public class LoginResponse { + private String token; + private String type = "Bearer"; private String username; + private String email; + private Set roles; + + // Constructor without token type (defaults to "Bearer") + public LoginResponse(String token, String username, String email, Set roles) { + this.token = token; + this.type = "Bearer"; + this.username = username; + this.email = email; + this.roles = roles; + } } \ No newline at end of file From e0a247ded34f6f6a9d261f9ef6556b740a340e13 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:46:49 +0530 Subject: [PATCH 10/24] Add RegisterRequest with validation and optional roles - New RegisterRequest DTO for user signup - Fields: username, email, password, roles --- .../auth_service/dto/RegisterRequest.java | 34 ++++++++++++++++++ .../auth_service/util/RegisterRequest.java | 35 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java new file mode 100644 index 0000000..e950f19 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java @@ -0,0 +1,34 @@ +package com.techtorque.auth_service.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Set; + +/** + * DTO for user registration request + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RegisterRequest { + + @NotBlank(message = "Username is required") + @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") + private String username; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 6, max = 40, message = "Password must be between 6 and 40 characters") + private String password; + + // Set of role names to assign to the user (optional) + private Set roles; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java new file mode 100644 index 0000000..f4d37b9 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java @@ -0,0 +1,35 @@ +package com.techtorque.auth_service.util; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Set; + +/** + * DTO for user registration request + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RegisterRequest { + + @NotBlank(message = "Username is required") + @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") + private String username; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 6, max = 40, message = "Password must be between 6 and 40 characters") + private String password; + + // Set of role names to assign to the user (optional) + // Valid values: "admin", "employee", "customer" + private Set roles; +} \ No newline at end of file From c9fc987d3c7dfd558d897b5bece60f6d95ad0e5e Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 00:53:01 +0530 Subject: [PATCH 11/24] Rewrite JwtUtil to add roles claim, robust parsing & Base64 secret --- .../techtorque/auth_service/util/JwtUtil.java | 203 +++++++++++++----- 1 file changed, 154 insertions(+), 49 deletions(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java b/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java index ca05db1..34f27ea 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java @@ -1,61 +1,166 @@ package com.techtorque.auth_service.util; -import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.nio.charset.StandardCharsets; +import java.security.Key; import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.List; +/** + * Utility class for JWT token operations + * Handles token generation, validation, and extraction of claims + */ @Component public class JwtUtil { - - @Value("${jwt.secret}") - private String secret; - - @Value("${jwt.expiration.time}") - private long expirationTime; - - public String generateToken(String username) { - SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - - return Jwts.builder() - .subject(username) - .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + expirationTime)) - .signWith(key) - .compact(); - } - - public boolean validateToken(String token, String username) { - try { - SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - String tokenUsername = Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token) - .getPayload() - .getSubject(); - - return tokenUsername.equals(username); - } catch (Exception e) { - return false; + + private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); + + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.expiration}") + private int jwtExpirationMs; + + /** + * Generate JWT token for user with roles + * @param userDetails Spring Security UserDetails object + * @param roles List of user roles + * @return JWT token string + */ + public String generateJwtToken(UserDetails userDetails, List roles) { + Map claims = new HashMap<>(); + claims.put("roles", roles); + return generateToken(claims, userDetails.getUsername()); + } + + /** + * Generate JWT token with custom claims + * @param extraClaims Additional claims to include in token + * @param username Username for the token subject + * @return JWT token string + */ + public String generateToken(Map extraClaims, String username) { + return Jwts.builder() + .setClaims(extraClaims) + .setSubject(username) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + + /** + * Extract username from JWT token + * @param token JWT token + * @return Username + */ + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + /** + * Extract expiration date from JWT token + * @param token JWT token + * @return Expiration date + */ + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + /** + * Extract roles from JWT token + * @param token JWT token + * @return List of roles + */ + @SuppressWarnings("unchecked") + public List extractRoles(String token) { + return extractClaim(token, claims -> (List) claims.get("roles")); + } + + /** + * Extract specific claim from JWT token + * @param token JWT token + * @param claimsResolver Function to extract specific claim + * @return Extracted claim + */ + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * Extract all claims from JWT token + * @param token JWT token + * @return All claims + */ + private Claims extractAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * Check if JWT token is expired + * @param token JWT token + * @return true if expired, false otherwise + */ + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + /** + * Validate JWT token against UserDetails + * @param token JWT token + * @param userDetails Spring Security UserDetails + * @return true if valid, false otherwise + */ + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + /** + * Validate JWT token structure and signature + * @param token JWT token + * @return true if valid, false otherwise + */ + public boolean validateJwtToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + return false; + } + + /** + * Get signing key for JWT token + * @return Signing key + */ + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); } - } - - public String extractUsername(String token) { - try { - SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - return Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token) - .getPayload() - .getSubject(); - } catch (Exception e) { - return null; - } - } } \ No newline at end of file From 119503cbe7e4aed4416dce60841642fec7febc13 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 21:42:12 +0530 Subject: [PATCH 12/24] Update AuthService for JWT login and user registration --- .../auth_service/service/AuthService.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java new file mode 100644 index 0000000..f523d07 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java @@ -0,0 +1,151 @@ +package com.techtorque.auth_service.service; + +import com.techtorque.auth_service.dto.LoginRequest; +import com.techtorque.auth_service.dto.LoginResponse; +import com.techtorque.auth_service.dto.RegisterRequest; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.RoleRepository; +import com.techtorque.auth_service.repository.UserRepository; +import com.techtorque.auth_service.util.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Service class for handling authentication operations + * Manages user login, registration, and JWT token generation + */ +@Service +@Transactional +public class AuthService { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtUtil jwtUtil; + + /** + * Authenticate user and generate JWT token + * @param loginRequest Login credentials + * @return LoginResponse with JWT token and user details + */ + public LoginResponse authenticateUser(LoginRequest loginRequest) { + // Authenticate user credentials + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), + loginRequest.getPassword() + ) + ); + + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + + // Extract roles from authorities + List roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(auth -> auth.replace("ROLE_", "")) // Remove ROLE_ prefix + .collect(Collectors.toList()); + + // Generate JWT token + String jwt = jwtUtil.generateJwtToken(userDetails, roles); + + // Get user details for response + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + Set roleNames = user.getRoles().stream() + .map(role -> role.getName().name()) + .collect(Collectors.toSet()); + + return LoginResponse.builder() + .token(jwt) + .username(user.getUsername()) + .email(user.getEmail()) + .roles(roleNames) + .build(); + } + + /** + * Register a new user with specified roles + * @param registerRequest Registration details + * @return Success message + */ + public String registerUser(RegisterRequest registerRequest) { + // Check if username already exists + if (userRepository.existsByUsername(registerRequest.getUsername())) { + throw new RuntimeException("Error: Username is already taken!"); + } + + // Check if email already exists + if (userRepository.existsByEmail(registerRequest.getEmail())) { + throw new RuntimeException("Error: Email is already in use!"); + } + + // Create new user + User user = User.builder() + .username(registerRequest.getUsername()) + .email(registerRequest.getEmail()) + .password(passwordEncoder.encode(registerRequest.getPassword())) + .enabled(true) + .roles(new HashSet<>()) + .build(); + + // Assign roles + Set strRoles = registerRequest.getRoles(); + Set roles = new HashSet<>(); + + if (strRoles == null || strRoles.isEmpty()) { + // Default role is CUSTOMER + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new RuntimeException("Error: Customer Role not found.")); + roles.add(customerRole); + } else { + strRoles.forEach(roleName -> { + switch (roleName) { + case "admin": + Role adminRole = roleRepository.findByName(RoleName.ADMIN) + .orElseThrow(() -> new RuntimeException("Error: Admin Role not found.")); + roles.add(adminRole); + break; + case "employee": + Role employeeRole = roleRepository.findByName(RoleName.EMPLOYEE) + .orElseThrow(() -> new RuntimeException("Error: Employee Role not found.")); + roles.add(employeeRole); + break; + default: + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new RuntimeException("Error: Customer Role not found.")); + roles.add(customerRole); + } + }); + } + + user.setRoles(roles); + userRepository.save(user); + + return "User registered successfully!"; + } +} \ No newline at end of file From 938a38b57bb76cec5514501defea6afc3f86783e Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 22:27:21 +0530 Subject: [PATCH 13/24] Update AuthController --- .../controller/AuthController.java | 90 ++++++++++++++----- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java b/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java index 9cfb63e..d7a3f41 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java @@ -2,42 +2,92 @@ import com.techtorque.auth_service.dto.LoginRequest; import com.techtorque.auth_service.dto.LoginResponse; -import com.techtorque.auth_service.service.UserService; -import com.techtorque.auth_service.util.JwtUtil; +import com.techtorque.auth_service.dto.RegisterRequest; +import com.techtorque.auth_service.service.AuthService; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +/** + * REST Controller for authentication endpoints + * Handles login, registration, and health check requests + */ @RestController -@RequestMapping("/") +@RequestMapping("/api/auth") +@CrossOrigin(origins = "*", maxAge = 3600) public class AuthController { @Autowired - private UserService userService; - - @Autowired - private JwtUtil jwtUtil; + private AuthService authService; + /** + * User login endpoint + * @param loginRequest Login credentials + * @return JWT token and user details + */ @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { + try { + LoginResponse loginResponse = authService.authenticateUser(loginRequest); + return ResponseEntity.ok(loginResponse); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new MessageResponse("Error: " + e.getMessage())); + } + } + + /** + * User registration endpoint + * @param registerRequest Registration details + * @return Success message + */ + @PostMapping("/register") + public ResponseEntity registerUser(@Valid @RequestBody RegisterRequest registerRequest) { try { - if (userService.authenticate(loginRequest.getUsername(), loginRequest.getPassword())) { - String token = jwtUtil.generateToken(loginRequest.getUsername()); - LoginResponse response = new LoginResponse(token, loginRequest.getUsername()); - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("{\"error\":\"Invalid credentials\"}"); - } + String message = authService.registerUser(registerRequest); + return ResponseEntity.ok(new MessageResponse(message)); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("{\"error\":\"Internal server error\"}"); + return ResponseEntity.badRequest() + .body(new MessageResponse("Error: " + e.getMessage())); } } + /** + * Health check endpoint + * @return Service status + */ @GetMapping("/health") - public ResponseEntity health() { - return ResponseEntity.ok("{\"status\":\"UP\"}"); + public ResponseEntity health() { + return ResponseEntity.ok(new MessageResponse("Authentication Service is running!")); + } + + /** + * Test endpoint for authenticated users + * @return Test message + */ + @GetMapping("/test") + public ResponseEntity test() { + return ResponseEntity.ok(new MessageResponse("Test endpoint accessible!")); + } + + /** + * Inner class for simple message responses + */ + public static class MessageResponse { + private String message; + + public MessageResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } } } \ No newline at end of file From e9d14175a681d6c0fe1ff95a9a3be3b48d7c45a6 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 23:27:02 +0530 Subject: [PATCH 14/24] Fix UserService.java --- .../auth_service/service/UserService.java | 124 ++++++++++++++---- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java index ca1608c..734e2a7 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java @@ -3,44 +3,116 @@ import com.techtorque.auth_service.entity.User; import com.techtorque.auth_service.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; +import java.util.Collection; +import java.util.stream.Collectors; +/** + * Service class implementing Spring Security UserDetailsService + * Handles user authentication and authorization + */ @Service -public class UserService { +public class UserService implements UserDetailsService { @Autowired private UserRepository userRepository; - public boolean authenticate(String username, String password) { - Optional userOpt = userRepository.findByUsername(username); - if (userOpt.isPresent()) { - User user = userOpt.get(); - // For now, we'll do plain text password comparison - // In production, you should use BCrypt or similar - return user.getPassword().equals(password) && user.getEnabled(); - } - return false; - } - - public boolean userExists(String username) { - return userRepository.existsByUsername(username); + /** + * Load user by username for Spring Security authentication + * @param username Username to load + * @return UserDetails object for Spring Security + * @throws UsernameNotFoundException if user not found + */ + @Override + @Transactional + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsernameWithRoles(username) + .orElseThrow(() -> new UsernameNotFoundException("User Not Found: " + username)); + + return UserPrincipal.create(user); } - public User createUser(String username, String password, String email) { - if (userRepository.existsByUsername(username)) { - throw new RuntimeException("Username already exists"); + /** + * Inner class representing user principal for Spring Security + */ + public static class UserPrincipal implements UserDetails { + private Long id; + private String username; + private String email; + private String password; + private Collection authorities; + + public UserPrincipal(Long id, String username, String email, String password, + Collection authorities) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.authorities = authorities; } - if (userRepository.existsByEmail(email)) { - throw new RuntimeException("Email already exists"); + + public static UserPrincipal create(User user) { + Collection authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName().name())) + .collect(Collectors.toList()); + + return new UserPrincipal( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + authorities + ); } - User user = new User(username, password, email); - return userRepository.save(user); - } - - public Optional findByUsername(String username) { - return userRepository.findByUsername(username); + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } } } \ No newline at end of file From 84ed0ffb3a7df822c907a1de78735ea0a3f854ec Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 23:27:56 +0530 Subject: [PATCH 15/24] Update Dataseeder.java --- .../auth_service/config/DataSeeder.java | 90 ++++++++++++++++--- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java b/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java index 3127c65..2faf9e0 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java @@ -1,49 +1,115 @@ package com.techtorque.auth_service.config; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.RoleRepository; import com.techtorque.auth_service.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import java.util.HashSet; +import java.util.Set; + +/** + * Data seeder to initialize roles and default users with proper security + * Runs at application startup to ensure required data exists + */ @Component public class DataSeeder implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(DataSeeder.class); + @Autowired + private RoleRepository roleRepository; + @Autowired private UserRepository userRepository; + @Autowired + private PasswordEncoder passwordEncoder; + @Override public void run(String... args) throws Exception { + logger.info("Starting data seeding..."); + + // First, create roles if they don't exist + seedRoles(); + + // Then, seed users with proper roles seedUsers(); + + logger.info("Data seeding completed successfully!"); + } + + /** + * Create all required roles in the system + */ + private void seedRoles() { + createRoleIfNotExists(RoleName.ADMIN); + createRoleIfNotExists(RoleName.EMPLOYEE); + createRoleIfNotExists(RoleName.CUSTOMER); } + /** + * Create role if it doesn't exist + * @param roleName Role name to create + */ + private void createRoleIfNotExists(RoleName roleName) { + if (!roleRepository.existsByName(roleName)) { + Role role = new Role(roleName); + roleRepository.save(role); + logger.info("Created role: {}", roleName); + } + } + + /** + * Create default users with proper password encoding and role assignments + */ private void seedUsers() { - logger.info("Starting data seeding..."); - // Check if users already exist to avoid duplicates if (userRepository.count() > 0) { - logger.info("Users already exist in database. Skipping seeding."); + logger.info("Users already exist in database. Skipping user seeding."); return; } - // Create default test users - createUserIfNotExists("user", "password", "user@techtorque.com"); - createUserIfNotExists("admin", "admin123", "admin@techtorque.com"); - createUserIfNotExists("testuser", "test123", "test@techtorque.com"); - createUserIfNotExists("demo", "demo123", "demo@techtorque.com"); + // Create default test users with roles + createUserWithRole("admin", "admin123", "admin@techtorque.com", RoleName.ADMIN); + createUserWithRole("employee", "emp123", "employee@techtorque.com", RoleName.EMPLOYEE); + createUserWithRole("customer", "cust123", "customer@techtorque.com", RoleName.CUSTOMER); - logger.info("Data seeding completed successfully!"); + // Keep your original test users as customers + createUserWithRole("user", "password", "user@techtorque.com", RoleName.CUSTOMER); + createUserWithRole("testuser", "test123", "test@techtorque.com", RoleName.CUSTOMER); + createUserWithRole("demo", "demo123", "demo@techtorque.com", RoleName.CUSTOMER); } - private void createUserIfNotExists(String username, String password, String email) { + /** + * Create user with encoded password and assigned role + * @param username Username for the user + * @param password Plain text password (will be encoded) + * @param email User's email + * @param roleName Role to assign to the user + */ + private void createUserWithRole(String username, String password, String email, RoleName roleName) { if (!userRepository.existsByUsername(username)) { - User user = new User(username, password, email); + // Create user with encoded password + User user = new User(username, passwordEncoder.encode(password), email); + + // Assign role + Set roles = new HashSet<>(); + Role role = roleRepository.findByName(roleName) + .orElseThrow(() -> new RuntimeException("Role " + roleName + " not found")); + roles.add(role); + user.setRoles(roles); + + // Save user userRepository.save(user); - logger.info("Created user: {} with email: {}", username, email); + logger.info("Created user: {} with email: {} and role: {}", username, email, roleName); } else { logger.info("User {} already exists, skipping...", username); } From b7d57be39ed77684439d6ac2c9720b326a4e3fa1 Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 23:28:20 +0530 Subject: [PATCH 16/24] Update SecurityConfig.java --- .../auth_service/config/SecurityConfig.java | 100 ++++++++++++++---- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java index c23f45d..3d27585 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java @@ -1,35 +1,99 @@ package com.techtorque.auth_service.config; +import com.techtorque.auth_service.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.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; +/** + * Security configuration class + * Configures JWT-based authentication and authorization + */ @Configuration @EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { + @Autowired + private UserService userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + /** + * JWT authentication filter bean + * @return AuthTokenFilter instance + */ + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + /** + * Password encoder bean using BCrypt + * @return BCryptPasswordEncoder instance + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Authentication provider bean + * @return DaoAuthenticationProvider configured with UserDetailsService and PasswordEncoder + */ + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + /** + * Authentication manager bean + * @param authConfig Authentication configuration + * @return AuthenticationManager instance + * @throws Exception if configuration fails + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + /** + * Security filter chain configuration + * @param http HttpSecurity configuration + * @return SecurityFilterChain configured for JWT authentication + * @throws Exception if configuration fails + */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authz -> authz - // Allow unauthenticated access to login and health endpoints - .requestMatchers("/login", "/health").permitAll() - // Allow unauthenticated access to OpenAPI and Swagger UI endpoints - .requestMatchers( - "/v3/api-docs/**", - "/v3/api-docs", - "/swagger-ui/**", - "/swagger-ui.html", - "/swagger-ui/index.html", - "/swagger-ui/index.html/**", - "/swagger-resources/**", - "/webjars/**" - ).permitAll() - .anyRequest().authenticated() - ); + http.cors().and().csrf().disable() + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .authorizeHttpRequests() + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/h2-console/**").permitAll() + .requestMatchers("/api/auth/test").authenticated() + .anyRequest().authenticated(); + + // Disable frame options for H2 console + http.headers().frameOptions().disable(); + + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } From 18559233249a5c7b1e54ae66dbfae1ae674e431b Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 23:28:48 +0530 Subject: [PATCH 17/24] Create AuthTokenFilter.java --- .../auth_service/config/AuthTokenFilter.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java new file mode 100644 index 0000000..32fc015 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java @@ -0,0 +1,72 @@ +package com.techtorque.auth_service.config; + +import com.techtorque.auth_service.service.UserService; +import com.techtorque.auth_service.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * JWT Authentication Filter + * Processes JWT tokens from HTTP requests and sets up Spring Security context + */ +public class AuthTokenFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private UserService userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtil.validateJwtToken(jwt)) { + String username = jwtUtil.extractUsername(jwt); + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, + userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e); + } + + filterChain.doFilter(request, response); + } + + /** + * Extract JWT token from Authorization header + * @param request HTTP request + * @return JWT token or null if not found + */ + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + + return null; + } +} \ No newline at end of file From 8f0256adc26a9176f143e6ed23bba2edced8dccb Mon Sep 17 00:00:00 2001 From: Suweka Date: Thu, 25 Sep 2025 23:29:33 +0530 Subject: [PATCH 18/24] Create AuthEntryPointJwt.java --- .../config/AuthEntryPointJwt.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java new file mode 100644 index 0000000..95a2b9e --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java @@ -0,0 +1,44 @@ +package com.techtorque.auth_service.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT Authentication Entry Point + * Handles unauthorized access attempts + */ +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + logger.error("Unauthorized error: {}", authException.getMessage()); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} \ No newline at end of file From 26adcbf7d84831725356ff91573e0228c6217a48 Mon Sep 17 00:00:00 2001 From: Suweka Date: Fri, 26 Sep 2025 17:50:13 +0530 Subject: [PATCH 19/24] Create Permission entity to represent individual permissions in the system --- .../auth_service/entity/Permission.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java new file mode 100644 index 0000000..ea9f618 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java @@ -0,0 +1,37 @@ +package com.techtorque.auth_service.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * Permission entity to represent individual permissions in the system + * Each permission represents a specific action that can be performed + */ +@Entity +@Table(name = "permissions") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Permission { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // Unique permission name (e.g., CREATE_USER, VIEW_REPORTS) + @Column(unique = true, nullable = false) + private String name; + + // Human-readable description of what this permission allows + private String description; + + // Many-to-Many relationship with Role - a permission can be assigned to multiple roles + @ManyToMany(mappedBy = "permissions") + private Set roles; +} \ No newline at end of file From 09a94fdfb66edd5a3fe6eee8709a9ed91b9693e8 Mon Sep 17 00:00:00 2001 From: Suweka Date: Fri, 26 Sep 2025 18:08:19 +0530 Subject: [PATCH 20/24] Create Class containing all permission names used in the system --- .../entity/PermissionConstants.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java new file mode 100644 index 0000000..8884696 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java @@ -0,0 +1,39 @@ +package com.techtorque.auth_service.entity; + +/** + * Constants class containing all permission names used in the system + * This centralizes permission management and prevents typos + */ +public class PermissionConstants { + + // ============ ADMIN PERMISSIONS ============ + // User management permissions - only admins can create/update/delete users + public static final String CREATE_EMPLOYEE = "CREATE_EMPLOYEE"; // Only admins can create employees + public static final String CREATE_ADMIN = "CREATE_ADMIN"; // Only admins can create other admins + public static final String UPDATE_USER = "UPDATE_USER"; + public static final String DELETE_USER = "DELETE_USER"; + public static final String VIEW_ALL_USERS = "VIEW_ALL_USERS"; + + // Role management - only admins can assign/remove roles + public static final String MANAGE_ROLES = "MANAGE_ROLES"; + + // System administration + public static final String SYSTEM_ADMIN = "SYSTEM_ADMIN"; + + // ============ EMPLOYEE PERMISSIONS ============ + // Employee can view and update customer data for support purposes + public static final String VIEW_CUSTOMER_DATA = "VIEW_CUSTOMER_DATA"; + public static final String UPDATE_CUSTOMER_DATA = "UPDATE_CUSTOMER_DATA"; + + // Employee can access business reports + public static final String VIEW_REPORTS = "VIEW_REPORTS"; + + // ============ CUSTOMER PERMISSIONS ============ + // Basic profile management - all users can view/update their own profile + public static final String VIEW_OWN_PROFILE = "VIEW_OWN_PROFILE"; + public static final String UPDATE_OWN_PROFILE = "UPDATE_OWN_PROFILE"; + + // Customer-specific actions + public static final String PLACE_ORDER = "PLACE_ORDER"; + public static final String VIEW_ORDER_HISTORY = "VIEW_ORDER_HISTORY"; +} \ No newline at end of file From ce6a61384a1f8996dc4b573144dfd0ef57e99f23 Mon Sep 17 00:00:00 2001 From: Suweka Date: Fri, 26 Sep 2025 21:40:29 +0530 Subject: [PATCH 21/24] Implement the restricted registration system Overview of Changes: Public Registration: Only allows CUSTOMER role Admin-only Employee Creation: Only admins can create employee accounts Data Seeder: Creates default admin account Enhanced Security: Proper role-based access control --- .../auth_service/config/DataSeeder.java | 3 +- .../auth_service/config/SecurityConfig.java | 36 +- .../auth_service/dto/CreateAdminRequest.java | 25 ++ .../dto/CreateEmployeeRequest.java | 26 ++ .../auth_service/dto/LoginResponse.java | 4 - .../techtorque/auth_service/dto/UserDto.java | 28 ++ .../techtorque/auth_service/entity/Role.java | 33 +- .../repository/PermissionRepository.java | 39 ++ .../auth_service/service/AuthService.java | 27 +- .../auth_service/service/UserService.java | 333 ++++++++++++++---- 10 files changed, 406 insertions(+), 148 deletions(-) create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java create mode 100644 auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java b/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java index 2faf9e0..0339a2a 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java @@ -61,7 +61,8 @@ private void seedRoles() { */ private void createRoleIfNotExists(RoleName roleName) { if (!roleRepository.existsByName(roleName)) { - Role role = new Role(roleName); + Role role = new Role(); // Use default constructor + role.setName(roleName); // Set the role name roleRepository.save(role); logger.info("Created role: {}", roleName); } diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java index 3d27585..0814494 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java @@ -16,10 +16,6 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -/** - * Security configuration class - * Configures JWT-based authentication and authorization - */ @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) @@ -31,28 +27,16 @@ public class SecurityConfig { @Autowired private AuthEntryPointJwt unauthorizedHandler; - /** - * JWT authentication filter bean - * @return AuthTokenFilter instance - */ @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } - /** - * Password encoder bean using BCrypt - * @return BCryptPasswordEncoder instance - */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - /** - * Authentication provider bean - * @return DaoAuthenticationProvider configured with UserDetailsService and PasswordEncoder - */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @@ -61,37 +45,21 @@ public DaoAuthenticationProvider authenticationProvider() { return authProvider; } - /** - * Authentication manager bean - * @param authConfig Authentication configuration - * @return AuthenticationManager instance - * @throws Exception if configuration fails - */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } - /** - * Security filter chain configuration - * @param http HttpSecurity configuration - * @return SecurityFilterChain configured for JWT authentication - * @throws Exception if configuration fails - */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeHttpRequests() - .requestMatchers("/api/auth/**").permitAll() - .requestMatchers("/h2-console/**").permitAll() - .requestMatchers("/api/auth/test").authenticated() + .requestMatchers("/auth/**").permitAll() // Fixed path mapping + .requestMatchers("/auth/test").authenticated() .anyRequest().authenticated(); - // Disable frame options for H2 console - http.headers().frameOptions().disable(); - http.authenticationProvider(authenticationProvider()); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java new file mode 100644 index 0000000..c628c42 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java @@ -0,0 +1,25 @@ +package com.techtorque.auth_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request DTO for creating admin accounts + * Only existing admins can use this endpoint + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CreateAdminRequest { + + private String username; + private String email; + private String password; + + // Optional: Additional admin-specific fields + private String firstName; + private String lastName; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java new file mode 100644 index 0000000..f8713db --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java @@ -0,0 +1,26 @@ +package com.techtorque.auth_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request DTO for creating employee accounts + * Only admins can use this endpoint + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CreateEmployeeRequest { + + private String username; + private String email; + private String password; + + // Optional: Additional employee-specific fields + private String firstName; + private String lastName; + private String department; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java index 6ec6644..975c979 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java @@ -7,9 +7,6 @@ import java.util.Set; -/** - * DTO for login response containing JWT token and user information - */ @Data @NoArgsConstructor @AllArgsConstructor @@ -22,7 +19,6 @@ public class LoginResponse { private String email; private Set roles; - // Constructor without token type (defaults to "Bearer") public LoginResponse(String token, String username, String email, Set roles) { this.token = token; this.type = "Bearer"; diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java new file mode 100644 index 0000000..14d909f --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java @@ -0,0 +1,28 @@ +package com.techtorque.auth_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * Data Transfer Object for User information + * Used to transfer user data without exposing sensitive information + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserDto { + + private Long id; + private String username; + private String email; + private Boolean enabled; + private LocalDateTime createdAt; + private Set roles; // Role names as strings + private Set permissions; // Permission names as strings +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java index 8de7d76..f65ab4b 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java @@ -1,14 +1,16 @@ package com.techtorque.auth_service.entity; import jakarta.persistence.*; -import lombok.Data; -import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; /** - * Role entity for managing user roles in the system - * Contains role information and maps to RoleName enum + * Role entity representing user roles in the system + * Each role contains multiple permissions that define what actions can be performed */ @Entity @Table(name = "roles") @@ -22,16 +24,25 @@ public class Role { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Enumerated(EnumType.STRING) + // Role name from the RoleName enum (ADMIN, EMPLOYEE, CUSTOMER) @Column(unique = true, nullable = false) + @Enumerated(EnumType.STRING) private RoleName name; - @Column + // Human-readable description of the role private String description; - // Constructor for easy role creation - public Role(RoleName name) { - this.name = name; - this.description = name.getDescription(); - } + // Many-to-Many relationship with User - a role can be assigned to multiple users + @ManyToMany(mappedBy = "roles") + private Set users; + + // Many-to-Many relationship with Permission - a role contains multiple permissions + // EAGER fetch ensures permissions are loaded when we load a role + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "role_permissions", // Junction table name + joinColumns = @JoinColumn(name = "role_id"), // Foreign key to role + inverseJoinColumns = @JoinColumn(name = "permission_id") // Foreign key to permission + ) + private Set permissions; } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java b/auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java new file mode 100644 index 0000000..4ff500c --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java @@ -0,0 +1,39 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Permission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.Set; + +/** + * Repository interface for Permission entity + * Provides database operations for permission management + */ +@Repository +public interface PermissionRepository extends JpaRepository { + + /** + * Find a permission by its name + * @param name The permission name to search for (e.g., "CREATE_EMPLOYEE") + * @return Optional containing the permission if found + */ + Optional findByName(String name); + + /** + * Find multiple permissions by their names + * Useful when assigning multiple permissions to a role + * @param names Set of permission names to search for + * @return Set of permissions found + */ + Set findByNameIn(Set names); + + /** + * Check if a permission exists by name + * Useful for validation before creating new permissions + * @param name The permission name to check + * @return true if permission exists, false otherwise + */ + boolean existsByName(String name); +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java index f523d07..d085215 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java @@ -24,10 +24,6 @@ import java.util.Set; import java.util.stream.Collectors; -/** - * Service class for handling authentication operations - * Manages user login, registration, and JWT token generation - */ @Service @Transactional public class AuthService { @@ -47,13 +43,7 @@ public class AuthService { @Autowired private JwtUtil jwtUtil; - /** - * Authenticate user and generate JWT token - * @param loginRequest Login credentials - * @return LoginResponse with JWT token and user details - */ public LoginResponse authenticateUser(LoginRequest loginRequest) { - // Authenticate user credentials Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), @@ -63,16 +53,13 @@ public LoginResponse authenticateUser(LoginRequest loginRequest) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - // Extract roles from authorities List roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) - .map(auth -> auth.replace("ROLE_", "")) // Remove ROLE_ prefix + .map(auth -> auth.replace("ROLE_", "")) .collect(Collectors.toList()); - // Generate JWT token String jwt = jwtUtil.generateJwtToken(userDetails, roles); - // Get user details for response User user = userRepository.findByUsername(userDetails.getUsername()) .orElseThrow(() -> new RuntimeException("User not found")); @@ -83,28 +70,20 @@ public LoginResponse authenticateUser(LoginRequest loginRequest) { return LoginResponse.builder() .token(jwt) .username(user.getUsername()) - .email(user.getEmail()) + .email(user.getEmail()) // This was missing in the error .roles(roleNames) .build(); } - /** - * Register a new user with specified roles - * @param registerRequest Registration details - * @return Success message - */ public String registerUser(RegisterRequest registerRequest) { - // Check if username already exists if (userRepository.existsByUsername(registerRequest.getUsername())) { throw new RuntimeException("Error: Username is already taken!"); } - // Check if email already exists if (userRepository.existsByEmail(registerRequest.getEmail())) { throw new RuntimeException("Error: Email is already in use!"); } - // Create new user User user = User.builder() .username(registerRequest.getUsername()) .email(registerRequest.getEmail()) @@ -113,12 +92,10 @@ public String registerUser(RegisterRequest registerRequest) { .roles(new HashSet<>()) .build(); - // Assign roles Set strRoles = registerRequest.getRoles(); Set roles = new HashSet<>(); if (strRoles == null || strRoles.isEmpty()) { - // Default role is CUSTOMER Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) .orElseThrow(() -> new RuntimeException("Error: Customer Role not found.")); roles.add(customerRole); diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java index 734e2a7..c8027b3 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java @@ -1,118 +1,305 @@ package com.techtorque.auth_service.service; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.RoleRepository; import com.techtorque.auth_service.repository.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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 org.springframework.transaction.annotation.Transactional; -import java.util.Collection; +import java.util.*; import java.util.stream.Collectors; /** - * Service class implementing Spring Security UserDetailsService - * Handles user authentication and authorization + * Service class for user management with restricted registration + * - Only customers can register publicly + * - Only admins can create employees and other admins + * - Implements Spring Security's UserDetailsService for authentication */ @Service +@RequiredArgsConstructor +@Transactional public class UserService implements UserDetailsService { - - @Autowired - private UserRepository userRepository; - + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + /** * Load user by username for Spring Security authentication - * @param username Username to load - * @return UserDetails object for Spring Security + * This method is called during login to authenticate the user + * @param username The username to authenticate + * @return UserDetails object with user info and authorities * @throws UsernameNotFoundException if user not found */ @Override - @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = userRepository.findByUsernameWithRoles(username) - .orElseThrow(() -> new UsernameNotFoundException("User Not Found: " + username)); - - return UserPrincipal.create(user); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .authorities(getAuthorities(user)) // Convert roles/permissions to Spring Security authorities + .accountExpired(false) + .accountLocked(!user.getEnabled()) // Account locked if user is disabled + .credentialsExpired(false) + .disabled(!user.getEnabled()) + .build(); } - + /** - * Inner class representing user principal for Spring Security + * Convert user roles and permissions to Spring Security GrantedAuthority objects + * This enables role-based and permission-based security checks + * @param user The user whose authorities to build + * @return Collection of granted authorities */ - public static class UserPrincipal implements UserDetails { - private Long id; - private String username; - private String email; - private String password; - private Collection authorities; + private Collection getAuthorities(User user) { + Set authorities = new HashSet<>(); - public UserPrincipal(Long id, String username, String email, String password, - Collection authorities) { - this.id = id; - this.username = username; - this.email = email; - this.password = password; - this.authorities = authorities; - } - - public static UserPrincipal create(User user) { - Collection authorities = user.getRoles().stream() - .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName().name())) - .collect(Collectors.toList()); + // Add role-based authorities (prefixed with ROLE_) + user.getRoles().forEach(role -> { + authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName().name())); - return new UserPrincipal( - user.getId(), - user.getUsername(), - user.getEmail(), - user.getPassword(), - authorities + // Add permission-based authorities (used for @PreAuthorize checks) + role.getPermissions().forEach(permission -> + authorities.add(new SimpleGrantedAuthority(permission.getName())) ); - } + }); - public Long getId() { - return id; + return authorities; + } + + /** + * Register a new customer (public registration) + * Only allows CUSTOMER role creation through public endpoint + * @param username Unique username + * @param email Unique email + * @param password Plain text password (will be encoded) + * @return The created customer user + * @throws RuntimeException if username or email already exists + */ + public User registerCustomer(String username, String email, String password) { + // Validate username doesn't exist + if (userRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("Username already exists: " + username); } - public String getEmail() { - return email; + // Validate email doesn't exist + if (userRepository.findByEmail(email).isPresent()) { + throw new RuntimeException("Email already exists: " + email); } - - @Override - public String getUsername() { - return username; + + // Get CUSTOMER role from database + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new RuntimeException("Customer role not found")); + + // Create user with CUSTOMER role only + User user = User.builder() + .username(username) + .email(email) + .password(passwordEncoder.encode(password)) + .enabled(true) + .roles(Set.of(customerRole)) // Only CUSTOMER role + .build(); + + return userRepository.save(user); + } + + /** + * Create an employee account (admin only) + * Only admins can call this method + * @param username Unique username + * @param email Unique email + * @param password Plain text password (will be encoded) + * @return The created employee user + * @throws RuntimeException if username or email already exists + */ + public User createEmployee(String username, String email, String password) { + // Validate username doesn't exist + if (userRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("Username already exists: " + username); } - @Override - public String getPassword() { - return password; + // Validate email doesn't exist + if (userRepository.findByEmail(email).isPresent()) { + throw new RuntimeException("Email already exists: " + email); } - - @Override - public Collection getAuthorities() { - return authorities; + + // Get EMPLOYEE role from database + Role employeeRole = roleRepository.findByName(RoleName.EMPLOYEE) + .orElseThrow(() -> new RuntimeException("Employee role not found")); + + // Create user with EMPLOYEE role + User user = User.builder() + .username(username) + .email(email) + .password(passwordEncoder.encode(password)) + .enabled(true) + .roles(Set.of(employeeRole)) // Only EMPLOYEE role + .build(); + + return userRepository.save(user); + } + + /** + * Create an admin account (admin only) + * Only existing admins can call this method + * @param username Unique username + * @param email Unique email + * @param password Plain text password (will be encoded) + * @return The created admin user + * @throws RuntimeException if username or email already exists + */ + public User createAdmin(String username, String email, String password) { + // Validate username doesn't exist + if (userRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("Username already exists: " + username); } - @Override - public boolean isAccountNonExpired() { - return true; + // Validate email doesn't exist + if (userRepository.findByEmail(email).isPresent()) { + throw new RuntimeException("Email already exists: " + email); } + + // Get ADMIN role from database + Role adminRole = roleRepository.findByName(RoleName.ADMIN) + .orElseThrow(() -> new RuntimeException("Admin role not found")); + + // Create user with ADMIN role + User user = User.builder() + .username(username) + .email(email) + .password(passwordEncoder.encode(password)) + .enabled(true) + .roles(Set.of(adminRole)) // Only ADMIN role + .build(); + + return userRepository.save(user); + } + + /** + * Find user by username + * @param username Username to search for + * @return Optional containing user if found + */ + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + /** + * Find user by email + * @param email Email to search for + * @return Optional containing user if found + */ + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + /** + * Get all users in the system (admin only) + * @return List of all users + */ + public List findAllUsers() { + return userRepository.findAll(); + } + + /** + * Get all permissions for a user (from all their roles) + * @param username Username to get permissions for + * @return Set of permission names + */ + public Set getUserPermissions(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); - @Override - public boolean isAccountNonLocked() { - return true; - } + return user.getRoles().stream() + .flatMap(role -> role.getPermissions().stream()) + .map(permission -> permission.getName()) + .collect(Collectors.toSet()); + } + + /** + * Get all roles for a user + * @param username Username to get roles for + * @return Set of role names + */ + public Set getUserRoles(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); - @Override - public boolean isCredentialsNonExpired() { - return true; - } + return user.getRoles().stream() + .map(role -> role.getName().name()) + .collect(Collectors.toSet()); + } + + /** + * Enable a user account (admin only) + * @param username Username to enable + */ + public void enableUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + user.setEnabled(true); + userRepository.save(user); + } + + /** + * Disable a user account (admin only) + * @param username Username to disable + */ + public void disableUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + user.setEnabled(false); + userRepository.save(user); + } + + /** + * Delete a user from the system (admin only) + * @param username Username to delete + * @throws RuntimeException if user not found + */ + public void deleteUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + userRepository.delete(user); + } + + /** + * Check if a user has a specific role + * @param username Username to check + * @param roleName Role to check for + * @return true if user has the role + */ + public boolean hasRole(String username, RoleName roleName) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); - @Override - public boolean isEnabled() { - return true; - } + return user.getRoles().stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + /** + * Check if a user has a specific permission + * @param username Username to check + * @param permissionName Permission to check for + * @return true if user has the permission through any of their roles + */ + public boolean hasPermission(String username, String permissionName) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + + return user.getRoles().stream() + .flatMap(role -> role.getPermissions().stream()) + .anyMatch(permission -> permission.getName().equals(permissionName)); } } \ No newline at end of file From b634ab61b7fe81dface0ab331b344b71963c49d9 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 27 Sep 2025 19:03:49 +0530 Subject: [PATCH 22/24] Refactor application.properties for clarity and consistency in configuration --- .../src/main/resources/application.properties | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties index 445b012..054e02c 100644 --- a/auth-service/src/main/resources/application.properties +++ b/auth-service/src/main/resources/application.properties @@ -1,12 +1,12 @@ -# Application Configuration spring.application.name=auth-service + server.port=8081 # JWT Configuration jwt.secret=${JWT_SECRET:YourSuperSecretKeyForJWTGoesHereAndItMustBeVeryLongForSecurityPurposes} -jwt.expiration=86400000 +jwt.expiration.time=86400000 -# PostgreSQL Database Configuration +# Database Configuration for local testing, it is recommended to make a database, using the following name, and make a user with the given username and password. spring.datasource.url=jdbc:postgresql://localhost:5432/techtorque spring.datasource.username=techtorque spring.datasource.password=techtorque123 @@ -16,8 +16,4 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.properties.hibernate.format_sql=true - -# Logging (optional but helpful for debugging) -logging.level.com.techtorque.auth_service=DEBUG -logging.level.org.springframework.security=DEBUG \ No newline at end of file +spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file From 3cc52524dc175fa27ed633c6d143efea32907db5 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 27 Sep 2025 19:04:25 +0530 Subject: [PATCH 23/24] Update pom.xml to align dependencies and properties with project standards --- auth-service/pom.xml | 225 ++++++++++++++++++------------------------- 1 file changed, 96 insertions(+), 129 deletions(-) diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 0ec9015..98b389e 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -1,137 +1,104 @@ - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 3.5.6 - - - - com.techtorque - auth-service - 0.0.1-SNAPSHOT - auth-service - Authentication Service with JWT and Role-based Security - - - 21 - UTF-8 - 0.11.5 - 1.18.32 - - - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-validation - - - - - com.h2database - h2 - runtime - - - - - io.jsonwebtoken - jjwt-api - ${jjwt.version} - - - io.jsonwebtoken - jjwt-impl - ${jjwt.version} - runtime - - - io.jsonwebtoken - jjwt-jackson - ${jjwt.version} - runtime - + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + + com.techtorque + auth-service + 0.0.1-SNAPSHOT + auth-service + auth-service of techtorque + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + - - - org.projectlombok - lombok - ${lombok.version} - true - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + io.jsonwebtoken + jjwt-api + 0.12.5 + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.5 + runtime + - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.13 - + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.13 + - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - + - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - ${java.version} - - - org.projectlombok - lombok - ${lombok.version} - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + - - org.springframework.boot - spring-boot-maven-plugin - - - From 3746645409cb16ae17914948b490146a8e0d7ba0 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 27 Sep 2025 19:22:28 +0530 Subject: [PATCH 24/24] fix(app): Resolve build failures & startup errors This commit addresses a series of critical build and runtime errors that occurred after upgrading dependencies and refactoring the security configuration. The application now compiles, starts, and runs successfully on Spring Boot 3. The primary issues and their resolutions include: - **Compilation Failures in SecurityConfig:** The project failed to compile due to the use of deprecated and removed APIs from Spring Security 6 (e.g., `WebSecurityConfigurerAdapter`, `.and()` chains). - **Fix:** Refactored `SecurityConfig` to use the modern, component-based Lambda DSL for defining the `SecurityFilterChain` bean. - **JWT Library Mismatches:** The `JwtUtil` class was using methods incompatible with the `jjwt` library version specified in the `pom.xml`, causing both `cannot find symbol` errors and deprecation warnings. - **Fix:** Updated `JwtUtil` to use the modern builder patterns from the `jjwt:0.12.x` API for both token generation and parsing. - **Circular Dependency at Runtime:** The application failed to start due to a circular dependency between `SecurityConfig` (which provides the `PasswordEncoder`) and `UserService` (which needs it). - **Fix:** Resolved the cycle by applying the `@Lazy` annotation to the `PasswordEncoder` injection within the `UserService` constructor, deferring its initialization. - **Configuration Property Error:** The application failed to start because it could not find the `jwt.expiration` property. - **Fix:** Corrected a typo in `application.properties` by renaming the key from `jwt.expiration.time` to `jwt.expiration`. --- auth-service/pom.xml | 6 +- .../auth_service/config/SecurityConfig.java | 105 +++---- .../auth_service/service/UserService.java | 10 +- .../techtorque/auth_service/util/JwtUtil.java | 257 ++++++++---------- .../src/main/resources/application.properties | 2 +- 5 files changed, 182 insertions(+), 198 deletions(-) diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 98b389e..31668e3 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -67,18 +67,18 @@ io.jsonwebtoken jjwt-api - 0.12.5 + 0.12.6 io.jsonwebtoken jjwt-impl - 0.12.5 + 0.12.6 runtime io.jsonwebtoken jjwt-jackson - 0.12.5 + 0.12.6 runtime diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java index 0814494..2851528 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java @@ -10,6 +10,7 @@ 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.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; @@ -18,51 +19,63 @@ @Configuration @EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true) +@EnableMethodSecurity // The 'prePostEnabled = true' is the default and not needed public class SecurityConfig { - - @Autowired - private UserService userDetailsService; - - @Autowired - private AuthEntryPointJwt unauthorizedHandler; - - @Bean - public AuthTokenFilter authenticationJwtTokenFilter() { - return new AuthTokenFilter(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public DaoAuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(userDetailsService); - authProvider.setPasswordEncoder(passwordEncoder()); - return authProvider; - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.cors().and().csrf().disable() - .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() - .authorizeHttpRequests() - .requestMatchers("/auth/**").permitAll() // Fixed path mapping - .requestMatchers("/auth/test").authenticated() - .anyRequest().authenticated(); - - http.authenticationProvider(authenticationProvider()); - http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } + + @Autowired + private UserService userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // 1. Disable CSRF and CORS using the new lambda style + .csrf(AbstractHttpConfigurer::disable) + .cors(AbstractHttpConfigurer::disable) // For production, you should configure this properly + + // 2. Set up exception handling + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + + // 3. Set the session management to stateless + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // 4. Set up authorization rules + .authorizeHttpRequests(auth -> auth + // Be specific with your paths. Your controller is likely under /api/v1/auth + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() + .anyRequest().authenticated() + ); + + // 5. Add your custom provider and filter + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java index c8027b3..d8b02de 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java @@ -6,6 +6,8 @@ import com.techtorque.auth_service.repository.RoleRepository; import com.techtorque.auth_service.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -25,7 +27,6 @@ * - Implements Spring Security's UserDetailsService for authentication */ @Service -@RequiredArgsConstructor @Transactional public class UserService implements UserDetailsService { @@ -33,6 +34,13 @@ public class UserService implements UserDetailsService { private final RoleRepository roleRepository; private final PasswordEncoder passwordEncoder; + @Autowired + public UserService(UserRepository userRepository, RoleRepository roleRepository, @Lazy PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + } + /** * Load user by username for Spring Security authentication * This method is called during login to authenticate the user diff --git a/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java b/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java index 34f27ea..97d6388 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java @@ -1,6 +1,11 @@ +// In: /src/main/java/com/techtorque/auth_service/util/JwtUtil.java package com.techtorque.auth_service.util; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import org.slf4j.Logger; @@ -9,158 +14,116 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import java.security.Key; +import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.List; -/** - * Utility class for JWT token operations - * Handles token generation, validation, and extraction of claims - */ @Component public class JwtUtil { - - private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); - - @Value("${jwt.secret}") - private String jwtSecret; - - @Value("${jwt.expiration}") - private int jwtExpirationMs; - - /** - * Generate JWT token for user with roles - * @param userDetails Spring Security UserDetails object - * @param roles List of user roles - * @return JWT token string - */ - public String generateJwtToken(UserDetails userDetails, List roles) { - Map claims = new HashMap<>(); - claims.put("roles", roles); - return generateToken(claims, userDetails.getUsername()); - } - - /** - * Generate JWT token with custom claims - * @param extraClaims Additional claims to include in token - * @param username Username for the token subject - * @return JWT token string - */ - public String generateToken(Map extraClaims, String username) { - return Jwts.builder() - .setClaims(extraClaims) - .setSubject(username) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) - .signWith(getSignInKey(), SignatureAlgorithm.HS256) - .compact(); - } - - /** - * Extract username from JWT token - * @param token JWT token - * @return Username - */ - public String extractUsername(String token) { - return extractClaim(token, Claims::getSubject); - } - - /** - * Extract expiration date from JWT token - * @param token JWT token - * @return Expiration date - */ - public Date extractExpiration(String token) { - return extractClaim(token, Claims::getExpiration); - } - - /** - * Extract roles from JWT token - * @param token JWT token - * @return List of roles - */ - @SuppressWarnings("unchecked") - public List extractRoles(String token) { - return extractClaim(token, claims -> (List) claims.get("roles")); - } - - /** - * Extract specific claim from JWT token - * @param token JWT token - * @param claimsResolver Function to extract specific claim - * @return Extracted claim - */ - public T extractClaim(String token, Function claimsResolver) { - final Claims claims = extractAllClaims(token); - return claimsResolver.apply(claims); - } - - /** - * Extract all claims from JWT token - * @param token JWT token - * @return All claims - */ - private Claims extractAllClaims(String token) { - return Jwts.parserBuilder() - .setSigningKey(getSignInKey()) - .build() - .parseClaimsJws(token) - .getBody(); - } - - /** - * Check if JWT token is expired - * @param token JWT token - * @return true if expired, false otherwise - */ - private Boolean isTokenExpired(String token) { - return extractExpiration(token).before(new Date()); - } - - /** - * Validate JWT token against UserDetails - * @param token JWT token - * @param userDetails Spring Security UserDetails - * @return true if valid, false otherwise - */ - public Boolean validateToken(String token, UserDetails userDetails) { - final String username = extractUsername(token); - return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - /** - * Validate JWT token structure and signature - * @param token JWT token - * @return true if valid, false otherwise - */ - public boolean validateJwtToken(String token) { - try { - Jwts.parserBuilder() - .setSigningKey(getSignInKey()) - .build() - .parseClaimsJws(token); - return true; - } catch (MalformedJwtException e) { - logger.error("Invalid JWT token: {}", e.getMessage()); - } catch (ExpiredJwtException e) { - logger.error("JWT token is expired: {}", e.getMessage()); - } catch (UnsupportedJwtException e) { - logger.error("JWT token is unsupported: {}", e.getMessage()); - } catch (IllegalArgumentException e) { - logger.error("JWT claims string is empty: {}", e.getMessage()); - } - return false; - } - - /** - * Get signing key for JWT token - * @return Signing key - */ - private Key getSignInKey() { - byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); - return Keys.hmacShaKeyFor(keyBytes); + + private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); + + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.expiration}") + private long jwtExpirationMs; + + /** + * Generates a JWT token for a user with their roles. + */ + public String generateJwtToken(UserDetails userDetails, List roles) { + Map claims = new HashMap<>(); + claims.put("roles", roles); + return generateToken(claims, userDetails.getUsername()); + } + + /** + * Creates the token using the modern builder pattern. + * This fixes all the deprecation warnings. + */ + public String generateToken(Map extraClaims, String username) { + Date now = new Date(); + Date expirationDate = new Date(now.getTime() + jwtExpirationMs); + + return Jwts.builder() + .claims(extraClaims) // Modern way to set claims + .subject(username) + .issuedAt(now) + .expiration(expirationDate) + .signWith(getSignInKey()) // Modern way to sign (algorithm is inferred from the key) + .compact(); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + @SuppressWarnings("unchecked") + public List extractRoles(String token) { + return extractClaim(token, claims -> (List) claims.get("roles")); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * Extracts all claims using the modern parser builder. + * This part of your code was already correct. + */ + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + /** + * Validates the token structure and signature without checking expiration against UserDetails. + */ + public boolean validateJwtToken(String token) { + try { + Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); } + return false; + } + + /** + * Generates a SecretKey object from the Base64 encoded secret string. + */ + private SecretKey getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); + } } \ No newline at end of file diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties index 054e02c..9a9fa67 100644 --- a/auth-service/src/main/resources/application.properties +++ b/auth-service/src/main/resources/application.properties @@ -4,7 +4,7 @@ server.port=8081 # JWT Configuration jwt.secret=${JWT_SECRET:YourSuperSecretKeyForJWTGoesHereAndItMustBeVeryLongForSecurityPurposes} -jwt.expiration.time=86400000 +jwt.expiration=86400000 # Database Configuration for local testing, it is recommended to make a database, using the following name, and make a user with the given username and password. spring.datasource.url=jdbc:postgresql://localhost:5432/techtorque