Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ WORKDIR /src
# Copy source code from local to container
COPY . /src

# Build application and skip test cases
RUN mvn clean install -DskipTests=true
# Build application and run tests
RUN mvn clean install

#--------------------------------------
# Stage 2
Expand Down
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.bankapp.config;

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.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
String requestUri = request.getRequestURI();

logger.error("Access denied for user: {} - Attempted to access: {} - Error: {}",
username, requestUri, accessDeniedException.getMessage());

request.setAttribute("error", "You do not have permission to access this resource");
request.setAttribute("status", HttpServletResponse.SC_FORBIDDEN);
request.getRequestDispatcher("/error").forward(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.bankapp.config;

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.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

String requestUri = request.getRequestURI();
logger.warn("Unauthorized access attempt to: {} - Error: {}", requestUri, authException.getMessage());

if (isAjaxRequest(request)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"Please log in to access this resource\"}");
} else {
response.sendRedirect("/login");
}
}

private boolean isAjaxRequest(HttpServletRequest request) {
String ajaxHeader = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(ajaxHeader);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.example.bankapp.config;

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.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);

@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {

String username = request.getParameter("username");
String errorMessage;

if (exception instanceof BadCredentialsException) {
logger.error("Authentication failed - bad credentials for user: {}", username);
errorMessage = "Invalid username or password";
} else if (exception instanceof DisabledException) {
logger.error("Authentication failed - account disabled for user: {}", username);
errorMessage = "Your account has been disabled";
} else if (exception instanceof LockedException) {
logger.error("Authentication failed - account locked for user: {}", username);
errorMessage = "Your account has been locked";
} else {
logger.error("Authentication failed for user: {} - Error: {}", username, exception.getMessage());
errorMessage = "Authentication failed. Please try again.";
}

String encodedMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
response.sendRedirect("/login?error=true&message=" + encodedMessage);
}
}
26 changes: 23 additions & 3 deletions src/main/java/com/example/bankapp/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.example.bankapp.config;

import com.example.bankapp.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -16,26 +18,40 @@
@EnableWebSecurity
public class SecurityConfig {

private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

@Autowired
AccountService accountService;

@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;

@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
logger.info("Configuring security filter chain");

http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/register").permitAll()
.requestMatchers("/register", "/error", "/images/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard", true)
.failureHandler(authenticationFailureHandler)
.permitAll()
)
.logout(logout -> logout
Expand All @@ -45,6 +61,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.exceptionHandling(exception -> exception
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.sameOrigin())
);
Expand All @@ -54,7 +74,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
logger.info("Configuring global authentication");
auth.userDetailsService(accountService).passwordEncoder(passwordEncoder());

}
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/example/bankapp/dto/AmountRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.bankapp.dto;

import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;

import java.math.BigDecimal;

public class AmountRequest {

@NotNull(message = "Amount is required")
@DecimalMin(value = "0.01", message = "Amount must be greater than zero")
private BigDecimal amount;

public AmountRequest() {
}

public AmountRequest(BigDecimal amount) {
this.amount = amount;
}

public BigDecimal getAmount() {
return amount;
}

public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/example/bankapp/dto/RegistrationRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.bankapp.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public class RegistrationRequest {

@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
@Pattern(regexp = "^[\\w.-]+$", message = "Username can only contain alphanumeric characters, underscores, dots, and hyphens")
private String username;

@NotBlank(message = "Password is required")
@Size(min = 6, max = 100, message = "Password must be between 6 and 100 characters")
private String password;

public RegistrationRequest() {
}

public RegistrationRequest(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;
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/example/bankapp/dto/TransferRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.bankapp.dto;

import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

import java.math.BigDecimal;

public class TransferRequest {

@NotBlank(message = "Recipient username is required")
@Pattern(regexp = "^[\\w.-]+$", message = "Recipient username can only contain alphanumeric characters, underscores, dots, and hyphens")
private String toUsername;

@NotNull(message = "Amount is required")
@DecimalMin(value = "0.01", message = "Amount must be greater than zero")
private BigDecimal amount;

public TransferRequest() {
}

public TransferRequest(String toUsername, BigDecimal amount) {
this.toUsername = toUsername;
this.amount = amount;
}

public String getToUsername() {
return toUsername;
}

public void setToUsername(String toUsername) {
this.toUsername = toUsername;
}

public BigDecimal getAmount() {
return amount;
}

public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.bankapp.exception;

public class AccountNotFoundException extends RuntimeException {

public AccountNotFoundException(String message) {
super(message);
}

public AccountNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.bankapp.exception;

public class DuplicateUsernameException extends RuntimeException {

public DuplicateUsernameException(String message) {
super(message);
}

public DuplicateUsernameException(String message, Throwable cause) {
super(message, cause);
}
}
Loading