Как стать автором
Обновить
101.94
KTS
Создаем цифровые продукты для бизнеса

Подключение OAuth в iOS за полчаса

Время на прочтение9 мин
Количество просмотров5.1K

Привет! Меня зовут Лена, я занимаюсь iOS-разработкой в KTS.

В большинстве случаев мобильное приложение должно уметь авторизовывать пользователя для доступа к данным, и зачастую это должно происходить с помощью сторонних сервисов. В таких случаях используется OAuth2.0 — протокол, позволяющий авторизоваться в другом сервисе без необходимости передавать логин и пароль в приложение и давать ограниченный набор доступов к ресурсам сервиса. 

В мобильных приложениях используется Authorization Code Flow with Proof Key for Code Exchange (PKCE). Подробнее о выборе flow читайте в нашей предыдущей статье. Эта статья является продолжением.

Сегодня мы рассмотрим вариант реализации OAuth-авторизации с помощью библиотеки AppAuth-iOS. Она одна из самых популярных и довольна проста в использовании. Весь код из статьи доступен в Github.

Что будет в статье:

📱 Базовая настройка

В качестве стороннего сервиса мы взяли OAuth Github. Общая настройка аналогична настройке android-приложения из предыдущей статьи: первым делом зарегистрируем приложение OAuth в Github.

При регистрации установите CALLBACK_URL для вашего приложения на сервисе. На этот URL будет происходить перенаправление после авторизации, и ваше приложение будет его перехватывать.

В качестве CALLBACK_URL будем использовать ru.kts.oauth://github.com/callback. Не забывайте использовать кастомную схему ru.kts.oauth, чтобы только ваше приложение могло перехватить редирект.

После регистрации OAuth-приложения в Github у вас должны быть доступны client_id и client_secret, который нужно сгенерировать. Сохраните их.

Теперь нужно понять, на какой URL переходить для авторизации на веб-странице Github, и по какому обменивать код на токен. Ответ лежит в документации по Github OAuth.

Для авторизации нужно определить скоупы, к которым Github предоставит доступ. Представим, что нам в приложении нужны доступ к информации пользователя и его репозиториям: user, repo.

После настройки приложения OAuth Github мы должны иметь следующие данные, которые добавим в проект в качестве свойств конфигурации:

struct AuthConfiguration {
    static let baseUrl = "https://github.com/"
    static let authUri = "login/oauth/authorize"
    static let tokenUri = "login/oauth/access_token"
    static let endSessionUri = "logout"
    static let scopes = ["user", "repo"]
    static let clientId = "..."
    static let clientSecret = "..."
    static let callbackUrl = "ru.kts.oauth://github.com/callback"
    static let logoutCallbackUrl = "ru.kts.oauth://github.com/logout_callback"
}

Обратите внимание, что callbackUrl и logoutCallbackUrl имеют кастомную схему “ru.kts.oauth”. Чтобы приложение могло перехватить редирект, необходимо прописать эту схему в Info.plist (URL Schemes, URLIdentifier):

Подключаем библиотеку AppAuth через cocoapods. В работе с Github OAuth есть один нюанс — Github возвращает ответ в формате xml, хотя спецификация OAuth требует ответ в формате json. В библиотеке AppAuth нет возможности добавить кастомный заголовок запроса для указания формата ответа, поэтому форкнем ее и добавим заголовки в исходный код.

Подключение библиотеки AppAuth

Теперь в Podfile пишем следующую строку, указывая наш репо:

pod 'AppAuth', :git => 'git@github.com:elenakacharmina/AppAuth-iOS.git'

Выполняем команду в терминале: 

pod install

Переходим к реализации авторизации.

📱 Авторизация

Наша задача:

  • создать метод, внутри которого сформируем запрос авторизации и вызовем метод библиотеки AppAuth

  • реализовать перехват редиректа для завершения процесса

1. Создаем сессию авторизации. Для начала в AppDelegate объявим переменную типа OIDExternalUserAgentSession, которая будет удерживать текущий сеанс авторизации  и будет использоваться при перехвате редиректа, чтобы продолжить процесс авторизации (реализация ниже):

class AppDelegate: UIResponder, UIApplicationDelegate {
    var currentAuthorizationFlow: OIDExternalUserAgentSession?
}

Код

Все классы/протоколы библиотеки AppAuth имеют префикс OID.

2. Создаем конфигурацию авторизации. В библиотеке AppAuth представлена сущность OIDAuthorizationService, которая позволяет выполнять запросы, связанные с OAuth. 

Запросы представлены сущностями OIDAuthorizationRequest (запрос авторизации), OIDTokenRequest (запрос обновления токена), OIDEndSessionRequest (запрос логаута). Каждый запрос требует в инициализаторе конфигурацию OIDServiceConfiguration — набор endpoint uri. Мы создадим общую константу для всех запросов и вынесем ее в новый класс репозиторий OAuthRepository:

class OAuthRepository {
    private let configuration = OIDServiceConfiguration(
		authorizationEndpoint: URL(string: AuthConfiguration.baseUrl + AuthConfiguration.authUri)!,
		tokenEndpoint: URL(string: AuthConfiguration.baseUrl + AuthConfiguration.tokenUri)!,
		issuer: nil,
		registrationEndpoint: nil,
		endSessionEndpoint: URL(string: AuthConfiguration.baseUrl + AuthConfiguration.endSessionUri)!)
}

Код

В нашем случае поля issuer (The OpenID Connect issuer) и registrationEndpoint (uri регистрации) не используются.

3. Создаем запрос авторизации. Вся логика работа с данными, связанная с OAuth, будет располагаться в OAuthRepository, разместим в нем метод login. 

Формируем запрос:

let request = OIDAuthorizationRequest(
		configuration: configuration,
		clientId: AuthConfiguration.clientId,
		clientSecret: AuthConfiguration.clientSecret,
		scopes: AuthConfiguration.scopes,
		redirectURL: URL(string: AuthConfiguration.callbackUrl)!,
		responseType: OIDResponseTypeCode,
		additionalParameters: nil)
  • Поле responseType сообщает о том, в каком виде мы хотим получить ответ после авторизации. Другие возможные значения — OIDResponseTypeToken, OIDResponseTypeIDToken. Github всегда возвращает только код, поэтому используем значение OIDResponseTypeCode.

  • additionalParameters — словарь, ключи которого будут добавлены в URL запроса в виде дополнительных query-параметров. Они могут понадобиться в некоторых сервисах OAuth, но в нашем примере мы их не используем

Код

4. Выполняем запрос авторизации. Следующий шаг — вызов метода библиотеки AppAuth для выполнения запроса.

Если нет задачи открывать страницы в приложении в SafariVC как авторизованный пользователь, можно обойтись стандартным методом из документации библиотеки AppAuth. Под капотом метод использует OIDExternalUserAgentIOS с отдельной сессией ASWebAuthenticationSession, которая затем не шарит куки с другими SafariViewController. Нам это не подходит.

Чтобы сессия шарилась с SavariVC, мы реализуем кастомный OIDExternalUserAgentIOSSafariViewController, который реализует интерфейс OIDExternalUserAgentиз библиотеки. Потом это позволит открывать страницы в SafariViewController без авторизации.

Пример реализации можно взять по ссылке и добавить в проект, а подробнее про варианты UserAgent можно прочитать в статье.

5. Создаем OIDExternalUserAgentIOSSafariViewController:

let agent = OIDExternalUserAgentIOSSafariViewController(
      presentingViewController: viewController
)

Теперь воспользуемся методом авторизации с кастомным externalUserAgent:

let appDelegate = UIApplication.shared.delegate as! AppDelegate

appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
    byPresenting: request, 
    externalUserAgent: agent) { [weak self] authState, error in
 // реализация ниже 
}

Код

В результате вызова этого метода откроется SafariVC со страницей авторизации, а по ее успешному окончанию сервис перенаправит нас на url, указанный в поле redirectURL  запроса. В данном случае — “ru.kts.oauth://github.com/callback”. 

В параметрах редирект uri будет содержаться интересующий нас code. Этот редирект необходимо перехватить, а затем обменять код на токен.

6. Перехватываем редирект авторизации и получаем токен. Для перехвата редиректа реализуем в sceneDelegate метод scene(_, openURLContexts:):

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
  
    if let authorizationFlow = appDelegate.currentAuthorizationFlow,
       let url = URLContexts.first?.url,
       authorizationFlow.resumeExternalUserAgentFlow(with: url) {
         
        appDelegate.currentAuthorizationFlow = nil
    }
}

Код

Здесь происходит проверка наличия текущего flow авторизации в приложении и вызывается метод resumeExternalUserAgentFlow, который проверяет: совпадает ли данный uri с указанным при запросе. Если метод возвращает true, SafariVC скрывается и запускается процесс обмена кода на токен. Обмен кода на токен происходит под капотом AppAuth. 

для версий ios ниже 13 — application(_:, open url: URL, options:) в AppDelegate: пример из документации

7. Сохраняем токены. В результате обмена кода на токен мы попадаем в обработчик метода OIDAuthState.authState, который мы использовали в предыдущем шаге. Необходимо проверить отсутствие ошибки и сохранить полученный токен. Для простоты access и refresh токены сохраняем в UserDefaults:

appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request,
                                                                  externalUserAgent: agent)
{ [weak self] authState, error in
    if error == nil {
        let tokenModel = TokenModel(access: authState?.lastTokenResponse?.accessToken,
                                    refresh: authState?.lastTokenResponse?.refreshToken)
        self?.userDefaultsHelper.setToken(value: tokenModel)
    }
}

Код

На реальном проекте это небезопасно, лучше использовать keychain.

7. Тестируем авторизацию.

Запускаем код.

У нас уже реализована авторизация OAuth и получение токена:


Чтобы проверить валидность полученного токена, по кнопке пушим новый экран, на котором пытаемся получить данные с https://api.github.com/user при помощи токена.


тут выводятся личные данные
тут выводятся личные данные

Чтобы убедиться, что сессия авторизации шарит куки с SafariVC, откроем в нем страницу Github-пользователя по кнопке:

📱 Обновление токена

В Github OAuth refresh-токен не используется, но во многих других сервисах access-токен протухает. Поэтому привожу пример обновления токена. 

1. Формируем запрос обновления токена. Для начала нужно сформировать OIDTokenRequest:

func refreshToken() {
    guard let refreshToken = userDefaultsHelper.getToken()?.refresh else { return }
    
    let requestRefresh = OIDTokenRequest(
        configuration: configuration,
        grantType: OIDGrantTypeRefreshToken,
        authorizationCode: nil,
        redirectURL: nil,
        clientID: AuthConfiguration.clientId,
        clientSecret: AuthConfiguration.clientSecret,
        scope: nil,
        refreshToken: refreshToken,
        codeVerifier: nil,
        additionalParameters: nil)
}

В запросе кроме уже известных параметров, которые мы видели при авторизации, необходимо передать в параметр grantType значение OIDGrantTypeRefreshToken и сохраненный refresh-токен. В неиспользуемые параметры передаем nil.

2. Выполняем обновление токена. Вызываем метод  OIDAuthorizationService.perform, при успешном обновлении сохраняем новые значения access- и refresh-токенов:

func refreshToken() {
    guard let refreshToken = userDefaultsHelper.getToken()?.refresh else { return }
    
    let requestRefresh = OIDTokenRequest(
        configuration: configuration,
        grantType: OIDGrantTypeRefreshToken,
        authorizationCode: nil,
        redirectURL: nil,
        clientID: AuthConfiguration.clientId,
        clientSecret: AuthConfiguration.clientSecret,
        scope: nil,
        refreshToken: refreshToken,
        codeVerifier: nil,
        additionalParameters: nil)
  	
  	OIDAuthorizationService.perform(requestRefresh) { [weak self] tokenResponse, error in
				if error == nil {
          	let tokenModel = TokenModel(access: tokenResponse?.accessToken,
                                        refresh: tokenResponse?.refreshToken)
          	self?.userDefaultsHelper.setToken(value: tokenModel)
        }
		}
}

Эту реализацию можно проверить, если создать приложение Github Apps вместо OAuth и заменить clientId и clientSecret в AuthConfiguration.

📱 Логаут

При использовании OAuth-авторизации недостаточно просто очистить сохраненный токен из хранилища. Куки в SafariVC при этом не очищаются и при попытке повторной авторизации пользователь автоматически зайдет в сервис под предыдущим аккаунтом без возможности ввести другой логин/пароль.

Поэтому для реализации логаута тоже необходимо сформировать запрос в OIDEndSessionRequest…

let request =  OIDEndSessionRequest(configuration: configuration,
                                    idTokenHint: accessToken,
                                    postLogoutRedirectURL: URL(string: AuthConfiguration.logoutCallbackUrl)!,
                                    additionalParameters: nil)

    …и пробросить его в метод OIDAuthorizationService.present:

let agent = OIDExternalUserAgentIOSSafariViewController(presentingViewController: viewController)

let appDelegate = UIApplication.shared.delegate as! AppDelegate

appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, externalUserAgent: agent) { [weak self] (response, error) in
		if let error = error {
      	completion(.failure(CustomError.logoutError))
		} else {
      	self?.userDefaultsHelper.clearToken()
    }
}

В GitHub OAuth нет редиректа после логаута, поэтому пользователю придется закрывать окно самостоятельно.

Это вызовет ошибку в callback, поэтому в финальной версии проверки на наличие ошибки нет, а просто происходит очистка сохраненных токенов:

appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, externalUserAgent: agent) { [weak self] (response, error) in
		self?.userDefaultsHelper.clearToken()
}

📲 Выводы

Весь код проекта расположен в github

Мы реализовали все необходимые методы для поддержки OAuth: авторизация, обновление токена и логаут. Использование библиотеки помогает довольно просто интегрировать OAuth в приложение. Однако в некоторых сервисах — как Github OAuth — есть свои особенности использования, которые требуют кастомных решений.

А как вы реализуете OAuth в приложениях? Был ли опыт работы с AppAuth? С какими проблемами OAuth сталкивались на практике?


Другие наши статьи по iOS-разработке:

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Я реализую OAuth в приложении:
0% с помощью SDK стороннего сервиса0
33.33% вручную3
77.78% с помощью библиотек (AppAuth и другие)7
Проголосовали 9 пользователей. Воздержались 7 пользователей.
Теги:
Хабы:
Всего голосов 12: ↑12 и ↓0+12
Комментарии0

Публикации

Информация

Сайт
kts.tech
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия