OAuth клиент для Kotlin: интеграция с провайдерами через ktor-client и kotlinx-serialization

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

OAuth клиент для Kotlin: интеграция с провайдерами через ktor-client и kotlinx-serialization

OAuth 2.0 — это стандартный протокол авторизации, который позволяет приложениям получать ограниченный доступ к ресурсам пользователя без передачи его учётных данных. В экосистеме Kotlin, благодаря совместимости с JVM и мультиплатформенности, можно эффективно реализовать OAuth-клиент с использованием современных библиотек. В этой статье мы рассмотрим создание OAuth-клиента на Kotlin с применением ktor-client для HTTP-запросов и kotlinx-serialization для обработки JSON.

Основные компоненты OAuth-клиента

  • HTTP-клиент — ktor-client для отправки запросов к серверам авторизации и ресурсов.
  • Сериализация — kotlinx-serialization для парсинга ответов в data-классы.
  • Хранилище токенов — in-memory или постоянное (SharedPreferences, файлы).
  • Менеджер состояний — обработка authorization code, refresh token, CSRF-защита.

Подключение зависимостей

Для работы с OAuth в Kotlin добавьте в build.gradle.kts (или build.gradle) следующие зависимости:

// build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-client-core:2.3.12")
    implementation("io.ktor:ktor-client-cio:2.3.12") // движок
    implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

Базовая архитектура OAuth-клиента

Разделим логику на три слоя:

  • Модели (DTO) — data-классы для запросов/ответов.
  • Сервис — класс, управляющий потоками авторизации.
  • Клиент — обёртка над ktor-client с методами для разных грантов.

Модели данных

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class TokenResponse(
    @SerialName("access_token") val accessToken: String,
    @SerialName("token_type") val tokenType: String = "Bearer",
    @SerialName("expires_in") val expiresIn: Long? = null,
    @SerialName("refresh_token") val refreshToken: String? = null,
    @SerialName("scope") val scope: String? = null,
    @SerialName("id_token") val idToken: String? = null // для OpenID Connect
)

@Serializable
data class OAuthError(
    val error: String,
    @SerialName("error_description") val errorDescription: String? = null
)

Клиент OAuth2

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

class OAuth2Client(
    private val clientId: String,
    private val clientSecret: String,
    private val redirectUri: String,
    private val tokenEndpoint: String,
    private val authEndpoint: String
) {
    private val httpClient = HttpClient(CIO) {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                isLenient = true
            })
        }
    }

    // Генерация URL для редиректа на страницу авторизации
    fun getAuthorizationUrl(state: String, scope: String = "openid profile email"): String {
        return "$authEndpoint?response_type=code" +
                "&client_id=$clientId" +
                "&redirect_uri=${java.net.URLEncoder.encode(redirectUri, "UTF-8")}" +
                "&scope=${java.net.URLEncoder.encode(scope, "UTF-8")}" +
                "&state=$state"
    }

    // Обмен authorization code на токены
    suspend fun exchangeCodeForToken(code: String): Result<TokenResponse> {
        return try {
            val response: HttpResponse = httpClient.post(tokenEndpoint) {
                contentType(ContentType.Application.FormUrlEncoded)
                setBody(buildMap {
                    put("grant_type", "authorization_code")
                    put("code", code)
                    put("redirect_uri", redirectUri)
                    put("client_id", clientId)
                    put("client_secret", clientSecret)
                })
            }
            if (response.status.isSuccess()) {
                Result.success(response.body<TokenResponse>())
            } else {
                val error = response.body<OAuthError>()
                Result                    

Рекомендаций