Search code examples
springspring-bootspring-security-oauth2spring-oauth2

Successful Spring OAuth2 login with empty authorities


I implemented the login of my Spring Boot web app using OAuth2 and everything works fine. The only problem is that the logged in user does not has the authorities information saved inside the session so each time I request a url and the controller has the annotation @PreAuthorize("hasRole('USER')") I get rejected.

SecurityConfiguration class:

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@EnableJpaRepositories(basePackageClasses = UserRepository.class)
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .formLogin()
                    .loginPage("/login")
                    .failureUrl("/login?error=true")
                    .and()
                .logout()
                    .logoutSuccessUrl("/")
                    .deleteCookies("JSESSIONID")
                    .invalidateHttpSession(true)
                .and()
                .oauth2Login()
                    .loginPage("/login")
                    .failureUrl("/login?error=true")
                .userInfoEndpoint()
                    .userService(customOAuth2UserService)
                .and()
                .failureHandler(oAuth2AuthenticationFailureHandler);
    }


    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

This is the CustomOAuth2UserService class:

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserService userService;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);

        try {
            return processOAuth2User(oAuth2UserRequest, oAuth2User);
        }catch (Exception ex) {
            // Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
        }
    }

    private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());
        if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
            throw new RuntimeException("Id not found from OAuth2 provider");
        }

        User user;
        try {
            user = userService.getByEmail(oAuth2UserInfo.getEmail());
            if(!user.getProvider().toString().equalsIgnoreCase(oAuth2UserRequest.getClientRegistration().getRegistrationId())) throw new EmailAlreadyTakenException("email-already-taken");
        } catch (UserNotFoundException e) {
            user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
        }

        return new CustomUserDetails(user);
    }

    private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
        User user = new User();
        user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()));
        Identity identity = new Identity(user);
        if(oAuth2UserInfo.getFirstName() != null && !oAuth2UserInfo.getFirstName().equalsIgnoreCase(""))
            identity.setFirstName(oAuth2UserInfo.getFirstName());
        if(oAuth2UserInfo.getLastName() != null && !oAuth2UserInfo.getLastName().equalsIgnoreCase(""))
            identity.setSecondName(oAuth2UserInfo.getLastName());
        user.setIdentity(identity);
        user.setEmail(oAuth2UserInfo.getEmail());
        user.setConfirmedRegistration(true);
        boolean flag = false;
        String username = oAuth2UserInfo.getName().toLowerCase().replaceAll("\\s+", "");
        user.setUsername(username);
        return userService.addFacebookUser(user);

    }

}

This a part of the application.properties file:

spring.security.oauth2.client.registration.facebook.client-id=***
spring.security.oauth2.client.registration.facebook.client-secret=***
spring.security.oauth2.client.registration.facebook.scope=email,public_profile

spring.security.oauth2.client.registration.google.client-id=***
spring.security.oauth2.client.registration.google.client-secret=***
spring.security.oauth2.client.registration.google.scope=email,profile


spring.security.oauth2.client.provider.facebook.authorizationUri = https://www.facebook.com/v3.0/dialog/oauth
spring.security.oauth2.client.provider.facebook.tokenUri = https://graph.facebook.com/v3.0/oauth/access_token
spring.security.oauth2.client.provider.facebook.userInfoUri = https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture

Once logged in the user can call this url /users/{username} but when he login with facebook or google through OAuth2, he gets rejected because the authorities list is empty. When he login with his webapp credential, the authorities list contains USER_ROLE and he is allowed to procede.

@PreAuthorize("hasRole('USER')")
    @GetRequest("users/{username}")
    public String getUser(@PathVariable String username, @PathVariable String subsection, Model model, Principal principal) throws IllegalAccessException, UserNotFoundException {
        User user = userService.getByUsername(principal.getName());
        model.addAttribute("user", user);
        return "user";
    }

Inside principal object there are:

When logged in with OAuth2:

  • principal: type CustomUserDetails (user information)
  • authorizedClientRegistrationId: type String ("google", "facebook")
  • authorities: type Collections$UnmodifiableRandomAccessList (empty)
  • details: null
  • authenticated: type boolean (true)

When logged in with local credentials:

  • principal: type CustomUserDetails (user information)
  • credentials: null
  • authorities: type Collections$UnmodifiableRandomAccessList
    • index:0 type SimpleGrantedAuthority ("USER_ROLE")
  • details: type WebAuthenticationDetails (remote address, sessionId)
  • authenticated: type boolean (true)

Solution

  • After some time of debugging I found the solution! I was not configuring correctly the roles of my user. Inside the registerNewUser method of my custom OAuth2UserService I wasn't setting the Role of the User. I just added the line:

    user.setRoles(new HashSet<>(Collections.singletonList(new Role("ROLE_USER"))));
    

    and everything started to work! So now when the OAuth2User's authorities get asked, it just calls the getAuthorities of CustomUserDetails (my implementation of OAuth2User) and it calls the getRoles method of the User.

    CustomUserDetails class:

    public class CustomUserDetails extends User implements UserDetails, OAuth2User {
    
        public CustomUserDetails() {
        }
    
        public CustomUserDetails(String username, String email, String password, Set<Role> roles) {
            super(username, email, password, roles);
        }
    
        public CustomUserDetails(User user) {
            super(user.getUsername(), user.getEmail(), user.getPassword(), user.getRoles());
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return getRoles()
                    .stream()
                    .map(role -> new SimpleGrantedAuthority(role.getRole()))
                    .collect(Collectors.toList());
        }
    
        @Override
        public Map<String, Object> getAttributes() {
            return null;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    
        @Override
        public String getName() {
            return null;
        }
    }