Reputation: 13
I’m implementing OAuth2 login in my Spring Boot 3 application using GitHub as an authentication provider. However, I’m unable to retrieve the user's email from GitHub—it always returns null. i'm building a job board application and i want users to first register with email and password, or with github, and they'll be directed to the onboarding page where they'll input details such as usertype-recruiter or jobseeker etc.
What I've Tried Configured application.yml with the correct scope:
yaml
Copy
Edit
security:
oauth2:
client:
registration:
github:
client-id: xxx
client-secret: xxx
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: user:email
Checked GitHub Profile Settings:
My email is set to "Public" in GitHub. I've Logged OAuth2 Attributes: The email field is missing (null), but other details like name and id are available. Confirmed GitHub Provides Email via API:
My Questions Why is GitHub’s OAuth2 response not including the email, even when my email is public? Is there a way to configure Spring Security to fetch the email directly, or do I need a separate API request?
Here is how I've tried to fetch user details from the access token
@Service
@RequiredArgsConstructor
public class CustomOauth2UserService extends DefaultOAuth2UserService {
private static final Logger log = LoggerFactory.getLogger(CustomOauth2UserService.class);
private final UserConnectedAccountRepository userConnectedAccountRepository;
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oAuth2User.getName();
String email = oAuth2User.getAttribute("email");
oAuth2User.getAttributes().forEach((key, value) -> log.info("key {}, value {}", key, value));
Optional<UserConnectedAccount> userConnectedAccount = userConnectedAccountRepository.findByProviderIdAndProvider(providerId, provider);
if (userConnectedAccount.isEmpty()) {
userRepository.findUserByEmail(email)
.ifPresentOrElse(user -> connectAccount(providerId, provider, user),
() -> createUser(providerId, provider, oAuth2User)
);
}
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(null)),
oAuth2User.getAttributes(),
"email"
);
}
public void createUser(String providerId, String provider, OAuth2User oAuth2User) {
AppUser appUser = new AppUser(oAuth2User);
appUser = userRepository.save(appUser);
connectAccount(providerId, provider, appUser);
}
private void connectAccount(String providerId, String provider, AppUser appUser) {
UserConnectedAccount newUserConnectedAccount = new UserConnectedAccount(providerId, provider, appUser);
userConnectedAccountRepository.save(newUserConnectedAccount);
}
}
and here is my security configuration. I'm using session based authentication.
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private final GlobalAuthenticationEntryPoint globalAuthenticationEntryPoint;
private final Oauth2LoginSuccessHandler oauth2LoginSuccessHandler;
private final CustomOauth2UserService customOauth2UserService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(request -> request
.requestMatchers("/api/v1/auth/**", "/login/**", "/**", "/oauth2/authorization/**").permitAll()
.anyRequest().authenticated());
http.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(customOauth2UserService))
.successHandler(oauth2LoginSuccessHandler)
);
http.exceptionHandling(customizer ->
customizer.authenticationEntryPoint(globalAuthenticationEntryPoint)
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager() {
var authenticationProvider = new DaoAuthenticationProvider(passwordEncoder);
authenticationProvider.setUserDetailsService(userService);
return new ProviderManager(authenticationProvider);
}
@Bean
public SecurityContextRepository SecurityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
return new SecurityContextLogoutHandler();
}
}
Upvotes: 0
Views: 82
Reputation: 11099
TL;DR
We need to fetch email addresses from GitHub, identify the primary email address, and add it to the OAuth2User
if present. A custom OAuth2UserService<OAuth2UserRequest, OAuth2User>
is responsible for handling this flow. Please note that scope = "user:email"
is required.
Code is verified with Spring Boot 3.4.3.
RestClientConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder.build();
}
}
GitHubEmailFetcher
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.List;
@Service
public record GitHubEmailFetcher(RestClient restClient) {
private static final String EMAILS_URL = "https://api.github.com/user/emails";
private static final String BEARER_PREFIX = "Bearer ";
public String fetchPrimaryEmailAddress(String token) {
List<GitHubEmailVm> emailVmList = restClient
.get()
.uri(EMAILS_URL)
.header(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + token)
.header(HttpHeaders.ACCEPT_ENCODING, MediaType.APPLICATION_JSON_VALUE)
.retrieve()
.body(new ParameterizedTypeReference<>() {
});
if (emailVmList == null || emailVmList.isEmpty()) {
return null;
}
return emailVmList.stream()
.filter(GitHubEmailVm::primary)
.findFirst()
.map(GitHubEmailVm::email)
.orElse(null);
}
private record GitHubEmailVm(String email, Boolean primary) {
}
}
CustomOAuth2UserService
This class is equivalent to your CustomOauth2UserService, but instead of using inheritance, I’m opting for composition (favoring a "has-a" relationship over an "is-a" relationship).
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
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.client.userinfo.DefaultOAuth2UserService;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
// "login" is default for GitHub, change to "email" if that's what you want
private static final String NAME_ATTRIBUTE = "login";
private static final String EMAIL_KEY = "email";
private final GitHubEmailFetcher emailFetcher;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
public CustomOAuth2UserService(GitHubEmailFetcher emailFetcher) {
this.emailFetcher = emailFetcher;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = delegate.loadUser(userRequest);
String primaryEmailAddress = extractPrimaryEmailAddress(
oauth2User,
userRequest.getAccessToken().getTokenValue());
// return oauth2User if primaryEmailAddress is null
// alternative: Throw exception
if (primaryEmailAddress == null) {
return oauth2User;
}
// insert your createUser code here
// Clone the original attributes into a mutable map
Map<String, Object> updatedAttributes = new HashMap<>(oauth2User.getAttributes());
// Add the fetched email to the attributes map
updatedAttributes.put(EMAIL_KEY, primaryEmailAddress);
// Return a new DefaultOAuth2User with the updated attributes
return new DefaultOAuth2User(
oauth2User.getAuthorities(), // or Collections.emptyList()
updatedAttributes,
NAME_ATTRIBUTE);
}
private String extractPrimaryEmailAddress(
OAuth2User oauth2User,
String token) {
String primaryEmailAddress = oauth2User.getAttribute(EMAIL_KEY);
if (!(primaryEmailAddress == null || primaryEmailAddress.isBlank())) {
return primaryEmailAddress;
}
return emailFetcher.fetchPrimaryEmailAddress(token);
}
}
Upvotes: 0