Implement OIDC, GitHub, Google attribute extractor
This commit is contained in:
@ -4,6 +4,8 @@ import com.backend.user.service.anyame.entity.User;
|
|||||||
import com.backend.user.service.anyame.model.AuthorizedUser;
|
import com.backend.user.service.anyame.model.AuthorizedUser;
|
||||||
import com.backend.user.service.anyame.service.UserAuthorityService;
|
import com.backend.user.service.anyame.service.UserAuthorityService;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -38,6 +40,28 @@ public class AuthorizedUserMapper {
|
|||||||
.username(userEntity.getName())
|
.username(userEntity.getName())
|
||||||
.active(userEntity.isActive())
|
.active(userEntity.isActive())
|
||||||
.authorities(authorities)
|
.authorities(authorities)
|
||||||
|
.attributes(attributes)
|
||||||
.build();
|
.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,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,25 +0,0 @@
|
|||||||
package com.backend.user.service.anyame.configurer;
|
|
||||||
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
|
||||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
|
||||||
|
|
||||||
public class SocialConfigurer extends AbstractHttpConfigurer<SocialConfigurer, HttpSecurity> {
|
|
||||||
private AuthenticationFailureHandler failureHandler;
|
|
||||||
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(HttpSecurity http) throws Exception {
|
|
||||||
http.oauth2Login(c -> {
|
|
||||||
if (this.successHandler != null) {
|
|
||||||
c.successHandler(this.successHandler);
|
|
||||||
}
|
|
||||||
if (this.failureHandler != null) {
|
|
||||||
c.failureHandler(this.failureHandler);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,15 @@
|
|||||||
package com.backend.user.service.anyame.controller;
|
package com.backend.user.service.anyame.controller;
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
public class HelloController {
|
public class HelloController {
|
||||||
@ -17,7 +21,8 @@ public class HelloController {
|
|||||||
@GetMapping("/principalhello")
|
@GetMapping("/principalhello")
|
||||||
public String principalhello() {
|
public String principalhello() {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
return authentication.getName();
|
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||||
|
return String.format("%s, %s", authentication.getName(), authorities.stream().map(GrantedAuthority::toString).collect(Collectors.joining(",")));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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,20 +1,22 @@
|
|||||||
package com.backend.user.service.anyame.model;
|
package com.backend.user.service.anyame.model;
|
||||||
|
|
||||||
import com.backend.user.service.anyame.entity.Role;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.User;
|
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 org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class AuthorizedUser extends User implements OAuth2User {
|
public class AuthorizedUser extends User implements OAuth2User, OidcUser {
|
||||||
private Long id;
|
private Long id;
|
||||||
private String email;
|
private String email;
|
||||||
private boolean active;
|
private boolean active;
|
||||||
private Set<Role> roles;
|
|
||||||
private Map<String, Object> attributes;
|
private Map<String, Object> attributes;
|
||||||
|
private OidcUserInfo userInfo;
|
||||||
|
private OidcIdToken idToken;
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
@ -41,6 +43,9 @@ public class AuthorizedUser extends User implements OAuth2User {
|
|||||||
this.id = builder.id;
|
this.id = builder.id;
|
||||||
this.email = builder.email;
|
this.email = builder.email;
|
||||||
this.active = builder.active;
|
this.active = builder.active;
|
||||||
|
this.attributes = builder.attributes;
|
||||||
|
this.userInfo = builder.oidcUserInfo;
|
||||||
|
this.idToken = builder.oidcIdToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -53,6 +58,21 @@ public class AuthorizedUser extends User implements OAuth2User {
|
|||||||
return getUsername();
|
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 {
|
public static class Builder {
|
||||||
private String email;
|
private String email;
|
||||||
private String password;
|
private String password;
|
||||||
@ -64,6 +84,9 @@ public class AuthorizedUser extends User implements OAuth2User {
|
|||||||
private boolean accountNonExpired = true;
|
private boolean accountNonExpired = true;
|
||||||
private boolean credentialsNonExpired = true;
|
private boolean credentialsNonExpired = true;
|
||||||
private boolean accountNonLocked = 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) {
|
public Builder(String email, String password, Collection<? extends GrantedAuthority> authorities) {
|
||||||
this.email = email;
|
this.email = email;
|
||||||
@ -121,6 +144,23 @@ public class AuthorizedUser extends User implements OAuth2User {
|
|||||||
return this;
|
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() {
|
public AuthorizedUser build() {
|
||||||
return new AuthorizedUser(this);
|
return new AuthorizedUser(this);
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,7 @@ import java.util.Optional;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByName(String name);
|
||||||
|
|
||||||
Optional<User> findByEmailOrName(String email, String name);
|
|
||||||
|
|
||||||
@Query("SELECT u FROM User u " +
|
@Query("SELECT u FROM User u " +
|
||||||
"LEFT JOIN FETCH u.roles " +
|
"LEFT JOIN FETCH u.roles " +
|
||||||
|
@ -5,6 +5,8 @@ import com.backend.user.service.anyame.entity.Role;
|
|||||||
import com.backend.user.service.anyame.entity.User;
|
import com.backend.user.service.anyame.entity.User;
|
||||||
import com.backend.user.service.anyame.repository.RoleRepository;
|
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||||
import com.backend.user.service.anyame.repository.UserRepository;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||||
@ -14,6 +16,8 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
|
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(CustomOAuth2UserService.class);
|
private static final Logger log = LoggerFactory.getLogger(CustomOAuth2UserService.class);
|
||||||
@ -21,11 +25,16 @@ public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequ
|
|||||||
private final RoleRepository roleRepository;
|
private final RoleRepository roleRepository;
|
||||||
private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
|
private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
|
||||||
private final AuthorizedUserMapper authorizedUserMapper;
|
private final AuthorizedUserMapper authorizedUserMapper;
|
||||||
|
private final OAuth2AttributeExtractorFactory attributeExtractorFactory;
|
||||||
|
|
||||||
public CustomOAuth2UserService(UserRepository userRepository, RoleRepository roleRepository, AuthorizedUserMapper authorizedUserMapper) {
|
public CustomOAuth2UserService(UserRepository userRepository,
|
||||||
|
RoleRepository roleRepository,
|
||||||
|
AuthorizedUserMapper authorizedUserMapper,
|
||||||
|
OAuth2AttributeExtractorFactory attributeExtractorFactory) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.roleRepository = roleRepository;
|
this.roleRepository = roleRepository;
|
||||||
this.authorizedUserMapper = authorizedUserMapper;
|
this.authorizedUserMapper = authorizedUserMapper;
|
||||||
|
this.attributeExtractorFactory = attributeExtractorFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -36,28 +45,41 @@ public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequ
|
|||||||
}
|
}
|
||||||
|
|
||||||
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) {
|
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) {
|
||||||
if (!oauth2User.getAttributes().containsKey("id")) {
|
|
||||||
throw new OAuth2AuthenticationException("oauth2User id is not provided");
|
|
||||||
}
|
|
||||||
if (!oauth2User.getAttributes().containsKey("name")) {
|
|
||||||
throw new OAuth2AuthenticationException("oauth2User name is not provided");
|
|
||||||
}
|
|
||||||
log.info("NAME IS: {}, {}, {}", oauth2User.getName(), oauth2User.getAttribute("login"), oauth2User.getAttribute("name"));
|
|
||||||
log.info("NAME IS: {}, {}, {}", oauth2User.getName(), oauth2User.getAttribute("login"), oauth2User.getAttribute("name"));
|
|
||||||
String registrationId = userRequest.getClientRegistration().getRegistrationId();
|
String registrationId = userRequest.getClientRegistration().getRegistrationId();
|
||||||
String email = oauth2User.getAttribute("email");
|
|
||||||
String name = oauth2User.getAttribute("name");
|
|
||||||
String providerId = oauth2User.getAttribute("id").toString();
|
|
||||||
|
|
||||||
User user = userRepository.findByEmail(email)
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
User user = userRepository.findByName(name)
|
||||||
.orElseGet(() -> createNewUser(email, name, registrationId, providerId));
|
.orElseGet(() -> createNewUser(email, name, registrationId, providerId));
|
||||||
|
|
||||||
// TODO: Should be toggleable nor documented behaviour
|
// TODO: Should be toggleable nor documented behaviour
|
||||||
// First user becomes admin
|
// First user becomes admin
|
||||||
if (userRepository.count() == 1) {
|
if (user.getId() == 1 && !user.isActive()) {
|
||||||
Role adminRole = roleRepository.findByName("ADMIN")
|
Role adminRole = roleRepository.findByName("ADMIN")
|
||||||
.orElseThrow(() -> new RuntimeException("Admin role not found"));
|
.orElseThrow(() -> new RuntimeException("Admin role not found"));
|
||||||
user.getRoles().add(adminRole);
|
user.getRoles().add(adminRole);
|
||||||
|
user.setActive(true);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +88,6 @@ public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequ
|
|||||||
|
|
||||||
private User createNewUser(String email, String name, String provider, String providerId) {
|
private User createNewUser(String email, String name, String provider, String providerId) {
|
||||||
User user = new User(name, email, provider, providerId);
|
User user = new User(name, email, provider, providerId);
|
||||||
user.setActive(true);
|
|
||||||
|
|
||||||
Role userRole = roleRepository.findByName("USER")
|
Role userRole = roleRepository.findByName("USER")
|
||||||
.orElseThrow(() -> new RuntimeException("User role not found"));
|
.orElseThrow(() -> new RuntimeException("User role not found"));
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
User user = userRepository.findByName(name)
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
Optional<User> userOptional = userRepository.findByEmailOrName(username, username);
|
Optional<User> userOptional = userRepository.findByName(username);
|
||||||
if (userOptional.isEmpty()) {
|
if (userOptional.isEmpty()) {
|
||||||
throw new UsernameNotFoundException("user not found " + username);
|
throw new UsernameNotFoundException("user not found " + username);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package com.backend.user.service.anyame.service;
|
|||||||
|
|
||||||
import com.backend.user.service.anyame.entity.User;
|
import com.backend.user.service.anyame.entity.User;
|
||||||
import com.backend.user.service.anyame.repository.PermissionRepository;
|
import com.backend.user.service.anyame.repository.PermissionRepository;
|
||||||
import com.backend.user.service.anyame.repository.UserRepository;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -15,11 +14,9 @@ import java.util.Set;
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public class UserAuthorityService {
|
public class UserAuthorityService {
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PermissionRepository permissionRepository;
|
private final PermissionRepository permissionRepository;
|
||||||
|
|
||||||
public UserAuthorityService(UserRepository userRepository, PermissionRepository permissionRepository) {
|
public UserAuthorityService(PermissionRepository permissionRepository) {
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.permissionRepository = permissionRepository;
|
this.permissionRepository = permissionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -29,7 +29,7 @@ spring:
|
|||||||
default-schema: main
|
default-schema: main
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: create
|
ddl-auto: none
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
Reference in New Issue
Block a user