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.
"Вход через Apple" был представлен в 2019 году и стал обязательным для всех приложений, которые используют авторизацию через социальные сети или внешние сервисы.
Я реализовывал функцию несколько раз и набил пару шишек на ней. Перед тем, как её реализовывать, хорошо подумайте, потому что самый главный вывод, который я сделал: не связывайтесь с авторизацией через Apple.
- За неё нужно платить 100 долларов в год.
- API очень запутанное, а отсутствие некоторых методов накладывает на архитектуру серверного приложения определённые ограничения.
- Сложно тестировать.
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, который подписан одним из этих ключей.
Не забудьте дополнительно проверить следующие поля:
- exp меньше текущего времени в UNIX.
- iss равен https://appleid.apple.com.
- aud равен нашему Bundle ID.
Генерируем Refresh Token
На этом шаге нам пригодится приватный ключ сервера, который мы уже сгенерировали и подписали ранее. Более подробная документация доступна на сайте Apple.
https://appleid.apple.com/auth/token
На этот адрес отправляем запрос с следующей формдатой:
- client_id с идентификатором приложения.
- client_secret с серверным токеном.
- grant_type с строкой authorization_code.
- code с authorizationCode, который нам прислало iOS приложение.
В ответ должен прилететь 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
В документации написано, что делать это нужно раз в сутки.
Адрес совпадает с генерацией, но немного меняются данные:
- grant_type с строкой refresh_token.
- refresh_token с собственно токеном, который мы получили.
Отзываем токены
Видимо, их стоит отзывать, когда пользователь разлогинивается в приложении. А ещё нужно отозвать все токены, которые нам выдал Apple, если пользователь решил удалить аккаунт в приложении. Сделать это можно только по одному за раз.
https://appleid.apple.com/auth/revoke
На этот адрес отправляем запрос с следующей формдатой:
- client_id с идентификатором приложения.
- client_secret с серверным токеном.
- token_type_hint с строкой refresh_token или access_token.
- token с самим токеном.
В случае успеха возвращается 200-ый код с пустым телом. В общем, на ответ можно просто забить.
Заключение
Я бы хотел написать, что авторизация через Apple – это круто, но нет.
- С 2019 года здесь есть какой-то бессмысленный набор токенов, который нужно валидировать и хранить. Зачем нужен access_token?
- Почему нельзя в любой момент получить электронную почту?
- Зачем делать столько лишних запросов на подтверждение, если ваши сервера сами подписывают Identity Token?
- Почему они не сделали метод для отзыва всех токенов за раз? Даже писал репорт им, что это тупость откровенная без такого метода жить, но мне никто не ответил.
- С 2022 года, если ты ещё неправильно хранишь и отзываешь эти токены, то нарушаешь гайдлайны App Store.
Самый главные подводные камни:
- Сохраняйте email и fullName на клиенте, пока точно не будете уверены, что регистрация была успешной.
- Сохраняйте токены вместе с вашей внутренней сессией.
- Валидируйте токены.
- Не забывайте, что токены нужно отзывать.
Comments
To post a comment, please log in or create an account.
Sign In