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
3 changes: 1 addition & 2 deletions src/main/java/com/example/bankapp/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public static PasswordEncoder passwordEncoder() {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/register").permitAll()
.anyRequest().authenticated()
Expand All @@ -41,7 +40,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logout(logout -> logout
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))
.logoutSuccessUrl("/login?logout")
.permitAll()
)
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=true

# Session cookie hardening
server.servlet.session.cookie.same-site=lax
server.servlet.session.cookie.http-only=true
17 changes: 12 additions & 5 deletions src/main/resources/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
border-bottom: 2px solid #B08D57;
}

.navbar-custom a {
.navbar-custom a,
.navbar-custom .btn-link {
color: #ffffff !important;
}

Expand Down Expand Up @@ -107,6 +108,7 @@
text-decoration: underline;
}
</style>
<noscript><style>#logout-link { display: none; }</style></noscript>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-custom">
Expand All @@ -117,7 +119,12 @@
<ul class="navbar-nav ml-auto">
<li class="nav-item"><a class="nav-link" href="/dashboard">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="/transactions">Transactions</a></li>
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
<li class="nav-item">
<form id="logout-form" method="post" th:action="@{/logout}" class="form-inline">
<a id="logout-link" class="nav-link" href="/logout" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
<noscript><button type="submit" class="nav-link btn btn-link">Logout</button></noscript>
</form>
</li>
</ul>
</nav>

Expand All @@ -141,7 +148,7 @@ <h4>Account Details</h4>
Deposit
</button>
<div class="collapse mt-3" id="depositForm">
<form method="post" action="/deposit" class="form-container">
<form method="post" th:action="@{/deposit}" class="form-container">
<div class="form-group">
<label>Amount:</label>
<input type="number" class="form-control" name="amount" required />
Expand All @@ -157,7 +164,7 @@ <h4>Account Details</h4>
Withdraw
</button>
<div class="collapse mt-3" id="withdrawForm">
<form method="post" action="/withdraw" class="form-container">
<form method="post" th:action="@{/withdraw}" class="form-container">
<div class="form-group">
<label>Amount:</label>
<input type="number" class="form-control" name="amount" required />
Expand All @@ -173,7 +180,7 @@ <h4>Account Details</h4>
Transfer Money
</button>
<div class="collapse mt-3" id="transferForm">
<form method="post" action="/transfer" class="form-container">
<form method="post" th:action="@{/transfer}" class="form-container">
<div class="form-group">
<label>Recipient Username:</label>
<input type="text" class="form-control" name="toUsername" required />
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@

<div class="container">
<h2 class="text-left">Login</h2>
<form method="post" action="/login">
<form method="post" th:action="@{/login}">
<div class="form-group">
<label>Username:</label>
<input type="text" class="form-control" name="username" required />
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@

<div class="container">
<h2 class="text-left">Register a New Account</h2>
<form method="post" action="/register">
<form method="post" th:action="@{/register}">
<div class="form-group">
<label>Username:</label>
<input type="text" class="form-control" name="username" required />
Expand Down
11 changes: 9 additions & 2 deletions src/main/resources/templates/transactions.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
border-bottom: 2px solid #B08D57;
}

.navbar-custom a {
.navbar-custom a,
.navbar-custom .btn-link {
color: #ffffff !important;
}

Expand Down Expand Up @@ -86,6 +87,7 @@
text-decoration: underline;
}
</style>
<noscript><style>#logout-link { display: none; }</style></noscript>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-custom">
Expand All @@ -96,7 +98,12 @@
<ul class="navbar-nav ml-auto">
<li class="nav-item"><a class="nav-link" href="/dashboard">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="/transactions">Transactions</a></li>
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
<li class="nav-item">
<form id="logout-form" method="post" th:action="@{/logout}" class="form-inline">
<a id="logout-link" class="nav-link" href="/logout" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
<noscript><button type="submit" class="nav-link btn btn-link">Logout</button></noscript>
</form>
</li>
</ul>
</nav>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.example.bankapp.controller;

import com.example.bankapp.config.SecurityConfig;
import com.example.bankapp.model.Account;
import com.example.bankapp.service.AccountService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import java.math.BigDecimal;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(BankController.class)
@Import(SecurityConfig.class)
class BankControllerCsrfTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private AccountService accountService;

private final Account account = new Account();

@BeforeEach
void setUp() {
account.setUsername("alice");
account.setBalance(new BigDecimal("1000"));
when(accountService.findAccountByUsername("alice")).thenReturn(account);
}

@Test
@WithMockUser(username = "alice")
void transferWithoutCsrfTokenIsForbidden() throws Exception {
mockMvc.perform(post("/transfer").param("toUsername", "mallory").param("amount", "500"))
.andExpect(status().isForbidden());
verify(accountService, never()).transferAmount(any(), anyString(), any());
}

@Test
@WithMockUser(username = "alice")
void transferWithCsrfTokenSucceeds() throws Exception {
mockMvc.perform(post("/transfer").param("toUsername", "bob").param("amount", "500").with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/dashboard"));
verify(accountService).transferAmount(account, "bob", new BigDecimal("500"));
}

@Test
@WithMockUser(username = "alice")
void withdrawWithoutCsrfTokenIsForbidden() throws Exception {
mockMvc.perform(post("/withdraw").param("amount", "500"))
.andExpect(status().isForbidden());
verify(accountService, never()).withdraw(any(), any());
}

@Test
@WithMockUser(username = "alice")
void withdrawWithCsrfTokenSucceeds() throws Exception {
mockMvc.perform(post("/withdraw").param("amount", "500").with(csrf()))
.andExpect(redirectedUrl("/dashboard"));
verify(accountService).withdraw(account, new BigDecimal("500"));
}

@Test
@WithMockUser(username = "alice")
void depositWithoutCsrfTokenIsForbidden() throws Exception {
mockMvc.perform(post("/deposit").param("amount", "500"))
.andExpect(status().isForbidden());
verify(accountService, never()).deposit(any(), any());
}

@Test
@WithMockUser(username = "alice")
void depositWithCsrfTokenSucceeds() throws Exception {
mockMvc.perform(post("/deposit").param("amount", "500").with(csrf()))
.andExpect(redirectedUrl("/dashboard"));
verify(accountService).deposit(account, new BigDecimal("500"));
}

@Test
@WithMockUser(username = "alice")
void dashboardFormsCarryCsrfToken() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("name=\"_csrf\"")));
}

@Test
@WithMockUser(username = "alice")
void logoutViaGetIsNotHandled() throws Exception {
mockMvc.perform(get("/logout"))
.andExpect(status().isNotFound());
}

@Test
@WithMockUser(username = "alice")
void logoutViaPostWithCsrfTokenSucceeds() throws Exception {
mockMvc.perform(post("/logout").with(csrf()))
.andExpect(redirectedUrl("/login?logout"));
}
}
Loading