Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 286
Gabrielus Avatar asked Feb 03 '18 14:02

Gabrielus


People also ask

What is OAuth 2.0 and how it works in spring boot?

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.

How does OAuth work in Spring Security?

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.

How does OAuth2 work in REST API spring boot?

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.


3 Answers

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

like image 82
Adrian Bob Avatar answered Oct 18 '22 05:10

Adrian Bob


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<*, *>
    }
}
like image 24
damato Avatar answered Oct 18 '22 03:10

damato


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
        }
    };
}
like image 1
Vijay Aggarwal Avatar answered Oct 18 '22 03:10

Vijay Aggarwal