I am working on an API service that is meant to do the following:
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.
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)
}