Ktor 3.4.0 Help

Bearer authentication in Ktor Client

Bearer authentication involves security tokens called bearer tokens. As an example, these tokens can be used as a part of OAuth flow to authorize users of your application by using external providers, such as Google, Facebook, Twitter, and so on. You can learn how the OAuth flow might look from the OAuth authorization flow section for a Ktor server.

Configure bearer authentication

A Ktor client allows you to configure a token to be sent in the Authorization header using the Bearer scheme. You can also specify the logic for refreshing a token if the old one is invalid. To configure the bearer provider, follow the steps below:

  1. Call the bearer function inside the install block.

    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 } } }
  2. Configure how to obtain the initial access and refresh tokens using the loadTokens callback. This callback is intended to load cached tokens from a local storage and return them as the BearerTokens instance.

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

    The abc123 access token is sent with each request in the Authorization header using the Bearer scheme:

    GET http://localhost:8080/ Authorization: Bearer abc123
  3. Specify how to obtain a new token if the old one is invalid using refreshTokens.

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

    This callback works as follows:

    a. The client makes a request to a protected resource using an invalid access token and gets a 401 (Unauthorized) response.

    b. The client calls refreshTokens automatically to obtain new tokens.

    c. The client makes one more request to a protected resource automatically using a new token this time.

  4. (Optional) Specify a condition for sending credentials without waiting for the 401 (Unauthorized) response. For example, you can check whether a request is made to a specified host.

    install(Auth) { bearer { // Load and refresh tokens ... sendWithoutRequest { request -> request.url.host == "www.googleapis.com" } } }
  5. (Optional) Use the cacheTokens option to control whether bearer tokens are cached between requests. Disabling caching forces the client to reload tokens for every request, which can be useful when tokens change frequently:

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

Example: Using Bearer authentication to access Google API

Let's take a look at how to use bearer authentication to access Google APIs, which use the OAuth 2.0 protocol for authentication and authorization. We'll investigate the client-auth-oauth-google console application that gets Google's profile information.

Obtain client credentials

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

  1. Create or sign in to a Google account.

  2. Open the Google Cloud Console and create an OAuth client ID with the Android application type. This client ID will be used to obtain an authorization grant.

OAuth authorization flow

The OAuth authorization flow looks as follows:

(1) --> [[[Authorization request|#step1]]] Resource owner (2) <-- [[[Authorization grant (code)|#step2]]] Resource owner (3) --> [[[Authorization grant (code)|#step3]]] Authorization server (4) <-- [[[Access and refresh tokens|#step4]]] Authorization server (5) --> [[[Request with valid token|#step5]]] Resource server (6) <-- [[[Protected resource|#step6]]] Resource server ⌛⌛⌛ Token expired (7) --> [[[Request with expired token|#step7]]] Resource server (8) <-- [[[401 Unauthorized response|#step8]]] Resource server (9) --> [[[Authorization grant (refresh token)|#step9]]] Authorization server (10) <-- [[[Access and refresh tokens|#step10]]] Authorization server (11) --> [[[Request with new token|#step11]]] Resource server (12) <-- [[[Protected resource|#step12]]] Resource server

The next sections explain how each step is implemented and how the Bearer authentication provider assists in accessing the API.

(1) -> Authorization request

The first step is to 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 client ID obtained earlier used to access the Google APIs.

  • scope: the scopes of resources required for a Ktor application. In this case, the application requests information about a user's profile.

  • response_type: a grant type used to get an access token. In this case, it is 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.

(2) <- Authorization grant (code)

Copy the authorization code from the browser, paste it in a console, and save it in a variable:

val authorizationCode = readln()

(3) -> Authorization grant (code)

Next, exchange the authorization code for tokens. To do this, create a client and install the ContentNegotiation plugin with the json serializer. This serializer is required to deserialize tokens received from the Google OAuth token endpoint.

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

Using the created client, you can securely 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()

As a result, the token endpoint sends tokens in a JSON object, which is deserialized to a TokenInfo class instance using the installed json serializer. 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, )

(4) <- Access and refresh 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!!))

(5) -> Request with 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, you need to adjust the client configuration:

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 already installed ContentNegotiation plugin with the json serializer is required to deserialize user information received from a resource server in a JSON format.

  • The Auth plugin with the bearer provider is configured as follows:

    • The loadTokens callback loads tokens from the storage.

    • The sendWithoutRequest callback sends the access token without waiting for the 401 Unauthorized response when accessing Google's protected 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 } }

(6) <- 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 )

(7) -> Request with expired token

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

(8) <- 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.

(9) -> Authorization grant (refresh token)

To obtain a new access token, you need to configure refreshTokens to make another request to the token endpoint. This time, the refresh_token grant type is used 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.

(10) <- Access and refresh tokens

After receiving new tokens, they need to be saved 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() }

(11) -> 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")

(12) <-- Protected resource

Given that the 401 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, )

For the full example, see client-auth-oauth-google.

19 December 2025