Karhin’s Blog
About technologies and life

"Вход через 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 – это круто, но нет.

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

Digital Apple iOS Swift SwiftUI UIKit Programming

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

Sign In

Why Should Lightning Go Away
Why Should Lightning Go Away

There's no secret that over the past year, Apple has considered removing the Lightning port from iPhones and accessories. Before the presentation, there were many discussions among Apple's sectarians.

1587
Контекстные меню в приложениях
Контекстные меню в приложениях

Контекстные меню подталкивают дизайнеров и разработчиков к тому, чтобы сбросить в них весь мусор в унифицированном виде, а интерфейс оставить чистым. Так лучше не делать.

1534
DALL-E: я снова верю в технологии
DALL-E: я снова верю в технологии

Попробовал новую систему для превращения текста в картинки. Показываю, что получилось.

1898
Первый iPhone
Первый iPhone

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

1950