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

Ни один сервис не обходится без логина. Часто в мобильных приложениях требуется интегрировать вход через сторонние соцсети — например, зайти через Google или VK. А при обучении мобильной разработке используются открытые API, где для авторизации используется OAuth.

Поэтому разработчикам мобильных приложений приходится работать с OAuth. В сети по этой теме есть разные материалы.

В этой статье я попробую структурированно закрыть нюансы OAuth в мобильных приложениях: на какие моменты стоит обратить внимание, какие способы реализации выбрать. А также поделюсь опытом настройки OAuth в Android-приложении с использованием библиотеки AppAuth.

OAuth и flow

Когда речь идет про авторизацию и аутентификацию, используются такие понятия как OAuth2 и OpenID. В статье я не буду их раскрывать, на Хабре уже есть такой материал:

Ниже мы рассмотрим детали, касающиеся мобильной разработки. Для наших целей неважны различия между OAuth2 и OpenID, поэтому дальше мы будем использовать общий термин OAuth.

В OAuth существуют различные flow, но не все подходят для использования в приложении:

  • Authorization Code Flow. Не подходит: код можно перехватить в зловредном приложении.

  • Resource Owner Password Credentials Flow. Требует введения credentials внутри приложения. Это нежелательно, если приложение и сервис не разрабатываются одной командой.

  • Client Credentials Flow. Подходит для авторизации самого клиента на основе client_id, client_password. Не требует введения credentials от пользователя.

  • Implicit Flow. Небезопасный и устаревший.

Принцип работы Authorization Code Flow with PKCE

Для мобильных клиентов рекомендуется использовать Authorization Code Flow c дополнением: Authorization Code Flow with Proof Key for Code Exchange (PKCE). Использовать именно этот flow важно для безопасности пользовательского входа в приложение. Рассмотрим его особенности. 

Этот flow основан на обычном Authorization Code Flow. Сначала вспомним его реализацию:

  1. Пользователь жмет кнопку Login.

  2. Приложение создает ссылку для авторизации на сервисе авторизации и открывает его в браузере.

  3. Пользователь видит экран с полями для ввода логина/пароля.

  4. Пользователь вводит логин/пароль и подтверждает необходимые доступы к данным.

  5. Сервис авторизации возвращает код авторизации в приложение с помощью редиректа. С кодом получить доступ к требуемым ресурсам из апи пока не получится. Чтобы редирект был перехвачен только приложением, обычно используются кастомные схемы, а не http(s). Иначе код может перехватить еще и браузер — в этом случае появляется окно выбора приложений.

  6. Приложение получает код из URL редиректа и обменивает код на токен. Дополнительно могут передаваться client_id, client_secret.

  7. Сервис авторизации возвращает access_token для доступа к ресурсам, refresh_token.

  8. Приложение с помощью полученного токена общается с сервисом API.

При использовании Authorization Code Flow with PKCE cхема немного меняется. Отличия выделены.

  1. Пользователь жмет кнопку Login.

  2. Генерируются code_verifier и code_challenge и сохраняются в приложении. Как происходит генерация, описано в RFC-7636.

    code_challenge является производным от code_verifier, обратная трансформация невозможна.

  3. Приложение создает ссылку для авторизации с учетом сгенерированного code_challenge. Ссылка открывается в браузере. В этот момент сервис авторизации тоже запоминает code_challenge для сессии. Таким образом, code_verifier остается только внутри приложения и не передается по сети.

  4. Пользователь видит экран с полями для ввода логина/пароля.

  5. Пользователь вводит логин/пароль.

  6. Сервис авторизации возвращает код авторизации в приложение с помощью редиректа. Обратите внимание, что code_challenge не возвращается от сервера вместе с кодом. О нем знают только сервис авторизации и мобильное приложение.

  7. Приложение обменивает код на токен. При обмене приложение отправляет code_verifier, который был сохранен в пункте 2.

  8. Сервис авторизации принимает code_verifier от мобильного приложения. Вычисляет от него code_challenge и сравнивает с code_challenge, переданным в пункте 3. Если они совпадают — возвращается токен.

  9. Сервис авторизации возвращает access_token для доступа к ресурсам, refresh_token.

  10. Приложение с помощью полученного токена общается с сервисом API.

Что могло бы произойти, если бы не использовались code_verifier и code_challenge?

MITM-атака для перехвата кода

Одной из реализаций OAuth является реализация с помощью внешнего браузера.

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

Именно в момент, когда система ищет приложение для обработки URL редиректа, возможен перехват редиректа зловредным приложением. Злоумышленник может создать приложение, которое перехватывает такие же редиректы, как у вас. Утекают все данные, которые находятся в строке редиректа.

Перехват редиректа в мобильном приложении
Перехват редиректа в мобильном приложении

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

При использовании обычного Authorization Code Flow чужое приложение (Malicious app) потенциально может получить код и обменять его на токен, аналогично тому, как это сделано в вашем приложении (Real app). Но с использованием code_verifier и code_challenge зловредный перехват становится бессмысленным. Чужое приложение не знает code_verifier и code_challenge, которые были сгенерированы внутри вашего приложения, и в редиректе они не возвращаются.
Без этих данных зловредное приложение не сможет обменять код на токен.

Стоит отметить, что такая атака не сработает, если использовать universal links (ios) и applink (android). Чтобы открыть редирект-ссылку в приложении, необходимо положить на сервер json-файл с описанием подписи вашего приложения.

Но часто мы не можем добавить json-файл на сервер, если авторизуемся с помощью внешнего сервиса, который разрабатываем не мы. Поэтому не всегда это может помочь.

Нюансы реализации

Каким образом открывать страницу логина?

Страница логина в OAuth представляет из себя веб-страницу. Есть следующие способы:

  • Использовать WebView внутри вашего приложения.

  • Открыть страницу во внешнем браузере.

  • Использовать ChromeCustomTabs, SafariVC.

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

WebView

Преимущества:

  • При отображении веб-страницы с WebView мы можем кастомизировать ui экрана полностью, как нам нужно.

  • Сам экран с WebView будет открыт быстрее страницы в браузере: все происходит в рамках одного процесса, без межпроцессного взаимодействия.

Недостатки:

  • Реализация через WebView не является безопасной в общем случае, и некоторые соцсети не позволяют использовать такой способ реализации OAuth, например Google.

  • Общая проблема в том, что WebView находятся в рамках приложения. Создатель зловредного приложения может вклиниться между пользователем и сервисом, в котором пользователь авторизируется, и перехватить пароль и логин. Хотя одна из целей протокола OAuth — противостоять этому. 

    На практике удавалось это обойти путем подмены user agent. Но это не соответствует политике Google, и делать это нельзя.

  • WebView выполняет js в процессе вашего приложения, что небезопасно уже для самого приложения. Если вы используете WebView внутри, рекомендую ознакомиться с советами по настройке для обеспечения дополнительной безопасности.

  • С использованием WebView ухудшается пользовательское удобство. Пользователь мог быть уже авторизован в сервисе в браузере, но WebView об этом не узнает, так как хранилище cookie у вебвью и браузера разное.

Из-за недостатков WebView не лучший вариант для реализации OAuth в мобильном приложении.

Browser

Второй вариант — открыть страницу во внешнем браузере, установленном на устройстве.

Преимущества:

  • Открыть страницу в браузере очень просто.

  • Ваше приложение не имеет контроля над браузером и открытой веб-страницей. Это обеспечивает дополнительную безопасность для пользователя.

  • Браузер сохраняет cookie пользователя. А значит, если пользователь был уже залогинен в сервисе, ему не придется заново вводить credentials.

Недостатки:

  • Открытие браузера тяжеловесная операция, потому что нам нужно запустить внешний процесс.

  • Вы не можете настраивать UI браузера, он открывается во внешнем окне.

  • Открывая браузер, вы покидаете навигационный стек приложения.

ChromeCustomTabs, SafariVC

ChromeCustomTabs(CCT) и SafariViewController(SafariVC) аналогично браузеру позволяют легко реализовать открытие веб-страниц в вашем приложении.

Они закрывают недостатки WebView:

  • Злоумышленник не сможет перехватить вводимые данные на странице логина.

  • Данные доступны браузеру и CCT/SafariVC.

    Обратите внимание: Начиная с ios 11, данные между браузером и между различными сессиями SafariVC больше не шарятся автоматически. Чтобы это реализовать, нужно использовать ASWebAuthenticationSession.
    Пример: ссылка на Github Gist

  • JS выполняется во внешнем процессе, это обезопасит ваше приложение.

Недостатки браузера тоже частично закрываются:

  • CCT позволяет производить прогрев в фоне, что позволяет быстро начать загружать страницу при ее открытии.

  • Открытый CCT не понижает приоритет процесса вашего приложения, потому что это может привести к убийству процесса системой.

  • имеются возможности настройки внешнего вида, хотя и ограниченные: CCT, SafariVC.

CCT изначально был сделан только для Chrome, а сейчас поддерживается в разных браузерах. Помимо него, в Android есть еще TrustedWebActivity. Подробнее про них можно почитать на официальной странице.

Этот подход является самым оптимальным. Он закрывает почти все недостатки предыдущих двух подходов.

Редирект в Chrome не срабатывает

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

В процессе тестирования реализации OAuth в Android мы столкнулись с тем, что Chrome с использованием CCT после успешной авторизации не перебрасывал нас обратно в приложение на некоторых устройствах. На это заведен баг в трекере

В Chrome сделали обновление, которое запрещает без пользовательского намерения переходить по URL с кастомной схемой. Это блокирует попадание пользователя в зловредное приложение. 

Для обхода этого ограничения сделали веб-страничку, на которую браузер редиректит после успешной авторизации. Веб-страница автоматически пытается сделать редирект уже внутрь приложения. Если этого не происходит, то есть Chrome заблокировал переход, пользователь может нажать на кнопку enter и перейти явно. Этот подход сработал.

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

С использованием OAuth вам не нужно забывать об обновлении токенов.

Обычно это похоже на то, как менять код на токен. Вы обращаетесь к api для получения токена и указываете grant_type=refresh_token и refresh_token, который вы получили изначально при логине.

Более подробную реализацию рассмотрим в примере.

Браузер отсутствует

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

Кроме Chrome, этот функционал поддерживается в SBrowser, Firefox и всех остальных современных браузерах. Но даже если такового у пользователя нет, откроется обычный браузер. 

На официальной странице рассказывают, как проверить браузеры с поддержкой CCT.

Логаут

В большинстве случаев при пользовательском логауте в приложении нужно почистить токены/файлы/БД/кеши.

Если же для авторизации вы используете ссt/safarivc, потом в браузере остаются куки авторизованного человека. При повторном логине вы заново войдете под первым аккаунтом автоматически. Почистить cookie из приложения не получится, потому что браузер — это отдельный процесс со своим хранилищем, и доступ к нему запрещен.

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

Варианты реализации OAuth

Мы рассмотрели OAuth flow для мобильных приложений и увидели, на какие нюансы стоит обратить внимание при реализации.

Существует несколько вариантов реализации. 

Использовать SDK сервиса, через который вы хотите авторизоваться

Плюсы:

  • простая реализация;

  • возможна авторизация через нативные приложения, если они установлены.

Минусы:

  • увеличение внешних зависимостей, особенно при большом количестве внешних сервисов;

  • нет контроля над реализацией.

Использование SDK мы рассматривать в текущей статье не будем, потому что для этого нужно изучать документацию SDK.

Реализовать вручную

Реализовать логику вручную внутри собственного приложения с использованием WebView или других реализаций (CCT/SafariVC).

Плюс:

  • получаем полный контроль над реализацией.

Минус:

  • приходится писать свой код, поддерживать его и учитывать вручную нюансы, о которых говорили выше.

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

Использовать библиотеки

Библиотеки должны поддерживать протоколы OAuth и OpenId и позволять общаться с любыми сервисами по этим протоколам. Примеры: 
AppAuth IOS
AppAuth Android
Auth0 Android

  • При использовании этого подхода нужно убедиться, что сервер аутентификации работает в соответствии с протоколом, и вам не придется костылить библиотеку, чтобы связаться с ним.

  • Если разобраться с библиотекой и знать, как она работает, реализация получается достаточно простой. Но на это требуется время.

  • Реализация авторизации будет универсальная для разных сервисов, не придется подключать дополнительные зависимости и писать много кода для каждого внешнего сервиса, если таких несколько.

  • Учтите, что реализация библиотеки может быть не совсем удобной для встраивания в ваше приложение. Используемые подходы общения с библиотекой могут отличаться от принятых в команде, и нужно будет писать обертки-бриджи. Пример: AppAuth в Android использует AsyncTask под капотом, но в приложении вы, скорее всего, используете корутины. Но обычно такие вещи можно интегрировать.

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

Реализация в Android-приложении

Давайте посмотрим, как можно реализовать OAuth в вашем Android-приложении с использованием AppAuth. Весь код доступен на Github.

Приложение простое: отображение информации о моем github-профиле.

Для этого при каждом запуске приложения будем открывать страницу github-авторизации. После успешной авторизации переводим пользователя на главную страницу, откуда можно получить информацию о текущем пользователе. 

При реализации нам необходимо разобраться с 3 ключевыми моментами:

  • авторизация пользователя;

  • обновление токена;

  • логаут пользователя.

Общая настройка

Первым делом зарегистрируем приложение OAuth в Github.

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

В качестве CALLBACK_URL будем использовать ru.kts.oauth://github.com/callback

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

После регистрации у вас должны быть доступны client_id и client_secret (его нужно сгенерировать). Сохраните их.

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

URL для авторизации: https://github.com/login/oauth/authorize
URL для обмена токена: 
https://github.com/login/oauth/access_token

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

С общими параметрами определились. Перейдем к Android-реализации.

Реализация Android 

Подключим библиотеку в проект:

implementation 'net.openid:appauth:0.9.1'

Запишем все настройки OAuth в один объект, чтобы было легко с ним работать:

object AuthConfig {
   const val AUTH_URI = "https://github.com/login/oauth/authorize"
   const val TOKEN_URI = "https://github.com/login/oauth/access_token"
   const val END_SESSION_URI = "https://github.com/logout"
   const val RESPONSE_TYPE = ResponseTypeValues.CODE
   const val SCOPE = "user,repo"
   const val CLIENT_ID = "..."
   const val CLIENT_SECRET = "..."
   const val CALLBACK_URL = "ru.kts.oauth://github.com/callback"
   const val LOGOUT_CALLBACK_URL = "ru.kts.oauth://github.com/logout_callback"
}

Тут по сравнению с общей настройкой добавились: 

  • RESPONSE_TYPE. Используем константу “code” из библиотеки AppAuth. Эта константа отвечает за то, что будет возвращено на клиент после авторизации пользователем в браузере. Варианты: code, token, id_token.

    В соответствии с OAuth Authorization Code Flow нам нужен code.

    На самом деле Github api не требует передачи параметра response_type и всегда возвращает только код. Но данный параметр может потребоваться для других сервисов.

  • END_SESSION_URI, LOGOUT_CALLBACK_URL. Настройки, необходимые для логаута.

Авторизация

Теперь откроем страницу авторизации с использованием CCT.

Для работы с CCT и выполнения автоматических операций обмена кода на токен библиотека AppAuth предоставляет сущность AuthorizationService. Эта сущность создается при входе на экран. При выходе с экрана она должна очиститься. В примере это делается внутри ViewModel экрана авторизации.

Создаем в init:

private val authService: AuthorizationService = AuthorizationService(getApplication())

Очищаем в onCleared:

authService.dispose()

Для открытия страницы авторизации в CCT нужен интент. Для этого получаем AuthorizationRequest на основе заполненных раньше данных в AuthConfig:

private val serviceConfiguration = AuthorizationServiceConfiguration(
   Uri.parse(AuthConfig.AUTH_URI),
   Uri.parse(AuthConfig.TOKEN_URI),
   null, // registration endpoint
   Uri.parse(AuthConfig.END_SESSION_URI)
)

fun getAuthRequest(): AuthorizationRequest {
   val redirectUri = AuthConfig.CALLBACK_URL.toUri()
   return AuthorizationRequest.Builder(
       serviceConfiguration,
       AuthConfig.CLIENT_ID,
       AuthConfig.RESPONSE_TYPE,
       redirectUri
   )
       .setScope(AuthConfig.SCOPE)
       .build()
}

Создаем интент:

// тут можно настроить вид chromeCustomTabs
val customTabsIntent = CustomTabsIntent.Builder().build()

val openAuthPageIntent = authService.getAuthorizationRequestIntent(
   getAuthRequest(),
   customTabsIntent
)

После этого открываем активити по интенту. Нам необходимо обработать результат активити, чтобы получить код.

Поэтому используем ActivityResultContracts. Также можно использовать startActivityForResult.

private val getAuthResponse = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
   val dataIntent = it.data ?: return
   handleAuthResponseIntent(dataIntent)
}

getAuthResponse.launch(openAuthPageIntent)

Под капотом будут открыты активити из библиотеки, которые возьмут на себя ответственность открытия CCT и обработку редиректа. А в активити вашего приложения уже прилетит результат операции.

Схема взаимодействия активити
Схема взаимодействия активити

Внутри openAuthPageIntent будет зашита вся информация, которую мы раньше указывали в AuthConfig, а также сгенерированный code_challenge.

AppAuth генерирует URL для открытия страницы авторизации под капотом: https://github.com/login/oauth/authorize?redirect_uri=ru.kts.oauth%3A%2F%2Fgithub.com%2Fcallback&client_id=3fe9464f41fc4bd2788b&response_type=code&state=mrhOJm7ot4C1aE9ND3lWdA&nonce=4zVLkQrhQ4L46hfQ1jdTHw&scope=user%2Crepo&code_challenge=gs23wPEpmJYv3cdmTRWNSQLvvnPtHUhtSv4zhbfKS_o&code_challenge_method=S256

Чтобы редирект был обработан корректно, мы должны указать, что наше приложение умеет обрабатывать открытие URL с нашей кастомной схемой ru.kts.oauth. Для этого внутри build.gradle модуля приложения внутри секции defaultСonfig укажем manifest placeholder:

manifestPlaceholders = [
   appAuthRedirectScheme: "ru.kts.oauth"
]

После этого в AndroidManifest.xml вашего приложения будет добавлена активити, которая обрабатывает ссылки с этой кастомной схемой. Merged manifest:

<activity 
    android:name="net.openid.appauth.RedirectUriReceiverActivity"
    android:exported="true" >
    <intent-filter>
      ...
      <data android:scheme="ru.kts.oauth" />
    </intent-filter>
</activity>

Также вы можете настроить редирект с использованием стандартных схем. Более детально можно прочитать в описании к репозиторию.




Теперь мы можем открыть страницу логина:

Дальше необходимо получить код и обменять его на токен. При этом в ответе может прийти ошибка авторизации, ее тоже нужно обработать.

Библиотека AppAuth дает возможность из результирующего интента ответа получить ошибку или запрос для обмена кода на токен:

private fun handleAuthResponseIntent(intent: Intent) {
   // пытаемся получить ошибку из ответа. null - если все ок
   val exception = AuthorizationException.fromIntent(intent)
   // пытаемся получить запрос для обмена кода на токен, null - если произошла ошибка
   val tokenExchangeRequest = AuthorizationResponse.fromIntent(intent)
       ?.createTokenExchangeRequest()
   when {
       // авторизация завершались ошибкой
       exception != null -> viewModel.onAuthCodeFailed(exception)
       // авторизация прошла успешно, меняем код на токен
       tokenExchangeRequest != null ->
           viewModel.onAuthCodeReceived(tokenExchangeRequest)
   }
}

Запрос на токен будет сформирован автоматически, в него будет добавлен тот code_verifier, code_challenge от которого передавался при открытии страницы авторизации. Поэтому вопрос его сохранения уже решен.

Вариант с ошибкой авторизации рассматривать не будем, тут можно показать Toast или Snackbar.

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

Под капотом в методе performTokenRequest происходит запуск устаревшего AsyncTask, поэтому API построен на колбэках.

fun performTokenRequest(
   authService: AuthorizationService,
   tokenRequest: TokenRequest,
   onComplete: () -> Unit,
   onError: () -> Unit
) {
   authService.performTokenRequest(tokenRequest, getClientAuthentication()) { response, ex ->
       when {
           response != null -> {
               //обмен кода на токен произошел успешно, сохраняем токены и завершаем авторизацию
               TokenStorage.accessToken = response.accessToken.orEmpty()
               TokenStorage.refreshToken = response.refreshToken
               onComplete()
           }
           //обмен кода на токен произошел неуспешно, показываем ошибку авторизации
           else -> onError()
       }
   }
}

Интерфейс колбэков можно достаточно просто превратить в suspend-вызов и использовать вместе с корутинами в вашем приложении. Вы можете посмотреть пример в проекте.

При выполнении обмена кода на токен по документации нам требуется отправлять client_secret. Поэтому при вызове метода performTokenRequest требуется передать объект ClientAuthentication. В библиотеке есть несколько имплементаций: ClientSecretBasic, ClientSecretPost, NoClientAuthentication. Выбирать нужно исходя из того, что требует сервер при обмене кода на токен. 

В случае с Github необходимо отправить client_secret следующим образом:

private fun getClientAuthentication(): ClientAuthentication {
   return ClientSecretPost(AuthConfig.CLIENT_SECRET)
}

Если сервис не требует client_secret, то можно использовать ClientSecretBasic("").

На этом мы закончили реализацию авторизации в Github с помощью AppAuth.

Еще раз кратко опишем шаги.

  1. Подключаем библиотеку.

  2. Создаем AuthConfig.

  3. Указываем manifestPlaceholder appAuthRedirectScheme.

  4. Создаем AuthorizationService, например во ViewModel.

  5. Авторизуем пользователя в вебе:

    1. создаем AuthorizationRequest;

    2. формируем intent;

    3. запускаем активити с CCT.

  6. Меняем код на токен:

    1. получаем TokenExchangeRequest из activity result intent;

    2. выполняем TokenExchangeRequest с помощью authService.performTokenRequest;

    3. сохраняем токены в колбеке.

Логаут

Для логаута нам нужно не только почистить токен внутри приложения, но и почистить cookie в браузере. Просто так это сделать не получится, потому что браузер — это внешнее приложение. Для этого нужно открыть страницу, по которой у вас очистятся cookie.

Для гитхаба это https://github.com/logout. Раньше указали в AuthConfig.END_SESSION_URI.

Идея открытия страницы такая же, как для страницы авторизации:

1. Формируем request:

val endSessionRequest = EndSessionRequest.Builder(authServiceConfig)
	//Требуется для некоторых сервисов, idToken получается при авторизации аналогично accessToken и refreshToken
  .setIdTokenHint(idToken) 
  // uri на который произойдет редирект после успешного логаута, не везде поддерживается
  .setPostLogoutRedirectUri(AuthConfig.LOGOUT_CALLBACK_URL.toUri()) 
  .build()

2. Формируем custom tabs intent:

val customTabsIntent = CustomTabsIntent.Builder().build()

3. Формируем итоговый интент: 

val endSessionIntent = authService.getEndSessionRequestIntent(
    endSessionRequest,
    customTabsIntent
)

4. Открываем страницу логаута:

private val logoutResponse = registerForActivityResult(
	ActivityResultContracts.StartActivityForResult()
) {}

logoutResponse.launch(endSessionIntent)

Пользователь переходит на страницу логаута, где чистится его сессия в браузере.




После логаута нам нужно перехватить редирект, чтобы вернуться в приложение. Не все сервисы позволяют указывать URL редиректа после логаута (github не позволяет). Поэтому пользователю нужно будет нажать на крестик в CCT.

После ручного закрытия активити с CCT мы получим result=cancelled, потому что редиректа в приложение не было.

В нашем примере с Github мы будем в любом случае очищать сессию и переходить на страницу входа в приложение.

private val logoutResponse = registerForActivityResult(
	ActivityResultContracts.StartActivityForResult()
) {
   // очищаем сессию и переходим на экран логина
   viewModel.webLogoutComplete()
}

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

При работе с OAuth и библиотекой AppAuth вам, как и всегда, важно поддерживать актуальность ваших токенов. access_token, полученный с сервера, может протухнуть. Для того, чтобы не выбрасывать пользователя на страницу логина, нужно попробовать обновить токен в такой ситуации. Это делается с помощью refresh_token.

Механизм обновления похож на механизм получения token с помощью AppAuth:

1. Формируем request для обновления токена

val refreshRequest = TokenRequest.Builder(
   authServiceConfig,
   AuthConfig.CLIENT_ID
)
   .setGrantType(GrantTypeValues.REFRESH_TOKEN)
   .setScopes(AuthConfig.SCOPE)
   .setRefreshToken(TokenStorage.refreshToken)
   .build()

Тут нам важно учесть 2 строчки:

.setGrantType(GrantTypeValues.REFRESH_TOKEN)
.setRefreshToken(TokenStorage.refreshToken)

В качестве grantType передаем refreshToken, и передаем непосредственно сам refreshToken из вашего хранилища, который был получен при авторизации.

2. Выполняем сформированный request:

authorizationService.performTokenRequest(refreshRequest) { response, ex ->
   when {
       response != null -> emitter.onSuccess(response)
       ex != null -> emitter.tryOnError(ex)
       else -> emitter.tryOnError(IllegalStateException("response and exception is null"))
   }
}

Этот код можно внедрить в те места, где у вас происходит обновление токена в проекте. Например, в OkHttp interceptor. Полный пример можно взять в репозитории по ссылке.

Если обновление токена произошло с ошибкой (например, refresh_token невалидный), необходимо разлогинить пользователя.
См. пример с логаутом.

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

Заключение

Код проекта в статье находится в моем репозитории на GitHub.

Мы рассмотрели нюансы реализации OAuth в мобильных приложениях и пример реализации в Android-приложении с помощью библиотеки AppAuth. Эта реализация позволит вам быстро засетапить OAuth в вашем приложении.

По нашему опыту, AppAuth позволяет упростить работу с OAuth в приложении, избавляя вас от написания деталей имплементаций. Однако она предъявляет требования к серверу авторизации. Если сервис не соответствует RFC-8252 (OAuth 2.0 for Native Apps),  возможно, AppAuth покроет не весь требуемый функционал.

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

Комментарии (0)