Введение в OAuth 2.0 и JWT на Swift
Современные мобильные и десктопные приложения редко обходятся без аутентификации. Два ключевых протокола — OAuth 2.0 и JWT (JSON Web Token) — стали стандартом де-факто для безопасной передачи данных между клиентом и сервером. В этой статье мы разберем, как реализовать OAuth 2.0 и JWT авторизацию на Swift для платформ iOS и macOS. Вы узнаете, как настроить поток получения токенов, сохранить их в Keychain и автоматически обновлять при истечении срока действия.
Статья рассчитана на начинающих разработчиков, которые уже знакомы с основами Swift и SwiftUI. Мы будем использовать URLSession для сетевых запросов и Keychain для безопасного хранения данных. Код написан на Swift 5.9 и совместим с iOS 15+ / macOS 12+.
Что такое OAuth 2.0 и JWT?
Прежде чем писать код, давайте разберемся с теорией.
OAuth 2.0
OAuth 2.0 — это протокол авторизации, который позволяет приложению получать ограниченный доступ к ресурсам пользователя без передачи его логина и пароля. Вместо этого используются токены доступа. Основные участники:
- Resource Owner — пользователь.
- Client — ваше приложение.
- Authorization Server — сервер, выдающий токены.
- Resource Server — API, предоставляющее данные.
Самый популярный поток для мобильных приложений — Authorization Code Flow with PKCE (Proof Key for Code Exchange). Он безопасен даже без client secret, который сложно хранить в приложении.
JWT (JSON Web Token)
JWT — это компактный и самодостаточный формат токена, который содержит JSON-данные. Он состоит из трех частей, разделенных точками:
- Header — тип токена и алгоритм подписи.
- Payload — полезные данные (claims), такие как id пользователя, роль, срок действия.
- Signature — подпись для проверки целостности.
JWT часто используется как access token в OAuth 2.0. Он позволяет серверу проверить подлинность запроса без обращения к базе данных.
Практическая реализация на Swift
Мы создадим простой класс AuthService, который будет управлять аутентификацией. Для примера будем использовать сервер https://api.example.com.
Шаг 1: Получение токенов через OAuth 2.0
Сначала реализуем метод для получения токенов по коду авторизации. Предположим, что мы уже получили authorizationCode через веб-вью (например, ASWebAuthenticationSession).
import Foundation
final class AuthService {
static let shared = AuthService()
private let baseURL = "https://api.example.com"
private let clientID = "your_client_id"
private let redirectURI = "yourapp://callback"
// MARK: - Получение токенов
func exchangeCodeForTokens(code: String, codeVerifier: String) async throws -> TokenResponse {
let url = URL(string: "\(baseURL)/oauth/token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let body = [
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectURI,
"client_id": clientID,
"code_verifier": codeVerifier
]
.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" }
.joined(separator: "&")
.data(using: .utf8)
request.httpBody = body
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw AuthError.invalidResponse
}
let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
return tokenResponse
}
}
// MARK: - Модели
struct TokenResponse: Codable {
let accessToken: String
let refreshToken: String
let expiresIn: Int
let tokenType: String
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case refreshToken = "refresh_token"
case expiresIn = "expires_in"
case tokenType = "token_type"
}
}
enum AuthError: Error {
case invalidResponse
case tokenExpired
case noRefreshToken
}
Шаг 2: Безопасное хранение токенов в Keychain
Хранить токены в UserDefaults — плохая практика. Используем Keychain. Для простоты создадим обертку.
import Security
final class KeychainManager {
static let shared = KeychainManager()
func save(key: String, data: Data) -> Bool {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: data
] as CFDictionary
SecItemDelete(query) // Удаляем старую запись
return SecItemAdd(query, nil) == errSecSuccess
}
func read(key: String) -> Data? {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
] as CFDictionary
var result: CFTypeRef?
SecItemCopyMatching(query, &result)
return result as? Data
}
func delete(key: String) {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key
] as CFDictionary
SecItemDelete(query)
}
}
Теперь добавим методы сохранения и чтения токенов в AuthService:
extension AuthService {
func saveTokens(_ tokens: TokenResponse) {
let accessData = tokens.accessToken.data(using: .utf8)!
let refreshData = tokens.refreshToken.data(using: .utf8)!
let expiresData = Data(from: tokens.expiresIn) // кастомный инициализатор
KeychainManager.shared.save(key: "access_token", data: accessData)
KeychainManager.shared.save(key: "refresh_token", data: refreshData)
KeychainManager.shared.save(key: "expires_in", data: expiresData)
}
func getAccessToken() -> String? {
guard let data = KeychainManager.shared.read(key: "access_token") else { return nil }
return String(data: data, encoding: .utf8)
}
}