Привет! Меня зовут Мялкин Максим, я занимаюсь мобильной разработкой в 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. Сначала вспомним его реализацию:
Пользователь жмет кнопку 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.
Пример: ссылка на 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 и для 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>
Также вы можете настроить редирект с использованием стандартных схем. Более детально можно прочитать в описании к репозиторию.
Теперь мы можем открыть страницу логина:
Дальше необходимо получить код и обменять его на токен. При этом в ответе может прийти ошибка авторизации, ее тоже нужно обработать.
Библиотека 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?
Другие статьи по Android-разработке для начинающих:
Статья про OAuth, из которой узнаете, на какие моменты стоит обратить внимание, какие способы реализации выбрать
Но это (не)точно: чего ждать мобильным разработчикам в 2023-м году
Другие статьи по Android-разработке для продвинутых:
Этапы работы во фреймворке Jetpack Compose, который упрощает написание и обновление визуального интерфейса приложения