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
|
219
pom.xml
219
pom.xml
@ -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>
|
||||
|
||||
</project>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -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;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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 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);
|
||||
}
|
@ -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);
|
||||
@Query("SELECT u FROM User u " +
|
||||
"LEFT JOIN FETCH u.roles " +
|
||||
"WHERE u.name = :name")
|
||||
Optional<User> findByNameWithRoles(@Param("name") String name);
|
||||
|
||||
Optional<User> findByEmailOrName(String email, 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:
|
||||
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
|
||||
include-message: always
|
Reference in New Issue
Block a user