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

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

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

Начать курс

Введение: зачем нужна авторизация в мобильных приложениях?

Современные iOS-приложения редко работают изолированно. Они общаются с серверами, отправляют данные, получают персонализированный контент. Но как серверу понять, что запрос отправлен именно тем пользователем, за которого он себя выдает? Здесь на помощь приходят OAuth 2.0 и JWT (JSON Web Token). Это не просто модные аббревиатуры, а стандарты безопасности, которые лежат в основе работы большинства современных API (Google, Facebook, GitHub, ваш собственный бэкенд).

В этой статье мы разберем, как работают OAuth 2.0 и JWT на Swift. Вы узнаете, чем они отличаются, как их правильно реализовать и какие подводные камни ждут новичка. Мы напишем код для получения, хранения и обновления токенов. Поехали!

1. Основы: OAuth 2.0 и JWT — в чем разница?

Многие путают эти два понятия. На самом деле, это разные уровни абстракции.

  • OAuth 2.0 — это протокол авторизации. Он описывает, как приложение (клиент) может получить доступ к ресурсам пользователя на стороннем сервере (например, к его фотографиям в Google) без передачи логина и пароля. Главная идея — выдача временных токенов доступа.
  • JWT — это формат токена. Это компактная, URL-безопасная строка, которая содержит JSON-данные (например, id пользователя, роль, срок действия) и подпись. JWT часто используется как access token в OAuth 2.0, но может применяться и отдельно.

Простая аналогия: OAuth 2.0 — это правила выдачи пропуска на стадион, а JWT — сам пластиковый пропуск с вашей фотографией и печатью.

В типичном сценарии iOS-приложение:

  1. Открывает браузер для входа через OAuth-провайдера (Google, Apple ID, свой сервер).
  2. Получает authorization code.
  3. Обменивает код на access token (часто в формате JWT) и refresh token.
  4. Использует access token для запросов к API.
  5. Когда access token истекает, использует refresh token для получения нового.

2. Структура JWT токена

JWT состоит из трех частей, разделенных точками:

header.payload.signature

Пример декодированного JWT:

// Header (заголовок)
{
  "alg": "HS256",
  "typ": "JWT"
}
// Payload (полезная нагрузка)
{
  "sub": "1234567890",
  "name": "Иван Петров",
  "iat": 1516239022,
  "exp": 1516242622
}
// Signature (подпись) - создается сервером

Важно: Payload JWT не зашифрован, он только закодирован в Base64. Любой может его прочитать. Секретность обеспечивает подпись — если сервер видит, что подпись не совпадает, он отклоняет токен. Поэтому никогда не храните пароли или секретные данные в JWT.

3. Реализация OAuth 2.0 и JWT на Swift

Теперь перейдем к практике. Мы напишем класс для работы с авторизацией. Для простоты будем использовать встроенный URLSession, хотя в реальных проектах часто используют Alamofire или SwiftUI + async/await.

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

struct AuthResponse: Codable {
    let accessToken: String
    let refreshToken: String?
    let expiresIn: Int
    
    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}

struct UserProfile: Codable {
    let id: String
    let name: String
    let email: String
}

3.2. Сервис авторизации

Создадим класс AuthService, который будет отвечать за получение и обновление токенов. Обратите внимание: в реальном приложении URL и clientSecret должны храниться безопасно (например, в Keychain или через сервер конфигурации).

class AuthService {
    static let shared = AuthService()
    private let baseURL = "https://yourapi.com"
    private let clientID = "YOUR_CLIENT_ID"
    private let clientSecret = "YOUR_CLIENT_SECRET"
    
    // MARK: - Получение токена по коду авторизации
    func exchangeCodeForToken(code: String, completion: @escaping (Result<AuthResponse, Error>) -> Void) {
        let url = URL(string: "\(baseURL)/oauth/token")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body: [String: String] = [
            "grant_type": "authorization_code",
            "code": code,
            "client_id": clientID,
            "client_secret": clientSecret,
            "redirect_uri": "yourapp://callback"
        ]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data, error == nil else {
                completion(.failure(error ?? NetworkError.unknown))
                return
            }
            do {
                let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
                self.saveTokens(authResponse)
                completion(.success(authResponse))
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }
    
    // MARK: - Обновление токена
    func refreshAccessToken(completion: @escaping (Result<AuthResponse, Error>) -> Void) {
        guard let refreshToken = loadRefreshToken() else {
            completion(.failure(NetworkError.noRefreshToken))
            return
        }
        
        let url = URL(string: "\(baseURL)/oauth/token")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body: [String: String] = [
            "grant_type": "refresh_token",
            "refresh_token": refreshToken,
            "client_id": clientID,
            "client_secret": clientSecret
        ]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data, error == nil else {
                completion(.failure(error ?? NetworkError.unknown))
                return
            }
            do {
                let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
                self.saveTokens                

Блоги

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