Sitemap

Integrating multiple OAuth flows into your next SpringBoot project

9 min readJun 30, 2024
Press enter or click to view image in full size
OAuth2 visualisation

Summary

This article demonstrates how to integrate multiple OAuth-Clients into a Spring-Boot backend joining data from multiple protected resources of a user.

When it comes to interacting with third party resources and data online within your own application, many services these days are utilising OAuth to guard their interfaces.

In our scenario we are facing the special challenge of having to combine our user data from multiple sources each secured via OAuth.

Since we are already using parts of the Spring framework in that project, we were wondering whether to use Spring’s convenient and lightweight OAuth2 library for managing data from multiple accounts relatively quickly and easily.

The sample backend application is written in Kotlin and uses Spring Boot, Spring Security, Spring Oauth2 and Spring WebFlux. It will be used together with a Typescript/React frontend. This article focusses on the backend part since the frontend will have very little to do with the whole authentication process except checking if the backend returns expected data and providing user access to login pages.

1. App context

Imagine wanting to display data from different protected sources in a User Interface. In this article we want to create a Dashboard that displays information from a users GitHub and Microsoft account.

There are several ways of tackling this.

For our project, we are using a React frontend and a Spring Boot backend.

For more mature projects in a distributed systems architecture you might want to manage access via a dedicated authorisation server, especially if creating and managing own users becomes a requirement in the future. We evaluated Keycloak for that scenario, more on our research on that will follow in a separate article. For early stages such as PoC or for experimental purposes you might want to implement a lightweight solution reducing infrastructure effort, which was the case in our scenario.

There are several ways of implementing the OAuth2-Protocol yourself, client-side, using a Backend as a proxy server to hide sensitive client-credentials, and possibly more.

However, implementing secure (multi)-SSO yourself is pretty error-prone and challenging especially if one is no expert when it comes to security.

Looking for a middle-ground between a separate authorisation server and a quick (and dirty) in-house development, we found the following arguments for shifting the core-responsibility for authentication and authorisation into the backend using Spring OAuth2-Clients:

  • Hiding sensitive data: We need access tokens to retrieve data from our protected resources. With Spring OAuth2-Clients, these tokens are tracked and maintained within a spring session and never exchanged with the frontend. Client-credentials needed to configure an OAuth-Client can be handled by the backend as well. (Note: This configuration can also be used as a BFF or Authorization Server sitting between a client and a resource server)
  • Application flow: The number of redirects/state changes a frontend has to handle during authentication flow is reduced when handling the flow itself in the backend.
  • Best-practice: Under the hood, Spring Security and Spring OAuth2 use the best practises for creating and validating all steps in the OAuth-workflow such as validating integrity using the state-parameter.
  • Ready to use: everything you need is already there, common use cases are quickly configured with very little code — and effort.

2. Setup

Start with initialising a standard Spring-Boot application. We will use Gradle as our build tool.

2.1 Dependencies

We need the following dependencies:

//Kotlin
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
//Spring
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

We use Spring WebFlux to provide a reactive, non-blocking API. Spring Security integrates and enables security functionality for us. Spring OAuth2-Client is a powerful framework for easily integrating OAuth2-Clients.

3. Spring Security Configuration

We will need to overwrite some of Spring Security’s auto-configuration. First, we create a configuration class:

@Configuration 
@EnableWebFluxSecurity
class OAuth2LoginConfig(
private val authConfig: AuthConfig)

AuthConfig is a data class that maps values from environment variables/default dev properties defined in the application.yaml. We will define credentials such as client-id and client-secret there (do not hardcode or commit sensitive credentials to version control).

4.1 Configuring the OAuth Clients

Spring OAuth2 comes with some pre-defined configurations for some well-known services: GitHub, Google, Facebook and Okta. These configurations hold some pre-defined values such as the client’s authorisation url. Since GitHub is one of these, all we have to do is apply the client-ids-and secrets we obtained when registering our application.

private fun githubClientRegistration(): ClientRegistration { 
return CommonOAuth2Provider.GITHUB.getBuilder(GITHUB)
.clientId(authConfig.githubClientId)
.clientSecret(authConfig.githubClientSecret)
.userNameAttributeName("id")
.scope("read:user")
.build()
}

For Azure Active Directory, we need to do some more configuration:

private fun aadClientRegistration(): ClientRegistration { 
val tenantId = authConfig.aadTenantId
return ClientRegistration.withRegistrationId(AZURE_ACTIVE_DIRECTORY)
.clientId(authConfig.aadClientId)
.clientSecret(authConfig.aadClientSecret)
.clientAuthenticationMethod(CLIENT_SECRET_BASIC)
.authorizationGrantType(AUTHORIZATION_CODE)
.authorizationUri("https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize")
.tokenUri("https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token")
.issuerUri("https://login.microsoftonline.com/$tenantId/v2.0")
.jwkSetUri("https://login.microsoftonline.com/$tenantId/discovery/keys")
.userInfoUri("https://graph.microsoft.com/oidc/userinfo")
.userNameAttributeName("sub")
.clientName("Microsoft")
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.scope("openid", "offline_access", "user.read")
.build()
}

Note that some properties are imported from a config-object:

object Config { 
const val GITHUB = "github"
const val AZURE_ACTIVE_DIRECTORY = "aad"
val CLIENT_REGISTRATION_ID_SET = setOf(GITHUB, AZURE_ACTIVE_DIRECTORY)
const val LOGOUT_URL = "/logout"
}

4.2 Providing the ClientRegistrations

To make our clients available we need to configure our own ClientRegistrationRepository-Bean:

@Bean 
fun clientRegistrationRepository(): ReactiveClientRegistrationRepository {
return InMemoryReactiveClientRegistrationRepository(githubClientRegistration(), aadClientRegistration())
}

4.3 Configuring ServerHttpSecurity

Now we need to make some adjustments to our Spring Security http-configuration.

For readability, I moved the different configuration parts into Kotlin extension functions.

@Bean 
fun httpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.configureAuthorizeExchanges()
.configureOAuth2Login(authConfig)
.configureSecurityContextRepository()
.configureCsrf()
.configureCsrfTokenFilter()
.configureLogout(authConfig)
return http.build()
}

authorizeExchanges

fun ServerHttpSecurity.configureAuthorizeExchanges(): ServerHttpSecurity { 
return this.authorizeExchange { exchanges ->
exchanges
.pathMatchers("/login**", "/logout**")
.permitAll()
.anyExchange()
.authenticated()
}
}

here we tell Spring Security that any request to our server except login/logout requests needs to be authenticated.

OAuth2Login

fun ServerHttpSecurity.configureOAuth2Login(authConfig: AuthConfig): ServerHttpSecurity { 
return this
.oauth2Login { oauth2Login: ServerHttpSecurity.OAuth2LoginSpec ->
oauth2Login
.authenticationSuccessHandler(RedirectToClientAuthSuccessHandler(authConfig))
.authenticationFailureHandler(RedirectServerAuthenticationFailureHandler(authConfig.authFailureUrl))
}.exceptionHandling { exceptions ->
exceptions
.authenticationEntryPoint(HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
}
}

this tells Spring Security that an OAuth2 user-login is the method for gaining access to our protected endpoints. We will also configure a custom SuccessHandler that performs a simple redirect to a confirmation page in the frontend and a corresponding FailureHandler.

SuccessHandler

class RedirectToClientAuthSuccessHandler( 
private val authConfig: AuthConfig
) : RedirectServerAuthenticationSuccessHandler() {
override fun onAuthenticationSuccess(webFilterExchange: WebFilterExchange, authentication: Authentication): Mono<Void> {
setRedirectLocation(webFilterExchange)
return super.onAuthenticationSuccess(webFilterExchange, authentication)
}

private fun setRedirectLocation(webFilterExchange: WebFilterExchange) {
val path = webFilterExchange.exchange.request.path.value()
try {
val clientRegistrationId = path.split("/").last()
val clientRedirectUrl = clientRedirectUrl(clientRegistrationId)
this.setLocation(URI.create(clientRedirectUrl))
} catch (e: Exception) {
this.setLocation(URI.create(authConfig.authFailureUrl))
}
}

private fun clientRedirectUrl(clientRegistrationId: String): String {
return when (clientRegistrationId) {
GITHUB -> authConfig.githubRedirectUrl
AZURE_ACTIVE_DIRECTORY -> authConfig.aadRedirectUrl
else -> throw IllegalStateException("onAuthenticationSuccess triggered with unsupported Client-Registration")
}
}

We know that our login-urls end with the clientRegistrationId which helps us to identify where to redirect to depending on the client.

The failure handler is just a simple RedirectServerAuthenticationFailureHandler

SecurityContextRepository

fun ServerHttpSecurity.configureSecurityContextRepository(): ServerHttpSecurity { 
return this
.securityContextRepository(MultiActiveAccountSecurityContextRepository())
}

The SecurityContextRepository is responsible for storing/loading SecurityContexts that hold information about an authenticated user from a WebSession (when using Spring WebFlux).

Spring stores the SecurityContext associated by a key.

When managing multiple active authentications and therefore multiple SecurityContexts, that key needs to be dynamically changed depending on the expected authentication for a request to prevent SecurityContexts from overriding one another when the same static default key is used.

We need to adjust some things here and implement a customized WebSessionServerSecurityContextRepository.

class MultiActiveAccountSecurityContextRepository : WebSessionServerSecurityContextRepository() { 

private var cacheSecurityContext = true

override fun setCacheSecurityContext(cacheSecurityContext: Boolean) {
this.cacheSecurityContext = cacheSecurityContext
}

override fun save(exchange: ServerWebExchange, context: SecurityContext?): Mono<Void> {
val clientSecurityContextKey = clientSecurityContextKey(exchange)
return exchange.session.flatMap { session: WebSession ->
if (context == null) {
session.attributes.remove(clientSecurityContextKey)
} else {
session.attributes[clientSecurityContextKey] = context
}
session.changeSessionId()
}
}

override fun load(exchange: ServerWebExchange): Mono<SecurityContext> {
val clientSecurityContextKey = clientSecurityContextKey(exchange)
val result = exchange.session.flatMap { session: WebSession ->
val context = session.attributes[clientSecurityContextKey] as SecurityContext?
Mono.justOrEmpty(context)
}
return if (cacheSecurityContext) result.cache() else result
}

private fun clientSecurityContextKey(exchange: ServerWebExchange): String {
val path = exchange.request.path.value()
return if (path.contains(LOGOUT_URL)) {
val key = extractFromQueryParam(exchange)
clientSecurityContextKey(key)
} else {
val key = extractClientKeyFromPath(exchange)
clientSecurityContextKey(key)
}
}

private fun extractClientKeyFromPath(exchange: ServerWebExchange): String? {
val path = exchange.request.path
return path.elements()
.map { it.value() }
.firstOrNull { CLIENT_REGISTRATION_ID_SET.contains(it) }
}

private fun extractFromQueryParam(exchange: ServerWebExchange): String {
val query = exchange.request.uri.query.split("=")
Assert.isTrue(query.size == 2, "Malformed Logout request. Single client key param expected.")
return query.last()
}
private fun clientSecurityContextKey(key: String?) = "${DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME}_${key?.uppercase()}"
}

CSRF protection

With Spring Security using sessions to manage accounts, we are now vulnerable to CSRF-Attacks. We will need to provide a _csrf token in the header of every POST/(PUT/DELETE)-request.

fun ServerHttpSecurity.configureCsrf(): ServerHttpSecurity { 
return this.csrf { csrf: ServerHttpSecurity.CsrfSpec ->
val delegate = XorServerCsrfTokenRequestAttributeHandler()
val repository = CookieServerCsrfTokenRepository()
repository.setCookieCustomizer {
it.sameSite(Cookie.SameSite.STRICT.attributeValue())
}
csrf
.csrfTokenRepository(repository)
.csrfTokenRequestHandler(delegate::handle)
}
}

Since we are using Spring WebFlux, we need to subscribe to the CSRF-Token Spring creates by default. To do so, we use a WebFilter that does basically just that so that the Token gets stored in a cookie.

fun ServerHttpSecurity.configureCsrfTokenFilter(): ServerHttpSecurity { 
return this
.addFilterAfter(AttachCsrfTokenFilter(), SecurityWebFiltersOrder.CSRF)
}
class AttachCsrfTokenFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
@Suppress("UNCHECKED_CAST")
val token: Mono<CsrfToken>? = exchange.attributes[CsrfToken::class.java.getName()] as Mono<CsrfToken>?
return token?.flatMap { chain.filter(exchange) } ?: chain.filter(exchange)
}
}

CORS configuration

To enable resource sharing between our frontend and backend, we need to add a server-side CORS-configuration:

@Bean 
fun corsConfigurationSource(config: AuthConfig): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = mutableListOf(config.corsAllowedOrigin)
configuration.allowedHeaders = mutableListOf("*")
configuration.setAllowedMethods(mutableListOf("GET", "PUT", "POST"))
configuration.exposedHeaders = mutableListOf("Set-Cookie")
configuration.allowCredentials = true
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}

The corsAllowedOrigin has to match the frontend base-url.

Logout

We customise very little about Springs default logout flow, only specifying our custom logout-url

fun ServerHttpSecurity.configureLogout(authConfig: AuthConfig): ServerHttpSecurity { 
return this.logout { logout: ServerHttpSecurity.LogoutSpec ->
val handler = RedirectServerLogoutSuccessHandler()
val logoutSuccessUrl = authConfig.logoutSuccessUrl
handler.setLogoutSuccessUrl(URI.create(logoutSuccessUrl))
logout.logoutSuccessHandler(handler)
}
}

4.4 Login Endpoints

For our use case, we want to login to GitHub and Microsoft at certain points in the application workflow. When the frontend needs data from a particular API, it will present a corresponding login-button to the user if it fails to obtain data from a check endpoint.

All we have to do here is to define a link to (since we did not customize that) Spring OAuths default login url:

<button onClick={openGithubLogin}>Login to GitHub</button>
export const openGithubLogin = async () => {
window.location.assign(githubLoginUrl);
}
export const githubLoginUrl='http://localhost:8080/oauth2/authorization/github'

4.5 Accessing Protected Resources

A quick recap:

We have configured our server to enable users to login into multiple accounts within one session. This is a simplified visualisation of what happens under the hood:

Press enter or click to view image in full size
Application OAuth flow diagram

At this point, one ore more active accounts can be associated with one user’s session. So how do we get access to an authenticated users access token to pass it to a WebClient?

This step is very simple. In our controllers, we can define expected active accounts with autowiring a Bean called OAuth2AuthorizedClient together with an annotation where we specify the key of the client. On resolving the client to an argument parameter, Spring also checks the token lifespan and renews it if refresh tokens are supported and provided by the 3rd-party-app.

@GetMapping("/foo") 
fun foo(@RegisteredOAuth2AuthorizedClient("aad") microsoft: OAuth2AuthorizedClient,
@RegisteredOAuth2AuthorizedClient("github") github: OAuth2AuthorizedClient
): Mono<MyResponse> {
val msAccessToken = microsoft.accessToken.tokenValue
val ghAccessToken = github.accessToken.tokenValue
return myService.getsomething(msAccessToken, ghAccessToken)
}

And that’s it. We are now ready to integrate the data of multiple products that support OAuth on behalf of a user. This is just one possibility to accomplish our goal, but when we look at our codebase, we find the auth-process nicely encapsulated with minimal interference with the rest of our application codebase. We also do not have to worry about having to refresh our access tokens since Spring can take care of that as well.

5. Next steps

As our journey continues, we get introduced to new requirements. To meet them, we will have to store data created by our application. Linking external accounts will become an optional feature and we will have to be able to create and manage our own users as well as let these users integrate various 3rd-party accounts. We will have a look at the changes we are going to make using this setup as a base in a future article.

A sample application implementing the contents of this article can be found on GitHub including a sample snippet for the frontend implementation.

--

--

Plexify GmbH
Plexify GmbH

Written by Plexify GmbH

We offer sophisticated Software Development Services and personell with Java, JavaScript, Golang and Python on the Google Cloud Platform. www.plexify.io

Responses (1)