diff --git a/pom.xml b/pom.xml index fc5bfeac..a479c3da 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,23 @@ spring-security-test test + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + diff --git a/src/main/java/com/example/bankapp/config/SecurityConfig.java b/src/main/java/com/example/bankapp/config/SecurityConfig.java index 4dbd1572..2d8c586f 100644 --- a/src/main/java/com/example/bankapp/config/SecurityConfig.java +++ b/src/main/java/com/example/bankapp/config/SecurityConfig.java @@ -1,15 +1,21 @@ package com.example.bankapp.config; +import com.example.bankapp.security.JwtAuthenticationFilter; import com.example.bankapp.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @@ -19,13 +25,38 @@ public class SecurityConfig { @Autowired AccountService accountService; + @Autowired + JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public static PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + @Order(1) + public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher("/api/**") + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/**").authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + @Order(2) + public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authz -> authz @@ -55,6 +86,5 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(accountService).passwordEncoder(passwordEncoder()); - } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/bankapp/controller/AuthController.java b/src/main/java/com/example/bankapp/controller/AuthController.java new file mode 100644 index 00000000..1d1d83c8 --- /dev/null +++ b/src/main/java/com/example/bankapp/controller/AuthController.java @@ -0,0 +1,73 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.dto.AuthResponse; +import com.example.bankapp.dto.LoginRequest; +import com.example.bankapp.dto.RegisterRequest; +import com.example.bankapp.model.Account; +import com.example.bankapp.security.JwtUtil; +import com.example.bankapp.service.AccountService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + private final AccountService accountService; + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public AuthController(AccountService accountService, JwtUtil jwtUtil, AuthenticationManager authenticationManager) { + this.accountService = accountService; + this.jwtUtil = jwtUtil; + this.authenticationManager = authenticationManager; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) + ); + + UserDetails userDetails = accountService.loadUserByUsername(request.getUsername()); + String token = jwtUtil.generateToken(userDetails); + + AuthResponse response = new AuthResponse(token, request.getUsername(), "Login successful"); + return ResponseEntity.ok(response); + } catch (BadCredentialsException e) { + Map error = new HashMap<>(); + error.put("error", "Invalid username or password"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", "Authentication failed: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterRequest request) { + try { + Account account = accountService.registerAccount(request.getUsername(), request.getPassword()); + + UserDetails userDetails = accountService.loadUserByUsername(account.getUsername()); + String token = jwtUtil.generateToken(userDetails); + + AuthResponse response = new AuthResponse(token, account.getUsername(), "Registration successful"); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } +} diff --git a/src/main/java/com/example/bankapp/controller/BankApiController.java b/src/main/java/com/example/bankapp/controller/BankApiController.java new file mode 100644 index 00000000..54ee1b8b --- /dev/null +++ b/src/main/java/com/example/bankapp/controller/BankApiController.java @@ -0,0 +1,110 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.dto.*; +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import com.example.bankapp.service.AccountService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api") +public class BankApiController { + + private final AccountService accountService; + + public BankApiController(AccountService accountService) { + this.accountService = accountService; + } + + @GetMapping("/account") + public ResponseEntity getAccount() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + Account account = accountService.findAccountByUsername(username); + AccountDTO accountDTO = new AccountDTO(account.getId(), account.getUsername(), account.getBalance()); + return ResponseEntity.ok(accountDTO); + } + + @PostMapping("/deposit") + public ResponseEntity deposit(@RequestBody DepositRequest request) { + try { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + Account account = accountService.findAccountByUsername(username); + accountService.deposit(account, request.getAmount()); + + Account updatedAccount = accountService.findAccountByUsername(username); + AccountDTO accountDTO = new AccountDTO(updatedAccount.getId(), updatedAccount.getUsername(), updatedAccount.getBalance()); + + Map response = new HashMap<>(); + response.put("message", "Deposit successful"); + response.put("account", accountDTO); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/withdraw") + public ResponseEntity withdraw(@RequestBody WithdrawRequest request) { + try { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + Account account = accountService.findAccountByUsername(username); + accountService.withdraw(account, request.getAmount()); + + Account updatedAccount = accountService.findAccountByUsername(username); + AccountDTO accountDTO = new AccountDTO(updatedAccount.getId(), updatedAccount.getUsername(), updatedAccount.getBalance()); + + Map response = new HashMap<>(); + response.put("message", "Withdrawal successful"); + response.put("account", accountDTO); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/transfer") + public ResponseEntity transfer(@RequestBody TransferRequest request) { + try { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + Account fromAccount = accountService.findAccountByUsername(username); + accountService.transferAmount(fromAccount, request.getToUsername(), request.getAmount()); + + Account updatedAccount = accountService.findAccountByUsername(username); + AccountDTO accountDTO = new AccountDTO(updatedAccount.getId(), updatedAccount.getUsername(), updatedAccount.getBalance()); + + Map response = new HashMap<>(); + response.put("message", "Transfer successful"); + response.put("account", accountDTO); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @GetMapping("/transactions") + public ResponseEntity> getTransactions() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + Account account = accountService.findAccountByUsername(username); + List transactions = accountService.getTransactionHistory(account); + + List transactionDTOs = transactions.stream() + .map(t -> new TransactionDTO(t.getId(), t.getAmount(), t.getType(), t.getTimestamp())) + .collect(Collectors.toList()); + + return ResponseEntity.ok(transactionDTOs); + } +} diff --git a/src/main/java/com/example/bankapp/dto/AccountDTO.java b/src/main/java/com/example/bankapp/dto/AccountDTO.java new file mode 100644 index 00000000..f948c625 --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/AccountDTO.java @@ -0,0 +1,42 @@ +package com.example.bankapp.dto; + +import java.math.BigDecimal; + +public class AccountDTO { + private Long id; + private String username; + private BigDecimal balance; + + public AccountDTO() { + } + + public AccountDTO(Long id, String username, BigDecimal balance) { + this.id = id; + this.username = username; + this.balance = balance; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } +} diff --git a/src/main/java/com/example/bankapp/dto/AuthResponse.java b/src/main/java/com/example/bankapp/dto/AuthResponse.java new file mode 100644 index 00000000..408648d9 --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/AuthResponse.java @@ -0,0 +1,40 @@ +package com.example.bankapp.dto; + +public class AuthResponse { + private String token; + private String username; + private String message; + + public AuthResponse() { + } + + public AuthResponse(String token, String username, String message) { + this.token = token; + this.username = username; + this.message = message; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/example/bankapp/dto/DepositRequest.java b/src/main/java/com/example/bankapp/dto/DepositRequest.java new file mode 100644 index 00000000..dee9f3db --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/DepositRequest.java @@ -0,0 +1,22 @@ +package com.example.bankapp.dto; + +import java.math.BigDecimal; + +public class DepositRequest { + private BigDecimal amount; + + public DepositRequest() { + } + + public DepositRequest(BigDecimal amount) { + this.amount = amount; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } +} diff --git a/src/main/java/com/example/bankapp/dto/LoginRequest.java b/src/main/java/com/example/bankapp/dto/LoginRequest.java new file mode 100644 index 00000000..ccd7b43d --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/LoginRequest.java @@ -0,0 +1,30 @@ +package com.example.bankapp.dto; + +public class LoginRequest { + private String username; + private String password; + + public LoginRequest() { + } + + public LoginRequest(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/bankapp/dto/RegisterRequest.java b/src/main/java/com/example/bankapp/dto/RegisterRequest.java new file mode 100644 index 00000000..e69b84ae --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/RegisterRequest.java @@ -0,0 +1,30 @@ +package com.example.bankapp.dto; + +public class RegisterRequest { + private String username; + private String password; + + public RegisterRequest() { + } + + 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/bankapp/dto/TransactionDTO.java b/src/main/java/com/example/bankapp/dto/TransactionDTO.java new file mode 100644 index 00000000..02e5a643 --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/TransactionDTO.java @@ -0,0 +1,53 @@ +package com.example.bankapp.dto; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class TransactionDTO { + private Long id; + private BigDecimal amount; + private String type; + private LocalDateTime timestamp; + + public TransactionDTO() { + } + + public TransactionDTO(Long id, BigDecimal amount, String type, LocalDateTime timestamp) { + this.id = id; + this.amount = amount; + this.type = type; + this.timestamp = timestamp; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } +} diff --git a/src/main/java/com/example/bankapp/dto/TransferRequest.java b/src/main/java/com/example/bankapp/dto/TransferRequest.java new file mode 100644 index 00000000..d36504b4 --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/TransferRequest.java @@ -0,0 +1,32 @@ +package com.example.bankapp.dto; + +import java.math.BigDecimal; + +public class TransferRequest { + private String toUsername; + 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; + } +} diff --git a/src/main/java/com/example/bankapp/dto/WithdrawRequest.java b/src/main/java/com/example/bankapp/dto/WithdrawRequest.java new file mode 100644 index 00000000..55a6988d --- /dev/null +++ b/src/main/java/com/example/bankapp/dto/WithdrawRequest.java @@ -0,0 +1,22 @@ +package com.example.bankapp.dto; + +import java.math.BigDecimal; + +public class WithdrawRequest { + private BigDecimal amount; + + public WithdrawRequest() { + } + + public WithdrawRequest(BigDecimal amount) { + this.amount = amount; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } +} diff --git a/src/main/java/com/example/bankapp/security/JwtAuthenticationFilter.java b/src/main/java/com/example/bankapp/security/JwtAuthenticationFilter.java new file mode 100644 index 00000000..fd698b06 --- /dev/null +++ b/src/main/java/com/example/bankapp/security/JwtAuthenticationFilter.java @@ -0,0 +1,65 @@ +package com.example.bankapp.security; + +import com.example.bankapp.service.AccountService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final AccountService accountService; + + public JwtAuthenticationFilter(JwtUtil jwtUtil, AccountService accountService) { + this.jwtUtil = jwtUtil; + this.accountService = accountService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + final String authorizationHeader = request.getHeader("Authorization"); + + String username = null; + String jwt = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + try { + username = jwtUtil.extractUsername(jwt); + } catch (Exception e) { + logger.error("Error extracting username from JWT: " + e.getMessage()); + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = accountService.loadUserByUsername(username); + + if (jwtUtil.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + } + + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getServletPath(); + return !path.startsWith("/api/") || path.startsWith("/api/auth/"); + } +} diff --git a/src/main/java/com/example/bankapp/security/JwtUtil.java b/src/main/java/com/example/bankapp/security/JwtUtil.java new file mode 100644 index 00000000..97c20cee --- /dev/null +++ b/src/main/java/com/example/bankapp/security/JwtUtil.java @@ -0,0 +1,74 @@ +package com.example.bankapp.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:defaultSecretKeyForJwtTokenGenerationThatIsAtLeast256BitsLong}") + private String secret; + + @Value("${jwt.expiration:86400000}") + private Long expiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createToken(claims, userDetails.getUsername()); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } +}