diff --git a/spring-security-modules/spring-security-web-boot-3/pom.xml b/spring-security-modules/spring-security-web-boot-3/pom.xml index 5f2a455294..5da993acd9 100644 --- a/spring-security-modules/spring-security-web-boot-3/pom.xml +++ b/spring-security-modules/spring-security-web-boot-3/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 spring-security-web-boot-3 0.0.1-SNAPSHOT @@ -23,6 +24,15 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-mongodb + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 3.3.1 + commons-io commons-io diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/MongoAuthApplication.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/MongoAuthApplication.java new file mode 100644 index 0000000000..53624c0dd8 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/MongoAuthApplication.java @@ -0,0 +1,18 @@ +package com.baeldung.mongoauth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +import com.baeldung.mongoauth.config.MongoConfig; +import com.baeldung.mongoauth.config.SecurityConfig; + +@SpringBootApplication +@Import({ SecurityConfig.class, MongoConfig.class }) +public class MongoAuthApplication { + + public static void main(String... args) { + SpringApplication.run(MongoAuthApplication.class, args); + } + +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/config/MongoConfig.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/config/MongoConfig.java new file mode 100644 index 0000000000..ddef7800de --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/config/MongoConfig.java @@ -0,0 +1,40 @@ +package com.baeldung.mongoauth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.util.SocketUtils; + +import com.mongodb.client.MongoClients; + +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig; +import de.flapdoodle.embed.mongo.config.MongodConfig; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.runtime.Network; + +@Configuration +public class MongoConfig { + + private static final String CONNECTION_STRING = "mongodb://%s:%d"; + private static final String HOST = "localhost"; + + @Bean + public MongoTemplate mongoTemplate() throws Exception { + + int randomPort = SocketUtils.findAvailableTcpPort(); + + ImmutableMongodConfig mongoDbConfig = MongodConfig.builder() + .version(Version.Main.PRODUCTION) + .net(new Net(HOST, randomPort, Network.localhostIsIPv6())) + .build(); + + MongodStarter starter = MongodStarter.getDefaultInstance(); + MongodExecutable mongodExecutable = starter.prepare(mongoDbConfig); + mongodExecutable.start(); + return new MongoTemplate(MongoClients.create(String.format(CONNECTION_STRING, HOST, randomPort)), "mongo_auth"); + } + +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/config/SecurityConfig.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/config/SecurityConfig.java new file mode 100644 index 0000000000..050d917492 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/config/SecurityConfig.java @@ -0,0 +1,59 @@ +package com.baeldung.mongoauth.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final UserDetailsService userDetailsService; + + public SecurityConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public AuthenticationManager customAuthenticationManager() throws Exception { + return authenticationManager(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(@Autowired AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService) + .passwordEncoder(bCryptPasswordEncoder()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .authorizeRequests() + .and() + .httpBasic() + .and() + .authorizeRequests() + .anyRequest() + .permitAll() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/controller/ResourceController.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/controller/ResourceController.java new file mode 100644 index 0000000000..a5d9e91083 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/controller/ResourceController.java @@ -0,0 +1,23 @@ +package com.baeldung.mongoauth.controller; + +import javax.annotation.security.RolesAllowed; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ResourceController { + + @RolesAllowed("ROLE_ADMIN") + @GetMapping("/admin") + public String admin() { + return "Hello Admin!"; + } + + @RolesAllowed({ "ROLE_ADMIN", "ROLE_USER" }) + @GetMapping("/user") + public String user() { + return "Hello User!"; + } + +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/Role.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/Role.java new file mode 100644 index 0000000000..e475e68460 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/Role.java @@ -0,0 +1,13 @@ +package com.baeldung.mongoauth.domain; + +public class Role { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/User.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/User.java new file mode 100644 index 0000000000..ffaea836a4 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/User.java @@ -0,0 +1,83 @@ +package com.baeldung.mongoauth.domain; + +import java.util.Objects; +import java.util.Set; + +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.MongoId; +import org.springframework.security.core.userdetails.UserDetails; + +@Document +public class User implements UserDetails { + private @MongoId ObjectId id; + private String username; + private String password; + private Set userRoles; + + public ObjectId getId() { + return id; + } + + @Override + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setUserRoles(Set userRoles) { + this.userRoles = userRoles; + } + + @Override + public Set getAuthorities() { + return this.userRoles; + } + + @Override + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + User user = (User) o; + return Objects.equals(username, user.username); + } + + @Override + public int hashCode() { + return Objects.hash(username); + } +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/UserRole.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/UserRole.java new file mode 100644 index 0000000000..ccfa3bd605 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/domain/UserRole.java @@ -0,0 +1,21 @@ +package com.baeldung.mongoauth.domain; + +import org.springframework.security.core.GrantedAuthority; + +public class UserRole implements GrantedAuthority { + + private Role role; + + @Override + public String getAuthority() { + return role.getName(); + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/repository/UserRepository.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/repository/UserRepository.java new file mode 100644 index 0000000000..c68f77ffbf --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.baeldung.mongoauth.repository; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import com.baeldung.mongoauth.domain.User; + +public interface UserRepository extends MongoRepository { + + @Query("{username:'?0'}") + User findUserByUsername(String username); + +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/MongoAuthUserDetailService.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/MongoAuthUserDetailService.java new file mode 100644 index 0000000000..5838504d40 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/MongoAuthUserDetailService.java @@ -0,0 +1,41 @@ +package com.baeldung.mongoauth.service; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import com.baeldung.mongoauth.repository.UserRepository; + +@Service +public class MongoAuthUserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + public MongoAuthUserDetailService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { + + com.baeldung.mongoauth.domain.User user = userRepository.findUserByUsername(userName); + + Set grantedAuthorities = new HashSet<>(); + + user.getAuthorities() + .forEach(role -> { + grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole() + .getName())); + }); + + return new User(user.getUsername(), user.getPassword(), grantedAuthorities); + } + +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/SecurityService.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/SecurityService.java new file mode 100644 index 0000000000..4204e4708b --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/SecurityService.java @@ -0,0 +1,5 @@ +package com.baeldung.mongoauth.service; + +public interface SecurityService { + boolean login(String username, String password); +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/SecurityServiceImpl.java b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/SecurityServiceImpl.java new file mode 100644 index 0000000000..f86ffaa26a --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/java/com/baeldung/mongoauth/service/SecurityServiceImpl.java @@ -0,0 +1,39 @@ +package com.baeldung.mongoauth.service; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +@Service +public class SecurityServiceImpl implements SecurityService { + + private final AuthenticationManager authenticationManager; + + private final UserDetailsService userDetailsService; + + public SecurityServiceImpl(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) { + this.authenticationManager = authenticationManager; + this.userDetailsService = userDetailsService; + } + + @Override + public boolean login(String username, String password) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); + + authenticationManager.authenticate(usernamePasswordAuthenticationToken); + + if (usernamePasswordAuthenticationToken.isAuthenticated()) { + SecurityContextHolder.getContext() + .setAuthentication(usernamePasswordAuthenticationToken); + + return true; + } + + return false; + } +} diff --git a/spring-security-modules/spring-security-web-boot-3/src/main/resources/application.properties b/spring-security-modules/spring-security-web-boot-3/src/main/resources/application.properties new file mode 100644 index 0000000000..a5b5fb9804 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.mongodb.embedded.version=4.4.9 \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java b/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java new file mode 100644 index 0000000000..b7994cad9e --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java @@ -0,0 +1,118 @@ +package com.baeldung.mongoauth; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; +import java.util.HashSet; + +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.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.baeldung.mongoauth.domain.Role; +import com.baeldung.mongoauth.domain.User; +import com.baeldung.mongoauth.domain.UserRole; + +@SpringBootTest(classes = { MongoAuthApplication.class }) +@AutoConfigureMockMvc +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class MongoAuthApplicationIntegrationTest { + + @Autowired + private WebApplicationContext context; + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private BCryptPasswordEncoder bCryptPasswordEncoder; + + private MockMvc mvc; + + private static final String USER_NAME = "user@gmail.com"; + private static final String ADMIN_NAME = "admin@gmail.com"; + private static final String PASSWORD = "password"; + + @BeforeEach + public void setup() { + + setUp(); + + mvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + private void setUp() { + Role roleUser = new Role(); + roleUser.setName("ROLE_USER"); + mongoTemplate.save(roleUser); + + User user = new User(); + user.setUsername(USER_NAME); + user.setPassword(bCryptPasswordEncoder.encode(PASSWORD)); + + UserRole userRole = new UserRole(); + userRole.setRole(roleUser); + user.setUserRoles(new HashSet<>(Collections.singletonList(userRole))); + mongoTemplate.save(user); + + User admin = new User(); + admin.setUsername(ADMIN_NAME); + admin.setPassword(bCryptPasswordEncoder.encode(PASSWORD)); + + Role roleAdmin = new Role(); + roleAdmin.setName("ROLE_ADMIN"); + mongoTemplate.save(roleAdmin); + + UserRole adminRole = new UserRole(); + adminRole.setRole(roleAdmin); + admin.setUserRoles(new HashSet<>(Collections.singletonList(adminRole))); + mongoTemplate.save(admin); + } + + @Test + void givenUserCredentials_whenInvokeUserAuthorizedEndPoint_thenReturn200() throws Exception { + mvc.perform(get("/user").with(httpBasic(USER_NAME, PASSWORD))) + .andExpect(status().isOk()); + } + + @Test + void givenUserNotExists_whenInvokeEndPoint_thenReturn401() throws Exception { + mvc.perform(get("/user").with(httpBasic("not_existing_user", "password"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void givenUserExistsAndWrongPassword_whenInvokeEndPoint_thenReturn401() throws Exception { + mvc.perform(get("/user").with(httpBasic(USER_NAME, "wrong_password"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void givenUserCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn403() throws Exception { + mvc.perform(get("/admin").with(httpBasic(USER_NAME, PASSWORD))) + .andExpect(status().isForbidden()); + } + + @Test + void givenAdminCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn200() throws Exception { + mvc.perform(get("/admin").with(httpBasic(ADMIN_NAME, PASSWORD))) + .andExpect(status().isOk()); + + mvc.perform(get("/user").with(httpBasic(ADMIN_NAME, PASSWORD))) + .andExpect(status().isOk()); + } + +}