Введение в OAuth 2.0 и JWT на Swift
Современные мобильные и десктопные приложения на Swift (iOS/macOS) всё чаще используют протокол OAuth 2.0 и формат JWT (JSON Web Token) для безопасной авторизации. OAuth 2.0 обеспечивает делегированный доступ к ресурсам, а JWT позволяет компактно и безопасно передавать данные пользователя. В этой статье мы разберём, как реализовать полную цепочку авторизации на Swift: от получения токена до его проверки и обновления.
Мы рассмотрим:
- Основы OAuth 2.0: потоки (flows) и роли
- Структуру JWT и его декодирование на Swift
- Практическую реализацию авторизации с использованием
URLSession - Хранение токенов в связке ключей (Keychain) и их обновление
Основы OAuth 2.0 для Swift-разработчика
OAuth 2.0 — это протокол авторизации, который позволяет приложению получать ограниченный доступ к ресурсам пользователя без передачи его учётных данных. В контексте Swift приложений чаще всего используется Authorization Code Flow с PKCE (Proof Key for Code Exchange) для мобильных приложений.
Ключевые компоненты OAuth 2.0
- Resource Owner — пользователь, владелец данных
- Client — ваше Swift-приложение
- Authorization Server — сервер, выдающий токены (например, Google, Facebook, ваш бэкенд)
- Resource Server — API, предоставляющее защищённые данные
Типичный поток на Swift выглядит так:
- Приложение открывает веб-вью (SFSafariViewController или ASWebAuthenticationSession) для аутентификации пользователя.
- После успешного входа сервер перенаправляет на callback URL с временным кодом.
- Приложение обменивает код на access token и refresh token.
- Access token используется для запросов к API, refresh token — для получения нового access token при истечении срока.
Структура JWT и его декодирование на Swift
JWT состоит из трёх частей, разделённых точками: header.payload.signature. Пример JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikl2YW4gUGV0cm92IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Декодирование payload на Swift (без проверки подписи):
import Foundation
struct JWT {
static func decodePayload(_ token: String) -> [String: Any]? {
let segments = token.components(separatedBy: ".")
guard segments.count == 3 else { return nil }
var base64 = segments[1]
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let length = Double(base64.lengthOfBytes(using: .utf8))
let requiredLength = 4 * ceil(length / 4.0)
let padding = Int(requiredLength - length)
if padding > 0 {
base64 += String(repeating: "=", count: padding)
}
guard let data = Data(base64Encoded: base64) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
}
}
// Использование
if let payload = JWT.decodePayload("your.jwt.token") {
print(payload["sub"] ?? "")
print(payload["exp"] ?? "")
}
Для полной проверки подписи JWT на стороне клиента используйте библиотеку SwiftJWT или JWTDecode, доступные через Swift Package Manager.
Реализация OAuth 2.0 и JWT на Swift
Шаг 1: Настройка URLSession и модели токенов
struct AuthToken: Codable {
let accessToken: String
let refreshToken: String?
let expiresIn: Int
let tokenType: String
}
class AuthService {
private let session = URLSession.shared
private let baseURL = "https://api.example.com"
func exchangeCodeForToken(code: String, completion: @escaping (Result<AuthToken, Error>) -> Void) {
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=yourapp://callback&client_id=YOUR_CLIENT_ID&code_verifier=VERIFIER"
request.httpBody = body.data(using: .utf8)
session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data", code: -1)))
return
}
do {
let token = try JSONDecoder().decode(AuthToken.self, from: data)
completion(.success(token))
} catch {
completion(.failure(error))
}
}.resume()
}
}
Шаг 2: Хранение токенов в Keychain
Для безопасного хранения токенов используйте Keychain Services. Пример простой обёртки:
import Security
class KeychainManager {
static func save(key: String, data: Data) -> Bool {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: data
] as CFDictionary
SecItemDelete(query)
return SecItemAdd(query, nil) == errSecSuccess
}
static func load(key: String) -> Data? {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
] as CFDictionary
var result: AnyObject?
SecItemCopyMatching(query, &result)
return result as? Data
}
}
Шаг 3: Автоматическое обновление токена
Когда access token истекает, используйте refresh token для получения нового:
func refreshAccessToken(refreshToken: String, completion: @escaping (Result<AuthToken, Error>) -> Void) {
let url = URL(string: "\(base