Karhin’s Blog
Apps, Design and Music

«Вход через Apple» был представлен в 2019 году и стал обязательным для всех приложений, которые используют авторизацию через социальные сети или внешние сервисы.

Я реализовывал функцию несколько раз и набил пару шишек на ней. Перед тем, как её реализовывать, хорошо подумайте, потому что самый главный вывод, который я сделал: не связывайтесь с авторизацией через Apple.

iOS приложение

Здесь нет ничего сложного, но есть некоторые моменты, на которые стоит обратить внимание. Для начала добавьте в настройках проекта одноименное разрешение.

В 2022 году при авторизации можно попросить полное имя и электронную почту.

Первый важный момент: запрашиваемые данные передаются лишь однажды при первом входе, поэтому обязательно сохраните их в Keychain. Если ваш сервер или сеть отвалится и вы не сможете с первого раза отправить эти данные, то боюсь, что ваш пользователь больше не сможет войти.

Для работы с Keychain я использую KeychainWrapper. Репозиторий давно не обновлялся, но и в API ничего не менялось.

Если вы всё-таки запрашивали данные, но потеряли их, то вход можно сбросить через настройки: Settings → Apple ID → Password & Security → Apps Using Apple ID → Stop using Apple ID.

Второй важный момент: это не работает в симуляторе. В iOS 16 может починили, но я не проверял.

Третий важный момент: в Catalyst кнопка из SwiftUI не работает и приложение просто падает. Багу сто лет. Может быть его уже исправили, но я не проверял.

В SwiftUI добавить кнопку очень просто. Не забудьте импортировать AuthenticationServices.

SignInWithAppleButton(.continue, onRequest: { request in
    request.requestedScopes = [.email, .fullName]
}, onCompletion: { result in
    switch result {
    case .success(let auth):
        // handle it
    case .failure(let error):
        // ¯\_(ツ)_/¯
        print(error)
    }
})

В UIKit тоже ничего сложного нет, но придётся написать чуть больше кода. Создать фирменную кнопку можно примерно вот так.

private lazy var signInButton: ASAuthorizationAppleIDButton = {
    let button = ASAuthorizationAppleIDButton(
        authorizationButtonType: .continue,
        authorizationButtonStyle: traitCollection.userInterfaceStyle == .dark ? .white : .black
    )
    button.cornerRadius = 13.0
    button.translatesAutoresizingMaskIntoConstraints = false
    button.addTarget(self, action: #selector(didTapSignInButton), for: .touchUpInside)
    return button
}()

При нажатии создаём запрос на авторизацию и ASAuthorizationController, которому нужно указать делегата.

@objc private func didTapSignInButton() {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.email]
    
    let controller = ASAuthorizationController(authorizationRequests: [ request ])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

extension ViewController: ASAuthorizationControllerDelegate {
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print(error)
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
        // handle it
    }
    
}

Что передавать на сервер?

Из объекта credential вам обязательно нужно достать identityToken и authorizationCode. Они необходимы для проверки токена на стороне сервера.

Эти объекты можно просто перевести в строки и передать как есть.

guard let identityTokenData = credential.identityToken else { return }
guard let authorizationCodeData = credential.authorizationCode else { return }
guard let identityToken = String(data: identityTokenData, encoding: .utf8) else { return }
guard let authorizationCode = String(data: authorizationCodeData, encoding: .utf8) else { return }

Identity Token содержит всю самую важную информацию: уникальный идентификатор пользователя, email и fullName.

Серверная часть

Если для вашего языка программирования есть хорошая библиотека для валидации токенов от Apple, то поздравляю, можете её взять и использовать. Скорее всего, её нет.

Четвёртый важный момент: c 30 июня 2022 года все приложения должны предоставлять функцию удаления аккаунта в приложении. Это требование напрямую касается всех, кто использует вход через Apple, так как необходимо отозвать токены, а нормального способа это сделать не существует.

Перед тем, как вы сможете валидировать токены, необходимо получить ваш приватный ключ в Apple Developer. Если вы отправляете пуши, то у вас он скорее всего есть.

Apple Developer → Certificates, Identifiers & Profiles → Keys. Не забудьте включить сервис Sign In with Apple.

Сохраните ключ в безопасное место и не делайте хардкод. Вместе с ключом вам нужно держать рядом Key ID, Team ID и Bundle ID вашего приложения.

Генерируем токен для сервера

Перед этим шагом поищите библиотеку для JWT, если у вас её ещё нет.

Он нужен для того, чтобы отправлять запросы на сервера Apple. Представляет из себя обычный JSON Web Token (JWT) в котором указаны Team ID и Bundle ID. Подписать его нужно вашим приватным ключом, который вы скачали у Apple.

В документации более подробно расписано про все поля. Ниже пример пейлоуда на Go, а вот пример генерации на питоне.

Для подписи используйте ES256.

type SecretKeyPayload struct {
	Iss string `json:"iss"`
	Iat int64  `json:"iat"`
	Exp int64  `json:"exp"`
	Aud string `json:"aud"`
	Sub string `json:"sub"`
}

Проверяем Identity Token

Сначала нужно скачать JSON Web Key (JWK) от Apple, которыми подписываются токены для авторизации. Адрес с ключами ниже.

https://appleid.apple.com/auth/keys

Не все библиотеки для JWT поддерживают JWK, но в этом нет ничего страшного. JWK представляет из себя просто массив ключей. Достаточно разобрать заголовок из JWT и найти публичный ключ у которого такой же kid.

Учитывайте, что ключи очень часто обновляются. Количество ключей и они сами постоянно меняются.

identityToken, который прислал нам iOS клиент, представляет из себя как раз таки JWT, который подписан одним из этих ключей.

Не забудьте дополнительно проверить следующие поля:

Генерируем Refresh Token

На этом шаге нам пригодится приватный ключ сервера, который мы уже сгенерировали и подписали ранее. Более подробная документация доступна на сайте Apple.

https://appleid.apple.com/auth/token

На этот адрес отправляем запрос с следующей формдатой:

В ответ должен прилететь JSON с следующей структурой. Не забудьте проверить HTTP статус, он должен быть 200.

type GeneratedTokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
	IdToken      string `json:"id_token"`
}

В ответе можно найти id_token, это новый железобетонный identityToken, который уже точно можно использовать внутри приложения.

А что дальше?

Можно добавить нового пользователя в систему и сгенерировать свой собственный токен, чтобы отдать его клиенту. Вы же не хотите такой ерундой заниматься каждый раз на эппловых токенах?

Пятый важный момент: обязательно сохраните в базу данных refresh_token и access_token. Желательно с привязкой к вашим внутренним сессиям.

Зачем это нужно? Я не знаю, спросите у Тима Кука или напишите петицию, что у лучших инженеров какое-то абсолютно дебильное API с ненужными действиями, потому что вы ничего не сможете сделать с этими токенами, кроме как отозвать или провалидировать.

Обновляем Access Token

В документации написано, что делать это нужно раз в сутки.

Адрес совпадает с генерацией, но немного меняются данные:

Отзываем токены

Видимо, их стоит отзывать, когда пользователь разлогинивается в приложении. А ещё нужно отозвать все токены, которые нам выдал Apple, если пользователь решил удалить аккаунт в приложении. Сделать это можно только по одному за раз.

https://appleid.apple.com/auth/revoke

На этот адрес отправляем запрос с следующей формдатой:

В случае успеха возвращается 200-ый код с пустым телом. В общем, на ответ можно просто забить.

Заключение

Я бы хотел написать, что авторизация через Apple – это круто, но нет.

Самый главные подводные камни:

6543 Digital Apple iOS Swift SwiftUI UIKit Programming

To post a comment, please log in or create an account.

Sign In

AI effect

I remember how fascinated I was by the first version of DALL·E, back when it was still invite-only, and the shock it gave me. The world had changed! It seemed to me that it was for the better.

Первый iPhone

Возле первого айфона часто вижу приписки «Революционный», «Прорывной» и так далее. У меня совсем другое мнение по этому поводу.

Красивый Email за десять минут

Собственный домен для почты – это не привилегия бизнеса или программистов. Сделать его проще простого, но нужно потратить десять минут.

Apple’s New Glass Design

It seems I've figured out what the design transition between iOS 6 and iOS 7 felt like. The only difference now is that there isn't a global paradigm shift, there's no need to remove shadows and textures and replace them with solid colors.