Search code examples
springspring-bootkotlinoauth-2.0openid

How can I use a third-party Oauth2 provider to authenticate requests to my ResourceServer


I am working on an API service that is meant to do the following:

  1. Allow users to sign in via Google.
  2. Create the user in the database based on the information retrieved.
  3. Provide the user with a JWT token to be used for authentication so that requests are uniquely identified with said user.
  4. Allow the user to be able to use the obtained token to perform API requests against my service.
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}

I am unsure how can I go about this and what exactly do I need. So far I have the following

Main Application class:

@SpringBootApplication
@EnableWebSecurity
@Configuration
class ApiServiceApplication {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http.authorizeHttpRequests {
            it.antMatchers("/", "/login", "/error", "/webjars/**").permitAll().anyRequest().authenticated()
        }
            .logout {
                it.logoutSuccessUrl("/").permitAll()
            }
            .exceptionHandling {
                it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
            }
            .oauth2Login { oauth2Login ->
                oauth2Login.loginPage("/login")
                oauth2Login.defaultSuccessUrl("/user", true)
            }
            .oauth2Client { oauth2Client -> }
            .csrf {
                it.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            }
        return http.build()
    }
}

fun main(args: Array<String>) {
    runApplication<ApiServiceApplication>(*args)
}

User Service class for saving the user to the DB

@RestController
class UserService : OidcUserService() {

    @Autowired
    lateinit var userRepository: UserRepository

    @Autowired
    lateinit var loginRepository: LoginRepository

    private val oauth2UserService = DefaultOAuth2UserService()

    @GetMapping("/login")
    fun authenticate(): RedirectView {
        return RedirectView("/oauth2/authorization/google")
    }

    override fun loadUser(userRequest: OidcUserRequest?): OidcUser {
        val loadedUser = oauth2UserService.loadUser(userRequest)
        val username = loadedUser.attributes["email"] as String
        var user = userRepository.findByUsername(username)
        if (user == null) {
            user = OauthUser()
            user.username = username
        }

        loadedUser.attributes.forEach { loadedAttribute ->
            val userAttribute = user.oauthAttributes.find { loadedAttribute.key == it.attributeKey && it.active }
            val newAttribute = OauthAttribute(loadedAttribute.key, loadedAttribute.value?.toString())
            if(userAttribute == null){
                user.oauthAttributes.add(newAttribute)
            }
            else if(userAttribute.attributeValue != loadedAttribute.value?.toString()){
                userAttribute.active = false
                user.oauthAttributes.add(newAttribute)
            }
        }
        user.oauthAuthorities = loadedUser.authorities.map { OauthAuthority(it.authority) }.toMutableList()
        user.oauthToken = OauthToken(
            userRequest?.accessToken?.tokenValue!!,
            Date.from(userRequest.accessToken.issuedAt),
            Date.from(userRequest.accessToken.expiresAt)
        )
        userRepository.save(user)
        val login = Login(user)
        loginRepository.save(login)
        return user
    }
}

I am not providing the data classes and corresponding repositories because what's above works fine - upon accessing the /login endpoint, the user is redirected to Google where after authentication the user is saved in the database along with the corresponding information.

My main issue is that I am not really sure how to go about authenticating each request. I've tried to provide an authentication Bearer in Postman that is the same as the one obtained from Google in the loadUser method, but I'm getting back 401 unauthorized codes. When I access the server through the browser and I authenticate I can access all the endpoints just fine, but I'm guessing that it's just my session that is authenticated.


Solution

  • I have managed to achieve what I wanted by doing the following:

    Adding a resource server definition to my spring.security.oauth2 configuration:

    spring:
      security:
        oauth2:
          client:
            registration:
              google:
                client-id: ${GOOGLE_CLIENT_ID}
                client-secret: ${GOOGLE_CLIENT_SECRET}
          resourceserver:
            jwt:
              issuer-uri: https://accounts.google.com
              jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
    

    Adding the OAuth2ResourceServerConfigurer and specifying the default JwtConfigurer via .oauth2ResourceServer().jwt(), and specifying the authorization matches for the path I want to be secured by JWT. I've also split the filter chains, thanks to the comment from ch4mp, so that only /api endpoint is secured via JWT:

    @Bean
    @Order(HIGHEST_PRECEDENCE)
    fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.antMatcher("/api/**").authorizeRequests { authorize ->
            authorize.antMatchers("/api/**").authenticated()
        }.exceptionHandling {
            it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
        }
            .csrf().disable()
            .oauth2ResourceServer().jwt()
        return http.build()
    }
    
    @Bean
    fun uiFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.authorizeRequests { authorize ->
            authorize.antMatchers("/", "/login", "/error", "/webjars/**").permitAll().anyRequest()
                .authenticated()
        }.logout {
            it.logoutSuccessUrl("/").permitAll()
        }.exceptionHandling {
            it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
        }.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .oauth2Login { oauth2Login ->
                oauth2Login.loginPage("/login")
                oauth2Login.defaultSuccessUrl("/", true)
            }.oauth2Client()
        return http.build()
    }
    

    Now, in the method mapped to the path I can do some more specific authentication logic:

    @GetMapping("/api/securedByJWT")
    fun getResponse(@AuthenticationPrincipal jwt: Jwt): ResponseEntity<String> {
        val email = jwt.claims["email"] as String
        val oauthUser = userRepository.findByUsername(email)
        if(oauthUser == null){
            return ResponseEntity("User not registered.", UNAUTHORIZED)
        }
        return ResponseEntity("Hello world!", HttpStatus.OK)
    }