diff --git a/pom.xml b/pom.xml index ba1eac8..f4f7a06 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ anyame-backend 0.0.1-SNAPSHOT anyame-backend - User service for anyame backend + User service for anyame @@ -50,6 +50,10 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.security + spring-security-oauth2-authorization-server + io.jsonwebtoken diff --git a/src/main/java/com/backend/user/service/anyame/component/AuthorizationProperties.java b/src/main/java/com/backend/user/service/anyame/component/AuthorizationProperties.java deleted file mode 100644 index 0fdd990..0000000 --- a/src/main/java/com/backend/user/service/anyame/component/AuthorizationProperties.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.backend.user.service.anyame.component; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.util.List; -import java.util.Optional; - -@Component -@ConfigurationProperties(prefix = "authorization") -public class AuthorizationProperties { - - private List roles; - private String hierarchy; - private String defaultRole; - - public List getRoles() { - return roles; - } - - public String getHierarchy() { - return hierarchy; - } - - public String getDefaultRole() { - return defaultRole; - } - - public AuthorizationProperties setRoles(List roles) { - this.roles = roles; - return this; - } - - public AuthorizationProperties setHierarchy(String hierarchy) { - this.hierarchy = hierarchy; - return this; - } - - public AuthorizationProperties setDefaultRole(String defaultRole) { - this.defaultRole = defaultRole; - return this; - } - - public Optional getAccessExpiry(String roleName) { - return roles.stream() - .filter(role -> role.getName().equals(roleName)) - .map(RoleConfig::getAccessExpiry) - .findFirst(); - } - - public Optional getRefreshExpiry(String roleName) { - return roles.stream() - .filter(role -> role.getName().equals(roleName)) - .map(RoleConfig::getRefreshExpiry) - .findFirst(); - } - - public static class RoleConfig { - - private String name; - private Duration accessExpiry; - private Duration refreshExpiry; - private List privileges; - - public String getName() { - return name; - } - - public Duration getAccessExpiry() { - return accessExpiry; - } - - public Duration getRefreshExpiry() { - return refreshExpiry; - } - - public List getPrivileges() { - return privileges; - } - - public RoleConfig setName(String name) { - this.name = name; - return this; - } - - public RoleConfig setAccessExpiry(Duration accessExpiry) { - this.accessExpiry = accessExpiry; - return this; - } - - public RoleConfig setRefreshExpiry(Duration refreshExpiry) { - this.refreshExpiry = refreshExpiry; - return this; - } - - public RoleConfig setPrivileges(List privileges) { - this.privileges = privileges; - return this; - } - - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/component/AuthorizationServerProperties.java b/src/main/java/com/backend/user/service/anyame/component/AuthorizationServerProperties.java new file mode 100644 index 0000000..b43b4a5 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/component/AuthorizationServerProperties.java @@ -0,0 +1,31 @@ +package com.backend.user.service.anyame.component; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "spring.security.oauth2.authorizationserver") +public class AuthorizationServerProperties { + + private String issuerUrl; + private String introspectionEndpoint; + + public String getIssuerUrl() { + return issuerUrl; + } + + public AuthorizationServerProperties setIssuerUrl(String issuerUrl) { + this.issuerUrl = issuerUrl; + return this; + } + + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + public AuthorizationServerProperties setIntrospectionEndpoint(String introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + return this; + } + +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/component/DefaultRoleProvider.java b/src/main/java/com/backend/user/service/anyame/component/DefaultRoleProvider.java deleted file mode 100644 index 11ee60c..0000000 --- a/src/main/java/com/backend/user/service/anyame/component/DefaultRoleProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.backend.user.service.anyame.component; - -import com.backend.user.service.anyame.entity.Role; -import com.backend.user.service.anyame.repository.RoleRepository; -import jakarta.annotation.PostConstruct; -import org.springframework.stereotype.Component; - -@Component -public class DefaultRoleProvider { - private final AuthorizationProperties authorizationProperties; - private final RoleRepository repository; - private Role defaultRole; - - public DefaultRoleProvider(AuthorizationProperties authorizationProperties, RoleRepository repository) { - this.authorizationProperties = authorizationProperties; - this.repository = repository; - } - - @PostConstruct - public void init() { - defaultRole = repository.findByName(authorizationProperties.getDefaultRole()).orElseThrow(); - } - - public Role getDefaultRole() { - return defaultRole; - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/component/JWTSecretProvider.java b/src/main/java/com/backend/user/service/anyame/component/JWTSecretProvider.java deleted file mode 100644 index a44afd0..0000000 --- a/src/main/java/com/backend/user/service/anyame/component/JWTSecretProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.backend.user.service.anyame.component; - -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import javax.crypto.SecretKey; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; - -@Component -public class JWTSecretProvider { - - @Value("${jwt.secret}") - private String jwtSecret; - private SecretKey secretKey; - - @PostConstruct - public void init() { - secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)); - } - - public SecretKey getSecretKey() { - return secretKey; - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/component/SetupDataLoader.java b/src/main/java/com/backend/user/service/anyame/component/SetupDataLoader.java deleted file mode 100644 index 1e727bb..0000000 --- a/src/main/java/com/backend/user/service/anyame/component/SetupDataLoader.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.backend.user.service.anyame.component; - -import com.backend.user.service.anyame.entity.Privilege; -import com.backend.user.service.anyame.entity.Role; -import com.backend.user.service.anyame.repository.PrivilegeRepository; -import com.backend.user.service.anyame.repository.RoleRepository; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -@Component -public class SetupDataLoader implements - ApplicationListener { - - boolean alreadySetup = false; - private final AuthorizationProperties authorizationProperties; - private final RoleRepository roleRepository; - private final PrivilegeRepository privilegeRepository; - - public SetupDataLoader(AuthorizationProperties authorizationProperties, - RoleRepository roleRepository, - PrivilegeRepository privilegeRepository) { - this.authorizationProperties = authorizationProperties; - this.roleRepository = roleRepository; - this.privilegeRepository = privilegeRepository; - } - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - if (alreadySetup) - return; - authorizationProperties.getRoles().forEach(roleConfig -> { - List privileges = roleConfig.getPrivileges() - .stream() - .map(this::createPrivilegeIfNotFound) - .toList(); - createRoleIfNotFound(roleConfig.getName(), privileges); - }); - } - - Privilege createPrivilegeIfNotFound(String name) { - Optional privilegeOptional = privilegeRepository.findByName(name); - if (privilegeOptional.isEmpty()) { - privilegeOptional = Optional.of(new Privilege(name)); - privilegeRepository.save(privilegeOptional.get()); - } - return privilegeOptional.get(); - } - - Role createRoleIfNotFound(String name, Collection privileges) { - Optional roleOptional = roleRepository.findByName(name); - if (roleOptional.isEmpty()) { - roleOptional = Optional.of(new Role(name)); - roleOptional.get().setPrivileges(privileges); - roleRepository.save(roleOptional.get()); - } - return roleOptional.get(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/config/AuthorizationServerConfig.java b/src/main/java/com/backend/user/service/anyame/config/AuthorizationServerConfig.java new file mode 100644 index 0000000..c2444e1 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/config/AuthorizationServerConfig.java @@ -0,0 +1,105 @@ +package com.backend.user.service.anyame.config; + +import com.backend.user.service.anyame.component.AuthorizationServerProperties; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.web.cors.CorsConfigurationSource; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +@Configuration(proxyBeanMethods = false) +public class AuthorizationServerConfig { + private final AuthorizationServerProperties authorizationServerProperties; + + public AuthorizationServerConfig(AuthorizationServerProperties authorizationServerProperties) { + this.authorizationServerProperties = authorizationServerProperties; + } + + @Bean + @Order(1) + public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http, @Qualifier("corsConfigurationSource") CorsConfigurationSource configurationSource) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + OAuth2AuthorizationServerConfigurer.authorizationServer(); + return http + .cors(c -> c.configurationSource(configurationSource)) + .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) + .with(authorizationServerConfigurer, (authorizationServer) -> + authorizationServer + .oidc(Customizer.withDefaults()) + ) + .authorizeHttpRequests((authorize) -> + authorize + .anyRequest().authenticated() + ) + .exceptionHandling((exceptions) -> exceptions + .defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), + new MediaTypeRequestMatcher(MediaType.ALL) + ) + ) + .build(); + } + + @Bean + public RegisteredClientRepository registeredClientRepository() { + RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("oidc-client") + .clientSecret("{noop}secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .redirectUri("http://localhost:5173/code") + .postLogoutRedirectUri("http://localhost:5173/") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) + .tokenSettings(TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .accessTokenTimeToLive(Duration.of(30, ChronoUnit.MINUTES)) + .refreshTokenTimeToLive(Duration.of(120, ChronoUnit.MINUTES)) + .reuseRefreshTokens(false) + .authorizationCodeTimeToLive(Duration.of(30, ChronoUnit.SECONDS)) + .build()) + .build(); + + return new InMemoryRegisteredClientRepository(oidcClient); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() + .issuer(authorizationServerProperties.getIssuerUrl()) + .tokenIntrospectionEndpoint(authorizationServerProperties.getIntrospectionEndpoint()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java b/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java index 6049794..23cbf0d 100644 --- a/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java +++ b/src/main/java/com/backend/user/service/anyame/config/SecurityConfig.java @@ -1,51 +1,42 @@ package com.backend.user.service.anyame.config; -import com.backend.user.service.anyame.component.AuthorizationProperties; -import org.passay.CharacterRule; -import org.passay.EnglishCharacterData; -import org.passay.LengthRule; -import org.passay.PasswordValidator; -import org.passay.WhitespaceRule; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; +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.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfigurationSource; -import java.util.Arrays; +import static org.springframework.security.config.Customizer.withDefaults; -@Configuration +@Configuration(proxyBeanMethods = false) public class SecurityConfig { @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(c -> c.anyRequest().permitAll()).build(); + @Order(2) + public SecurityFilterChain filterChain(HttpSecurity http, @Qualifier("corsConfigurationSource") CorsConfigurationSource configurationSource) throws Exception { + return http + .cors(c -> c.configurationSource(configurationSource)) + .authorizeHttpRequests(c -> + c.anyRequest().authenticated() + ) + .formLogin(withDefaults()) + .build(); } @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + public UserDetailsService users() { + UserDetails user = User.builder() + .username("admin") + .password("{noop}password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); } - @Bean - public PasswordValidator passwordValidator() { - return new PasswordValidator(Arrays.asList( - new LengthRule(8, 64), - new CharacterRule(EnglishCharacterData.UpperCase, 1), - new CharacterRule(EnglishCharacterData.LowerCase, 1), - new CharacterRule(EnglishCharacterData.Digit, 1), - new CharacterRule(EnglishCharacterData.Special, 1), - new WhitespaceRule() - )); - } - - @Bean - public RoleHierarchy roleHierarchy(AuthorizationProperties authorizationProperties) { - return RoleHierarchyImpl.fromHierarchy(authorizationProperties.getHierarchy()); - } - -} +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/config/WebConfig.java b/src/main/java/com/backend/user/service/anyame/config/WebConfig.java new file mode 100644 index 0000000..c8e94ea --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/config/WebConfig.java @@ -0,0 +1,38 @@ +package com.backend.user.service.anyame.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + + @Bean(name = "corsConfigurationSource") + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:5173")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/controller/AuthController.java b/src/main/java/com/backend/user/service/anyame/controller/AuthController.java deleted file mode 100644 index 6453b89..0000000 --- a/src/main/java/com/backend/user/service/anyame/controller/AuthController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.backend.user.service.anyame.controller; - -import com.backend.user.service.anyame.dto.AuthResponse; -import com.backend.user.service.anyame.dto.LoginRequest; -import com.backend.user.service.anyame.dto.RegisterRequest; -import com.backend.user.service.anyame.exception.UserAlreadyExistsException; -import com.backend.user.service.anyame.exception.InvalidCredentialsException; -import com.backend.user.service.anyame.exception.InvalidRoleException; -import com.backend.user.service.anyame.exception.NoExpiryDurationException; -import com.backend.user.service.anyame.exception.UnsafePasswordException; -import com.backend.user.service.anyame.service.AuthService; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; - -@RestController -@RequestMapping("/api/auth") -public class AuthController { - - private final AuthService authService; - - public AuthController(AuthService authService) { - this.authService = authService; - } - - @PostMapping("/register") - public ResponseEntity register(@RequestBody RegisterRequest request) { - try { - return ResponseEntity.ok(authService.register(request)); - } catch (UnsafePasswordException e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsafe password, length 8-64, 1 upper case, 1 lower case, 1 digit, 1 special symbol. No whitespaces in password."); - } catch (UserAlreadyExistsException e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "user already registered"); - } catch (NoExpiryDurationException e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "no expiry duration found, invalid server config"); - } catch (InvalidRoleException e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "invalid role defined: " + e.getInvalidRole()); - } - } - - @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest request) { - try { - return ResponseEntity.ok(authService.login(request)); - } catch (InvalidCredentialsException e) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials", e); - } catch (NoExpiryDurationException e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "no expiry duration found, invalid server config"); - } catch (InvalidRoleException e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "invalid role defined: " + e.getInvalidRole()); - } - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/controller/HelloController.java b/src/main/java/com/backend/user/service/anyame/controller/HelloController.java new file mode 100644 index 0000000..8b5ffaa --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/controller/HelloController.java @@ -0,0 +1,14 @@ +package com.backend.user.service.anyame.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class HelloController { + @GetMapping("/hello") + public String hello() { + return "Hello World"; + } +} diff --git a/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java b/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java deleted file mode 100644 index fceee93..0000000 --- a/src/main/java/com/backend/user/service/anyame/dto/AuthResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.backend.user.service.anyame.dto; - -import java.util.List; - -public record AuthResponse(String accessToken, String refreshToken, long id, String email, String name, List roles) { -} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/dto/LoginRequest.java b/src/main/java/com/backend/user/service/anyame/dto/LoginRequest.java deleted file mode 100644 index 4c509e6..0000000 --- a/src/main/java/com/backend/user/service/anyame/dto/LoginRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.backend.user.service.anyame.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -public record LoginRequest( - @NotBlank(message = "email must not be blank") - @Email(message = "email must be valid") - String email, - @NotNull(message = "password must not be null, but can be blank") - String password) { - -} diff --git a/src/main/java/com/backend/user/service/anyame/dto/RegisterRequest.java b/src/main/java/com/backend/user/service/anyame/dto/RegisterRequest.java deleted file mode 100644 index 86c65b3..0000000 --- a/src/main/java/com/backend/user/service/anyame/dto/RegisterRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.backend.user.service.anyame.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -public record RegisterRequest( - @NotBlank(message = "name must not be blank") - @Size(min = 3, max = 16, message = "name min length is 3, max is 16") - String name, - @NotBlank(message = "email must not be blank") - @Email(message = "email must be valid") - String email, - @NotNull(message = "password must not be null, but can be blank") - String password) { -} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/entity/Privilege.java b/src/main/java/com/backend/user/service/anyame/entity/Privilege.java deleted file mode 100644 index 686c895..0000000 --- a/src/main/java/com/backend/user/service/anyame/entity/Privilege.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.backend.user.service.anyame.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToMany; - -import java.util.Collection; - -@Entity -public class Privilege { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - private String name; - @ManyToMany(mappedBy = "privileges") - private Collection roles; - - protected Privilege() { - } - - public Privilege(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public Collection getRoles() { - return roles; - } - -} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/entity/Role.java b/src/main/java/com/backend/user/service/anyame/entity/Role.java deleted file mode 100644 index 8779eb8..0000000 --- a/src/main/java/com/backend/user/service/anyame/entity/Role.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.backend.user.service.anyame.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; - -import java.util.Collection; - -@Entity -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - private String name; - @ManyToMany(mappedBy = "roles") - private Collection users; - - @ManyToMany - @JoinTable( - name = "roles_privileges", - joinColumns = @JoinColumn( - name = "role_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn( - name = "privilege_id", referencedColumnName = "id")) - private Collection privileges; - - protected Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public Collection getUsers() { - return users; - } - - public Collection getPrivileges() { - return privileges; - } - - public Role setPrivileges(Collection privileges) { - this.privileges = privileges; - return this; - } - -} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/entity/User.java b/src/main/java/com/backend/user/service/anyame/entity/User.java deleted file mode 100644 index dd41ca9..0000000 --- a/src/main/java/com/backend/user/service/anyame/entity/User.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.backend.user.service.anyame.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; -import jakarta.persistence.Table; - -import java.util.Collection; - -@Entity -@Table(name = "users") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; - @Column(unique = true, nullable = false) - private String email; - private String password; - private boolean enabled = true; - - @ManyToMany - @JoinTable( - name = "users_roles", - joinColumns = @JoinColumn( - name = "user_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn( - name = "role_id", referencedColumnName = "id")) - private Collection roles; - - protected User() { - } - - public User(String name, String email, String password) { - this.name = name; - this.email = email; - this.password = password; - } - - public User(String name, String email) { - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public User setPassword(String password) { - this.password = password; - return this; - } - - public boolean isEnabled() { - return enabled; - } - - public User setEnabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - public Collection getRoles() { - return roles; - } - - public User setRoles(Collection roles) { - this.roles = roles; - return this; - } - -} - diff --git a/src/main/java/com/backend/user/service/anyame/exception/InvalidCredentialsException.java b/src/main/java/com/backend/user/service/anyame/exception/InvalidCredentialsException.java deleted file mode 100644 index d56e7da..0000000 --- a/src/main/java/com/backend/user/service/anyame/exception/InvalidCredentialsException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.backend.user.service.anyame.exception; - -public class InvalidCredentialsException extends Exception { - - public InvalidCredentialsException() { - super("invalid credentials"); - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/exception/InvalidRoleException.java b/src/main/java/com/backend/user/service/anyame/exception/InvalidRoleException.java deleted file mode 100644 index 153e343..0000000 --- a/src/main/java/com/backend/user/service/anyame/exception/InvalidRoleException.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.backend.user.service.anyame.exception; - -public class InvalidRoleException extends Exception { - private final String invalidRole; - - public InvalidRoleException(String invalidRole) { - this.invalidRole = invalidRole; - } - - public InvalidRoleException(String message, String invalidRole) { - super(message); - this.invalidRole = invalidRole; - } - - public String getInvalidRole() { - return invalidRole; - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/exception/NoExpiryDurationException.java b/src/main/java/com/backend/user/service/anyame/exception/NoExpiryDurationException.java deleted file mode 100644 index f159331..0000000 --- a/src/main/java/com/backend/user/service/anyame/exception/NoExpiryDurationException.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.backend.user.service.anyame.exception; - -public class NoExpiryDurationException extends Exception { - -} diff --git a/src/main/java/com/backend/user/service/anyame/exception/UnsafePasswordException.java b/src/main/java/com/backend/user/service/anyame/exception/UnsafePasswordException.java deleted file mode 100644 index 394286c..0000000 --- a/src/main/java/com/backend/user/service/anyame/exception/UnsafePasswordException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.backend.user.service.anyame.exception; - -public class UnsafePasswordException extends Exception { - - public UnsafePasswordException(String message) { - super(message); - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/exception/UserAlreadyExistsException.java b/src/main/java/com/backend/user/service/anyame/exception/UserAlreadyExistsException.java deleted file mode 100644 index fd83018..0000000 --- a/src/main/java/com/backend/user/service/anyame/exception/UserAlreadyExistsException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.backend.user.service.anyame.exception; - -public class UserAlreadyExistsException extends Exception { - - public UserAlreadyExistsException() { - super("user already exists"); - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/repository/PrivilegeRepository.java b/src/main/java/com/backend/user/service/anyame/repository/PrivilegeRepository.java deleted file mode 100644 index 4f8a585..0000000 --- a/src/main/java/com/backend/user/service/anyame/repository/PrivilegeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.backend.user.service.anyame.repository; - -import com.backend.user.service.anyame.entity.Privilege; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface PrivilegeRepository extends JpaRepository { - - Optional findByName(String name); - -} diff --git a/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java b/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java deleted file mode 100644 index 4204795..0000000 --- a/src/main/java/com/backend/user/service/anyame/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.backend.user.service.anyame.repository; - -import com.backend.user.service.anyame.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface RoleRepository extends JpaRepository { - - Optional findByName(String roleName); - -} diff --git a/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java b/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java deleted file mode 100644 index 0663e18..0000000 --- a/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.backend.user.service.anyame.repository; - -import com.backend.user.service.anyame.entity.User; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - - Optional findByEmail(String email); - - Optional findByEmailOrName(String email, String name); - -} diff --git a/src/main/java/com/backend/user/service/anyame/service/AuthService.java b/src/main/java/com/backend/user/service/anyame/service/AuthService.java deleted file mode 100644 index 8195f97..0000000 --- a/src/main/java/com/backend/user/service/anyame/service/AuthService.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.backend.user.service.anyame.service; - -import com.backend.user.service.anyame.component.DefaultRoleProvider; -import com.backend.user.service.anyame.dto.AuthResponse; -import com.backend.user.service.anyame.dto.LoginRequest; -import com.backend.user.service.anyame.dto.RegisterRequest; -import com.backend.user.service.anyame.entity.Role; -import com.backend.user.service.anyame.entity.User; -import com.backend.user.service.anyame.exception.UserAlreadyExistsException; -import com.backend.user.service.anyame.exception.InvalidCredentialsException; -import com.backend.user.service.anyame.exception.InvalidRoleException; -import com.backend.user.service.anyame.exception.NoExpiryDurationException; -import com.backend.user.service.anyame.exception.UnsafePasswordException; -import com.backend.user.service.anyame.repository.UserRepository; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.List; - -@Service -public class AuthService { - - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtService jwtService; - private final PasswordValidatorService passwordValidator; - private final DefaultRoleProvider defaultRoleProvider; - - public AuthService(UserRepository userRepository, - PasswordEncoder passwordEncoder, - JwtService jwtService, - PasswordValidatorService passwordValidator, - DefaultRoleProvider defaultRoleProvider) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.jwtService = jwtService; - this.passwordValidator = passwordValidator; - this.defaultRoleProvider = defaultRoleProvider; - } - - public AuthResponse register( - RegisterRequest request) throws UnsafePasswordException, UserAlreadyExistsException, NoExpiryDurationException, InvalidRoleException { - if (userRepository.findByEmailOrName(request.email(), request.name()).isPresent()) { - throw new UserAlreadyExistsException(); - } - - User user = new User(request.name(), request.email()); - user.setRoles(Collections.singleton(defaultRoleProvider.getDefaultRole())); - - if (request.password() != null && !request.password().isBlank()) { - if (!passwordValidator.validate(request.password())) { - throw new UnsafePasswordException("unsafe password"); - } - user.setPassword(passwordEncoder.encode(request.password())); - } - user = userRepository.save(user); - - return generateAuthResponse(user); - } - - public AuthResponse login(LoginRequest request) throws InvalidCredentialsException, NoExpiryDurationException, InvalidRoleException { - User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> new RuntimeException("Invalid credentials")); - - if (user.getPassword() == null || !passwordEncoder.matches(request.password(), user.getPassword())) { - throw new InvalidCredentialsException(); - } - - return generateAuthResponse(user); - } - - private AuthResponse generateAuthResponse(User user) throws NoExpiryDurationException, InvalidRoleException { - String accessToken = jwtService.generateAccessToken(user); - String refreshToken = jwtService.generateRefreshToken(user); - List roles = user.getRoles().stream().map(Role::getName).toList(); - return new AuthResponse(accessToken, refreshToken, user.getId(), user.getEmail(), user.getName(), roles); - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/service/AuthorityResolver.java b/src/main/java/com/backend/user/service/anyame/service/AuthorityResolver.java deleted file mode 100644 index 7679150..0000000 --- a/src/main/java/com/backend/user/service/anyame/service/AuthorityResolver.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.backend.user.service.anyame.service; - -import com.backend.user.service.anyame.entity.Privilege; -import com.backend.user.service.anyame.entity.Role; -import com.backend.user.service.anyame.entity.User; -import com.backend.user.service.anyame.exception.InvalidRoleException; -import com.backend.user.service.anyame.repository.RoleRepository; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Service -public class AuthorityResolver { - private final RoleRepository roleRepository; - private final RoleHierarchy roleHierarchy; - - public AuthorityResolver(RoleRepository roleRepository, RoleHierarchy roleHierarchy) { - this.roleRepository = roleRepository; - this.roleHierarchy = roleHierarchy; - } - - public List getAuthorities(User user) throws InvalidRoleException { - List grantedAuthorities = user.getRoles().stream() - .map(role -> new SimpleGrantedAuthority(role.getName())) - .collect(Collectors.toList()); - - Collection reachableAuthorities = - roleHierarchy.getReachableGrantedAuthorities(grantedAuthorities); - - Set privileges = new HashSet<>(); - for (GrantedAuthority authority : reachableAuthorities) { - String roleName = authority.getAuthority(); - Role role = roleRepository.findByName(roleName) - .orElseThrow(() -> new InvalidRoleException(roleName)); - if (role != null && role.getPrivileges() != null) { - privileges.addAll(role.getPrivileges().stream() - .map(Privilege::getName) - .collect(Collectors.toSet())); - } - } - - Set allAuthorities = reachableAuthorities.stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toSet()); - allAuthorities.addAll(privileges); - - return new ArrayList<>(allAuthorities); - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/service/JwtService.java b/src/main/java/com/backend/user/service/anyame/service/JwtService.java deleted file mode 100644 index f601764..0000000 --- a/src/main/java/com/backend/user/service/anyame/service/JwtService.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.backend.user.service.anyame.service; - -import com.backend.user.service.anyame.component.AuthorizationProperties; -import com.backend.user.service.anyame.component.JWTSecretProvider; -import com.backend.user.service.anyame.entity.Role; -import com.backend.user.service.anyame.entity.User; -import com.backend.user.service.anyame.exception.InvalidRoleException; -import com.backend.user.service.anyame.exception.NoExpiryDurationException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.Jwts; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.stereotype.Service; - -import java.time.Duration; -import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -@Service -public class JwtService { - - private final JWTSecretProvider secretProvider; - private final AuthorizationProperties authorizationProperties; - private final AuthorityResolver authorityResolver; - - public JwtService(JWTSecretProvider secretProvider, - AuthorizationProperties authorizationProperties, - AuthorityResolver authorityResolver) { - this.secretProvider = secretProvider; - this.authorizationProperties = authorizationProperties; - this.authorityResolver = authorityResolver; - } - - public String generateRefreshToken(User user) throws InvalidRoleException, NoExpiryDurationException { - List authorities = authorityResolver.getAuthorities(user); - List roles = user.getRoles().stream() - .map(Role::getName) - .toList(); - JwtBuilder builder = Jwts.builder() - .subject(user.getEmail()) - .claim("name", user.getName()) - .claim("id", user.getId()) - .claim("roles", roles) - .claim("authorities", authorities) - .claim("type", "refresh"); - Duration refreshExpiry = getMaxExpiry(user, - role -> authorizationProperties.getRefreshExpiry(role.getName())) - .orElseThrow(NoExpiryDurationException::new); - - Date issuedAt = new Date(); - Date expiryDate = Date.from(Instant.now().plus(refreshExpiry)); - - return builder.issuedAt(issuedAt) - .expiration(expiryDate) - .signWith(secretProvider.getSecretKey()) - .compact(); - } - - public String generateAccessToken(User user) throws InvalidRoleException, NoExpiryDurationException { - List authorities = authorityResolver.getAuthorities(user); - List roles = user.getRoles().stream() - .map(Role::getName) - .toList(); - JwtBuilder builder = Jwts.builder() - .subject(user.getEmail()) - .claim("name", user.getName()) - .claim("id", user.getId()) - .claim("roles", roles) - .claim("authorities", authorities) - .claim("type", "access"); - Duration accessExpiry = getMaxExpiry(user, - role -> authorizationProperties.getAccessExpiry(role.getName())) - .orElseThrow(NoExpiryDurationException::new); - - Date issuedAt = new Date(); - Date expiryDate = Date.from(Instant.now().plus(accessExpiry)); - - return builder.issuedAt(issuedAt) - .expiration(expiryDate) - .signWith(secretProvider.getSecretKey()) - .compact(); - } - - private Optional getMaxExpiry(User user, Function> mapper) { - return user.getRoles().stream() - .map(mapper) - .filter(Optional::isPresent) - .map(Optional::get) - .max(Duration::compareTo); - } - - public String extractEmail(String token) { - return getClaims(token).getSubject(); - } - - public boolean isTokenValid(String token, User user) { - return extractEmail(token).equals(user.getEmail()) && !isTokenExpired(token); - } - - private boolean isTokenExpired(String token) { - return getClaims(token).getExpiration().before(new Date()); - } - - private Claims getClaims(String token) { - return Jwts.parser() - .decryptWith(secretProvider.getSecretKey()) - .build() - .parseEncryptedClaims(token) - .getPayload(); - } - -} diff --git a/src/main/java/com/backend/user/service/anyame/service/PasswordValidatorService.java b/src/main/java/com/backend/user/service/anyame/service/PasswordValidatorService.java deleted file mode 100644 index bace87e..0000000 --- a/src/main/java/com/backend/user/service/anyame/service/PasswordValidatorService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.backend.user.service.anyame.service; - -import org.passay.PasswordData; -import org.passay.PasswordValidator; -import org.passay.RuleResult; -import org.springframework.stereotype.Service; - -@Service -public class PasswordValidatorService { - - private final PasswordValidator passwordValidator; - - public PasswordValidatorService(PasswordValidator passwordValidator) { - this.passwordValidator = passwordValidator; - } - - public boolean validate(String password) { - RuleResult result = passwordValidator.validate(new PasswordData(password)); - // TODO: Add HaveBeenIPwned support? - return result.isValid(); - } - -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4d4044c..2ece1f0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -18,13 +18,32 @@ authorization: default-role: ROLE_USER spring: + application: + name: anyame-user-service + security: + oauth2: + authorizationserver: + issuer-url: http://localhost:8080 + introspection-endpoint: /oauth2/token-info datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} jpa: hibernate: - ddl-auto: none + ddl-auto: create + +logging: + level: + root: DEBUG + org.springframework.security: DEBUG + logging.level.org.springframework.web: DEBUG + logging.level.org.springframework.security.oauth2: TRACE + org.apache.tomcat.util.net.NioEndpoint: ERROR + sun.rmi: ERROR + java.io: ERROR + javax.management: ERROR + server: error: - include-message: always + include-message: always \ No newline at end of file