Привет! Меня зовут Мялкин Максим, я занимаюсь мобильной разработкой в KTS.
Ни один сервис не обходится без логина. Часто в мобильных приложениях требуется интегрировать вход через сторонние соцсети — например, зайти через Google или VK. А при обучении мобильной разработке используются открытые API, где для авторизации используется OAuth.
Поэтому разработчикам мобильных приложений приходится работать с OAuth. В сети по этой теме есть разные материалы.
В этой статье я попробую структурированно закрыть нюансы OAuth в мобильных приложениях: на какие моменты стоит обратить внимание, какие способы реализации выбрать. А также поделюсь опытом настройки OAuth в Android-приложении с использованием библиотеки AppAuth.
OAuth и flow
Принцип работы Authorization Code Flow with PKCE
MITM для перехвата кода
Нюансы реализации
Как открывать страницу логина
WebView
Browser
ChromeCustomTabs, SafariVC
Редирект в Chrome не срабатывает
Обновление токенов
Браузер отсутствует
Логаут
Варианты реализации OAuth
Использовать SDK сервиса, через который вы хотите авторизоваться
Реализовать вручную
Использовать библиотеки
Реализация в Android-приложении
Общая настройка
Реализация в Android
Авторизация
Логаут
Обновление токена
Заключение
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. Сначала вспомним его реализацию:
Пользователь жмет кнопку Login
Приложение создает ссылку для авторизации на сервисе авторизации и открывает его в браузере
Пользователь видит экран с полями для ввода логина/пароля
Пользователь вводит логин/пароль и подтверждает необходимые доступы к данным
Сервис авторизации возвращает код авторизации в приложение с помощью редиректа. С кодом получить доступ к требуемым ресурсам из апи пока не получится. Чтобы редирект был перехвачен только приложением, обычно используются кастомные схемы, а не http(s). Иначе код может перехватить еще и браузер — в этом случае появляется окно выбора приложений
Приложение получает код из URL редиректа и обменивает код на токен. Дополнительно могут передаваться
client_id
,client_secret
Сервис авторизации возвращает
access_token
для доступа к ресурсам,refresh_token
Приложение с помощью полученного токена общается с сервисом API
При использовании Authorization Code Flow with PKCE cхема немного меняется. Отличия выделены.
Пользователь жмет кнопку Login
Генерируются code_verifier и code_challenge и сохраняются в приложении. Как происходит генерация, описано в RFC-7636.
code_challenge
является производным отcode_verifier
, обратная трансформация невозможнаПриложение создает ссылку для авторизации с учетом сгенерированного code_challenge. Ссылка открывается в браузере. В этот момент сервис авторизации тоже запоминает code_challenge для сессии. Таким образом, code_verifier остается только внутри приложения и не передается по сети
Пользователь видит экран с полями для ввода логина/пароля
Пользователь вводит логин/пароль
Сервис авторизации возвращает код авторизации в приложение с помощью редиректа. Обратите внимание, что
code_challenge
не возвращается от сервера вместе с кодом. О нем знают только сервис авторизации и мобильное приложениеПриложение обменивает код на токен. При обмене приложение отправляет code_verifier, который был сохранен в пункте 2
Сервис авторизации принимает code_verifier от мобильного приложения. Вычисляет от него code_challenge и сравнивает с code_challenge, переданным в пункте 3. Если они совпадают — возвращается токен
Сервис авторизации возвращает
access_token
для доступа к ресурсам,refresh_token
Приложение с помощью полученного токена общается с сервисом 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.
Пример: https://gist.github.com/MaxMyalkin/e30770d18f0201e4a68cafe23c3a3b2e
— 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: https://github.com/openid/AppAuth-iOS
AppAuth Android: https://github.com/openid/AppAuth-Android
Auth0 Android: https://github.com/auth0/Auth0.Android
При использовании этого подхода нужно убедиться, что сервер аутентификации работает в соответствии с протоколом, и вам не придется костылить библиотеку, чтобы связаться с ним
Если разобраться с библиотекой и знать, как она работает, реализация получается достаточно простой. Но на это требуется время
Реализация авторизации будет универсальная для разных сервисов, не придется подключать дополнительные зависимости и писать много кода для каждого внешнего сервиса, если таких несколько
Учтите, что реализация библиотеки может быть не совсем удобной для встраивания в ваше приложение. Используемые подходы общения с библиотекой могут отличаться от принятых в команде, и нужно будет писать обертки-бриджи. Пример: AppAuth в Android использует AsyncTask под капотом, но в приложении вы, скорее всего, используете корутины. Но обычно такие вещи можно интегрировать
В дальнейшем в статье мы рассмотрим реализацию входа с использованием библиотеки AppAuth. Тому есть несколько причин:
это достаточно популярная библиотека
ее рекомендует Google для реализации OAuth
у нее есть реализации и для Android и для iOS
Реализация в 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>
Также вы можете настроить редирект с использованием стандартных схем. Более детально можно прочитать в описании к репозиторию https://github.com/openid/AppAuth-Android.
Теперь мы можем открыть страницу логина:
Дальше необходимо получить код и обменять его на токен. При этом в ответе может прийти ошибка авторизации, ее тоже нужно обработать.
Библиотека 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.
Еще раз кратко опишем шаги.
Подключаем библиотеку
Создаем AuthConfig
Указываем
manifestPlaceholder appAuthRedirectScheme
Создаем AuthorizationService, например во ViewModel
Авторизуем пользователя в вебе
создаем AuthorizationRequest
формируем intent
запускаем активити с CCT
Меняем код на токен
получаем
TokenExchangeRequest
изactivity result intent
выполняем
TokenExchangeRequest
с помощьюauthService.performTokenRequest
сохраняем токены в колбеке
Логаут
Для логаута нам нужно не только почистить токен внутри приложения, но и почистить 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?