Авторизация ВКонтакте через WebView в Android приложении
Здравствуй дорогой друг, в этой статье, на простом примере мы рассмотрим, каким образом можно реализовать авторизацию и использование api социальной сети «ВКонтакте» без подключения официального SDK. Пример приложения можно скачать на github по ссылке в конце статьи.
Создаем проект, подключаем зависимости
В проекте я буду использовать kotlin, mvvm, binding, navgraph подразумевается, что ты уже знаешь, что это такое 🙂
Создаем новый проект на основе Empty Activity, я назову его OAuthWithVK_Example
Создание нового проекта


Добавляем в зависимости.
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1' implementation 'androidx.navigation:navigation-ui-ktx:2.4.1' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
Создаем необходимые классы и файлы
Создадим класс «App» расширяющий «Application», он будет представлять наше приложение и содержать экземпляр «AccountService» для хранения токена и экземпляр «Retrofit» с url для запросов к ВК api. Через companion object будем получать доступ к App и созданным экземплярам. По хорошему это нужно делать через DI, но для простоты примера сделаем так.
/** * Представляет приложение. */ class App : Application() < /** * Возвращает или устанавливает сервис хранения настроект. */ lateinit var accountService: IAccountService /** * Возвращает или устанавливает экземпляр ретрофита. */ lateinit var retrofit: Retrofit companion object < lateinit var application: App >override fun onCreate() < super.onCreate() application = this accountService = VKAccountService(getSharedPreferences("vk_account", MODE_PRIVATE)) retrofit = Retrofit.Builder() .baseUrl("https://api.vk.com/method/") .addConverterFactory(ScalarsConverterFactory.create()) .build() >>
Создадим интерфейс «IAccountService» и его реализацию «VKAccountService», сервис будет предоставлять возможность сохранять и получать token и userId.
/** * Определяет интерфейс получения и установки параметров аккаунта. */ interface IAccountService < /** * Возвращает или устанавливает токен. */ var token: String? /** * Возвращает или устанавливает идентификатор пользователя. */ var userId: String? >
Класс VKAccountService
/** * Представляет сервис сохранения пользовательских настроек. * @param sharedPreference Класс записи пользовательских настроек. */ internal class VKAccountService( private val sharedPreference: SharedPreferences ) : IAccountService < private val TOKEN = "token" private val USER_ID = "userId" companion object < const val SCOPE = "friends,stats" >override var token: String? get() < return sharedPreference.getString(TOKEN, null) >set(value) < with(sharedPreference.edit()) < if (value == null) < remove(TOKEN) >else < putString(TOKEN, value) >apply() > > override var userId: String? get() < return sharedPreference.getString(USER_ID, null) >set(value) < with(sharedPreference.edit()) < if (value == null) < remove(USER_ID) >else < putString(USER_ID, value) >apply() > > >
Создадим класс активити с именем «MainActivity» и соответствующий ему файл разметки «activity_main». Он будет содержать FragmentContainerView для навигации.
/** * Представляет основное активити приложения. */ class MainActivity : AppCompatActivity() < private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) val navController = (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController appBarConfiguration = AppBarConfiguration(navController.graph) setupActionBarWithNavController(navController, appBarConfiguration) >override fun onSupportNavigateUp(): Boolean < val navController = findNavController(R.id.nav_host_fragment) return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() >>
Файл разметки activity_main
Обновим файл манифеста, указав корневое активити.
Для навигации по фрагментам понадобится файл «nav_graph».
Теперь создадим первый класс фрагмента для авторизации, назовем его «AuthFragment». Здесь нам нужен только виджет WebView, который создадим программно. Для открытия окна авторизации нужен url с параметрами, создаем приватное поле с именем «_authParams», оно будет содержать строку с необходимой конфигурацией, далее передадим ее в WebView. В методе onViewCreated будем открывать окно аутентификации, реагировать на события ‘Подтверждение разрешений’, ‘Ошибка ввода логина/пароля’, ‘Успех’ и др. В коде я оставил TODO куда нужно будет вставить ваш client_id приложения, как его получить рассмотрим в конце статьи.
/** * Представляет фрагмент 'Войти в аккаунт'. */ class AuthFragment : Fragment() < private val webview by lazy < WebView(context!!) >private val _authParams = StringBuilder("https://oauth.vk.com/authorize?").apply < append(String.format("%s=%s", URLEncoder.encode("client_id", "UTF-8"), URLEncoder.encode(/*TODO Сюда вставить id приложения созданного в ВК в разделе "Developers"*/, "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("redirect_uri", "UTF-8"), URLEncoder.encode("https://oauth.vk.com/blank.html", "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("display", "UTF-8"), URLEncoder.encode("mobile", "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("scope", "UTF-8"), URLEncoder.encode(VKAccountService.SCOPE, "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("response_type", "UTF-8"), URLEncoder.encode("token", "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("v", "UTF-8"), URLEncoder.encode("5.131", "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("state", "UTF-8"), URLEncoder.encode("12345", "UTF-8")) + "&") append(String.format("%s=%s", URLEncoder.encode("revoke", "UTF-8"), URLEncoder.encode("1", "UTF-8"))) >.toString() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ) = webview override fun onViewCreated(view: View, savedInstanceState: Bundle?) < super.onViewCreated(view, savedInstanceState) if (App.application.accountService.token == null) < webview.webViewClient = AuthWebViewClient(context!!) < status ->when(status) < AuthStatus.AUTH -> < >AuthStatus.CONFIRM -> < >AuthStatus.ERROR -> < Toast.makeText(context, "Не верный логин или пароль", Toast.LENGTH_LONG).show() >AuthStatus.BLOCKED -> < showAuthWindow() Toast.makeText(context, "Аккаунт заблокирован", Toast.LENGTH_LONG).show() >AuthStatus.SUCCESS -> < val url = webview.url!! val tokenMather = Pattern.compile("access_token=\\w+").matcher(url) val userIdMather = Pattern.compile("user_id=\\w+").matcher(url) // Если есть совпадение с патерном. if (tokenMather.find() && userIdMather.find()) < val token = tokenMather.group().replace("access_token=".toRegex(), "") val userId = userIdMather.group().replace("user_id=".toRegex(), "") // Если токен и id получен. if (token.isNotEmpty() && userId.isNotEmpty()) < App.application.accountService.token = token App.application.accountService.userId = userId navigateToInfo() >> > > > > else < navigateToInfo() >> override fun onStart() < super.onStart() if (App.application.accountService.token == null) < showAuthWindow() >> private fun showAuthWindow() < CookieManager.getInstance().removeAllCookies(null) webview.loadUrl(_authParams) >private fun navigateToInfo() < findNavController().navigate(R.id.action_AuthFragment_to_InfoFragment) >>
В зависимости от того какое событие сейчас происходит (ввод пароля, ошибка, заблокированный аккаунт), текущий url у WebView будет изменяться, на основе этого будем определять текущий статус аутентификации. Для этого создадим класс «AuthWebViewClient» расширяющий «WebViewClient», переопределим метод onPageFinished в котором будем парсить текущую открытую ссылку.
/** * Представляет WebView клиент. * @param context Контекст. * @param onStatusChange Обработчик смены статуса аутентификации. */ class AuthWebViewClient( private val context: Context, private val onStatusChange: (status: AuthStatus) -> Unit ) : WebViewClient() < private var _currentUrl = "" override fun shouldOverrideUrlLoading(wv: WebView, url: String): Boolean < wv.loadUrl(url) return true >override fun onPageFinished(wv: WebView, url: String) < if (_currentUrl != url) < val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager //если открыто окно аутентификации. if (url.contains("https://oauth.vk.com/authorize")) < val scope = URLEncoder.encode(VKAccountService.SCOPE, "UTF-8") // Если открыто окно ввода логина и пароля. if (url.contains(scope)) < imm.showSoftInput(wv, 0) wv.visibility = View.VISIBLE onStatusChange(AuthStatus.AUTH) >// Если открыто окно подтверждения разрешений. if (url.contains("q_hash")) < onStatusChange(AuthStatus.CONFIRM) >// Если открыто окно с сообщением об неверно введеном пароле. if (url.contains("email")) < onStatusChange(AuthStatus.ERROR) >> // Если открыто окно заблокированного пользователя. if (url.contains("https://m.vk.com/login\\?act=blocked")) < onStatusChange(AuthStatus.BLOCKED) >// Если открыто окно для считывания токена. if (url.contains("https://oauth.vk.com/blank.html")) < wv.visibility = View.INVISIBLE onStatusChange(AuthStatus.SUCCESS) >> _currentUrl = url > >
Перечислим статусы аутентификации в enum, который назовем «AuthStatus», этот enum будем передаваться кэлбеком из класса AuthWebViewClient во фрагмент.
/** * Перечисляет статусы аутентификации клиента. */ enum class AuthStatus < /** * Статус ввода логина и пароля. */ AUTH, /** * Статус подтверждения разрешений. */ CONFIRM, /** * Статус завершения авторизации с ошибкой. */ ERROR, /** * Статус заблокированного пользователя. */ BLOCKED, /** * Статус успешного завершения авторизации. */ SUCCESS >
После верного ввода логина/пароля и подтверждения разрешений, будет получен и записан в память токен и идентификатор пользователя. С фрагментом аутентификации на этом все.
Приступим к созданию второго фрагмента, здесь мы сделаем всего 1 запрос на получение списка друзей. На экране покажем кнопку для выхода, textview для показа кол-ва друзей и скролящийся textview для показа списка друзей.
Создадим фрагмент с именем «InfoFragment» и соответствующий ему xml файл с разметкой «info_fragment».
/** * Представляет фрагмент 'Инфо'. */ class InfoFragment : Fragment() < private val _viewModel: InfoViewModel by viewModels() private var _binding: InfoFragmentBinding? = null private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View < _binding = InfoFragmentBinding.inflate(inflater, container, false) return binding.root >override fun onViewCreated(view: View, savedInstanceState: Bundle?) < super.onViewCreated(view, savedInstanceState) with(binding) < lifecycleOwner = this@InfoFragment.viewLifecycleOwner vm = _viewModel tvFriends.movementMethod = ScrollingMovementMethod() logout.setOnClickListener < App.application.accountService.token = null App.application.accountService.userId = null findNavController().navigate(R.id.action_InfoFragment_to_AuthFragment) >> > override fun onDestroyView() < super.onDestroyView() _binding = null >>
Файл разметки info_fragment
'/> " android:layout_marginVertical="16dp" android:scrollbars="vertical"/>
Запрос на список друзей будем делать во ViewModel, эту вью модель передадим в биндинг, LiveData сама будет устанавливать данные в TextView.
/** * Определяет модель представления фрагмента 'Инфо'. */ class InfoViewModel: ViewModel() < private val _count = MutableLiveData() private val _friends = MutableLiveData() /** * Возвращает кол-во друзей. */ val count: LiveData = _count /** * Возвращет список друзей. */ val friends: LiveData = _friends init < viewModelScope.launch < val response = App.application.retrofit.create(FriendsGetRequest::class.java).friendsGet( App.application.accountService.token. "5.131", "name" ) val friendsList = StringBuilder() val items = JSONObject(response).getJSONObject("response").getJSONArray("items") for (i in 0 until items.length()) < friendsList.append( "$$\n" ) > _count.postValue(JSONObject(response).getJSONObject("response").getString("count")) _friends.postValue(friendsList.toString()) > > >
Осталось написать интерфейс «FriendsGetRequest» с запросом для ретрофит и на этом с программной частью будем заканчивать 🙂
/** * Определяет запрос друзей пользователя. */ interface FriendsGetRequest < /** * Возвращает json со списком друзей. */ @GET("friends.get") suspend fun friendsGet( @Query("access_token") token: String, @Query("v") v: String, @Query("fields") fields: String ): String >
Теперь разберемся, как получить client_id, это один из параметров запроса на авторизацию, его выдает ВК для понимания, какое приложение собирается обращаться к его api. Что бы его получить зайдите на свою страницу ВК и найдите меню «Управление», если его нет в списке, нужно добавить его в настройках страницы.

Кликнув по меню «Управление» мы попадем в раздел «Мои приложения», для создания нового приложения нажмите кнопку «Создать»
Раздел «Мои приложения»

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

Когда приложение будет создано перейдите в меню «Настройки», там будет указан client_id, который нужно вставить в код на место TODO, все остальные настройки по желанию 🙂
Меню настройки приложения
OAuth в мобильных приложениях

Привет! Меня зовут Мялкин Максим, я занимаюсь мобильной разработкой в KTS.
Ни один сервис не обходится без логина. Часто в мобильных приложениях требуется интегрировать вход через сторонние соцсети — например, зайти через Google или VK. А при обучении мобильной разработке используются открытые API, где для авторизации используется OAuth.
Поэтому разработчикам мобильных приложений приходится работать с OAuth. В сети по этой теме есть разные материалы.
В этой статье я попробую структурированно закрыть нюансы OAuth в мобильных приложениях: на какие моменты стоит обратить внимание, какие способы реализации выбрать. А также поделюсь опытом настройки OAuth в Android-приложении с использованием библиотеки AppAuth.
- OAuth и flow
- Принцип работы Authorization Code Flow with PKCE
- MITM для перехвата кода
- Нюансы реализации
- Как открывать страницу логина
- WebView
- Browser
- ChromeCustomTabs, SafariVC
- Использовать SDK сервиса, через который вы хотите авторизоваться
- Реализовать вручную
- Использовать библиотеки
- Общая настройка
- Реализация в 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.
Недостатки браузера тоже частично закрываются:
- 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 и всех остальных современных браузерах. Но даже если такового у пользователя нет, откроется обычный браузер.
Логаут
В большинстве случаев при пользовательском логауте в приложении нужно почистить токены/файлы/БД/кеши.
Если же для авторизации вы используете ссt/safarivc, потом в браузере остаются куки авторизованного человека. При повторном логине вы заново войдете под первым аккаунтом автоматически. Почистить cookie из приложения не получится, потому что браузер — это отдельный процесс со своим хранилищем, и доступ к нему запрещен.
Чтобы разлогиниться, необходимо открыть страницу в браузере, по которой сервер авторизации почистит куки и перенаправит вас в приложение обратно.
Варианты реализации OAuth
Мы рассмотрели OAuth flow для мобильных приложений и увидели, на какие нюансы стоит обратить внимание при реализации.
Существует несколько вариантов реализации.
Использовать SDK сервиса, через который вы хотите авторизоваться
Плюсы:
- простая реализация;
- возможна авторизация через нативные приложения, если они установлены.
Минусы:
- увеличение внешних зависимостей, особенно при большом количестве внешних сервисов;
- нет контроля над реализацией.
Использование SDK мы рассматривать в текущей статье не будем, потому что для этого нужно изучать документацию SDK.
Реализовать вручную
Реализовать логику вручную внутри собственного приложения с использованием WebView или других реализаций (CCT/SafariVC).
Плюс:
- получаем полный контроль над реализацией.
Минус:
- приходится писать свой код, поддерживать его и учитывать вручную нюансы, о которых говорили выше.
Ручную реализацию мы рассматривать не будем, потому что она индивидуальна для каждого приложения и сервиса.
Использовать библиотеки
Библиотеки должны поддерживать протоколы OAuth и OpenId и позволять общаться с любыми сервисами по этим протоколам. Примеры:
AppAuth IOS
AppAuth Android
Auth0 Android- При использовании этого подхода нужно убедиться, что сервер аутентификации работает в соответствии с протоколом, и вам не придется костылить библиотеку, чтобы связаться с ним.
- Если разобраться с библиотекой и знать, как она работает, реализация получается достаточно простой. Но на это требуется время.
- Реализация авторизации будет универсальная для разных сервисов, не придется подключать дополнительные зависимости и писать много кода для каждого внешнего сервиса, если таких несколько.
- Учтите, что реализация библиотеки может быть не совсем удобной для встраивания в ваше приложение. Используемые подходы общения с библиотекой могут отличаться от принятых в команде, и нужно будет писать обертки-бриджи. Пример: AppAuth в Android использует AsyncTask под капотом, но в приложении вы, скорее всего, используете корутины. Но обычно такие вещи можно интегрировать.
В дальнейшем в статье мы рассмотрим реализацию входа с использованием библиотеки AppAuth. Тому есть несколько причин:
- это достаточно популярная библиотека;
- ее рекомендует Google для реализации OAuth;
- у нее есть реализации и для Android и для iOS.
Реализация в Android-приложении
Давайте посмотрим, как можно реализовать OAuth в вашем Android-приложении с использованием AppAuth. Весь код доступен на Github.
Приложение простое: отображение информации о моем github-профиле.
Для этого при каждом запуске приложения будем открывать страницу github-авторизации. После успешной авторизации переводим пользователя на главную страницу, откуда можно получить информацию о текущем пользователе.
При реализации нам необходимо разобраться с 3 ключевыми моментами:
- авторизация пользователя;
- обновление токена;
- логаут пользователя.
Общая настройка
При регистрации установите CALLBACK_URL для вашего приложения на сервисе. На этот URL будет происходить перенаправление после авторизации, и ваше приложение будет его перехватывать.
В качестве CALLBACK_URL будем использовать ru.kts.oauth://github.com/callback
Не забывайте использовать кастомную схему ru.kts.oauth , чтобы только ваше приложение могло перехватить редирект.
После регистрации у вас должны быть доступны client_id и client_secret (его нужно сгенерировать). Сохраните их.
Дальше нужно понять, на какой URL нужно переходить для авторизации на веб-странице Github, и по какому обменивать код на токен. Ответ можно найти в документации по Github OAuth.
Для авторизации нам нужно определить скоупы, к которым 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 экрана авторизации.
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
// тут можно настроить вид 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:
Также вы можете настроить редирект с использованием стандартных схем. Более детально можно прочитать в описании к репозиторию.

Теперь мы можем открыть страницу логина:
Дальше необходимо получить код и обменять его на токен. При этом в ответе может прийти ошибка авторизации, ее тоже нужно обработать.
Библиотека 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
Если сервис не требует 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, из которой узнаете, на какие моменты стоит обратить внимание, какие способы реализации выбрать
- Список фишек Android Studio для ускорения разработки
- Но это (не)точно: чего ждать мобильным разработчикам в 2023-м году
Другие статьи по Android-разработке для продвинутых:
- Этапы работы во фреймворке Jetpack Compose, который упрощает написание и обновление визуального интерфейса приложения
- Реализуем режим edge-to-edge, используя знания Window Inset
- KMM глазами iOS-разработчика
Android SDK
Android SDK позволяет добавить карту 2GIS в ваше Android-приложение. С помощью этого SDK можно отобразить карту на экране, добавить на неё маркеры, рисовать геометрические фигуры, строить маршруты, получать информацию об объектах, управлять камерой и так далее.
Примеры использования SDK можно найти в разделе Примеры. Описание всех классов и методов можно найти в разделе Справочник API.
Картографические данные поддерживают стандарты OGC.
Получение ключей доступа
Для работы с SDK нужно получить файл ключа dgissdk.key с обязательным указанием appId приложения, для которого этот ключ создается. Ключ будет использоваться для подключения к серверам 2GIS, получения географических данных, а также для использования офлайна и навигатора. Этот ключ уникален для этого типа SDK и не может быть использован с другими SDK от 2GIS.
Чтобы получить файл ключа, заполните форму на dev.2gis.ru.
Полученный файл ключа нужно добавить в assets приложения.
Установка
Android SDK доступен в двух версиях: полной и облегченной. Облегченная версия не включает навигатор и не позволяет строить маршруты.
Для установки SDK:
- Укажите пользовательский репозиторий в вашем файле build.gradle .
repositories < maven < url "https://artifactory.2gis.dev/sdk-maven-release" >>- Добавьте одну из следующих зависимостей.
Для получения облегченной версии SDK:
dependencies
Для получения полной версии SDK:
dependencies
Демонстрационный проект
Демонстрационное приложение с исходным кодом можно найти в нашем GitHub-репозитории.
Размер приложения
Размер вашего приложения может заметно увеличиться после добавления SDK в проект. Далее рассмотрим причину и возможные стратегии для борьбы с этим увеличением.
Процессорные архитектуры
Современные устройства Android могут иметь различные типы процессоров — x86, x86_64, armv7 и armv8. Для кода, написанного на Java или Kotlin, это не является проблемой, так как он исполняется в виртуальной машине и абстрагируется от конкретного железа. Однако, для нативного кода тип процессора играет важную роль.
Поэтому наша библиотека содержит 4 версии для всех типов ABI (Application Binary Interface), чтобы обеспечить поддержку всех этих типов процессоров.
Оптимизация размера
Чтобы сократить размер приложения, можно использовать разделение APK по типам ABI. Это позволит собирать и загружать только необходимые библиотеки для конкретного устройства пользователя. Инструкции по этому поводу можно найти в документации Android. Также, если вы используете App Bundle, Google Play может автоматически пересобирать приложение под нужные типы процессоров для конкретных устройств пользователей.
Проверка размера
Чтобы увидеть фактический размер APK, который будет загружаться пользователем, рекомендуем использовать инструмент bundletool. Это поможет получить APK, содержащий только необходимые библиотеки для конкретного устройства.
- Собрать app bundle, для этого нужно выбрать этот вариант сборки в Gradle.
- Из app bundle с помощью bundletool получить apk по необходимым критерями (например, только для arm64-v8a).
Полученный apk будет отражать реальный размер, который получит пользователь при загрузке приложения из Google Play.
Как создать пароль для приложения
Почтовые клиенты Яндекс.Почта, GMail и Mail.ru позволяют сторонним сервисам получить доступ к почтовому ящику, не получая данные аккаунта. Это нужно, чтобы обезопасить ваш почтовый аккаунт и защитить личные данные.
Такой доступ задается с помощью функции «Пароль для приложения». Вам нужно будет задать специальный пароль, который даст нашему приложению доступ к вашему почтовому ящику. Сейчас расскажем, как это сделать.
Использование пароля для приложения — обязательное условие для подключения Емейлтрекинга в ЛК UIS. Только в том случае, если ваш почтовый сервис не поддерживает пароли данного типа и не имеет функции задания пароля для приложения, используйте обычный пароль.
При создании пароля для приложения, ваш текущий пароль от почтового ящика остается неизменным. Но если вы измените свой обычный пароль, то вам также придется пересоздать пароль для приложения и обновить его в личном кабинете UIS.
Яндекс.Почта
1. Чтобы создать пароль для приложения в Яндекс Почте перейдите на страницу управления доступом в вашем аккаунте (ящика, который вы собираетесь подключить) и включите опцию «Пароли приложений». Убедитесь, что у вас включена двухфакторная аутентификация.

2. После включения этой опции у вас появится кнопка «Получить пароль приложения». Нажмите на нее.
3. Затем в поле «Тип приложения» выберите «Почта», в поле «Операционная система» выберите «Другое» и впишите UIS. В поле «Название программы» тоже впишите UIS.

Важно
Не закрывайте это окно, не скопировав пароль!

4. Скопируйте пароль и впишите его в поле «Пароль» окна добавления учетных данных.

Важно
Обязательно подключите протокол IMAP в настройках личного кабинета Яндекс.Почты.

GMail
1. Откройте страницу безопасности вашего аккаунта Google, в котором зарегистрирован email.
2. Убедитесь, что у вас включена опция двухэтапной аутентификации. Без нее задать пароль для приложения не получится.

3. Кликните на поле «Двухэтапная аутентификация». В самом низу открывшейся страницы вы увидите пункт «Пароли для приложений». Откройте его.

4. В поле «App name» впишите название приложения — UIS и нажмите на кнопку создания пароля.
5. Во всплывающем окне появится пароль, который нужно будет ввести в поле добавления учетных данных. Не закрывайте всплывающее окно, не скопировав пароль! Гугл показывает пароль от приложения только один раз.
- Как открывать страницу логина