Skip to content

Commit 32d522a

Browse files
codebase/spring-security-compromised-password [BAEL-8086] (#16812)
* BAEL-8086: adding codebase * BAEL-8086: adding logback core dependency * using rest controller advice * adding custom validation annotation * removing maven wrapper * removing maven wrapper
1 parent f5b7e44 commit 32d522a

File tree

17 files changed

+506
-2
lines changed

17 files changed

+506
-2
lines changed

spring-security-modules/pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
<module>spring-security-oauth2-testing</module>
5757
<module>spring-security-saml2</module>
5858
<module>spring-security-oauth2-bff/backend</module>
59-
<module>spring-security-pkce-spa</module>
59+
<module>spring-security-pkce-spa</module>
60+
<module>spring-security-compromised-password</module>
6061
</modules>
6162

62-
</project>
63+
</project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<artifactId>spring-security-compromised-password</artifactId>
7+
<version>0.0.1</version>
8+
<packaging>jar</packaging>
9+
<name>spring-security-compromised-password</name>
10+
<description>codebase demonstrating the validation and handling of compromised passwords via Spring Security</description>
11+
12+
<parent>
13+
<groupId>com.baeldung</groupId>
14+
<artifactId>parent-boot-3</artifactId>
15+
<version>0.0.1-SNAPSHOT</version>
16+
<relativePath>../../parent-boot-3</relativePath>
17+
</parent>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-web</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-security</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.springframework.boot</groupId>
30+
<artifactId>spring-boot-starter-validation</artifactId>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.springframework.boot</groupId>
34+
<artifactId>spring-boot-starter-data-jpa</artifactId>
35+
</dependency>
36+
<dependency>
37+
<groupId>com.h2database</groupId>
38+
<artifactId>h2</artifactId>
39+
<scope>runtime</scope>
40+
</dependency>
41+
<dependency>
42+
<groupId>org.projectlombok</groupId>
43+
<artifactId>lombok</artifactId>
44+
<optional>true</optional>
45+
</dependency>
46+
<dependency>
47+
<groupId>ch.qos.logback</groupId>
48+
<artifactId>logback-core</artifactId>
49+
<version>${logback-core.version}</version>
50+
</dependency>
51+
</dependencies>
52+
53+
<build>
54+
<plugins>
55+
<plugin>
56+
<groupId>org.springframework.boot</groupId>
57+
<artifactId>spring-boot-maven-plugin</artifactId>
58+
<configuration>
59+
<excludes>
60+
<exclude>
61+
<groupId>org.projectlombok</groupId>
62+
<artifactId>lombok</artifactId>
63+
</exclude>
64+
</excludes>
65+
</configuration>
66+
</plugin>
67+
</plugins>
68+
</build>
69+
70+
<properties>
71+
<java.version>17</java.version>
72+
<spring-boot.version>3.3.0</spring-boot.version>
73+
<logback-core.version>1.5.6</logback-core.version>
74+
</properties>
75+
76+
</project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.baeldung.security;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class Application {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(Application.class, args);
11+
}
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.baeldung.security.configuration;
2+
3+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
7+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8+
import org.springframework.security.config.http.SessionCreationPolicy;
9+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
10+
import org.springframework.security.crypto.password.PasswordEncoder;
11+
import org.springframework.security.web.SecurityFilterChain;
12+
import org.springframework.security.web.authentication.password.HaveIBeenPwnedRestApiPasswordChecker;
13+
import org.springframework.web.client.RestClient;
14+
15+
import lombok.RequiredArgsConstructor;
16+
import lombok.SneakyThrows;
17+
18+
@Configuration
19+
@RequiredArgsConstructor
20+
public class SecurityConfiguration {
21+
22+
private static final String[] WHITELISTED_API_ENDPOINTS = { "/users" };
23+
private static final String CUSTOM_COMPROMISED_PASSWORD_CHECK_URL = "https://api.example.com/password-check";
24+
25+
@Bean
26+
@SneakyThrows
27+
public SecurityFilterChain configure(final HttpSecurity http) {
28+
http.csrf(csrfConfigurer -> csrfConfigurer.disable())
29+
.sessionManagement(
30+
sessionConfigurer -> sessionConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
31+
.authorizeHttpRequests(authManager -> {
32+
authManager.requestMatchers(WHITELISTED_API_ENDPOINTS).permitAll().anyRequest().authenticated();
33+
});
34+
35+
return http.build();
36+
}
37+
38+
@Bean
39+
public PasswordEncoder passwordEncoder() {
40+
return new BCryptPasswordEncoder();
41+
}
42+
43+
@Bean
44+
@ConditionalOnMissingBean
45+
public CompromisedPasswordChecker compromisedPasswordChecker() {
46+
return new HaveIBeenPwnedRestApiPasswordChecker();
47+
}
48+
49+
@Bean
50+
public CompromisedPasswordChecker customCompromisedPasswordChecker() {
51+
RestClient customRestClient = RestClient.builder().baseUrl(CUSTOM_COMPROMISED_PASSWORD_CHECK_URL)
52+
.defaultHeader("X-API-KEY", "api-key").build();
53+
54+
HaveIBeenPwnedRestApiPasswordChecker compromisedPasswordChecker = new HaveIBeenPwnedRestApiPasswordChecker();
55+
compromisedPasswordChecker.setRestClient(customRestClient);
56+
return compromisedPasswordChecker;
57+
}
58+
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.baeldung.security.controller;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.web.bind.annotation.PostMapping;
5+
import org.springframework.web.bind.annotation.RequestBody;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import com.baeldung.security.dto.UserCreationRequestDto;
10+
import com.baeldung.security.service.UserService;
11+
12+
import lombok.RequiredArgsConstructor;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping(value = "/users")
17+
public class UserController {
18+
19+
private final UserService userService;
20+
21+
@PostMapping
22+
public ResponseEntity<Void> create(@RequestBody UserCreationRequestDto userCreationRequest) {
23+
userService.create(userCreationRequest);
24+
return ResponseEntity.ok().build();
25+
}
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.baeldung.security.dto;
2+
3+
import com.baeldung.security.validation.NotCompromised;
4+
5+
import jakarta.validation.constraints.Email;
6+
import jakarta.validation.constraints.NotBlank;
7+
import lombok.Getter;
8+
import lombok.Setter;
9+
10+
@Getter
11+
@Setter
12+
public class UserCreationRequestDto {
13+
14+
@NotBlank(message = "EmailId must not be empty")
15+
@Email(message = "EmailId must be of valid format")
16+
private String emailId;
17+
18+
@NotCompromised
19+
@NotBlank(message = "Password must not be empty")
20+
private String password;
21+
22+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.baeldung.security.entity;
2+
3+
import java.time.LocalDateTime;
4+
import java.time.ZoneOffset;
5+
import java.util.UUID;
6+
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.Id;
9+
import jakarta.persistence.PrePersist;
10+
import jakarta.persistence.Table;
11+
import lombok.AccessLevel;
12+
import lombok.Getter;
13+
import lombok.Setter;
14+
15+
@Getter
16+
@Setter
17+
@Entity
18+
@Table(name = "users")
19+
public class User {
20+
21+
@Id
22+
@Setter(AccessLevel.NONE)
23+
private UUID id;
24+
25+
private String emailId;
26+
27+
private String password;
28+
29+
@Setter(AccessLevel.NONE)
30+
private LocalDateTime createdAt;
31+
32+
@PrePersist
33+
void onCreate() {
34+
this.id = UUID.randomUUID();
35+
this.createdAt = LocalDateTime.now(ZoneOffset.UTC);
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.baeldung.security.exception;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.web.server.ResponseStatusException;
5+
6+
public class AccountAlreadyExistsException extends ResponseStatusException {
7+
8+
private static final long serialVersionUID = 7559248156354211878L;
9+
10+
public AccountAlreadyExistsException(final String reason) {
11+
super(HttpStatus.CONFLICT, reason);
12+
}
13+
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.baeldung.security.exception;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ProblemDetail;
5+
import org.springframework.security.authentication.password.CompromisedPasswordException;
6+
import org.springframework.web.bind.annotation.ExceptionHandler;
7+
import org.springframework.web.bind.annotation.RestControllerAdvice;
8+
import org.springframework.web.server.ResponseStatusException;
9+
10+
@RestControllerAdvice
11+
public class ExceptionResponseHandler {
12+
13+
@ExceptionHandler(CompromisedPasswordException.class)
14+
public ProblemDetail handle(CompromisedPasswordException exception) {
15+
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage());
16+
}
17+
18+
@ExceptionHandler(ResponseStatusException.class)
19+
public ProblemDetail handle(ResponseStatusException exception) {
20+
return ProblemDetail.forStatusAndDetail(exception.getStatusCode(), exception.getReason());
21+
}
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.baeldung.security.repository;
2+
3+
import java.util.UUID;
4+
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.stereotype.Repository;
7+
8+
import com.baeldung.security.entity.User;
9+
10+
@Repository
11+
public interface UserRepository extends JpaRepository<User, UUID> {
12+
13+
boolean existsByEmailId(final String emailId);
14+
15+
}

0 commit comments

Comments
 (0)