How To Build an OAuth 2 Client with Spring Boot 3

Posted on Sep 19, 2023 in
Reading time: 6 minutes

Intro

Hey friends! This is a jam-packed tutorial about using Spring Boot 3, the new RestClient, and the OAuth 2 client, to connect to the YouTube API. We built an application that updates the title of a video to always* reflect the current number of views. The app is even doing this in the background, so we have to extract the access token, which otherwise would only be available during the lifetime of a request!

  • well.. not always, as we will see, due to API limitations

🍿 Watch on YouTube or get the code from GitHub

Setup

We must create a new project in the Google Cloud Console to access the YouTube API. This is because the calls require additional permissions, which we can only get by using OAuth 2.

In the Google Cloud Console, we must enable the YouTube Data API before using it. Once done, we need to create a new credential. This requires filling in some data on the OAuth consent screen, such as name, email addresses, and scopes - which we leave empty for now. We are building a web application and must provide an authorized redirect URI. Since the app is running locally, we configure it to http://localhost:8080/login/oauth2/code/google.

Once done, we download the credentials file that includes the client ID and the client secret, among other things.

With these details, we can configure the OAuth client.

Let’s Code

The Spring OAuth2 client has a preset for Google, so we don’t have to provide any URLs (for now). However, we must extend the scope to access the YouTube API, so we add the scope https://www.googleapis.com/auth/youtube to our config.

spring:
security:
oauth2:
client:
registration:
google:
clientId: google-client-id
clientSecret: google-client-secret
scope: openid,profile,email,https://www.googleapis.com/auth/youtube

If we start the app now and access any arbitrary endpoint, we will be prompted to authenticate using Google. So, the whole flow is already working!

To let the application access our account later on, in the background, we need to find out our Google account ID. This is the principal’s name that is part of the security context. So we can add a simple controller and inject the authentication so we can read the ID. We take note of it since we will need it later.

 ...
package dev.axgr
 
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
 
@RestController
class AuthController {
 
@GetMapping("/me")
fun me(): String {
val auth = SecurityContextHolder
.getContext()
.authentication as? OAuth2AuthenticationToken
 
return auth?.name ?: "anonymous"
}
}

The authentication is only available as part of the current request. When the application is running in the background, there is no request. We will work around this in a bit and need the principal name for this.

Let’s build the YouTube service component that is performing the actual requests. It has a function to fetch the details of a single video and a function to update a single video.

The HTTP calls are performed by the Spring RestClient, which we first have to add to the Spring context to use it.

Below is our configuration.

 ...
package dev.axgr
 
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestClient
 
@ConfigurationProperties("youtube")
data class YouTubeProperties(val url: String, val video: String, val title: String, val principal: String)
 
@Configuration
@EnableConfigurationProperties(YouTubeProperties::class)
class YouTubeConfig(private val props: YouTubeProperties) {
 
@Bean
fun client(builder: RestClient.Builder): RestClient {
return builder
.baseUrl(props.url)
.build()
}
}

We add custom properties for the video we want to update, the title (a String template), and the principal name we extracted earlier.

The RestClient is now available, and this is what the calls look like:

 ...
package dev.axgr
 
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
 
@Service
class YouTube(private val client: RestClient) {
 
fun details(id: String): Video? {
val response = client.get()
.uri {
it
.path("/videos")
.queryParam("id", id)
.queryParam("part", "snippet,contentDetails,statistics")
.build()
}
.retrieve()
.body(VideoListResponse::class.java)
 
return response?.items?.firstOrNull()
}
 
fun update(video: Video, title: String) {
val request = VideoUpdateRequest(video.id, title, video.category)
 
client.put()
.uri {
it
.path("/videos")
.queryParam("part", "snippet,status,localizations")
.build()
}
.body(request)
.retrieve()
.toBodilessEntity()
}
}

The YouTube API returns a list of videos, so we extract the first result or return null if there is no result. The update function is a PUT call - note that this requires sending all relevant properties back to the API. We will only update the title in our current implementation, so this is fine. If you also wanted to preserve descriptions, tags, etc., these must also be passed.

Now that the integration is ready, we must provide the RestClient with an access token. This is best done using a request interceptor invoked around each HTTP request. This allows us to inspect or manipulate the request and the response.

 ...
package dev.axgr
 
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService
import org.springframework.stereotype.Component
 
@Component
class OAuth2TokenProvider(private val service: OAuth2AuthorizedClientService, val props: YouTubeProperties) : ClientHttpRequestInterceptor {
 
override fun intercept(request: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
val token = token()
 
if (token != null) {
request.headers.add(HttpHeaders.AUTHORIZATION, "Bearer $token")
return execution.execute(request, body)
}
 
throw IllegalStateException("No OAuth2 authentication found.")
}
 
private fun token(): String? {
val client: OAuth2AuthorizedClient? = service.loadAuthorizedClient("google", props.principal)
return client?.accessToken?.tokenValue
}
}

The authorized OAuth client is stored in a service, and we can get it by specifying the provider name, Google in our case, and the principal for whom we want the credentials - which happens to be the ID we noted earlier.

This gives us access to the access token and the interceptor can set the header accordingly.

Time to add the background job! We need to enable scheduling and can have a job that is performing our updates.

It would be nice to have the title being updated in real-time, but there is a catch. Reading a video costs 1 unit, updating a video costs 10, and a project has a limit of 10k units per day.

Reading and updating costs us 51 units, and doing this every second would exceed the limit in the first hour. Instead, we will update it every 10 minutes, which gives us:

6 * 51 * 24 = 7344 units/day

So we should be safe.

We can further optimize that by adding a simple caching variable: if the views haven’t changed since the last run, we don’t invoke the update call and save the 50 units. Win!

Now, we can start the app and authenticate at least once. Afterward, the background job runs the updates!

Offline Access

Here is the thing: the access tokens are short-lived, as they usually are. But we can request a refresh token from Google to mint new access tokens ourselves. This requires to pass an additional scope to YouTube:

spring:
security:
oauth2:
client:
 ...
registration:
google:
clientId: google-client-id
clientSecret: google-client-secret
scope: openid,profile,email,https://www.googleapis.com/auth/youtube
 
provider:
google:
authorizationUri: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent

Essentially, we are overriding the authorization URI to request offline access. This way, we will get a refresh token back from the API, which we can access using the client service.

With the refresh token we can request a new access token (i.e., whenever the current one has expired) by posting a request to their API:

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
 
client_id=your_client_id&
client_secret=your_client_secret&
refresh_token=refresh_token&
grant_type=refresh_token

With that in place the app is smoothly running in the background without further intervention.

The Dead Letter Queue

Love what you're seeing? By subscribing to my newsletter, not only will you be the first to know about fresh tutorials and videos, but you'll also unlock:

Subscribe now and become a part of our growing tech tribe!