diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 0ab0e95..40fbb79 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -67,6 +67,8 @@ jobs: -e ORACLE_DB_USER="${{ secrets.ORACLE_DB_USER }}" \ -e ORACLE_DB_PASSWORD="${{ secrets.ORACLE_DB_PASSWORD }}" \ -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ + -e JWT_MFA="${{ secrets.JWT_MFA }}" \ + -e MFA_SECRET="${{ secrets.MFA_SECRET }}" \ -e DOMAIN_NAME="${{ secrets.DOMAIN_NAME }}" \ "${{ secrets.DOCKER_HUB_USER }}"/taskmanagerauth:latest sudo docker image prune -f \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7bfc861..3d618e9 100644 --- a/pom.xml +++ b/pom.xml @@ -58,12 +58,12 @@ spring-security-test test - com.h2database h2 2.3.232 + @@ -79,6 +79,20 @@ 4.5.0 + + + com.warrenstrange + googleauth + 1.5.0 + + + + + com.google.crypto.tink + tink + 1.17.0 + + diff --git a/src/main/java/com/example/taskmanagerauth/config/JwtRequestFilter.java b/src/main/java/com/example/taskmanagerauth/config/JwtRequestFilter.java index 0d2ccb9..dfb1799 100644 --- a/src/main/java/com/example/taskmanagerauth/config/JwtRequestFilter.java +++ b/src/main/java/com/example/taskmanagerauth/config/JwtRequestFilter.java @@ -66,6 +66,14 @@ protected void doFilterInternal( return; } + String cookie_name; + + if (isMfaPath(request.getServletPath())) { + cookie_name = "mfa_access_token"; + } else { + cookie_name = "taskmanager_access_token"; + } + if (request.getCookies() == null || request.getCookies().length == 0) { exceptionManager.handleJwtNotProvidedException( new JwtNotProvidedException("No tokens were provided."), @@ -74,11 +82,11 @@ protected void doFilterInternal( return; } - String access_token = null; + String access_token; try { access_token = Arrays.stream(request.getCookies()) - .filter(cookie -> cookie.getName().equals("taskmanager_access_token")) + .filter(cookie -> cookie.getName().equals(cookie_name)) .toList().getFirst().getValue(); } catch (Exception exception) { exceptionManager.handleJwtNotProvidedException( @@ -97,7 +105,8 @@ protected void doFilterInternal( try { - if (jwtService.validateToken(access_token)) { + if (cookie_name.equals("taskmanager_access_token") && jwtService.validateToken(access_token) || + cookie_name.equals("mfa_access_token") && jwtService.validate2faToken(access_token)) { authorities = jwtService.extractAuthorities(access_token); username = jwtService.extractUser(access_token); @@ -165,4 +174,8 @@ private boolean isPermitAllPath(String servletPath) { return SecurityConfig.permitAllPaths.contains(servletPath); } -} + private boolean isMfaPath(String servletPath) { + return SecurityConfig.mfaPath.contains(servletPath); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/taskmanagerauth/config/SecurityConfig.java b/src/main/java/com/example/taskmanagerauth/config/SecurityConfig.java index ae2293b..0d3a47b 100644 --- a/src/main/java/com/example/taskmanagerauth/config/SecurityConfig.java +++ b/src/main/java/com/example/taskmanagerauth/config/SecurityConfig.java @@ -36,6 +36,11 @@ public class SecurityConfig { "/auth/login" ); + public static List mfaPath = List.of( + "/auth/2fa/setup", + "/auth/2fa/generate" + ); + @Bean public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource configurationSource) throws Exception { http diff --git a/src/main/java/com/example/taskmanagerauth/controller/MfaController.java b/src/main/java/com/example/taskmanagerauth/controller/MfaController.java new file mode 100644 index 0000000..7dc6515 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/controller/MfaController.java @@ -0,0 +1,72 @@ +package com.example.taskmanagerauth.controller; + +import com.example.taskmanagerauth.dto.impl.ApiResponse; +import com.example.taskmanagerauth.dto.impl.MfaRequest; +import com.example.taskmanagerauth.entity.User; +import com.example.taskmanagerauth.service.MfaService; +import com.example.taskmanagerauth.service.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MfaController { + + @Autowired + private MfaService mfaService; + + @Autowired + private UserService userService; + + private static final Logger logger = LoggerFactory.getLogger(MfaController.class); + + @PostMapping("/auth/2fa/setup") + public ResponseEntity> setup(@RequestBody MfaRequest mfaRequest) { + + if (logger.isDebugEnabled()) { + logger.debug("2FA trying to be enabled."); + } + + logger.info("POST HTTP request received at /api/auth/2fa/setup"); + + User user = userService.loadUserByContext(); + + mfaService.setupMfa(mfaRequest.getTotp(), user); + userService.saveUser(user); + + ApiResponse response = ApiResponse.of( + HttpStatus.OK.value(), + "Success", + null + ); + + return ResponseEntity.status(HttpStatus.OK).body(response); + + } + + @GetMapping("/auth/2fa/generate") + public ResponseEntity> generateUrl() { + + if (logger.isDebugEnabled()) { + logger.debug("Trying to generate 2FA URL."); + } + + logger.info("GET HTTP request received at /api/auth/2fa/generate"); + + ApiResponse response = ApiResponse.of( + HttpStatus.OK.value(), + "Success", + mfaService.generateMfaCode(userService.loadUserByContext()) + ); + + return ResponseEntity.status(HttpStatus.OK).body(response); + + } + +} diff --git a/src/main/java/com/example/taskmanagerauth/controller/UserController.java b/src/main/java/com/example/taskmanagerauth/controller/UserController.java index dca98bd..d175a9d 100644 --- a/src/main/java/com/example/taskmanagerauth/controller/UserController.java +++ b/src/main/java/com/example/taskmanagerauth/controller/UserController.java @@ -1,7 +1,14 @@ package com.example.taskmanagerauth.controller; -import com.example.taskmanagerauth.dto.ApiResponse; +import com.example.taskmanagerauth.dto.impl.ApiResponse; +import com.example.taskmanagerauth.dto.impl.LoginRequest; +import com.example.taskmanagerauth.dto.impl.RegisterRequest; +import com.example.taskmanagerauth.dto.responses.LoginResult; +import com.example.taskmanagerauth.dto.responses.MfaRequired; +import com.example.taskmanagerauth.dto.responses.Success; +import com.example.taskmanagerauth.dto.responses.TotpRequired; import com.example.taskmanagerauth.entity.User; +import com.example.taskmanagerauth.service.MfaService; import com.example.taskmanagerauth.service.UserService; import com.example.taskmanagerauth.service.JwtService; import jakarta.servlet.http.HttpServletResponse; @@ -10,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -26,6 +34,9 @@ public class UserController { @Autowired private JwtService jwtService; + @Autowired + private MfaService mfaService; + @GetMapping("/auth/validate") public ResponseEntity> validate() { @@ -46,7 +57,9 @@ public ResponseEntity> validate() { } @PostMapping("/auth/register") - public ResponseEntity> register(@RequestBody User user) { + public ResponseEntity> register( + @RequestBody RegisterRequest registerRequest + ) { if (logger.isDebugEnabled()) { logger.debug("Attempting to register..."); @@ -54,7 +67,11 @@ public ResponseEntity> register(@RequestBody User user) { logger.info("POST HTTP request received at /api/auth/register"); - userService.registerUser(user); + User user = userService.createDatabaseUser(registerRequest.getUsername(), registerRequest.getPassword()); + + userService.checkIfUserExists(user); + mfaService.instantiateMfaForUser(user); + userService.saveUser(user); ApiResponse response = ApiResponse.of( HttpStatus.OK.value(), @@ -67,8 +84,8 @@ public ResponseEntity> register(@RequestBody User user) { } @PostMapping("/auth/login") - public ResponseEntity> authenticate( - @RequestBody User user, + public ResponseEntity> login( + @RequestBody LoginRequest loginRequest, HttpServletResponse httpServletResponse ) { @@ -78,21 +95,54 @@ public ResponseEntity> authenticate( logger.info("POST HTTP request received at /api/auth/login"); - httpServletResponse.addCookie( - jwtService.generateJwtCookie( - userService.loadUserByUsernamePassword( - user.getUsername(), user.getPassword() - ) - ) - ); - - ApiResponse response = ApiResponse.of( - HttpStatus.OK.value(), - "Success", - null - ); - - return ResponseEntity.status(HttpStatus.OK).body(response); + // Process user + LoginResult result = userService.login(loginRequest); + + return switch (result) { + case Success success -> { + httpServletResponse.addCookie( + jwtService.generateJwtCookie( + success.userDetails() + ) + ); + yield ResponseEntity.status(HttpStatus.OK).body( + ApiResponse.of( + HttpStatus.OK.value(), + "Success", + null + ) + ); + } + case MfaRequired mfa -> { + httpServletResponse.addCookie( + jwtService.generate2faCookie( + mfa.userDetails() + ) + ); + yield ResponseEntity.status(HttpStatus.OK).body( + ApiResponse.of( + 362, + "Please enable mfa.", + null + ) + ); + } + case TotpRequired totp -> { + httpServletResponse.addCookie( + jwtService.generate2faCookie( + totp.userDetails() + ) + ); + yield ResponseEntity.status(HttpStatus.OK).body( + ApiResponse.of( + 462, + "TOTP not provided.", + null + ) + ); + } + + }; } diff --git a/src/main/java/com/example/taskmanagerauth/dto/ApiResponse.java b/src/main/java/com/example/taskmanagerauth/dto/impl/ApiResponse.java similarity index 96% rename from src/main/java/com/example/taskmanagerauth/dto/ApiResponse.java rename to src/main/java/com/example/taskmanagerauth/dto/impl/ApiResponse.java index 1080680..54423bb 100644 --- a/src/main/java/com/example/taskmanagerauth/dto/ApiResponse.java +++ b/src/main/java/com/example/taskmanagerauth/dto/impl/ApiResponse.java @@ -1,4 +1,4 @@ -package com.example.taskmanagerauth.dto; +package com.example.taskmanagerauth.dto.impl; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/src/main/java/com/example/taskmanagerauth/dto/impl/LoginRequest.java b/src/main/java/com/example/taskmanagerauth/dto/impl/LoginRequest.java new file mode 100644 index 0000000..d003d2c --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/impl/LoginRequest.java @@ -0,0 +1,43 @@ +package com.example.taskmanagerauth.dto.impl; + +public class LoginRequest { + + private String username; + private String password; + private String totp; + + public LoginRequest() {} + + public LoginRequest(String username, String password, String totp) { + this.username = username; + this.password = password; + this.totp = totp; + } + + // Getters & Setters + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getTotp() { + return totp; + } + + public void setTotp(String totp) { + this.totp = totp; + } + +} diff --git a/src/main/java/com/example/taskmanagerauth/dto/impl/MfaRequest.java b/src/main/java/com/example/taskmanagerauth/dto/impl/MfaRequest.java new file mode 100644 index 0000000..30fdd8b --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/impl/MfaRequest.java @@ -0,0 +1,21 @@ +package com.example.taskmanagerauth.dto.impl; + +public class MfaRequest { + + private String totp; + + public MfaRequest(String totp) { + this.totp = totp; + } + + // Getters & setters + + public String getTotp() { + return totp; + } + + public void setTotp(String totp) { + this.totp = totp; + } + +} diff --git a/src/main/java/com/example/taskmanagerauth/dto/impl/RegisterRequest.java b/src/main/java/com/example/taskmanagerauth/dto/impl/RegisterRequest.java new file mode 100644 index 0000000..3efba4c --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/impl/RegisterRequest.java @@ -0,0 +1,29 @@ +package com.example.taskmanagerauth.dto.impl; + +public class RegisterRequest { + + private String username; + private String password; + + public RegisterRequest(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/com/example/taskmanagerauth/dto/responses/LoginResult.java b/src/main/java/com/example/taskmanagerauth/dto/responses/LoginResult.java new file mode 100644 index 0000000..1aa6b67 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/responses/LoginResult.java @@ -0,0 +1,3 @@ +package com.example.taskmanagerauth.dto.responses; + +public sealed interface LoginResult permits Success, MfaRequired, TotpRequired {} diff --git a/src/main/java/com/example/taskmanagerauth/dto/responses/MfaRequired.java b/src/main/java/com/example/taskmanagerauth/dto/responses/MfaRequired.java new file mode 100644 index 0000000..13e6824 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/responses/MfaRequired.java @@ -0,0 +1,6 @@ +package com.example.taskmanagerauth.dto.responses; + +import org.springframework.security.core.userdetails.UserDetails; + +public record MfaRequired(UserDetails userDetails) implements LoginResult { +} diff --git a/src/main/java/com/example/taskmanagerauth/dto/responses/Success.java b/src/main/java/com/example/taskmanagerauth/dto/responses/Success.java new file mode 100644 index 0000000..a8dc64b --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/responses/Success.java @@ -0,0 +1,6 @@ +package com.example.taskmanagerauth.dto.responses; + +import org.springframework.security.core.userdetails.UserDetails; + +public record Success(UserDetails userDetails) implements LoginResult { +} diff --git a/src/main/java/com/example/taskmanagerauth/dto/responses/TotpRequired.java b/src/main/java/com/example/taskmanagerauth/dto/responses/TotpRequired.java new file mode 100644 index 0000000..0d4c3b4 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/dto/responses/TotpRequired.java @@ -0,0 +1,6 @@ +package com.example.taskmanagerauth.dto.responses; + +import org.springframework.security.core.userdetails.UserDetails; + +public record TotpRequired(UserDetails userDetails) implements LoginResult { +} diff --git a/src/main/java/com/example/taskmanagerauth/entity/Mfa.java b/src/main/java/com/example/taskmanagerauth/entity/Mfa.java new file mode 100644 index 0000000..ce71762 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/entity/Mfa.java @@ -0,0 +1,75 @@ +package com.example.taskmanagerauth.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; + +@Entity +@Table(name = "mfa") +public class Mfa { + + @Id + @SequenceGenerator(name = "mfa_seq", sequenceName = "mfa_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "mfa_seq") + private Long id; + + @OneToOne + @JoinColumn(name = "user_id", referencedColumnName = "id") + @JsonBackReference + private User user; + + @Column(name = "mfaEnabled", nullable = false) + private Boolean mfaEnabled; + + @Column(name = "mfaSecretKey", nullable = false) + private String mfaSecretKey; + + public Mfa() {} + + public Mfa(Long id, User user, Boolean mfaEnabled, String mfaSecretKey) { + this.id = id; + this.user = user; + this.mfaEnabled = mfaEnabled; + this.mfaSecretKey = mfaSecretKey; + } + + // Factory + + public static Mfa of(User user, Boolean mfaEnabled, String mfaSecretKey) { + return new Mfa(null, user, mfaEnabled, mfaSecretKey); + } + + // Getters & Setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Boolean getMfaEnabled() { + return mfaEnabled; + } + + public void setMfaEnabled(Boolean mfa_enabled) { + this.mfaEnabled = mfa_enabled; + } + + public String getMfaSecretKey() { + return mfaSecretKey; + } + + public void setMfaSecretKey(String mfa_secret_key) { + this.mfaSecretKey = mfa_secret_key; + } + +} diff --git a/src/main/java/com/example/taskmanagerauth/entity/User.java b/src/main/java/com/example/taskmanagerauth/entity/User.java index e64b537..c92712d 100644 --- a/src/main/java/com/example/taskmanagerauth/entity/User.java +++ b/src/main/java/com/example/taskmanagerauth/entity/User.java @@ -1,5 +1,6 @@ package com.example.taskmanagerauth.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -32,6 +33,10 @@ public class User { ) private Set roles; + @OneToOne(fetch = FetchType.EAGER, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private Mfa mfa; + public User() {} public User(Long id, String username, String password) { @@ -40,6 +45,17 @@ public User(Long id, String username, String password) { this.password = password; } + public User(Long id, String username, String password, Set roles) { + this.id = id; + this.username = username; + this.password = password; + this.roles = roles; + } + + public static User of(String username, String password) { + return new User(null, username, password, Set.of(Role.of("USER"))); + } + public Long getId() { return id; } @@ -76,4 +92,8 @@ public void setPassword(String password) { public void setRoles(Set roles) { this.roles = roles; } + public Mfa getMfa() { return mfa; } + + public void setMfa(Mfa mfa) { this.mfa = mfa; } + } diff --git a/src/main/java/com/example/taskmanagerauth/exception/handler/FilterExceptionManager.java b/src/main/java/com/example/taskmanagerauth/exception/handler/FilterExceptionManager.java index 994e2fd..3d3feb3 100644 --- a/src/main/java/com/example/taskmanagerauth/exception/handler/FilterExceptionManager.java +++ b/src/main/java/com/example/taskmanagerauth/exception/handler/FilterExceptionManager.java @@ -1,14 +1,12 @@ package com.example.taskmanagerauth.exception.handler; -import com.example.taskmanagerauth.dto.ApiResponse; +import com.example.taskmanagerauth.dto.impl.ApiResponse; import com.example.taskmanagerauth.exception.server.ExpiredJwtException; import com.example.taskmanagerauth.exception.server.InvalidCredentialsException; import com.example.taskmanagerauth.exception.server.InvalidJwtException; import com.example.taskmanagerauth.exception.server.JwtNotProvidedException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/example/taskmanagerauth/exception/handler/GlobalExceptionManager.java b/src/main/java/com/example/taskmanagerauth/exception/handler/GlobalExceptionManager.java index 7a87cc0..80c2133 100644 --- a/src/main/java/com/example/taskmanagerauth/exception/handler/GlobalExceptionManager.java +++ b/src/main/java/com/example/taskmanagerauth/exception/handler/GlobalExceptionManager.java @@ -1,6 +1,6 @@ package com.example.taskmanagerauth.exception.handler; -import com.example.taskmanagerauth.dto.ApiResponse; +import com.example.taskmanagerauth.dto.impl.ApiResponse; import com.example.taskmanagerauth.exception.server.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -103,4 +103,64 @@ public ResponseEntity> handleExpiredJwtException(ExpiredJwtE } + @ExceptionHandler(TotpNotProvidedException.class) + public ResponseEntity> handleTotpNotProvidedException(TotpNotProvidedException exception) { + + String message = "Bad Request: One time password not provided."; + + ApiResponse response = ApiResponse.of( + 462, // Custom code for requiring TOTP + message, + exception.getMessage() + ); + + return ResponseEntity.status(HttpStatus.OK).body(response); + + } + + @ExceptionHandler(MfaNotEnabledException.class) + public ResponseEntity> handleMfaNotEnabledException(MfaNotEnabledException exception) { + + String message = "Bad Request: Please set up MFA for your account."; + + ApiResponse apiResponse = ApiResponse.of( + 362, // Custom code for requiring TOTP + message, + exception.getMessage() + ); + + return ResponseEntity.status(HttpStatus.OK).body(apiResponse); + + } + + @ExceptionHandler(TotpInvalidException.class) + public ResponseEntity> handleTotpInvalidException(TotpInvalidException exception) { + + String message = "Bad Request: One time password was incorrect."; + + ApiResponse response = ApiResponse.of( + HttpStatus.FORBIDDEN.value(), + message, + exception.getMessage() + ); + + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); + + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntimeException(RuntimeException exception) { + + String message = "Internal Server Error"; + + ApiResponse response = ApiResponse.of( + HttpStatus.FORBIDDEN.value(), + message, + exception.getMessage() + ); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + + } + } diff --git a/src/main/java/com/example/taskmanagerauth/exception/server/MfaNotEnabledException.java b/src/main/java/com/example/taskmanagerauth/exception/server/MfaNotEnabledException.java new file mode 100644 index 0000000..894156c --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/exception/server/MfaNotEnabledException.java @@ -0,0 +1,7 @@ +package com.example.taskmanagerauth.exception.server; + +public class MfaNotEnabledException extends RuntimeException { + public MfaNotEnabledException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/taskmanagerauth/exception/server/TotpInvalidException.java b/src/main/java/com/example/taskmanagerauth/exception/server/TotpInvalidException.java new file mode 100644 index 0000000..6a4d811 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/exception/server/TotpInvalidException.java @@ -0,0 +1,7 @@ +package com.example.taskmanagerauth.exception.server; + +public class TotpInvalidException extends RuntimeException { + public TotpInvalidException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/taskmanagerauth/exception/server/TotpNotProvidedException.java b/src/main/java/com/example/taskmanagerauth/exception/server/TotpNotProvidedException.java new file mode 100644 index 0000000..b9fc167 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/exception/server/TotpNotProvidedException.java @@ -0,0 +1,7 @@ +package com.example.taskmanagerauth.exception.server; + +public class TotpNotProvidedException extends RuntimeException{ + public TotpNotProvidedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/taskmanagerauth/repository/MfaRepository.java b/src/main/java/com/example/taskmanagerauth/repository/MfaRepository.java new file mode 100644 index 0000000..fdf5fde --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/repository/MfaRepository.java @@ -0,0 +1,8 @@ +package com.example.taskmanagerauth.repository; + +import com.example.taskmanagerauth.entity.Mfa; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MfaRepository extends JpaRepository {} diff --git a/src/main/java/com/example/taskmanagerauth/service/DefaultPasswordEncodingService.java b/src/main/java/com/example/taskmanagerauth/service/DefaultPasswordEncodingService.java index eb5a45c..451b5a1 100644 --- a/src/main/java/com/example/taskmanagerauth/service/DefaultPasswordEncodingService.java +++ b/src/main/java/com/example/taskmanagerauth/service/DefaultPasswordEncodingService.java @@ -14,6 +14,10 @@ public String encode(String password) { return passwordEncoder.encode(password); } + public Boolean matches(String rawPassword, String encoded) { + return passwordEncoder.matches(rawPassword, encoded); + } + public PasswordEncoder getEncoder() { return passwordEncoder; } diff --git a/src/main/java/com/example/taskmanagerauth/service/JwtService.java b/src/main/java/com/example/taskmanagerauth/service/JwtService.java index f25963f..7b9de31 100644 --- a/src/main/java/com/example/taskmanagerauth/service/JwtService.java +++ b/src/main/java/com/example/taskmanagerauth/service/JwtService.java @@ -21,11 +21,16 @@ @Component public class JwtService { - public JwtService(@Value("${jwt.secret}") String secret) { + public JwtService( + @Value("${jwt.secret}") String secret, + @Value("${jwt.mfa}") String secret2FA + ) { this.secret = secret; + this.secret2FA = secret2FA; } private final String secret; + private final String secret2FA; private final Duration EXPIRATION_TIMER = Duration.ofMinutes(10); // 10 minutes public long getExpirationTimerInMillis() { @@ -40,8 +45,11 @@ public String getSecret() { return secret; } - public String generateToken(UserDetails userDetails) { - Algorithm algorithm = Algorithm.HMAC512(getSecret()); + public String getSecret2FA() { + return secret2FA; + } + + private String createToken(UserDetails userDetails, Algorithm algorithm) { return JWT.create() .withSubject(userDetails.getUsername()) .withClaim("authorities", userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()) @@ -50,6 +58,16 @@ public String generateToken(UserDetails userDetails) { .sign(algorithm); } + public String generateToken(UserDetails userDetails) { + Algorithm algorithm = Algorithm.HMAC512(getSecret()); + return createToken(userDetails, algorithm); + } + + public String generate2FAToken(UserDetails userDetails) { + Algorithm algorithm = Algorithm.HMAC512(getSecret2FA()); + return createToken(userDetails, algorithm); + } + public String extractUser(String token) { try { DecodedJWT jwt = JWT.decode(token); @@ -68,9 +86,17 @@ public List extractAuthorities(String token) { } } + public boolean validate2faToken(String token) { + return validate(token, getSecret2FA()); + } + public boolean validateToken(String token) { + return validate(token, getSecret()); + } + + private boolean validate(String token, String secret) { try { - Algorithm algorithm = Algorithm.HMAC512(getSecret()); + Algorithm algorithm = Algorithm.HMAC512(secret); JWTVerifier verifier = JWT.require(algorithm) .build(); @@ -86,10 +112,20 @@ public boolean validateToken(String token) { } public Cookie generateJwtCookie(UserDetails userDetails) { + String jwt = generateToken(userDetails); + return createCookie("taskmanager_access_token", jwt); + } + + public Cookie generate2faCookie(UserDetails userDetails) { + String jwt = generate2FAToken(userDetails); + return createCookie("mfa_access_token", jwt); + } + + private Cookie createCookie(String cookie_name, String jwt) { Cookie cookie = new Cookie( - "taskmanager_access_token", - generateToken(userDetails) + cookie_name, + jwt ); cookie.setHttpOnly(true); diff --git a/src/main/java/com/example/taskmanagerauth/service/MfaService.java b/src/main/java/com/example/taskmanagerauth/service/MfaService.java new file mode 100644 index 0000000..5940554 --- /dev/null +++ b/src/main/java/com/example/taskmanagerauth/service/MfaService.java @@ -0,0 +1,189 @@ +package com.example.taskmanagerauth.service; + +import com.example.taskmanagerauth.entity.Mfa; +import com.example.taskmanagerauth.entity.User; +import com.example.taskmanagerauth.exception.server.MfaNotEnabledException; +import com.example.taskmanagerauth.exception.server.TotpInvalidException; +import com.example.taskmanagerauth.exception.server.TotpNotProvidedException; +import com.google.crypto.tink.*; +import com.google.crypto.tink.aead.AeadConfig; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Base64; + +@Service +public class MfaService { + + private final GoogleAuthenticator authenticator = new GoogleAuthenticator(); + private final KeysetHandle mfaKey; + + @Autowired + public MfaService( + @Value("${mfa.secret}") String mfaSecretKeySet + ) { + mfaKey = getMfaKey(mfaSecretKeySet); + } + + /** + * Get the Base64 encoded key from the environment to be used for decoding/encoding. + * @param mfaKeySet Base64 encoded keyset, loaded from GitHub secrets + * @return (KeysetHandle) The object representation of the Base64 imported string + */ + private KeysetHandle getMfaKey(String mfaKeySet) { + + try { + + AeadConfig.register(); + + byte[] keysetBytes = Base64.getDecoder().decode(mfaKeySet); + + try (ByteArrayInputStream input = new ByteArrayInputStream(keysetBytes)) { + + return CleartextKeysetHandle.read( + BinaryKeysetReader.withInputStream(input) + ); + + } + + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } + + } + + /** + * Get the time-based one time password as a number + * @param totp String representation of the TOTP code + * @return (int) Integer representation of the TOTP code + */ + private int getTotp(String totp) { + + if (totp == null || totp.isEmpty()) { + throw new TotpNotProvidedException("One time password not provided."); + } + + int totp_num; + + try { + totp_num = Integer.parseInt(totp); + } catch (NumberFormatException exception) { + throw new TotpNotProvidedException("One time password not provided."); + } + + return totp_num; + + } + + public boolean hasMfaEnabled(User user) { + return user.getMfa().getMfaEnabled(); + } + + /** + * Encrypt using mfaKey + * @param key (String) A key to be encrypted + * @return The encrypted, base64 representation + */ + public String encrypt(String key) { + + try { + Aead aead = mfaKey.getPrimitive(RegistryConfiguration.get(), Aead.class); + + byte[] stringAsBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] cipherText = aead.encrypt(stringAsBytes, "taskmanagerauth".getBytes()); + + return Base64.getEncoder().encodeToString(cipherText); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + + } + + /** + * Decrypt using mfaKey + * @param encryptedKey (String) An encrypted key + * @return The decrypted, String representation + */ + public String decrypt(String encryptedKey) { + + try { + Aead aead = mfaKey.getPrimitive(RegistryConfiguration.get(), Aead.class); + + byte[] cipherText = Base64.getDecoder().decode(encryptedKey); + byte[] decrypted = aead.decrypt(cipherText, "taskmanagerauth".getBytes()); + + return new String(decrypted, StandardCharsets.UTF_8); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + + } + + /** + * Generate the TOTP code for usage on frontend + * @return (String) The otpauth code + */ + public String generateMfaCode(User user) { + return "otpauth://totp/TaskManagerAuth:" + user.getId() + "?secret=" + decrypt(user.getMfa().getMfaSecretKey()) + "&issuer=TaskManagerAuth\n"; + } + + /** + * Validate a time-based one time password + * @param totp One time password + * @param user User + */ + public void validatePassword(String totp, User user) { + + if (!hasMfaEnabled(user)) { + throw new MfaNotEnabledException("Mfa not enabled."); + } + + if (totp.isEmpty()) { + throw new TotpNotProvidedException("Please provide a TOTP code."); + } + + int totp_num = getTotp(totp); + + if (!authenticator.authorize(decrypt(user.getMfa().getMfaSecretKey()), totp_num)) { + throw new TotpInvalidException("Incorrect TOTP provided."); + } + + } + + /** + * Using a TOTP code the user provides, activate the user's MFA if correct + * @param totp Code provided + * @param user The user + */ + public void setupMfa(String totp, User user) { + + int totp_num = getTotp(totp); + + if (!authenticator.authorize(decrypt(user.getMfa().getMfaSecretKey()), totp_num)) { + throw new TotpInvalidException("Incorrect TOTP provided."); + } + + user.getMfa().setMfaEnabled(true); + + } + + /** + * Create a row in the mfa table + * @param user The user + */ + public void instantiateMfaForUser(User user) { + + GoogleAuthenticatorKey key = authenticator.createCredentials(); + Mfa mfa = new Mfa(null, user, false, encrypt(key.getKey())); + user.setMfa(mfa); + + } + +} diff --git a/src/main/java/com/example/taskmanagerauth/service/PasswordEncodingService.java b/src/main/java/com/example/taskmanagerauth/service/PasswordEncodingService.java index 37d3a29..58ee623 100644 --- a/src/main/java/com/example/taskmanagerauth/service/PasswordEncodingService.java +++ b/src/main/java/com/example/taskmanagerauth/service/PasswordEncodingService.java @@ -4,5 +4,6 @@ public interface PasswordEncodingService { String encode(String password); + Boolean matches(String rawPassword, String encoded); PasswordEncoder getEncoder(); } diff --git a/src/main/java/com/example/taskmanagerauth/service/UserService.java b/src/main/java/com/example/taskmanagerauth/service/UserService.java index b9be530..5439c21 100644 --- a/src/main/java/com/example/taskmanagerauth/service/UserService.java +++ b/src/main/java/com/example/taskmanagerauth/service/UserService.java @@ -1,17 +1,22 @@ package com.example.taskmanagerauth.service; +import com.example.taskmanagerauth.dto.impl.LoginRequest; +import com.example.taskmanagerauth.dto.responses.LoginResult; +import com.example.taskmanagerauth.dto.responses.MfaRequired; +import com.example.taskmanagerauth.dto.responses.Success; +import com.example.taskmanagerauth.dto.responses.TotpRequired; import com.example.taskmanagerauth.entity.Role; import com.example.taskmanagerauth.entity.User; -import com.example.taskmanagerauth.exception.server.InvalidCredentialsException; -import com.example.taskmanagerauth.exception.server.UsernameTakenException; +import com.example.taskmanagerauth.exception.server.*; import com.example.taskmanagerauth.repository.UserRepository; -import com.example.taskmanagerauth.service.JwtService; import jakarta.transaction.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -25,21 +30,49 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordEncodingService passwordEncoder; - private final JwtService jwtService; + private final MfaService mfaService; @Autowired public UserService( UserRepository userRepository, PasswordEncodingService passwordEncoder, - JwtService jwtService + MfaService mfaService ) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; - this.jwtService = jwtService; + this.mfaService = mfaService; } private static final Logger logger = LoggerFactory.getLogger(UserService.class); + // Wrapper methods + + public LoginResult login(LoginRequest loginRequest) { + + User user = getUserByUsernameAndPassword(loginRequest.getUsername(), loginRequest.getPassword()); + UserDetails userDetails = createUserDetails(user); + + try { + mfaService.validatePassword(loginRequest.getTotp(), user); + return new Success(userDetails); + } catch (MfaNotEnabledException exception) { + return new MfaRequired(userDetails); + } catch (TotpNotProvidedException exception) { + return new TotpRequired(userDetails); + } + + } + + // UserDetail services + + public UserDetails createUserDetails(User user) { + return new org.springframework.security.core.userdetails.User( + String.valueOf(user.getId()), + user.getPassword(), + mapRolesToAuthorities(user.getRoles()) + ); + } + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { @@ -51,11 +84,7 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx () -> new UsernameNotFoundException("Invalid credentials provided.") ); - return new org.springframework.security.core.userdetails.User( - String.valueOf(user.getId()), // Use the immutable ID instead of username - user.getPassword(), - mapRolesToAuthorities(user.getRoles()) - ); + return createUserDetails(user); } @@ -89,25 +118,76 @@ public UserDetails loadUserByUsernamePassword(String username, String password) } + public User loadUserByContext() { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserDetails userDetails = (UserDetails)authentication.getPrincipal(); + Long userId = Long.decode(userDetails.getUsername()); + + return userRepository.findById(userId).orElseThrow( + () -> new InvalidCredentialsException("User not found.") + ); + + } + private Collection mapRolesToAuthorities(Collection roles) { return roles.stream() .map(role -> new SimpleGrantedAuthority(role.getName())) .toList(); } + // Retrieve User objects + + public User createDatabaseUser(String username, String password) { + return User.of(username, passwordEncoder.encode(password)); + } + + public User getUserByUsernameAndPassword(String username, String password) { + + if (logger.isDebugEnabled()) { + logger.debug("Attempting getUserByUsernameAndPassword with {}", username); + } + + User user = userRepository.findByUsername(username).orElseThrow( + () -> new UsernameNotFoundException("Invalid credentials provided.") + ); + + if (!passwordEncoder.matches(password, user.getPassword())) { + throw new InvalidCredentialsException("Invalid credentials provided."); + } + + return user; + + } + + public User getUserById(UserDetails userDetails) { + + if (logger.isDebugEnabled()) { + logger.debug("Attempting to retrieve user with id {}", userDetails.getUsername()); + } + + return userRepository.findById(Long.parseLong(userDetails.getUsername())).orElseThrow( + () -> new InvalidCredentialsException("User not found.") + ); + + } + + public void checkIfUserExists(User user) { + if (userRepository.findByUsername(user.getUsername()).isPresent()) { + throw new UsernameTakenException("A user with this name already exists."); + } + } + + // Transactionals + @Transactional - public void registerUser(User user) { + public void saveUser(User user) { if (logger.isDebugEnabled()) { logger.debug("Attempting to save user {}", user.getUsername()); } - if (userRepository.findByUsername(user.getUsername()).isEmpty()) { - user.setPassword(passwordEncoder.encode(user.getPassword())); - userRepository.saveAndFlush(user); - } else { - throw new UsernameTakenException("Please provide a different username."); - } + userRepository.saveAndFlush(user); } diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 64a9531..02f8537 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -15,7 +15,7 @@ spring: # Logging logging: level: - root: "INFO" + root: "DEBUG" file: name: "logs/application.log" pattern: @@ -33,6 +33,10 @@ server: jwt: secret: "Test" + mfa: "Test2" + +mfa: + secret: "CMmRpMMOEmQKWAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EiIaILBjvpHue4z0MJuNMpRTZDsvpgvXT5jVNA/1Su8RwTB1GAEQARjJkaTDDiAB" domain: - name: "http://localhost:9095" \ No newline at end of file + name: "http://localhost:3000" \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41ddac9..e6bb0c3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: jpa: database-platform: org.hibernate.dialect.OracleDialect hibernate: - ddl-auto: none + ddl-auto: create-drop # Logging logging: @@ -27,6 +27,10 @@ server: jwt: secret: ${JWT_SECRET} + mfa: ${JWT_MFA} + +mfa: + secret: ${MFA_SECRET} domain: name: ${DOMAIN_NAME} \ No newline at end of file diff --git a/src/test/java/com/example/taskmanagerauth/integration/controller/UserControllerIT.java b/src/test/java/com/example/taskmanagerauth/integration/controller/UserControllerIT.java index c4b06ab..94e7ed2 100644 --- a/src/test/java/com/example/taskmanagerauth/integration/controller/UserControllerIT.java +++ b/src/test/java/com/example/taskmanagerauth/integration/controller/UserControllerIT.java @@ -2,17 +2,22 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; -import com.example.taskmanagerauth.dto.ApiResponse; -import com.example.taskmanagerauth.entity.Role; +import com.example.taskmanagerauth.dto.impl.ApiResponse; +import com.example.taskmanagerauth.dto.impl.LoginRequest; +import com.example.taskmanagerauth.dto.impl.MfaRequest; +import com.example.taskmanagerauth.dto.impl.RegisterRequest; +import com.example.taskmanagerauth.entity.Mfa; import com.example.taskmanagerauth.entity.User; +import com.example.taskmanagerauth.repository.MfaRepository; import com.example.taskmanagerauth.repository.UserRepository; +import com.example.taskmanagerauth.service.MfaService; import com.example.taskmanagerauth.service.PasswordEncodingService; import com.example.taskmanagerauth.service.JwtService; +import com.warrenstrange.googleauth.GoogleAuthenticator; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.test.context.ActiveProfiles; @@ -23,11 +28,11 @@ import java.util.Date; import java.util.List; -import java.util.Set; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @ExtendWith(SpringExtension.class) @ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class UserControllerIT { @@ -35,23 +40,34 @@ public class UserControllerIT { public UserControllerIT( RestTemplate testRestTemplate, UserRepository userRepository, + MfaRepository mfaRepository, PasswordEncodingService passwordEncodingService, - JwtService jwtService + JwtService jwtService, + MfaService mfaService ) { this.testRestTemplate = testRestTemplate; this.userRepository = userRepository; + this.mfaRepository = mfaRepository; this.passwordEncodingService = passwordEncodingService; this.jwtService = jwtService; + this.mfaService = mfaService; } private final RestTemplate testRestTemplate; private final UserRepository userRepository; + private final MfaRepository mfaRepository; private final PasswordEncodingService passwordEncodingService; private final JwtService jwtService; + private final MfaService mfaService; + + private String secretKey; + private String cookie; private static final String LOGIN_QUERY_URL = "https://localhost:9095/api/auth/login"; private static final String REGISTER_QUERY_URL = "https://localhost:9095/api/auth/register"; private static final String VALIDATE_QUERY_URL = "https://localhost:9095/api/auth/validate"; + private static final String GENERATE_QUERY_URL = "https://localhost:9095/api/auth/2fa/generate"; + private static final String SETUP_QUERY_URL = "https://localhost:9095/api/auth/2fa/setup"; HttpEntity HttpEntityFactory(T data) { return new HttpEntity<>(data); @@ -87,10 +103,7 @@ HttpHeaders invalidHeaderFactory() { @Order(1) void testRegisterSuccess() { - User payload = new User(); - payload.setUsername("test_user"); - payload.setPassword("test_pass"); - payload.setRoles(Set.of(Role.of("USER"))); + RegisterRequest payload = new RegisterRequest("test_user", "test_pass"); ResponseEntity> response = testRestTemplate.exchange( REGISTER_QUERY_URL, @@ -106,22 +119,120 @@ void testRegisterSuccess() { assertNull(response.getBody().getData()); assertEquals(HttpStatus.OK.value(), response.getBody().getStatus()); - // Database + // User database assertions User databaseUser = userRepository.findAll().getFirst(); assertEquals(payload.getUsername(), databaseUser.getUsername()); - assertTrue(passwordEncodingService.getEncoder().matches(payload.getPassword(), databaseUser.getPassword())); - assertEquals(1, payload.getRoles().size()); - assertEquals("USER", payload.getRoles().stream().findFirst().orElseThrow().getName()); + assertTrue(passwordEncodingService.matches(payload.getPassword(), databaseUser.getPassword())); + + // Mfa database assertions + Mfa databaseMfa = mfaRepository.findAll().getFirst(); + assertEquals(payload.getUsername(), databaseMfa.getUser().getUsername()); + assertEquals(false, databaseMfa.getMfaEnabled()); } @Test @Order(2) + void testLoginWithoutTotp() { + + LoginRequest payload = new LoginRequest("test_user", "test_pass", ""); + + ResponseEntity> response = testRestTemplate.exchange( + LOGIN_QUERY_URL, + HttpMethod.POST, + HttpEntityFactory(payload), + new ParameterizedTypeReference<>() {} + ); + + // Assertions + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertEquals("Please enable mfa.", response.getBody().getMessage()); + assertEquals(362, response.getBody().getStatus()); + + // Get cookie + String testJWT = response.getHeaders().getFirst("Set-Cookie"); + assertNotNull(testJWT); + + testJWT = testJWT.substring(testJWT.indexOf("=") + 1, testJWT.indexOf(";")); + + jwtService.validate2faToken(testJWT); + + assertEquals("1", jwtService.extractUser(testJWT)); + assertEquals("USER", jwtService.extractAuthorities(testJWT).getFirst()); + + // For use in next test + this.cookie = testJWT; + + } + + @Test + @Order(3) + void generateTotp() { + + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.COOKIE, "mfa_access_token=" + this.cookie); + + ResponseEntity> response = testRestTemplate.exchange( + GENERATE_QUERY_URL, + HttpMethod.GET, + HttpEntityFactory(null, headers), + new ParameterizedTypeReference<>() {} + ); + + // Assertions + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertEquals("Success", response.getBody().getMessage()); + assertEquals(HttpStatus.OK.value(), response.getBody().getStatus()); + + this.secretKey = mfaService.decrypt(mfaRepository.findAll().getFirst().getMfaSecretKey()); + + assertEquals( + "otpauth://totp/TaskManagerAuth:1?secret=" + secretKey + "&issuer=TaskManagerAuth\n", + response.getBody().getData() + ); + + } + + @Test + @Order(4) + void setupTotp() { + + GoogleAuthenticator authenticator = new GoogleAuthenticator(); + MfaRequest payload = new MfaRequest(String.valueOf(authenticator.getTotpPassword(secretKey))); + + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.COOKIE, "mfa_access_token=" + this.cookie); + + + ResponseEntity> response = testRestTemplate.exchange( + SETUP_QUERY_URL, + HttpMethod.POST, + HttpEntityFactory(payload, headers), + new ParameterizedTypeReference<>() {} + ); + + // Assertions + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertEquals("Success", response.getBody().getMessage()); + assertEquals(HttpStatus.OK.value(), response.getBody().getStatus()); + + // Database + User userDB = userRepository.findAll().getFirst(); + assertTrue(userDB.getMfa().getMfaEnabled()); + + } + + @Test + @Order(5) void testLoginSuccess() { - User payload = new User(1L, "test_user", "test_pass"); - payload.setRoles(Set.of(Role.of("USER"))); + GoogleAuthenticator authenticator = new GoogleAuthenticator(); + String totp = String.valueOf(authenticator.getTotpPassword(secretKey)); + LoginRequest payload = new LoginRequest("test_user", "test_pass", totp); ResponseEntity> response = testRestTemplate.exchange( LOGIN_QUERY_URL, @@ -149,7 +260,7 @@ void testLoginSuccess() { } @Test - @Order(3) + @Order(6) void testValidateSuccess() { ResponseEntity> response = testRestTemplate.exchange( diff --git a/src/test/java/com/example/taskmanagerauth/unit/config/JwtRequestFilterTests.java b/src/test/java/com/example/taskmanagerauth/unit/config/JwtRequestFilterTests.java index 238aaa0..7d554d9 100644 --- a/src/test/java/com/example/taskmanagerauth/unit/config/JwtRequestFilterTests.java +++ b/src/test/java/com/example/taskmanagerauth/unit/config/JwtRequestFilterTests.java @@ -54,7 +54,7 @@ public class JwtRequestFilterTests { @BeforeEach void setUp() { - JwtService jwtService = new JwtService("Test"); + JwtService jwtService = new JwtService("Test", "Test2"); this.jwtRequestFilter = new JwtRequestFilter(userService, jwtService, filterExceptionManager); } diff --git a/src/test/java/com/example/taskmanagerauth/unit/dto/ApiResponseTests.java b/src/test/java/com/example/taskmanagerauth/unit/dto/ApiResponseTests.java index 01c4d1f..4fb97ab 100644 --- a/src/test/java/com/example/taskmanagerauth/unit/dto/ApiResponseTests.java +++ b/src/test/java/com/example/taskmanagerauth/unit/dto/ApiResponseTests.java @@ -1,6 +1,6 @@ package com.example.taskmanagerauth.unit.dto; -import com.example.taskmanagerauth.dto.ApiResponse; +import com.example.taskmanagerauth.dto.impl.ApiResponse; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; diff --git a/src/test/java/com/example/taskmanagerauth/unit/entity/MfaTests.java b/src/test/java/com/example/taskmanagerauth/unit/entity/MfaTests.java new file mode 100644 index 0000000..2377f46 --- /dev/null +++ b/src/test/java/com/example/taskmanagerauth/unit/entity/MfaTests.java @@ -0,0 +1,37 @@ +package com.example.taskmanagerauth.unit.entity; + +import com.example.taskmanagerauth.entity.Mfa; +import com.example.taskmanagerauth.entity.User; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.*; + +@ActiveProfiles("test") +public class MfaTests { + + /** + * Test the basic behavior of the Mfa entity class + */ + @Test + void testBasicBehavior() { + + // Creation + Mfa testMfa = new Mfa(); + + // Setters + testMfa.setMfaEnabled(true); + testMfa.setUser(new User(1L, "Test", "Test")); + testMfa.setId(1L); + testMfa.setMfaSecretKey("Test"); + + // Getters + assertEquals(true, testMfa.getMfaEnabled()); + assertEquals(1L, testMfa.getUser().getId()); + assertEquals("Test", testMfa.getMfaSecretKey()); + assertEquals("Test", testMfa.getUser().getUsername()); + assertEquals("Test", testMfa.getUser().getPassword()); + + } + +} diff --git a/src/test/java/com/example/taskmanagerauth/unit/entity/UserTests.java b/src/test/java/com/example/taskmanagerauth/unit/entity/UserTests.java index 5157366..823679f 100644 --- a/src/test/java/com/example/taskmanagerauth/unit/entity/UserTests.java +++ b/src/test/java/com/example/taskmanagerauth/unit/entity/UserTests.java @@ -1,5 +1,6 @@ package com.example.taskmanagerauth.unit.entity; +import com.example.taskmanagerauth.entity.Mfa; import com.example.taskmanagerauth.entity.Role; import com.example.taskmanagerauth.entity.User; import org.junit.jupiter.api.Test; @@ -23,6 +24,7 @@ void testBasicBehavior() { // Creation User testUser = new User(); + Mfa mfa = Mfa.of(testUser, true, "Test"); // Setters testUser.setId(1L); @@ -30,6 +32,7 @@ void testBasicBehavior() { testUser.setRoles(Set.of(Role.of("USER"))); testUser.setUsername("Test username"); testUser.setLastAccessedAt(LocalDateTime.of(2000, 12, 30, 12, 30, 45)); + testUser.setMfa(mfa); // Getters assertEquals(1L, testUser.getId()); @@ -37,6 +40,8 @@ void testBasicBehavior() { assertEquals("USER", testUser.getRoles().stream().findFirst().orElseThrow().getName()); assertEquals("Test username", testUser.getUsername()); assertEquals(LocalDateTime.of(2000, 12, 30, 12, 30, 45), testUser.getLastAccessedAt()); + assertEquals(true, testUser.getMfa().getMfaEnabled()); + assertEquals("Test", testUser.getMfa().getMfaSecretKey()); } diff --git a/src/test/java/com/example/taskmanagerauth/unit/service/JwtServiceTests.java b/src/test/java/com/example/taskmanagerauth/unit/service/JwtServiceTests.java index 695b16d..64aa4dc 100644 --- a/src/test/java/com/example/taskmanagerauth/unit/service/JwtServiceTests.java +++ b/src/test/java/com/example/taskmanagerauth/unit/service/JwtServiceTests.java @@ -24,7 +24,7 @@ public class JwtServiceTests { @BeforeEach void setUp() { - this.jwtService = new JwtService("Test"); + this.jwtService = new JwtService("Test","Test2"); this.userDetails = new User( "Test id", // <- ID's are used instead of usernames