Compare commits
5 Commits
main
...
feature/ss
Author | SHA1 | Date | |
---|---|---|---|
55397b87c6 | |||
76bc5d4853 | |||
aa7ccc45ea | |||
2b43e8f7a6 | |||
958521526b |
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
DATABASE_URL=jdbc:postgresql://localhost:5433/postgres
|
||||||
|
DATABASE_USERNAME=username
|
||||||
|
DATABASE_PASSWORD=password
|
19
pom.xml
19
pom.xml
@ -12,7 +12,7 @@
|
|||||||
<artifactId>anyame-backend</artifactId>
|
<artifactId>anyame-backend</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>anyame-backend</name>
|
<name>anyame-backend</name>
|
||||||
<description>User service for anyame backend</description>
|
<description>User service for anyame</description>
|
||||||
<url/>
|
<url/>
|
||||||
<licenses>
|
<licenses>
|
||||||
<license/>
|
<license/>
|
||||||
@ -50,6 +50,23 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-authorization-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-database-postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
package com.backend.user.service.anyame;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.entity.Permission;
|
||||||
|
import com.backend.user.service.anyame.entity.Role;
|
||||||
|
import com.backend.user.service.anyame.repository.PermissionRepository;
|
||||||
|
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RoleDataInitializer implements ApplicationRunner {
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final PermissionRepository permissionRepository;
|
||||||
|
|
||||||
|
public RoleDataInitializer(RoleRepository roleRepository, PermissionRepository permissionRepository) {
|
||||||
|
this.roleRepository = roleRepository;
|
||||||
|
this.permissionRepository = permissionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
if (permissionRepository.count() == 0) {
|
||||||
|
initializePermissions();
|
||||||
|
initializeRoles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializePermissions() {
|
||||||
|
savePermission("CREATE_ANIME");
|
||||||
|
savePermission("EDIT_ANIME");
|
||||||
|
savePermission("DELETE_ANIME");
|
||||||
|
savePermission("APPROVE_ANIME");
|
||||||
|
|
||||||
|
savePermission("VIEW_USERS");
|
||||||
|
savePermission("EDIT_USER");
|
||||||
|
savePermission("BAN_USER");
|
||||||
|
savePermission("DELETE_USER");
|
||||||
|
|
||||||
|
savePermission("MODERATE_COMMENTS");
|
||||||
|
savePermission("MODERATE_REVIEWS");
|
||||||
|
savePermission("HANDLE_REPORTS");
|
||||||
|
savePermission("BLACKLIST_CONTENT");
|
||||||
|
|
||||||
|
savePermission("MANAGE_ROLES");
|
||||||
|
savePermission("VIEW_ANALYTICS");
|
||||||
|
savePermission("SYSTEM_CONFIG");
|
||||||
|
savePermission("BACKUP_DATA");
|
||||||
|
|
||||||
|
savePermission("VIEW_PREMIUM_CONTENT");
|
||||||
|
savePermission("DOWNLOAD_CONTENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeRoles() {
|
||||||
|
Role userRole = saveRole("USER");
|
||||||
|
Role moderatorRole = saveRole("MODERATOR");
|
||||||
|
Role adminRole = saveRole("ADMIN");
|
||||||
|
|
||||||
|
moderatorRole.getParentRoles().add(userRole);
|
||||||
|
moderatorRole.getPermissions().addAll(Arrays.asList(
|
||||||
|
findPermission("MODERATE_COMMENTS"),
|
||||||
|
findPermission("MODERATE_REVIEWS"),
|
||||||
|
findPermission("HANDLE_REPORTS"),
|
||||||
|
findPermission("EDIT_ANIME"),
|
||||||
|
findPermission("APPROVE_ANIME"),
|
||||||
|
findPermission("VIEW_USERS")
|
||||||
|
));
|
||||||
|
|
||||||
|
adminRole.getParentRoles().add(moderatorRole);
|
||||||
|
adminRole.getPermissions().addAll(Arrays.asList(
|
||||||
|
findPermission("DELETE_ANIME"),
|
||||||
|
findPermission("EDIT_USER"),
|
||||||
|
findPermission("BAN_USER"),
|
||||||
|
findPermission("DELETE_USER"),
|
||||||
|
findPermission("MANAGE_ROLES"),
|
||||||
|
findPermission("VIEW_ANALYTICS"),
|
||||||
|
findPermission("SYSTEM_CONFIG"),
|
||||||
|
findPermission("BACKUP_DATA"),
|
||||||
|
findPermission("BLACKLIST_CONTENT")
|
||||||
|
));
|
||||||
|
|
||||||
|
roleRepository.saveAll(Arrays.asList(moderatorRole, adminRole));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Permission savePermission(String name) {
|
||||||
|
Permission permission = new Permission(name);
|
||||||
|
return permissionRepository.save(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Role saveRole(String name) {
|
||||||
|
Role role = new Role(name);
|
||||||
|
return roleRepository.save(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Permission findPermission(String name) {
|
||||||
|
return permissionRepository.findByName(name)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Permission not found: " + name));
|
||||||
|
}
|
||||||
|
}
|
@ -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<RoleConfig> roles;
|
|
||||||
private String hierarchy;
|
|
||||||
private String defaultRole;
|
|
||||||
|
|
||||||
public List<RoleConfig> getRoles() {
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHierarchy() {
|
|
||||||
return hierarchy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDefaultRole() {
|
|
||||||
return defaultRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthorizationProperties setRoles(List<RoleConfig> 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<Duration> getAccessExpiry(String roleName) {
|
|
||||||
return roles.stream()
|
|
||||||
.filter(role -> role.getName().equals(roleName))
|
|
||||||
.map(RoleConfig::getAccessExpiry)
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Duration> 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<String> privileges;
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Duration getAccessExpiry() {
|
|
||||||
return accessExpiry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Duration getRefreshExpiry() {
|
|
||||||
return refreshExpiry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> 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<String> privileges) {
|
|
||||||
this.privileges = privileges;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.backend.user.service.anyame.component;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.entity.User;
|
||||||
|
import com.backend.user.service.anyame.model.AuthorizedUser;
|
||||||
|
import com.backend.user.service.anyame.service.UserAuthorityService;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class AuthorizedUserMapper {
|
||||||
|
private final UserAuthorityService authorityService;
|
||||||
|
|
||||||
|
public AuthorizedUserMapper(UserAuthorityService authorityService) {
|
||||||
|
this.authorityService = authorityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizedUser mapFormLogin(User userEntity) {
|
||||||
|
Set<GrantedAuthority> authorities = authorityService.getUserAuthorities(userEntity);
|
||||||
|
return new AuthorizedUser.Builder(userEntity.getEmail(), userEntity.getPassword(), Collections.emptyList())
|
||||||
|
.id(userEntity.getId())
|
||||||
|
.username(userEntity.getName())
|
||||||
|
.active(userEntity.isActive())
|
||||||
|
.authorities(authorities)
|
||||||
|
.accountNonExpired(true)
|
||||||
|
.credentialsNonExpired(true)
|
||||||
|
.accountNonLocked(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizedUser mapOAuth2User(User userEntity, Map<String, Object> attributes) {
|
||||||
|
Set<GrantedAuthority> authorities = authorityService.getUserAuthorities(userEntity);
|
||||||
|
return new AuthorizedUser.Builder(userEntity.getEmail(), "", Collections.emptyList())
|
||||||
|
.id(userEntity.getId())
|
||||||
|
.username(userEntity.getName())
|
||||||
|
.active(userEntity.isActive())
|
||||||
|
.authorities(authorities)
|
||||||
|
.attributes(attributes)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizedUser mapOidcUser(User userEntity, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Set<GrantedAuthority> authorities = authorityService.getUserAuthorities(userEntity);
|
||||||
|
|
||||||
|
AuthorizedUser.Builder builder = new AuthorizedUser.Builder(userEntity.getEmail(), "", Collections.emptyList())
|
||||||
|
.id(userEntity.getId())
|
||||||
|
.username(userEntity.getName())
|
||||||
|
.active(userEntity.isActive())
|
||||||
|
.authorities(authorities)
|
||||||
|
.attributes(attributes);
|
||||||
|
|
||||||
|
if (idToken != null) {
|
||||||
|
builder.oidcIdToken(idToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInfo != null) {
|
||||||
|
builder.oidcUserInfo(userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.backend.user.service.anyame.component;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.service.UserAuthorityService;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
|
||||||
|
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
|
||||||
|
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomOpaqueTokenAuthenticationConverter implements OpaqueTokenAuthenticationConverter {
|
||||||
|
private final OpaqueTokenIntrospector introspector;
|
||||||
|
private final UserAuthorityService authorityService;
|
||||||
|
|
||||||
|
public CustomOpaqueTokenAuthenticationConverter(OpaqueTokenIntrospector introspector, UserAuthorityService authorityService) {
|
||||||
|
this.introspector = introspector;
|
||||||
|
this.authorityService = authorityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
|
||||||
|
return new BearerTokenAuthentication(authenticatedPrincipal, introspectedToken, authenticatedPrincipal.getAuthorities());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<ContextRefreshedEvent> {
|
|
||||||
|
|
||||||
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<Privilege> privileges = roleConfig.getPrivileges()
|
|
||||||
.stream()
|
|
||||||
.map(this::createPrivilegeIfNotFound)
|
|
||||||
.toList();
|
|
||||||
createRoleIfNotFound(roleConfig.getName(), privileges);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Privilege createPrivilegeIfNotFound(String name) {
|
|
||||||
Optional<Privilege> 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<Privilege> privileges) {
|
|
||||||
Optional<Role> roleOptional = roleRepository.findByName(name);
|
|
||||||
if (roleOptional.isEmpty()) {
|
|
||||||
roleOptional = Optional.of(new Role(name));
|
|
||||||
roleOptional.get().setPrivileges(privileges);
|
|
||||||
roleRepository.save(roleOptional.get());
|
|
||||||
}
|
|
||||||
return roleOptional.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,108 @@
|
|||||||
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.component.AuthorizationServerProperties;
|
||||||
|
import com.backend.user.service.anyame.component.CustomOpaqueTokenAuthenticationConverter;
|
||||||
|
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.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.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.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.security.web.util.matcher.RequestMatcher;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class AuthorizationServerConfig {
|
||||||
|
private final AuthorizationServerProperties authorizationServerProperties;
|
||||||
|
private final CustomOpaqueTokenAuthenticationConverter opaqueTokenAuthenticationConverter;
|
||||||
|
|
||||||
|
public AuthorizationServerConfig(AuthorizationServerProperties authorizationServerProperties, CustomOpaqueTokenAuthenticationConverter opaqueTokenAuthenticationConverter) {
|
||||||
|
this.authorizationServerProperties = authorizationServerProperties;
|
||||||
|
this.opaqueTokenAuthenticationConverter = opaqueTokenAuthenticationConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||||
|
OAuth2AuthorizationServerConfigurer.authorizationServer();
|
||||||
|
|
||||||
|
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||||
|
|
||||||
|
return http
|
||||||
|
.csrf(c -> c.ignoringRequestMatchers(endpointsMatcher))
|
||||||
|
.securityMatcher(endpointsMatcher)
|
||||||
|
.with(authorizationServerConfigurer, (authorizationServer) ->
|
||||||
|
authorizationServer
|
||||||
|
.oidc(withDefaults())
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests((authorize) ->
|
||||||
|
authorize
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.oauth2ResourceServer(c -> c
|
||||||
|
.opaqueToken(opaqueTokenConfigurer -> opaqueTokenConfigurer
|
||||||
|
.authenticationConverter(opaqueTokenAuthenticationConverter)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.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("$2a$12$IdGgEQv2Zmtx.dEHvUhxJ.Pi3x9lufrvcfkQ8e4t2pwhD7F8swEJu")
|
||||||
|
.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 AuthorizationServerSettings authorizationServerSettings() {
|
||||||
|
return AuthorizationServerSettings.builder()
|
||||||
|
.issuer(authorizationServerProperties.getIssuerUrl())
|
||||||
|
.tokenIntrospectionEndpoint(authorizationServerProperties.getIntrospectionEndpoint())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class ConfigUtilities {
|
||||||
|
@Bean
|
||||||
|
public OAuth2AuthorizationService oAuth2AuthorizationService() {
|
||||||
|
return new InMemoryOAuth2AuthorizationService();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.exception.UnsupportedOAuth2ProviderException;
|
||||||
|
import com.backend.user.service.anyame.service.extractor.OAuth2AttributeExtractorFactory;
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.beans.factory.config.ServiceLocatorFactoryBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OAuth2AttributeExtractorConfig {
|
||||||
|
@Bean("oauth2AttributeExtractorFactory")
|
||||||
|
public FactoryBean<?> serviceLocatorFactoryBean() {
|
||||||
|
ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
|
||||||
|
factoryBean.setServiceLocatorInterface(OAuth2AttributeExtractorFactory.class);
|
||||||
|
factoryBean.setServiceLocatorExceptionClass(UnsupportedOAuth2ProviderException.class);
|
||||||
|
return factoryBean;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.exception.UnsupportedOidcProviderException;
|
||||||
|
import com.backend.user.service.anyame.service.extractor.OidcAttributeExtractorFactory;
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.beans.factory.config.ServiceLocatorFactoryBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OidcAttributeExtractorConfig {
|
||||||
|
@Bean("oidcAttributeExtractorFactory")
|
||||||
|
public FactoryBean<?> serviceLocatorFactoryBean() {
|
||||||
|
ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
|
||||||
|
factoryBean.setServiceLocatorInterface(OidcAttributeExtractorFactory.class);
|
||||||
|
factoryBean.setServiceLocatorExceptionClass(UnsupportedOidcProviderException.class);
|
||||||
|
return factoryBean;
|
||||||
|
}
|
||||||
|
}
|
@ -1,51 +1,52 @@
|
|||||||
package com.backend.user.service.anyame.config;
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
import com.backend.user.service.anyame.component.AuthorizationProperties;
|
import com.backend.user.service.anyame.service.CustomOAuth2UserService;
|
||||||
import org.passay.CharacterRule;
|
import com.backend.user.service.anyame.service.CustomUserDetailsService;
|
||||||
import org.passay.EnglishCharacterData;
|
|
||||||
import org.passay.LengthRule;
|
|
||||||
import org.passay.PasswordValidator;
|
|
||||||
import org.passay.WhitespaceRule;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
|
||||||
@Configuration
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity(prePostEnabled = true)
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final CustomUserDetailsService userDetailsService;
|
||||||
|
private final CustomOAuth2UserService oauth2UserService;
|
||||||
|
|
||||||
|
public SecurityConfig(CustomUserDetailsService userDetailsService,
|
||||||
|
CustomOAuth2UserService oauth2UserService) {
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.oauth2UserService = oauth2UserService;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@Order(2)
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(c -> c.anyRequest().permitAll()).build();
|
return http
|
||||||
|
.authorizeHttpRequests(c ->
|
||||||
|
c.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.formLogin(withDefaults())
|
||||||
|
.oauth2Login(c ->
|
||||||
|
c.userInfoEndpoint(userInfo -> userInfo
|
||||||
|
.userService(oauth2UserService)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.userDetailsService(userDetailsService)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<CorsFilter> corsFilter() {
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
|
||||||
|
config.addAllowedOrigin("http://127.0.0.1:5173,http://localhost:5173");
|
||||||
|
config.addAllowedHeader(CorsConfiguration.ALL);
|
||||||
|
config.addExposedHeader(CorsConfiguration.ALL);
|
||||||
|
config.addAllowedMethod(CorsConfiguration.ALL);
|
||||||
|
|
||||||
|
source.registerCorsConfiguration("/**", config);
|
||||||
|
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
|
||||||
|
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
}
|
@ -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<AuthResponse> 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<AuthResponse> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.backend.user.service.anyame.controller;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class HelloController {
|
||||||
|
@GetMapping("/hello")
|
||||||
|
public String hello() {
|
||||||
|
return "Hello World";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/principalhello")
|
||||||
|
public String principalhello() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||||
|
return String.format("%s, %s", authentication.getName(), authorities.stream().map(GrantedAuthority::toString).collect(Collectors.joining(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<String> roles) {
|
|
||||||
}
|
|
@ -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) {
|
|
||||||
|
|
||||||
}
|
|
@ -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) {
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.backend.user.service.anyame.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "permissions")
|
||||||
|
public class Permission {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
protected Permission() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permission(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
@ -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<Role> roles;
|
|
||||||
|
|
||||||
protected Privilege() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Privilege(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Role> getRoles() {
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,34 +1,33 @@
|
|||||||
package com.backend.user.service.anyame.entity;
|
package com.backend.user.service.anyame.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
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;
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "roles")
|
||||||
public class Role {
|
public class Role {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
@ManyToMany(mappedBy = "roles")
|
|
||||||
private Collection<User> users;
|
|
||||||
|
|
||||||
@ManyToMany
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "roles_privileges",
|
name = "role_hierarchy",
|
||||||
joinColumns = @JoinColumn(
|
joinColumns = @JoinColumn(name = "child_role_id"),
|
||||||
name = "role_id", referencedColumnName = "id"),
|
inverseJoinColumns = @JoinColumn(name = "parent_role_id")
|
||||||
inverseJoinColumns = @JoinColumn(
|
)
|
||||||
name = "privilege_id", referencedColumnName = "id"))
|
private Set<Role> parentRoles = new HashSet<>();
|
||||||
private Collection<Privilege> privileges;
|
|
||||||
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
|
@JoinTable(name = "role_permissions",
|
||||||
|
joinColumns = @JoinColumn(name = "role_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "permission_id")
|
||||||
|
)
|
||||||
|
private Set<Permission> permissions = new HashSet<>();
|
||||||
|
|
||||||
protected Role() {
|
protected Role() {
|
||||||
}
|
}
|
||||||
@ -45,17 +44,21 @@ public class Role {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<User> getUsers() {
|
public Set<Role> getParentRoles() {
|
||||||
return users;
|
return parentRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Privilege> getPrivileges() {
|
public Set<Permission> getPermissions() {
|
||||||
return privileges;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Role setPrivileges(Collection<Privilege> privileges) {
|
public Set<Permission> getAllPermissions() {
|
||||||
this.privileges = privileges;
|
Set<Permission> allPermissions = new HashSet<>(permissions);
|
||||||
return this;
|
|
||||||
|
for (Role parentRole : parentRoles) {
|
||||||
|
allPermissions.addAll(parentRole.getAllPermissions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return allPermissions;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,16 +1,9 @@
|
|||||||
package com.backend.user.service.anyame.entity;
|
package com.backend.user.service.anyame.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
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;
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
@ -19,33 +12,46 @@ public class User {
|
|||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
|
private String name;
|
||||||
|
@Column(unique = true)
|
||||||
private String email;
|
private String email;
|
||||||
private String password;
|
private String password;
|
||||||
private boolean enabled = true;
|
private boolean active;
|
||||||
|
|
||||||
@ManyToMany
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "users_roles",
|
name = "user_roles",
|
||||||
joinColumns = @JoinColumn(
|
joinColumns = @JoinColumn(name = "user_id"),
|
||||||
name = "user_id", referencedColumnName = "id"),
|
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||||||
inverseJoinColumns = @JoinColumn(
|
)
|
||||||
name = "role_id", referencedColumnName = "id"))
|
private Set<Role> roles = new HashSet<>();
|
||||||
private Collection<Role> roles;
|
|
||||||
|
private String provider;
|
||||||
|
private String providerId;
|
||||||
|
|
||||||
protected User() {
|
protected User() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public User(String name, String email, String password) {
|
public User(String name, String email, String provider, String providerId) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.password = password;
|
this.provider = provider;
|
||||||
|
this.providerId = providerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User(String name, String email) {
|
public Set<Role> getRoles() {
|
||||||
this.name = name;
|
return roles;
|
||||||
this.email = email;
|
}
|
||||||
|
|
||||||
|
public Set<Permission> getAllPermissions() {
|
||||||
|
Set<Permission> allPermissions = new HashSet<>();
|
||||||
|
|
||||||
|
for (Role role : roles) {
|
||||||
|
allPermissions.addAll(role.getAllPermissions());
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
@ -64,28 +70,19 @@ public class User {
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User setPassword(String password) {
|
public boolean isActive() {
|
||||||
this.password = password;
|
return active;
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public void setActive(boolean active) {
|
||||||
return enabled;
|
this.active = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User setEnabled(boolean enabled) {
|
public String getProvider() {
|
||||||
this.enabled = enabled;
|
return provider;
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Role> getRoles() {
|
public String getProviderId() {
|
||||||
return roles;
|
return providerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User setRoles(Collection<Role> roles) {
|
|
||||||
this.roles = roles;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package com.backend.user.service.anyame.exception;
|
|
||||||
|
|
||||||
public class InvalidCredentialsException extends Exception {
|
|
||||||
|
|
||||||
public InvalidCredentialsException() {
|
|
||||||
super("invalid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package com.backend.user.service.anyame.exception;
|
|
||||||
|
|
||||||
public class NoExpiryDurationException extends Exception {
|
|
||||||
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package com.backend.user.service.anyame.exception;
|
|
||||||
|
|
||||||
public class UnsafePasswordException extends Exception {
|
|
||||||
|
|
||||||
public UnsafePasswordException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.backend.user.service.anyame.exception;
|
||||||
|
|
||||||
|
public class UnsupportedOAuth2ProviderException extends Exception {
|
||||||
|
public UnsupportedOAuth2ProviderException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsupportedOAuth2ProviderException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.backend.user.service.anyame.exception;
|
||||||
|
|
||||||
|
public class UnsupportedOidcProviderException extends Exception {
|
||||||
|
public UnsupportedOidcProviderException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsupportedOidcProviderException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package com.backend.user.service.anyame.exception;
|
|
||||||
|
|
||||||
public class UserAlreadyExistsException extends Exception {
|
|
||||||
|
|
||||||
public UserAlreadyExistsException() {
|
|
||||||
super("user already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,176 @@
|
|||||||
|
package com.backend.user.service.anyame.model;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class AuthorizedUser extends User implements OAuth2User, OidcUser {
|
||||||
|
private Long id;
|
||||||
|
private String email;
|
||||||
|
private boolean active;
|
||||||
|
private Map<String, Object> attributes;
|
||||||
|
private OidcUserInfo userInfo;
|
||||||
|
private OidcIdToken idToken;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthorizedUser(Builder builder) {
|
||||||
|
super(
|
||||||
|
builder.username,
|
||||||
|
builder.password,
|
||||||
|
builder.enabled,
|
||||||
|
builder.accountNonExpired,
|
||||||
|
builder.credentialsNonExpired,
|
||||||
|
builder.accountNonLocked,
|
||||||
|
builder.authorities
|
||||||
|
);
|
||||||
|
this.id = builder.id;
|
||||||
|
this.email = builder.email;
|
||||||
|
this.active = builder.active;
|
||||||
|
this.attributes = builder.attributes;
|
||||||
|
this.userInfo = builder.oidcUserInfo;
|
||||||
|
this.idToken = builder.oidcIdToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getAttributes() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getClaims() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OidcUserInfo getUserInfo() {
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OidcIdToken getIdToken() {
|
||||||
|
return idToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
private Collection<? extends GrantedAuthority> authorities;
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private boolean active;
|
||||||
|
private boolean enabled = true;
|
||||||
|
private boolean accountNonExpired = true;
|
||||||
|
private boolean credentialsNonExpired = true;
|
||||||
|
private boolean accountNonLocked = true;
|
||||||
|
private Map<String, Object> attributes;
|
||||||
|
private OidcUserInfo oidcUserInfo;
|
||||||
|
private OidcIdToken oidcIdToken;
|
||||||
|
|
||||||
|
public Builder(String email, String password, Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
this.email = email;
|
||||||
|
this.password = password;
|
||||||
|
this.authorities = authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder id(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder username(String username) {
|
||||||
|
this.username = username;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder password(String password) {
|
||||||
|
this.password = password;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder email(String email) {
|
||||||
|
this.email = email;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder active(boolean active) {
|
||||||
|
this.active = active;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder enabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder accountNonExpired(boolean accountNonExpired) {
|
||||||
|
this.accountNonExpired = accountNonExpired;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder credentialsNonExpired(boolean credentialsNonExpired) {
|
||||||
|
this.credentialsNonExpired = credentialsNonExpired;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder accountNonLocked(boolean accountNonLocked) {
|
||||||
|
this.accountNonLocked = accountNonLocked;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder authorities(Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
this.authorities = authorities;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder attributes(Map<String, Object> attributes) {
|
||||||
|
this.attributes = attributes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Builder oidcUserInfo(OidcUserInfo oidcUserInfo) {
|
||||||
|
this.oidcUserInfo = oidcUserInfo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder oidcIdToken(OidcIdToken oidcIdToken) {
|
||||||
|
this.oidcIdToken = oidcIdToken;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public AuthorizedUser build() {
|
||||||
|
return new AuthorizedUser(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizedUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
super(username, password, authorities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizedUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.backend.user.service.anyame.repository;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.entity.Permission;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PermissionRepository extends JpaRepository<Permission, Long> {
|
||||||
|
Optional<Permission> findByName(String name);
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT p FROM User u " +
|
||||||
|
"JOIN u.roles r " +
|
||||||
|
"JOIN r.permissions p " +
|
||||||
|
"WHERE u.name = :name")
|
||||||
|
Set<Permission> findByUserName(@Param("name") String name);
|
||||||
|
}
|
@ -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<Privilege, Long> {
|
|
||||||
|
|
||||||
Optional<Privilege> findByName(String name);
|
|
||||||
|
|
||||||
}
|
|
@ -2,11 +2,21 @@ package com.backend.user.service.anyame.repository;
|
|||||||
|
|
||||||
import com.backend.user.service.anyame.entity.Role;
|
import com.backend.user.service.anyame.entity.Role;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Repository
|
||||||
public interface RoleRepository extends JpaRepository<Role, Long> {
|
public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||||
|
Optional<Role> findByName(String name);
|
||||||
|
|
||||||
Optional<Role> findByName(String roleName);
|
@Query("SELECT r FROM Role r " +
|
||||||
|
"LEFT JOIN FETCH r.permissions " +
|
||||||
|
"LEFT JOIN FETCH r.parentRoles " +
|
||||||
|
"WHERE r.id IN :roleIds")
|
||||||
|
List<Role> findRolesWithPermissionsAndParents(@Param("roleIds") Set<Long> roleIds);
|
||||||
}
|
}
|
@ -2,13 +2,20 @@ package com.backend.user.service.anyame.repository;
|
|||||||
|
|
||||||
import com.backend.user.service.anyame.entity.User;
|
import com.backend.user.service.anyame.entity.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
Optional<User> findByName(String name);
|
||||||
|
|
||||||
Optional<User> findByEmail(String email);
|
@Query("SELECT u FROM User u " +
|
||||||
|
"LEFT JOIN FETCH u.roles " +
|
||||||
Optional<User> findByEmailOrName(String email, String name);
|
"WHERE u.name = :name")
|
||||||
|
Optional<User> findByNameWithRoles(@Param("name") String name);
|
||||||
|
|
||||||
|
Optional<User> findByProviderId(String name);
|
||||||
}
|
}
|
@ -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<String> roles = user.getRoles().stream().map(Role::getName).toList();
|
|
||||||
return new AuthResponse(accessToken, refreshToken, user.getId(), user.getEmail(), user.getName(), roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<String> getAuthorities(User user) throws InvalidRoleException {
|
|
||||||
List<GrantedAuthority> grantedAuthorities = user.getRoles().stream()
|
|
||||||
.map(role -> new SimpleGrantedAuthority(role.getName()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
Collection<? extends GrantedAuthority> reachableAuthorities =
|
|
||||||
roleHierarchy.getReachableGrantedAuthorities(grantedAuthorities);
|
|
||||||
|
|
||||||
Set<String> 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<String> allAuthorities = reachableAuthorities.stream()
|
|
||||||
.map(GrantedAuthority::getAuthority)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
allAuthorities.addAll(privileges);
|
|
||||||
|
|
||||||
return new ArrayList<>(allAuthorities);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.backend.user.service.anyame.service;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.component.AuthorizedUserMapper;
|
||||||
|
import com.backend.user.service.anyame.entity.Role;
|
||||||
|
import com.backend.user.service.anyame.entity.User;
|
||||||
|
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||||
|
import com.backend.user.service.anyame.repository.UserRepository;
|
||||||
|
import com.backend.user.service.anyame.service.extractor.OAuth2AttributeExtractor;
|
||||||
|
import com.backend.user.service.anyame.service.extractor.OAuth2AttributeExtractorFactory;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CustomOAuth2UserService.class);
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
|
||||||
|
private final AuthorizedUserMapper authorizedUserMapper;
|
||||||
|
private final OAuth2AttributeExtractorFactory attributeExtractorFactory;
|
||||||
|
|
||||||
|
public CustomOAuth2UserService(UserRepository userRepository,
|
||||||
|
RoleRepository roleRepository,
|
||||||
|
AuthorizedUserMapper authorizedUserMapper,
|
||||||
|
OAuth2AttributeExtractorFactory attributeExtractorFactory) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.roleRepository = roleRepository;
|
||||||
|
this.authorizedUserMapper = authorizedUserMapper;
|
||||||
|
this.attributeExtractorFactory = attributeExtractorFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
OAuth2User oauth2User = delegate.loadUser(userRequest);
|
||||||
|
|
||||||
|
return processOAuth2User(userRequest, oauth2User);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) {
|
||||||
|
String registrationId = userRequest.getClientRegistration().getRegistrationId();
|
||||||
|
|
||||||
|
OAuth2AttributeExtractor extractor = attributeExtractorFactory.create(registrationId);
|
||||||
|
if (extractor == null) {
|
||||||
|
throw new OAuth2AuthenticationException("Unsupported OAuth2 provider: " + registrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> attributes = oauth2User.getAttributes();
|
||||||
|
|
||||||
|
String email = extractor.extractEmail(attributes);
|
||||||
|
String name = extractor.extractName(attributes);
|
||||||
|
String providerId = extractor.extractId(attributes);
|
||||||
|
String avatarUrl = extractor.extractAvatarUrl(attributes);
|
||||||
|
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
throw new OAuth2AuthenticationException("Name is required but not provided by " + registrationId);
|
||||||
|
}
|
||||||
|
if (providerId == null || providerId.trim().isEmpty()) {
|
||||||
|
throw new OAuth2AuthenticationException("User ID is required but not provided by " + registrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("OAuth2 User - Provider: {}, Email: {}, Name: {}, ID: {}",
|
||||||
|
registrationId, email, name, providerId);
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Handle user name conflict
|
||||||
|
User user = userRepository.findByProviderId(providerId)
|
||||||
|
.orElseGet(() -> createNewUser(email, name, registrationId, providerId));
|
||||||
|
|
||||||
|
// TODO: Should be toggleable nor documented behaviour
|
||||||
|
// First user becomes admin
|
||||||
|
if (user.getId() == 1 && !user.isActive()) {
|
||||||
|
Role adminRole = roleRepository.findByName("ADMIN")
|
||||||
|
.orElseThrow(() -> new RuntimeException("Admin role not found"));
|
||||||
|
user.getRoles().add(adminRole);
|
||||||
|
user.setActive(true);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizedUserMapper.mapOAuth2User(user, oauth2User.getAttributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private User createNewUser(String email, String name, String provider, String providerId) {
|
||||||
|
User user = new User(name, email, provider, providerId);
|
||||||
|
|
||||||
|
Role userRole = roleRepository.findByName("USER")
|
||||||
|
.orElseThrow(() -> new RuntimeException("User role not found"));
|
||||||
|
user.getRoles().add(userRole);
|
||||||
|
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package com.backend.user.service.anyame.service;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.component.AuthorizedUserMapper;
|
||||||
|
import com.backend.user.service.anyame.entity.Role;
|
||||||
|
import com.backend.user.service.anyame.entity.User;
|
||||||
|
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||||
|
import com.backend.user.service.anyame.repository.UserRepository;
|
||||||
|
import com.backend.user.service.anyame.service.extractor.OidcAttributeExtractor;
|
||||||
|
import com.backend.user.service.anyame.service.extractor.OidcAttributeExtractorFactory;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CustomOIDCUserService extends OidcUserService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CustomOIDCUserService.class);
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final OidcUserService delegate = new OidcUserService();
|
||||||
|
private final AuthorizedUserMapper authorizedUserMapper;
|
||||||
|
private final OidcAttributeExtractorFactory attributeExtractorFactory;
|
||||||
|
|
||||||
|
public CustomOIDCUserService(UserRepository userRepository,
|
||||||
|
RoleRepository roleRepository,
|
||||||
|
AuthorizedUserMapper authorizedUserMapper,
|
||||||
|
OidcAttributeExtractorFactory attributeExtractorFactory) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.roleRepository = roleRepository;
|
||||||
|
this.authorizedUserMapper = authorizedUserMapper;
|
||||||
|
this.attributeExtractorFactory = attributeExtractorFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
OidcUser oidcUser = delegate.loadUser(userRequest);
|
||||||
|
|
||||||
|
return processOIDCUser(userRequest, oidcUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OidcUser processOIDCUser(OidcUserRequest userRequest, OidcUser oidcUser) {
|
||||||
|
String registrationId = userRequest.getClientRegistration().getRegistrationId();
|
||||||
|
|
||||||
|
OidcAttributeExtractor extractor = attributeExtractorFactory.create(registrationId);
|
||||||
|
if (extractor == null) {
|
||||||
|
throw new OAuth2AuthenticationException("Unsupported OIDC provider: " + registrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
OidcIdToken idToken = oidcUser.getIdToken();
|
||||||
|
OidcUserInfo userInfo = oidcUser.getUserInfo();
|
||||||
|
|
||||||
|
String email = extractor.extractEmail(idToken, userInfo);
|
||||||
|
String name = extractor.extractName(idToken, userInfo);
|
||||||
|
String subject = extractor.extractSubject(idToken, userInfo);
|
||||||
|
String preferredUsername = extractor.extractPreferredUsername(idToken, userInfo);
|
||||||
|
String picture = extractor.extractPicture(idToken, userInfo);
|
||||||
|
String givenName = extractor.extractGivenName(idToken, userInfo);
|
||||||
|
String familyName = extractor.extractFamilyName(idToken, userInfo);
|
||||||
|
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
throw new OAuth2AuthenticationException("Email is required but not provided by " + registrationId);
|
||||||
|
}
|
||||||
|
if (subject == null || subject.trim().isEmpty()) {
|
||||||
|
throw new OAuth2AuthenticationException("Subject is required but not provided by " + registrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("OIDC User - Provider: {}, Email: {}, Name: {}, Subject: {}",
|
||||||
|
registrationId, email, name, subject);
|
||||||
|
|
||||||
|
// TODO: Handle user name conflict
|
||||||
|
User user = userRepository.findByProviderId(subject)
|
||||||
|
.orElseGet(() -> createNewUser(email, name, registrationId, subject, picture, preferredUsername));
|
||||||
|
|
||||||
|
// TODO: Should be toggleable nor documented behaviour
|
||||||
|
// First user becomes admin
|
||||||
|
if (user.getId() == 1 && !user.isActive()) {
|
||||||
|
Role adminRole = roleRepository.findByName("ADMIN")
|
||||||
|
.orElseThrow(() -> new RuntimeException("Admin role not found"));
|
||||||
|
user.getRoles().add(adminRole);
|
||||||
|
user.setActive(true);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizedUserMapper.mapOidcUser(user, oidcUser.getAttributes(), idToken, userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private User createNewUser(String email, String name, String provider, String subject, String picture, String preferredUsername) {
|
||||||
|
User user = new User(name, email, provider, subject);
|
||||||
|
|
||||||
|
Role userRole = roleRepository.findByName("USER")
|
||||||
|
.orElseThrow(() -> new RuntimeException("User role not found"));
|
||||||
|
user.getRoles().add(userRole);
|
||||||
|
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.backend.user.service.anyame.service;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.component.AuthorizedUserMapper;
|
||||||
|
import com.backend.user.service.anyame.entity.User;
|
||||||
|
import com.backend.user.service.anyame.repository.UserRepository;
|
||||||
|
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 java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final AuthorizedUserMapper authorizedUserMapper;
|
||||||
|
|
||||||
|
public CustomUserDetailsService(UserRepository userRepository, AuthorizedUserMapper authorizedUserMapper) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.authorizedUserMapper = authorizedUserMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
Optional<User> userOptional = userRepository.findByName(username);
|
||||||
|
if (userOptional.isEmpty()) {
|
||||||
|
throw new UsernameNotFoundException("user not found " + username);
|
||||||
|
}
|
||||||
|
return authorizedUserMapper.mapFormLogin(userOptional.get());
|
||||||
|
}
|
||||||
|
}
|
@ -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<String> authorities = authorityResolver.getAuthorities(user);
|
|
||||||
List<String> 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<String> authorities = authorityResolver.getAuthorities(user);
|
|
||||||
List<String> 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<Duration> getMaxExpiry(User user, Function<Role, Optional<Duration>> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
package com.backend.user.service.anyame.service;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.entity.User;
|
||||||
|
import com.backend.user.service.anyame.repository.PermissionRepository;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class UserAuthorityService {
|
||||||
|
|
||||||
|
private final PermissionRepository permissionRepository;
|
||||||
|
|
||||||
|
public UserAuthorityService(PermissionRepository permissionRepository) {
|
||||||
|
this.permissionRepository = permissionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<GrantedAuthority> getUserAuthorities(User user) {
|
||||||
|
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||||
|
|
||||||
|
user.getRoles().forEach(role ->
|
||||||
|
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()))
|
||||||
|
);
|
||||||
|
|
||||||
|
permissionRepository.findByUserName(user.getName()).forEach(permission ->
|
||||||
|
authorities.add(new SimpleGrantedAuthority(permission.getName()))
|
||||||
|
);
|
||||||
|
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.backend.user.service.anyame.service.extractor;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component("github")
|
||||||
|
public class GitHubOAuth2AttributeExtractor implements OAuth2AttributeExtractor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractEmail(Map<String, Object> attributes) {
|
||||||
|
return (String) attributes.get("email");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractName(Map<String, Object> attributes) {
|
||||||
|
String name = (String) attributes.get("name");
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
name = (String) attributes.get("login");
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractId(Map<String, Object> attributes) {
|
||||||
|
return attributes.get("id").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractAvatarUrl(Map<String, Object> attributes) {
|
||||||
|
return (String) attributes.get("avatar_url");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.backend.user.service.anyame.service.extractor;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component("google")
|
||||||
|
public class GoogleOidcAttributeExtractor implements OidcAttributeExtractor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractEmail(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Object email = getClaim("email", idToken, userInfo);
|
||||||
|
return email != null ? email.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractName(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Object name = getClaim("name", idToken, userInfo);
|
||||||
|
return name != null ? name.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractSubject(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
// Subject is always in ID token
|
||||||
|
return idToken.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractPreferredUsername(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Object username = getClaim("preferred_username", idToken, userInfo);
|
||||||
|
if (username == null) {
|
||||||
|
username = getClaim("email", idToken, userInfo); // fallback to email
|
||||||
|
}
|
||||||
|
return username != null ? username.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractPicture(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Object picture = getClaim("picture", idToken, userInfo);
|
||||||
|
return picture != null ? picture.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractGivenName(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Object givenName = getClaim("given_name", idToken, userInfo);
|
||||||
|
return givenName != null ? givenName.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String extractFamilyName(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
Object familyName = getClaim("family_name", idToken, userInfo);
|
||||||
|
return familyName != null ? familyName.toString() : null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.backend.user.service.anyame.service.extractor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface OAuth2AttributeExtractor {
|
||||||
|
String extractEmail(Map<String, Object> attributes);
|
||||||
|
|
||||||
|
String extractName(Map<String, Object> attributes);
|
||||||
|
|
||||||
|
String extractId(Map<String, Object> attributes);
|
||||||
|
|
||||||
|
String extractAvatarUrl(Map<String, Object> attributes);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.backend.user.service.anyame.service.extractor;
|
||||||
|
|
||||||
|
public interface OAuth2AttributeExtractorFactory {
|
||||||
|
OAuth2AttributeExtractor create(String provider);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.backend.user.service.anyame.service.extractor;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||||
|
|
||||||
|
public interface OidcAttributeExtractor {
|
||||||
|
String extractEmail(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
String extractName(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
String extractSubject(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
String extractPreferredUsername(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
String extractPicture(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
String extractGivenName(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
String extractFamilyName(OidcIdToken idToken, OidcUserInfo userInfo);
|
||||||
|
|
||||||
|
default String constructName(String givenName, String familyName, String preferredUsername, String email) {
|
||||||
|
if (givenName != null && familyName != null) {
|
||||||
|
return givenName + " " + familyName;
|
||||||
|
}
|
||||||
|
if (givenName != null) {
|
||||||
|
return givenName;
|
||||||
|
}
|
||||||
|
if (familyName != null) {
|
||||||
|
return familyName;
|
||||||
|
}
|
||||||
|
if (preferredUsername != null) {
|
||||||
|
return preferredUsername;
|
||||||
|
}
|
||||||
|
return email.split("@")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
default Object getClaim(String claimName, OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
|
if (userInfo != null && userInfo.getClaims().containsKey(claimName)) {
|
||||||
|
return userInfo.getClaim(claimName);
|
||||||
|
}
|
||||||
|
if (idToken != null && idToken.getClaims().containsKey(claimName)) {
|
||||||
|
return idToken.getClaim(claimName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.backend.user.service.anyame.service.extractor;
|
||||||
|
|
||||||
|
public interface OidcAttributeExtractorFactory {
|
||||||
|
OidcAttributeExtractor create(String provider);
|
||||||
|
}
|
@ -1,30 +1,47 @@
|
|||||||
jwt:
|
|
||||||
secret: ${JWT_SECRET}
|
|
||||||
authorization:
|
|
||||||
roles:
|
|
||||||
- name: ROLE_USER
|
|
||||||
access-expiry: '1d'
|
|
||||||
refresh-expiry: '90d'
|
|
||||||
privileges:
|
|
||||||
- READ_PRIVILEGE
|
|
||||||
- name: ROLE_ADMIN
|
|
||||||
access-expiry: '30m'
|
|
||||||
refresh-expiry: '2h'
|
|
||||||
privileges:
|
|
||||||
- WRITE_PRIVILEGE
|
|
||||||
- CHANGE_PASSWORD_PRIVILEGE
|
|
||||||
hierarchy: |
|
|
||||||
ROLE_ADMIN > ROLE_USER
|
|
||||||
default-role: ROLE_USER
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
application:
|
||||||
|
name: anyame-user-service
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
authorizationserver:
|
||||||
|
issuer-url: http://localhost:8080
|
||||||
|
introspection-endpoint: /oauth2/token-info
|
||||||
|
client:
|
||||||
|
registration:
|
||||||
|
github:
|
||||||
|
clientId: ${GITHUB_CLIENT_ID}
|
||||||
|
clientSecret: ${GITHUB_CLIENT_SECRET}
|
||||||
|
scope:
|
||||||
|
- user:email
|
||||||
|
- read:user
|
||||||
|
|
||||||
|
google:
|
||||||
|
clientId: ${GOOGLE_CLIENT_ID}
|
||||||
|
clientSecret: ${GOOGLE_CLIENT_SECRET}
|
||||||
datasource:
|
datasource:
|
||||||
url: ${DATABASE_URL}
|
url: ${DATABASE_URL}
|
||||||
username: ${DATABASE_USERNAME}
|
username: ${DATABASE_USERNAME}
|
||||||
password: ${DATABASE_PASSWORD}
|
password: ${DATABASE_PASSWORD}
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/migration/structure, classpath:db/migration/data
|
||||||
|
validate-on-migrate: true
|
||||||
|
default-schema: main
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
|
|
||||||
|
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:
|
server:
|
||||||
error:
|
error:
|
||||||
include-message: always
|
include-message: always
|
Reference in New Issue
Block a user