OAuth 2.0 и JWT авторизация на Swift: пошаговое руководство для начинающих

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

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

Начать курс

Введение в 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)
    }
}

                
                

Блоги

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