My main objective is to store the client-id of the each user, once they login with google. This github repo contains most of what I needed till now. The two main files of concern are OAuthSecurityConfig.java and UserRestController.java.
When I navigate to /user
, the Principal contains all the details I need on the user. Thus I can use the following snippets to get the data I need:
Authentication a = SecurityContextHolder.getContext().getAuthentication();
String clientId = ((OAuth2Authentication) a).getOAuth2Request().getClientId();
I can then store the clientId in a repo
User user = new User(clientId);
userRepository.save(user);
The problem with this is that users do not have to navigate to /user
. Thus, one can navigate to /score/user1
without being registered.
This API is meant to be a backend for an android application in the future, so a jquery redirect to /user
would be insecure and would not work.
Things I have tried:
Attempt 1
I created the following class:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
}
return new UserRepositoryUserDetails(user);
}
}
and overrode the WebSecurityConfigurerAdapter
with:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
Both overridden methods are not called when a user logs in (I checked with a System.out.println
)
Attempt 2
I tried adding .userDetailsService(customUserDetailsService)
to:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// Starts authorizing configurations.
.authorizeRequests()
// Do not require auth for the "/" and "/index.html" URLs
.antMatchers("/", "/**.html", "/**.js").permitAll()
// Authenticate all remaining URLs.
.anyRequest().fullyAuthenticated()
.and()
.userDetailsService(customUserDetailsService)
// Setting the logout URL "/logout" - default logout URL.
.logout()
// After successful logout the application will redirect to "/" path.
.logoutSuccessUrl("/")
.permitAll()
.and()
// Setting the filter for the URL "/google/login".
.addFilterAt(filter(), BasicAuthenticationFilter.class)
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
Both methods were still not called, and I don't feel like I am any closer to the solution. Any help will be greatly appreciated.
OAuth2 is an authorization framework that enables the application Web Security to access the resources from the client. To build an OAuth2 application, we need to focus on the Grant Type (Authorization code), Client ID and Client secret.
It serves as an open authorization protocol for enabling a third party application to get limited access to an HTTP service on behalf of the resource owner. It can do so while not revealing the identity or the long-term credentials of the user. A third-party application itself can also use it on its behalf.
It works by delegating user authentication to the service that hosts the user account and authorizing third-party applications to access the user account. Oauth2 provides authorization flows for web and desktop applications, and mobile devices.
The way to go here is to provide a custom OidcUserService and override the loadUser() method because Google login is based on OpenId Connect.
First define a model class to hold the extracted data, something like this:
public class GoogleUserInfo {
private Map<String, Object> attributes;
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("sub");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
return (String) attributes.get("email");
}
}
Then create the custom OidcUserService with the loadUser() method which first calls the provided framework implementiation and then add your own logic for persisting the user data you need, something like this:
@Service
public class CustomOidcUserService extends OidcUserService {
@Autowired
private UserRepository userRepository;
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
try {
return processOidcUser(userRequest, oidcUser);
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) {
GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());
// see what other data from userRequest or oidcUser you need
Optional<User> userOptional = userRepository.findByEmail(googleUserInfo.getEmail());
if (!userOptional.isPresent()) {
User user = new User();
user.setEmail(googleUserInfo.getEmail());
user.setName(googleUserInfo.getName());
// set other needed data
userRepository.save(user);
}
return oidcUser;
}
}
And register the custom OidcUserService in the security configuration class:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomOidcUserService customOidcUserService;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(customOidcUserService);
}
}
Mode detailed explanation can be found in the documentation:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced-oidc-user-service
In case of some one else is stuck with this, my solution was to create a custom class extending from
OAuth2ClientAuthenticationProcessingFilter
and then override the successfulAuthentication
method to get the user authentication details and save it to my database.
Example (kotlin):
On your ssoFilter method (if you followed this tutorial https://spring.io/guides/tutorials/spring-boot-oauth2) or wharever you used to register your ouath clients, change the use of
val googleFilter = Auth2ClientAuthenticationProcessingFilter("/login/google");
for your custom class
val googleFilter = CustomAuthProcessingFilter("login/google")
and of course declare the CustomAuthProcessingFilter class
class CustomAuthProcessingFilter(defaultFilterProcessesUrl: String?)
: OAuth2ClientAuthenticationProcessingFilter(defaultFilterProcessesUrl) {
override fun successfulAuthentication(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication?) {
super.successfulAuthentication(request, response, chain, authResult)
// Check if user is authenticated.
if (authResult === null || !authResult.isAuthenticated) {
return
}
// Use userDetails to grab the values you need like socialId, email, userName, etc...
val userDetails: LinkedHashMap<*, *> = userAuthentication.details as LinkedHashMap<*, *>
}
}
You can listen to AuthenticationSuccessEvent
. For example:
@Bean
ApplicationListener<AuthenticationSuccessEvent> doSomething() {
return new ApplicationListener<AuthenticationSuccessEvent>() {
@Override
void onApplicationEvent(AuthenticationSuccessEvent event){
OAuth2Authentication authentication = (OAuth2Authentication) event.authentication;
// get required details from OAuth2Authentication instance and proceed further
}
};
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With