Ktor 3.4.2 Help

Bearer authentication in Ktor Client

Bearer authentication uses security tokens called bearer tokens. These tokens are commonly used in OAuth 2.0 flows to authorize users through external providers, such as Google, Facebook, and X.

You can learn more about the OAuth process in the OAuth authorization flow section of the Ktor server documentation.

Add dependencies

To enable authentication, include the ktor-client-auth artifact in the build script:

implementation("io.ktor:ktor-client-auth:$ktor_version")
implementation "io.ktor:ktor-client-auth:$ktor_version"
<dependency> <groupId>io.ktor</groupId> <artifactId>ktor-client-auth-jvm</artifactId> <version>${ktor_version}</version> </dependency>

Configure bearer authentication

A Ktor client allows you to send a token in the Authorization header using the Bearer scheme. You can also define logic that refreshes tokens when they expire.

To configure bearer authentication, install the Auth plugin and configure the bearer provider:

import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.auth.* //... val client = HttpClient(CIO) { install(Auth) { bearer { // Configure bearer authentication } } }

Load tokens

Use the loadTokens {} callback to provide the initial access and refresh tokens. Typically, this callback loads cached tokens from local storage and returns them as a BearerTokens instance.

install(Auth) { bearer { loadTokens { // Load tokens from a local storage and return them as the 'BearerTokens' instance BearerTokens("abc123", "xyz111") } } }

In this example, the client sends the abc123 access token in the Authorization header:

GET http://localhost:8080/ Authorization: Bearer abc123

Refresh tokens

Use the refreshTokens {} callback to define how the client obtains new tokens when the current access token becomes invalid:

install(Auth) { bearer { // Load tokens ... refreshTokens { // this: RefreshTokensParams // Refresh tokens and return them as the 'BearerTokens' instance BearerTokens("def456", "xyz111") } } }

The refresh process works as follows:

  1. The client makes a request to a protected resource using an invalid access token.

  2. The resource server returns a 401 Unauthorized response.

  3. The client automatically invokes the refreshTokens {} callback to obtain new tokens.

  4. The client retries the request to a protected resource using the new token.

When multiple requests fail with 401 Unauthorized at the same time, the client performs the token refresh only once. The first request that receives the 401 response triggers the refreshTokens {} callback. Other requests wait for the refresh operation to complete and are then retried with the new token.

Send credentials without waiting for 401

By default, the client sends credentials only after receiving a 401 Unauthorized response.

You can override this behavior using the sendWithoutRequest {} callback function. This callback determines whether the client should attach credentials before sending the request.

For example, the following configuration always sends the token when accessing Google APIs:

install(Auth) { bearer { // Load and refresh tokens ... sendWithoutRequest { request -> request.url.host == "www.googleapis.com" } } }

Cache tokens

Use the cacheTokens property to control whether bearer tokens are cached between requests.

If caching is disabled, the client calls the loadTokens {} function for every request:

install(Auth) { bearer { cacheTokens = false // Reloads tokens for every request loadTokens { loadDynamicTokens() } } }

Disabling caching can be useful when tokens change frequently.

Example: Using Bearer authentication to access Google API

This example demonstrates how to use bearer authentication with Google APIs, which use the OAuth 2.0 protocol for authentication and authorization.

The example application client-auth-oauth-google retrieves the user's Google profile information.

Obtain client credentials

To access Google APIs, you first need to obtain OAuth client credentials:

  1. Create or sign in to a Google account.

  2. Open the Google Cloud Console

  3. Create an OAuth client ID with the Android application type. You will use this client ID to obtain an authorization grant.

OAuth authorization flow

The OAuth authorization flow consists of the following steps:

  1. The client sends an authorization request to the resource owner.

  2. The resource owner returns an authorization code.

  3. The client sends the authorization code to the authorization server.

  4. The authorization server returns access and refresh tokens.

  5. The client sends a request to the resource server using the access token.

  6. The resource server returns the protected resource.

  7. After the access token expires, the client sends a request with the expired token.

  8. The resource server responds with 401 Unauthorized.

  9. The client sends the refresh token to the authorization server.

  10. The authorization server returns new access and refresh tokens.

  11. The client sends a new request to the resource server using the new access token.

  12. The resource server returns the protected resource.

The following sections explain how the Ktor client implements each step.

Authorization request

First, construct the authorization URL used to request the necessary permissions. This is done by appending the required query parameters:

val authorizationUrlQuery = parameters { append("client_id", System.getenv("GOOGLE_CLIENT_ID")) append("scope", "https://www.googleapis.com/auth/userinfo.profile") append("response_type", "code") append("redirect_uri", "http://127.0.0.1:8080") append("access_type", "offline") }.formUrlEncode() println("https://accounts.google.com/o/oauth2/auth?$authorizationUrlQuery") println("Open a link above, get the authorization code, insert it below, and press Enter.")
  • client_id: the OAuth client ID used to access the Google APIs.

  • scope: the permissions requested by the application. In this case, it is information about a user's profile.

  • response_type: a grant type used to get an access token. Set to "code" to obtain an authorization code.

  • redirect_uri: the http://127.0.0.1:8080 value indicates that the Loopback IP address flow is used to get the authorization code.

  • access_type: set to offline so that the application can refresh access tokens when the user is not present at the browser.

Authorization grant (code)

After granting access, the browser returns an authorization code. Copy the code and store it in a variable:

val authorizationCode = readln()

Exchange authorization code for tokens

Next, exchange the authorization code for tokens. To do this, create a client and install the ContentNegotiation plugin with the JSON serializer:

val client = HttpClient(CIO) { install(ContentNegotiation) { json() } }

This serializer is required to deserialize tokens received from the Google OAuth token endpoint.

Using the created client, pass the authorization code and other necessary options to the token endpoint as form parameters:

val tokenInfo: TokenInfo = client.submitForm( url = "https://accounts.google.com/o/oauth2/token", formParameters = parameters { append("grant_type", "authorization_code") append("code", authorizationCode) append("client_id", System.getenv("GOOGLE_CLIENT_ID")) append("client_secret", System.getenv("GOOGLE_CLIENT_SECRET")) append("redirect_uri", "http://127.0.0.1:8080") } ).body()

The token endpoint returns a JSON response that the client deserializes into a TokenInfo instance. The TokenInfo class looks as follows:

import kotlinx.serialization.* @Serializable data class TokenInfo( @SerialName("access_token") val accessToken: String, @SerialName("expires_in") val expiresIn: Int, @SerialName("refresh_token") val refreshToken: String? = null, val scope: String, @SerialName("token_type") val tokenType: String, @SerialName("id_token") val idToken: String, )

Store tokens

Once the tokens are received, store them so they can be supplied to the loadTokens {} and refreshTokens {} callbacks. In this example, the storage is a mutable list of BearerTokens:

val bearerTokenStorage = mutableListOf<BearerTokens>() bearerTokenStorage.add(BearerTokens(tokenInfo.accessToken, tokenInfo.refreshToken!!))

Send a request with a valid token

Now that valid tokens are available, the client can make a request to the protected Google API and retrieve user information.

Before doing that, configure the client to use bearer authentication:

val client = HttpClient(CIO) { install(ContentNegotiation) { json() } install(Auth) { bearer { loadTokens { bearerTokenStorage.last() } sendWithoutRequest { request -> request.url.host == "www.googleapis.com" } } } }

The following settings are specified:

  • The loadTokens callback retrieves tokens from storage.

  • The sendWithoutRequest {} callback sends the access token without waiting for the 401 Unauthorized response when calling the Google API.

With this client, you can now make a request to the protected resource:

while (true) { println("Make a request? Type 'yes' and press Enter to proceed.") when (readln()) { "yes" -> { val response: HttpResponse = client.get("https://www.googleapis.com/oauth2/v2/userinfo") try { val userInfo: UserInfo = response.body() println("Hello, ${userInfo.name}!") } catch (e: Exception) { val errorInfo: ErrorInfo = response.body() println(errorInfo.error.message) } } else -> return@runBlocking } }

Access the protected resource

The resource server returns information about a user in a JSON format. You can deserialize the response into the UserInfo class instance and display a personal greeting:

val userInfo: UserInfo = response.body() println("Hello, ${userInfo.name}!")

The UserInfo class looks as follows:

import kotlinx.serialization.* @Serializable data class UserInfo( val id: String, val name: String, @SerialName("given_name") val givenName: String, @SerialName("family_name") val familyName: String, val picture: String, val locale: String )

Request with expired token

At some point, the client repeats the request from Step 5, but with an expired access token.

401 Unauthorized response

When the token is no longer valid, the resource server returns a 401 Unauthorized response. The client then invokes the refreshTokens {} callback, which is responsible for obtaining new tokens.

Refresh the access token

To obtain a new access token, configure the refreshTokens {} callback to make another request to the token endpoint. This time, use the refresh_token grant type instead of authorization_code:

install(Auth) { bearer { refreshTokens { val refreshTokenInfo: TokenInfo = client.submitForm( url = "https://accounts.google.com/o/oauth2/token", formParameters = parameters { append("grant_type", "refresh_token") append("client_id", System.getenv("GOOGLE_CLIENT_ID")) append("refresh_token", oldTokens?.refreshToken ?: "") } ) { markAsRefreshTokenRequest() }.body() } } }

The refreshTokens {} callback uses RefreshTokensParams as a receiver and allows you to access the following settings:

  • The client instance, which can be used to submit form parameters.

  • The oldTokens property is used to access the refresh token and send it to the token endpoint.

  • The .markAsRefreshTokenRequest() function exposed by HttpRequestBuilder marks the request for refreshing auth tokens, resulting in a special handling of it.

Save refreshed tokens

After receiving new tokens, save them in the token storage. With this, the refreshTokens {} callback looks as follows:

refreshTokens { val refreshTokenInfo: TokenInfo = client.submitForm( url = "https://accounts.google.com/o/oauth2/token", formParameters = parameters { append("grant_type", "refresh_token") append("client_id", System.getenv("GOOGLE_CLIENT_ID")) append("refresh_token", oldTokens?.refreshToken ?: "") } ) { markAsRefreshTokenRequest() }.body() bearerTokenStorage.add(BearerTokens(refreshTokenInfo.accessToken, oldTokens?.refreshToken!!)) bearerTokenStorage.last() }

Request with new token

With the refreshed access token stored, the next request to the protected resource should succeed:

val response: HttpResponse = client.get("https://www.googleapis.com/oauth2/v2/userinfo")

Handle API errors

Given that the 401 Unauthorized response returns JSON data with error details, update the example to read error responses as an ErrorInfo object:

val response: HttpResponse = client.get("https://www.googleapis.com/oauth2/v2/userinfo") try { val userInfo: UserInfo = response.body() println("Hello, ${userInfo.name}!") } catch (e: Exception) { val errorInfo: ErrorInfo = response.body() println(errorInfo.error.message) }

The ErrorInfo class is defined as follows:

import kotlinx.serialization.* @Serializable data class ErrorInfo(val error: ErrorDetails) @Serializable data class ErrorDetails( val code: Int, val message: String, val status: String, )
07 April 2026