Ktor 2.3.10 Help

Bearer authentication

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. Optionally, 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" } } }

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

As the first step, we need to obtain client credentials required for accessing Google APIs:

  1. Create a Google account.

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

OAuth authorization flow

The OAuth authorization flow for our application 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

Let's investigate how each step is implemented and how the Bearer authentication provider helps us access the API.

(1) -> Authorization request

As the first step, we need to build the authorization link that is used to request the desired permissions. To do this, we need to append specified query parameters to the URL:

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: a client ID obtained earlier is used to access Google APIs.

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

  • response_type: a grant type used to get an access token. In our case, we need 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: The access type is set to offline since our console application needs to refresh access tokens when the user is not present at the browser.

(2) <- Authorization grant (code)

At this step, we 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)

Now we are ready to exchange the authorization code for tokens. To do this, we need to 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, we 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

When tokens are received, we can save them in a storage. In our example, a storage is a mutable list of BearerTokens instances. This means that we can pass its elements to the loadTokens and refreshTokens callbacks.

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

(5) -> Request with valid token

Now we have valid tokens, so we can make a request to the protected Google API and get information about a user. First, we 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 is configured to send credentials without waiting for the 401 (Unauthorized) response only to a host providing access to protected resources.

This client can be used to make a request to the protected resource:

while (true) {

(6) <- Protected resource

The resource server returns information about a user in a JSON format. We can deserialize the response into the UserInfo class instance and show 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 makes a request as in Step 5 but with the expired access token.

(8) <- 401 Unauthorized response

The resource server returns the 401 unauthorized response, so the client should invoke the refreshTokens callback.

(9) -> Authorization grant (refresh token)

To obtain a new access token, we need to configure refreshTokens and make another request to the token endpoint. This time, we 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() } } }

Note that the refreshTokens callback uses RefreshTokensParams as a receiver and allows you to access the following settings:

  • The client instance. In the code snippet above, we use it 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, we can save them in the storage, so refreshTokens 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

At this step, the request to the protected resource contains a new token and should work fine.

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, we need to update our sample to receive the information about an error as a 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 looks 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, )

You can find the full example here: client-auth-oauth-google.

Last modified: 02 April 2024