Compare commits

5 Commits

52 changed files with 1397 additions and 926 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
DATABASE_URL=jdbc:postgresql://localhost:5433/postgres
DATABASE_USERNAME=username
DATABASE_PASSWORD=password

217
pom.xml
View File

@ -1,109 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.backend.user.service</groupId>
<artifactId>anyame-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>anyame-backend</name>
<description>User service for anyame backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.backend.user.service</groupId>
<artifactId>anyame-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>anyame-backend</name>
<description>User service for anyame</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
<jjwt.version>0.12.6</jjwt.version>
<spring-dotenv.version>4.0.0</spring-dotenv.version>
<passay.version>1.6.6</passay.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<jjwt.version>0.12.6</jjwt.version>
<spring-dotenv.version>4.0.0</spring-dotenv.version>
<passay.version>1.6.6</passay.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId>
<version>${spring-dotenv.version}</version>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>${passay.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId>
<version>${spring-dotenv.version}</version>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>${passay.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -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));
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,51 +1,52 @@
package com.backend.user.service.anyame.config;
import com.backend.user.service.anyame.component.AuthorizationProperties;
import org.passay.CharacterRule;
import org.passay.EnglishCharacterData;
import org.passay.LengthRule;
import org.passay.PasswordValidator;
import org.passay.WhitespaceRule;
import com.backend.user.service.anyame.service.CustomOAuth2UserService;
import com.backend.user.service.anyame.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.password.PasswordEncoder;
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 {
private final CustomUserDetailsService userDetailsService;
private final CustomOAuth2UserService oauth2UserService;
public SecurityConfig(CustomUserDetailsService userDetailsService,
CustomOAuth2UserService oauth2UserService) {
this.userDetailsService = userDetailsService;
this.oauth2UserService = oauth2UserService;
}
@Bean
@Order(2)
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
public PasswordEncoder passwordEncoder() {
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());
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}

View File

@ -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(",")));
}
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,34 +1,33 @@
package com.backend.user.service.anyame.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
@ManyToMany(mappedBy = "roles")
private Collection<User> users;
@ManyToMany
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "roles_privileges",
joinColumns = @JoinColumn(
name = "role_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(
name = "privilege_id", referencedColumnName = "id"))
private Collection<Privilege> privileges;
name = "role_hierarchy",
joinColumns = @JoinColumn(name = "child_role_id"),
inverseJoinColumns = @JoinColumn(name = "parent_role_id")
)
private Set<Role> parentRoles = new HashSet<>();
@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() {
}
@ -45,17 +44,21 @@ public class Role {
return name;
}
public Collection<User> getUsers() {
return users;
public Set<Role> getParentRoles() {
return parentRoles;
}
public Collection<Privilege> getPrivileges() {
return privileges;
public Set<Permission> getPermissions() {
return permissions;
}
public Role setPrivileges(Collection<Privilege> privileges) {
this.privileges = privileges;
return this;
}
public Set<Permission> getAllPermissions() {
Set<Permission> allPermissions = new HashSet<>(permissions);
for (Role parentRole : parentRoles) {
allPermissions.addAll(parentRole.getAllPermissions());
}
return allPermissions;
}
}

View File

@ -1,16 +1,9 @@
package com.backend.user.service.anyame.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
@ -19,33 +12,46 @@ public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true, nullable = false)
private String name;
@Column(unique = true)
private String email;
private String password;
private boolean enabled = true;
private boolean active;
@ManyToMany
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(
name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(
name = "role_id", referencedColumnName = "id"))
private Collection<Role> roles;
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
private String provider;
private String providerId;
protected User() {
}
public User(String name, String email, String password) {
public User(String name, String email, String provider, String providerId) {
this.name = name;
this.email = email;
this.password = password;
this.provider = provider;
this.providerId = providerId;
}
public User(String name, String email) {
this.name = name;
this.email = email;
public Set<Role> getRoles() {
return roles;
}
public Set<Permission> getAllPermissions() {
Set<Permission> allPermissions = new HashSet<>();
for (Role role : roles) {
allPermissions.addAll(role.getAllPermissions());
}
return allPermissions;
}
public Long getId() {
@ -64,28 +70,19 @@ public class User {
return password;
}
public User setPassword(String password) {
this.password = password;
return this;
public boolean isActive() {
return active;
}
public boolean isEnabled() {
return enabled;
public void setActive(boolean active) {
this.active = active;
}
public User setEnabled(boolean enabled) {
this.enabled = enabled;
return this;
public String getProvider() {
return provider;
}
public Collection<Role> getRoles() {
return roles;
public String getProviderId() {
return providerId;
}
public User setRoles(Collection<Role> roles) {
this.roles = roles;
return this;
}
}

View File

@ -1,9 +0,0 @@
package com.backend.user.service.anyame.exception;
public class InvalidCredentialsException extends Exception {
public InvalidCredentialsException() {
super("invalid credentials");
}
}

View File

@ -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;
}
}

View File

@ -1,5 +0,0 @@
package com.backend.user.service.anyame.exception;
public class NoExpiryDurationException extends Exception {
}

View File

@ -1,9 +0,0 @@
package com.backend.user.service.anyame.exception;
public class UnsafePasswordException extends Exception {
public UnsafePasswordException(String message) {
super(message);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,9 +0,0 @@
package com.backend.user.service.anyame.exception;
public class UserAlreadyExistsException extends Exception {
public UserAlreadyExistsException() {
super("user already exists");
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -2,11 +2,21 @@ package com.backend.user.service.anyame.repository;
import com.backend.user.service.anyame.entity.Role;
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.Set;
@Repository
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);
}

View File

@ -2,13 +2,20 @@ package com.backend.user.service.anyame.repository;
import com.backend.user.service.anyame.entity.User;
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;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByName(String name);
Optional<User> findByEmail(String email);
Optional<User> findByEmailOrName(String email, String name);
@Query("SELECT u FROM User u " +
"LEFT JOIN FETCH u.roles " +
"WHERE u.name = :name")
Optional<User> findByNameWithRoles(@Param("name") String name);
Optional<User> findByProviderId(String name);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,5 @@
package com.backend.user.service.anyame.service.extractor;
public interface OAuth2AttributeExtractorFactory {
OAuth2AttributeExtractor create(String provider);
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
package com.backend.user.service.anyame.service.extractor;
public interface OidcAttributeExtractorFactory {
OidcAttributeExtractor create(String provider);
}

View File

@ -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:
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:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
flyway:
enabled: true
locations: classpath:db/migration/structure, classpath:db/migration/data
validate-on-migrate: true
default-schema: main
jpa:
hibernate:
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:
error:
include-message: always