Search code examples
springspring-bootoauthfacebook-loginspring-oauth2

Registering User with Facebook Login OAuth for Springboot


I've built a web service that supports login and registration natively through forms on the site. Now, I'd like to supplement that by allowing users to login/signup using a Facebook account. I managed to get the OAuth Login flow for Facebook working, but now I'm trying to figure out the "registration" part of this flow. I know I can add a custom success handler to persist the user information to the database. As a hacky example:

@Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .formLogin {
                it.loginPage("/login")
                it.usernameParameter("email")
                it.passwordParameter("password")
            }
            .oauth2Login {
                it.loginPage("/login")
                it.clientRegistrationRepository(clientRegistrationRepository())
                it.successHandler { _, _, authentication ->
                    if (authentication.isAuthenticated) {
                        userRepository.save(...) // persist registration details
                    }
                }
            }

However, I have a few questions about this approach and ensuring I'm doing this in idiomatic "springboot" way.

Part 1 - Handling Roles

What's the best strategy for handling roles? For users who register directly through the site I assign a default ROLE_USER, but admins may also grant additional roles like: ROLE_EDITOR, etc. When logging in through Facebook, spring is just assigning the authority OAUTH2_USER to the user. I'd like to augment or replace that with the roles the user has in the database. Is it as simple as adding logic in the successHandler to fetch the user info from the datastore and add the roles in the datastore to the principal object?

Part 2 - Handling User IDs

How should I handle generating links on the website so the user can view their profile? Currently I do that like so:

<a sec:authorize="hasRole('USER')" th:href="@{/profile/{id}(id=${#authentication.getPrincipal().id})}">View Profile</a>

However, the OAuth2User does not have an ID. It doesn't seem to be a value I can set either, unlike the user roles. Ideally, I'd like to avoid custom logic in the views and elsewhere to determine if the user is authenticated via OAuth or not.

System Information

  • Kotlin: 2.3.4
  • Springboot: 3.1.3
  • Spring Security: 6.x

Solution

  • Okay here's what I ended up doing.

    1. Define a new OAuth2UserService which implements the OAuth2UserService interface
    2. Define a new user
    3. Update the security configuration to use the new OAuth2UserService

    OAuthUserService

    class OAuth2EmailExistsException(override val message: String): AuthenticationException(message)
    
    
    @Service
    class FacebookOAuth2UserService(
        private val userRepository: UserRepository,
        private val clockService: ClockService,
        private val idService: IdService,
        private val defaultOAuth2UserService: DefaultOAuth2UserService
    ): OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    
        override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
            val oauthUser = defaultOAuth2UserService.loadUser(userRequest)
    
            val id = oauthUser.name
            val email = oauthUser.attributes["email"] as String
            val name = oauthUser.attributes["name"] as String
    
            val persistedUser = userRepository.findByoAuthId(id).getOrElse {
    
                if (userRepository.existsByEmail(email)) {
                    throw OAuth2EmailExistsException("The email associated with this Facebook account is already present in the system")
                }
    
                userRepository.save(User(
                    id = idService.objectId(),
                    firstName = name,
                    lastName = "",
                    password = "",
                    email = email,
                    status = UserStatus.ACTIVE,
                    roles = setOf(SimpleGrantedAuthority(AuthRoles.USER.roleName())),
                    joinDate = clockService.now().toEpochMilli(),
                    timeOfPreviousNameUpdate = 0,
                    oAuthId = id,
                    source = RegistrationSource.FACEBOOK
                ))
            }
    
            return FacebookOAuth2User(persistedUser, oauthUser.attributes)
        }
    }
    

    New OAuth2 User

    class FacebookOAuth2User(
        private val user: User,
        private val attributes: MutableMap<String, Any>
    ): OAuth2User, AuthUserDetails {
    
        val id = user.id
    
        override fun getUserId(): String = user.id
    
        // Facebook serves the name as a single entity, so we'll just store it in the
        // first name column
        override fun getName(): String = user.firstName
    
        override fun getAttributes(): MutableMap<String, Any> = attributes
    
        override fun getAuthorities(): Set<GrantedAuthority> = user.authorities
    
        override fun getPassword(): String = user.password
    
        override fun getUsername(): String = user.oAuthId!!
    
        override fun isAccountNonExpired() = user.isAccountNonLocked
    
        override fun isAccountNonLocked() = user.isAccountNonLocked
    
        override fun isCredentialsNonExpired() = user.isCredentialsNonExpired
    
        override fun isEnabled() = user.isEnabled
    }
    

    OAuth2Configuration

    @Configuration
    class OAuth2Configuration() {
    
        @Bean
        fun defaultOAuth2UserService(): DefaultOAuth2UserService = DefaultOAuth2UserService()
    }
    

    Security Configuration

    @Configuration
    class SecurityConfiguration(
        private val facebookOAuth2UserService: FacebookOAuth2UserService,
        private val environment: Environment
    ) {
        fun clientRegistrationRepository(): ClientRegistrationRepository {
            return InMemoryClientRegistrationRepository(
                CommonOAuth2Provider.FACEBOOK.getBuilder("facebook")
                    .clientId(environment.getRequiredProperty("FACEBOOK_APP_KEY"))
                    .clientSecret(environment.getRequiredProperty("FACEBOOK_APP_SECRET"))
                    .build()
            )
        }
    
        @Bean
        fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
            http
                .formLogin {
                    it.loginPage("/login")
                    it.usernameParameter("email")
                    it.passwordParameter("password")
                }
                .oauth2Login {
                    it.loginPage("/login")
                    it.clientRegistrationRepository(clientRegistrationRepository())
                    it.userInfoEndpoint {
                        it.userService(facebookOAuth2UserService)
                    }
                    it.failureHandler { _, response, exception ->
                        val errorParam = when (exception) {
                            is OAuth2EmailExistsException -> "oauthEmailExists"
                            else -> "oauthError"
                        }
                        response.sendRedirect("/login?$errorParam")
                    }
                }
    ...