How do you save users who have logged in with OAuth 2 (Spring)?
Asked Answered
K

4

16

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 WebSecurityConfigurerAdapterwith:

@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.

Kansas answered 3/2, 2018 at 14:25 Comment(0)
S
22

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

Sebrinasebum answered 22/4, 2019 at 17:17 Comment(0)
M
3

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<*, *>
    }
}
Macario answered 27/3, 2018 at 6:10 Comment(0)
C
3

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
        }
    };
}
Cozmo answered 30/8, 2018 at 0:27 Comment(0)
S
0

I've found the solution in this website: https://www.codejava.net/frameworks/spring-boot/oauth2-login-with-google-example

I created a CustomOAuth2User

public class CustomOAuth2User implements OAuth2User {

    private OAuth2User oauth2User;

    public CustomOAuth2User(OAuth2User oauth2User) {
        this.oauth2User = oauth2User;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return oauth2User.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return oauth2User.getAuthorities();
    }

    @Override
    public String getName() {
        return oauth2User.getAttribute("name");
    }

    public String getEmail() {
        return oauth2User.<String>getAttribute("email");
    }
}

then a CustomOAuth2UserService

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User user =  super.loadUser(userRequest);
        return new CustomOAuth2User(user);
    }
}

And finally inside my config class

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomOAuth2UserService oauthUserService;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers("/").permitAll();
                    auth.anyRequest().authenticated();
                })
                .oauth2Login(oauth2 -> oauth2
                        .userInfoEndpoint(userInfo -> userInfo.userService(oauthUserService)).successHandler(new AuthenticationSuccessHandler() {

                            @Override
                            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                                                Authentication authentication) throws IOException, ServletException {

                                CustomOAuth2User oauthUser = (CustomOAuth2User) authentication.getPrincipal();

                                //Here you call your service to save the user
                                System.out.println(oauthUser.getName());

                                response.sendRedirect("/");
                            }
                        })
                )
                .formLogin(Customizer.withDefaults())
                .build();
    }
}

And this is how you can get the data that google or any other provider will return and save it in the database.

Saarinen answered 7/2 at 20:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.