Turn even the most unsuccessful screenshot or screen recording into something you wouldn't be embarrassed to share on social networks or post on your blog.
"Вход через 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