From 76bc5d4853a700d43a6d574b47cd763ae282f64e Mon Sep 17 00:00:00 2001 From: bivashy Date: Wed, 11 Jun 2025 01:18:53 +0500 Subject: [PATCH] Implement OIDC, GitHub, Google attribute extractor --- .../component/AuthorizedUserMapper.java | 24 +++++ .../OAuth2AttributeExtractorConfig.java | 19 ++++ .../config/OidcAttributeExtractorConfig.java | 19 ++++ .../anyame/configurer/SocialConfigurer.java | 25 ----- .../anyame/controller/HelloController.java | 7 +- .../UnsupportedOAuth2ProviderException.java | 11 ++ .../UnsupportedOidcProviderException.java | 11 ++ .../service/anyame/model/AuthorizedUser.java | 48 ++++++++- .../anyame/repository/UserRepository.java | 4 +- .../service/CustomOAuth2UserService.java | 51 ++++++--- .../anyame/service/CustomOIDCUserService.java | 100 ++++++++++++++++++ .../service/CustomUserDetailsService.java | 2 +- .../anyame/service/UserAuthorityService.java | 5 +- .../GitHubOAuth2AttributeExtractor.java | 33 ++++++ .../GoogleOidcAttributeExtractor.java | 54 ++++++++++ .../extractor/OAuth2AttributeExtractor.java | 13 +++ .../OAuth2AttributeExtractorFactory.java | 5 + .../extractor/OidcAttributeExtractor.java | 46 ++++++++ .../OidcAttributeExtractorFactory.java | 5 + src/main/resources/application.yaml | 2 +- 20 files changed, 430 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/backend/user/service/anyame/config/OAuth2AttributeExtractorConfig.java create mode 100644 src/main/java/com/backend/user/service/anyame/config/OidcAttributeExtractorConfig.java delete mode 100644 src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java create mode 100644 src/main/java/com/backend/user/service/anyame/exception/UnsupportedOAuth2ProviderException.java create mode 100644 src/main/java/com/backend/user/service/anyame/exception/UnsupportedOidcProviderException.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/CustomOIDCUserService.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/extractor/GitHubOAuth2AttributeExtractor.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/extractor/GoogleOidcAttributeExtractor.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractor.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractorFactory.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractor.java create mode 100644 src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractorFactory.java diff --git a/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java b/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java index 1068924..5d2140b 100644 --- a/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java +++ b/src/main/java/com/backend/user/service/anyame/component/AuthorizedUserMapper.java @@ -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.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; @@ -38,6 +40,28 @@ public class AuthorizedUserMapper { .username(userEntity.getName()) .active(userEntity.isActive()) .authorities(authorities) + .attributes(attributes) .build(); } + + public AuthorizedUser mapOidcUser(User userEntity, Map attributes, OidcIdToken idToken, OidcUserInfo userInfo) { + Set 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(); + } } \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/config/OAuth2AttributeExtractorConfig.java b/src/main/java/com/backend/user/service/anyame/config/OAuth2AttributeExtractorConfig.java new file mode 100644 index 0000000..a7ae20b --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/config/OAuth2AttributeExtractorConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/config/OidcAttributeExtractorConfig.java b/src/main/java/com/backend/user/service/anyame/config/OidcAttributeExtractorConfig.java new file mode 100644 index 0000000..15c9ad4 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/config/OidcAttributeExtractorConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java b/src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java deleted file mode 100644 index 0e21c08..0000000 --- a/src/main/java/com/backend/user/service/anyame/configurer/SocialConfigurer.java +++ /dev/null @@ -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 { - 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); - } - }); - } -} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/controller/HelloController.java b/src/main/java/com/backend/user/service/anyame/controller/HelloController.java index 1943684..e768743 100644 --- a/src/main/java/com/backend/user/service/anyame/controller/HelloController.java +++ b/src/main/java/com/backend/user/service/anyame/controller/HelloController.java @@ -1,11 +1,15 @@ 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 { @@ -17,7 +21,8 @@ public class HelloController { @GetMapping("/principalhello") public String principalhello() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return authentication.getName(); + Collection authorities = authentication.getAuthorities(); + return String.format("%s, %s", authentication.getName(), authorities.stream().map(GrantedAuthority::toString).collect(Collectors.joining(","))); } } \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/exception/UnsupportedOAuth2ProviderException.java b/src/main/java/com/backend/user/service/anyame/exception/UnsupportedOAuth2ProviderException.java new file mode 100644 index 0000000..5e3c2b7 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/exception/UnsupportedOAuth2ProviderException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/exception/UnsupportedOidcProviderException.java b/src/main/java/com/backend/user/service/anyame/exception/UnsupportedOidcProviderException.java new file mode 100644 index 0000000..6f24737 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/exception/UnsupportedOidcProviderException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java b/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java index 1ef8547..ea3951a 100644 --- a/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java +++ b/src/main/java/com/backend/user/service/anyame/model/AuthorizedUser.java @@ -1,20 +1,22 @@ 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.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; -import java.util.Set; -public class AuthorizedUser extends User implements OAuth2User { +public class AuthorizedUser extends User implements OAuth2User, OidcUser { private Long id; private String email; private boolean active; - private Set roles; private Map attributes; + private OidcUserInfo userInfo; + private OidcIdToken idToken; public Long getId() { return id; @@ -41,6 +43,9 @@ public class AuthorizedUser extends User implements OAuth2User { 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 @@ -53,6 +58,21 @@ public class AuthorizedUser extends User implements OAuth2User { return getUsername(); } + @Override + public Map 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; @@ -64,6 +84,9 @@ public class AuthorizedUser extends User implements OAuth2User { private boolean accountNonExpired = true; private boolean credentialsNonExpired = true; private boolean accountNonLocked = true; + private Map attributes; + private OidcUserInfo oidcUserInfo; + private OidcIdToken oidcIdToken; public Builder(String email, String password, Collection authorities) { this.email = email; @@ -121,6 +144,23 @@ public class AuthorizedUser extends User implements OAuth2User { return this; } + public Builder attributes(Map 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); } diff --git a/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java b/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java index 7c42e95..40f910d 100644 --- a/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java +++ b/src/main/java/com/backend/user/service/anyame/repository/UserRepository.java @@ -10,9 +10,7 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); - - Optional findByEmailOrName(String email, String name); + Optional findByName(String name); @Query("SELECT u FROM User u " + "LEFT JOIN FETCH u.roles " + diff --git a/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java b/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java index 8252ddb..19fa28e 100644 --- a/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java +++ b/src/main/java/com/backend/user/service/anyame/service/CustomOAuth2UserService.java @@ -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.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; @@ -14,6 +16,8 @@ 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 { private static final Logger log = LoggerFactory.getLogger(CustomOAuth2UserService.class); @@ -21,11 +25,16 @@ public class CustomOAuth2UserService implements OAuth2UserService 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)); // TODO: Should be toggleable nor documented behaviour // First user becomes admin - if (userRepository.count() == 1) { + 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); } @@ -66,7 +88,6 @@ public class CustomOAuth2UserService implements OAuth2UserService new RuntimeException("User role not found")); diff --git a/src/main/java/com/backend/user/service/anyame/service/CustomOIDCUserService.java b/src/main/java/com/backend/user/service/anyame/service/CustomOIDCUserService.java new file mode 100644 index 0000000..24f0d6b --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/CustomOIDCUserService.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java b/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java index cecb620..89ba398 100644 --- a/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java +++ b/src/main/java/com/backend/user/service/anyame/service/CustomUserDetailsService.java @@ -22,7 +22,7 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - Optional userOptional = userRepository.findByEmailOrName(username, username); + Optional userOptional = userRepository.findByName(username); if (userOptional.isEmpty()) { throw new UsernameNotFoundException("user not found " + username); } diff --git a/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java b/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java index 889ebb0..8427500 100644 --- a/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java +++ b/src/main/java/com/backend/user/service/anyame/service/UserAuthorityService.java @@ -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.repository.PermissionRepository; -import com.backend.user.service.anyame.repository.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; @@ -15,11 +14,9 @@ import java.util.Set; @Transactional(readOnly = true) public class UserAuthorityService { - private final UserRepository userRepository; private final PermissionRepository permissionRepository; - public UserAuthorityService(UserRepository userRepository, PermissionRepository permissionRepository) { - this.userRepository = userRepository; + public UserAuthorityService(PermissionRepository permissionRepository) { this.permissionRepository = permissionRepository; } diff --git a/src/main/java/com/backend/user/service/anyame/service/extractor/GitHubOAuth2AttributeExtractor.java b/src/main/java/com/backend/user/service/anyame/service/extractor/GitHubOAuth2AttributeExtractor.java new file mode 100644 index 0000000..deb94ce --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/extractor/GitHubOAuth2AttributeExtractor.java @@ -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 attributes) { + return (String) attributes.get("email"); + } + + @Override + public String extractName(Map 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 attributes) { + return attributes.get("id").toString(); + } + + @Override + public String extractAvatarUrl(Map attributes) { + return (String) attributes.get("avatar_url"); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/extractor/GoogleOidcAttributeExtractor.java b/src/main/java/com/backend/user/service/anyame/service/extractor/GoogleOidcAttributeExtractor.java new file mode 100644 index 0000000..caec622 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/extractor/GoogleOidcAttributeExtractor.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractor.java b/src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractor.java new file mode 100644 index 0000000..92e75e5 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractor.java @@ -0,0 +1,13 @@ +package com.backend.user.service.anyame.service.extractor; + +import java.util.Map; + +public interface OAuth2AttributeExtractor { + String extractEmail(Map attributes); + + String extractName(Map attributes); + + String extractId(Map attributes); + + String extractAvatarUrl(Map attributes); +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractorFactory.java b/src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractorFactory.java new file mode 100644 index 0000000..57fedb1 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/extractor/OAuth2AttributeExtractorFactory.java @@ -0,0 +1,5 @@ +package com.backend.user.service.anyame.service.extractor; + +public interface OAuth2AttributeExtractorFactory { + OAuth2AttributeExtractor create(String provider); +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractor.java b/src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractor.java new file mode 100644 index 0000000..134c0ae --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractor.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractorFactory.java b/src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractorFactory.java new file mode 100644 index 0000000..bfed389 --- /dev/null +++ b/src/main/java/com/backend/user/service/anyame/service/extractor/OidcAttributeExtractorFactory.java @@ -0,0 +1,5 @@ +package com.backend.user.service.anyame.service.extractor; + +public interface OidcAttributeExtractorFactory { + OidcAttributeExtractor create(String provider); +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a49f315..77cc244 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -29,7 +29,7 @@ spring: default-schema: main jpa: hibernate: - ddl-auto: create + ddl-auto: none logging: level: