Главное Авторские колонки Вакансии Вопросы
52 0 В избр. Сохранено
Авторизуйтесь
Вход с паролем

OAuth 2.0 в мобильном приложении: чек-лист для iOS, Android и сервера

OAuth 2.0 в мобильном приложении: чек-лист для iOS, Android и сервера Никто не любит разбирать авторизацию уже после релиза. Пользователь нажал «Войти», браузер уехал не туда, access_token истёк посре
Мнение автора может не совпадать с мнением редакции

OAuth 2.0 в мобильном приложении: чек-лист для iOS, Android и сервера Никто не любит разбирать авторизацию уже после релиза. Пользователь нажал «Войти», браузер уехал не туда, access_token истёк посреди покупки, refresh_token пропал после переустановки, а logout вроде бы очистил экран, но сессию на сервере не тронул. Я не раз видел такие истории на проектах, и после пары таких случаев начинаешь воспринимать OAuth 2.0 не как «добавим кнопку входа», а как набор очень конкретных договорённостей между мобильным клиентом, backend и провайдером авторизации. Материал — прикладная шпаргалка для мобильных разработчиков, QA, аналитиков, backend-разработчиков и тимлидов. Её удобно открывать прямо на ревью требований: пройтись по вопросам и заранее зафиксировать, что приложение делает на iOS и Android в happy path, при ошибках, плохой сети, смене пароля и отзыве доступа. С чего начать OAuth 2.0 описывает роли, но в мобильной разработке полезнее сразу перевести их в части проекта: | Роль OAuth 2.0 | Что это в мобильном проекте | Что может сломаться | |—-|—-|—-| | Resource Owner | Пользователь | Отменил вход, сменил пароль, отозвал доступ | | Client | Мобильное приложение | Нельзя безопасно хранить client_secret как у confidential client | | Authorization Server | Сервер авторизации | Не совпал redirect URI, не поддержан PKCE | | Resource Server | API приложения | Принял истёкший токен, вернул не тот код ошибки | | Access Token | Короткоживущий токен доступа | Истёк между двумя API-запросами | | Refresh Token | Токен для обновления доступа | Утёк, был отозван, протух, перезаписался | Главная договорённость здесь простая: мобильное приложение — это public client. Его бинарник можно разобрать, поэтому секрет, зашитый в приложение, секретом считать нельзя. Для нативных приложений актуальная рекомендация из RFC 8252 — использовать Authorization Code Flow с PKCE и внешний user-agent: системный браузер, Custom Tabs на Android или ASWebAuthenticationSession на iOS. Выбор flow Первый вопрос к требованиям: какой OAuth flow используем? Для мобильного приложения нормальный вариант выглядит так: 1. Приложение открывает системный браузер с URL авторизации. 2. Пользователь проходит логин и consent screen у провайдера. 3. Authorization server возвращает пользователя в приложение через redirect URI. 4. Приложение получает authorization code. 5. Приложение обменивает code на access_token и, если разрешено, refresh_token. 6. Для защиты обмена используется PKCE: code_verifier и code_challenge. Что нужно зафиксировать до разработки: Поддерживает ли провайдер Authorization Code + PKCE? Какой метод PKCE используется: S256 или plain? Для новых интеграций лучше требовать S256. Будет ли выдаваться refresh_token мобильному клиенту? Какой срок жизни у access_token? Есть ли rotation для refresh_token, когда при каждом обновлении выдаётся новый refresh token? Какие scope реально нужны приложению? Что сервер возвращает при invalid_grant, invalid_token, interaction_required? Мы сами однажды пытались описать авторизацию одной строкой в ТЗ: «Вход через OAuth 2.0». Не сработало вообще. Для разработчика и тестировщика такая формулировка не отвечает почти ни на один вопрос: где открывается логин, как обновляется токен, что делать после отмены, какие ошибки показывать пользователю. Браузер или WebView В мобильных приложениях часто возникает соблазн встроить страницу логина в WebView: всё под контролем, можно подогнать внешний вид, не нужно уводить пользователя в браузер. Для OAuth это плохая идея. RFC 8252 рекомендует нативным приложениям использовать внешний user-agent, потому что пароль пользователь вводит не в приложение, а в доверенный контекст провайдера. На iOS это обычно ASWebAuthenticationSession, на Android — браузер или Chrome Custom Tabs. Библиотека AppAuth как раз построена вокруг этого подхода. Вопросы к требованиям: Открываем авторизацию только во внешнем user-agent? Что делаем, если на устройстве нет подходящего браузера? Нужно ли сохранять SSO-сессию между приложениями одного провайдера? Как пользователь понимает, что вводит пароль на стороне провайдера, а не внутри нашего приложения? Что происходит, если пользователь закрыл браузер кнопкой «Отмена» или системным жестом? Типовой баг здесь довольно приземлённый: приложение ждёт callback бесконечно, потому что пользователь закрыл браузер, а команда не описала сценарий отмены. На экране остаётся spinner, QA заводит баг «зависает вход», а backend к этому вообще не относится. Redirect URI и deep links Redirect — это место, где мобильная авторизация ломается чаще, чем хочется. Есть два распространённых подхода: | Вариант | Пример | Плюсы | Риски | |—-|—-|—-|—-| | Custom scheme | com.example.app:/oauth2redirect | Просто настроить | Другое приложение может зарегистрировать такой же scheme | | Claimed HTTPS redirect | https://auth.example.com/callback | Лучше привязка к домену через App Links / Universal Links | Нужна настройка сайта, assetlinks.json, AASA | Что нужно проверить: Redirect URI на клиенте байт-в-байт совпадает с тем, что зарегистрирован на authorization server? На Android настроены intent filters для нужного scheme/host/path? На iOS настроены URL Types или Associated Domains? Что будет, если приложение не установлено, а пользователь открыл redirect-ссылку? Может ли staging-приложение случайно поймать production redirect? Разделены ли redirect URI для dev, stage и prod? Для Android пример intent filter для custom scheme может выглядеть так: xml Если используется HTTPS redirect, для Android App Links дополнительно нужна привязка домена через Digital Asset Links, а для iOS Universal Links — файл apple-app-site-association. Это не «мобильная мелочь», а часть OAuth-сценария: без неё пользователь может не вернуться в приложение. Токены и хранение Access token — не пароль, но относиться к нему нужно как к секрету. Он даёт доступ к ресурсам до истечения срока жизни или отзыва. Вопросы к требованиям: Где храним access_token? Где храним refresh_token? Что делаем, если secure storage недоступен или сброшен? Нужно ли хранить токены после logout? Как приложение ведёт себя после восстановления из backup на новом устройстве? Можно ли использовать биометрию для разблокировки локального доступа к токену? Для iOS базовый вариант — Keychain. Для Android — EncryptedSharedPreferences или собственное хранилище на базе Android Keystore. Обычные SharedPreferences, plist или SQLite без шифрования для refresh token не подходят. Но и secure storage не закрывает все риски. Если устройство скомпрометировано, если пользователь поставил вредоносную клавиатуру, если приложение пишет токены в логи — Keychain и Keystore не спасут. Поэтому отдельно стоит проверять: не попадают ли токены в crash reports; не логируются ли заголовки Authorization; не сохраняются ли токены в analytics events; не отображаются ли токены в debug UI; не уезжают ли токены в backup. Refresh token access_token обычно живёт недолго. Чтобы пользователь не логинился каждые несколько минут, мобильное приложение использует refresh_token, если authorization server его выдаёт. Алгоритм обновления лучше описывать прямо: 1. API-запрос получает 401 Unauthorized. 2. Клиент проверяет, есть ли сохранённый refresh_token. 3. Если refresh уже выполняется, остальные запросы ждут его результат. 4. Клиент отправляет запрос на token endpoint с grant_type=refresh_token. 5. Если сервер вернул новый access_token, клиент повторяет исходный запрос один раз. 6. Если сервер вернул invalid_grant, клиент очищает локальную сессию и переводит пользователя на логин. 7. Если сеть недоступна, клиент не удаляет токены сразу, а показывает ошибку сети. Пример token refresh на Python ниже не претендует на замену мобильной библиотеки. Он показывает серверный контракт и обработку ошибок, которые мобильная команда должна ожидать от token endpoint. python import time from dataclasses import dataclass from typing import Any import requests @dataclass class TokenSet: access_token: str refresh_token: str expires_at: int class OAuthTokenError(Exception): def __init__(self, error: str, description: str) -> None: super().__init__(f"{error}: {description}«) self.error = error self.description = description def refresh_access_token( token_endpoint: str, client_id: str, refresh_token: str, timeout_seconds: int = 10, ) -> TokenSet: response = requests.post( token_endpoint, data={ «grant_type»: «refresh_token», «client_id»: client_id, «refresh_token»: refresh_token, }, headers={ «Accept»: «application/json», «Content-Type»: «application/x-www-form-urlencoded», }, timeout=timeout_seconds, ) payload: dict[str, Any] = response.json() if response.status_code != 200: error = str(payload.get("error«, «unknown_error»)) description = str(payload.get("error_description«, «Token refresh failed»)) raise OAuthTokenError(error, description) access_token = str(payload["access_token"]) next_refresh_token = str(payload.get("refresh_token", refresh_token)) expires_in = int(payload.get("expires_in«, 3600)) expires_at = int(time.time()) + expires_in return TokenSet( access_token=access_token, refresh_token=next_refresh_token, expires_at=expires_at, ) if __name__ == «__main__»: try: tokens = refresh_access_token( token_endpoint="https://auth.example.org/oauth/token", client_id="mobile-app", refresh_token="sample-refresh-token", ) print(f"Access token expires at Unix time: {tokens.expires_at}«) except OAuthTokenError as error: if error.error == «invalid_grant»: print("Refresh token is invalid. User must sign in again.") else: print(f"OAuth error: {error}") except requests.Timeout: print("Token endpoint timeout. Keep local session and retry later.") В production вместо sample-refresh-token будет значение из secure storage. Смысл не в самом коде, а в контракте: мобильный клиент должен отличать истёкшую сессию от временной сетевой ошибки. Отдельный вопрос — rotation refresh token. Если сервер выдаёт новый refresh token при каждом обновлении, клиент обязан атомарно заменить старый токен. Иначе легко получить неприятный сценарий: приложение обновило access_token, упало до сохранения нового refresh_token, а старый сервер уже отозвал. AppAuth на Android и iOS Если провайдер совместим с OAuth 2.0 / OpenID Connect, лучше не писать весь flow руками. AppAuth есть для Android и iOS, он поддерживает Authorization Code Flow с PKCE и работу через внешний user-agent. Что нужно согласовать: Используем ли discovery document .well-known/openid-configuration? Если discovery недоступен, кто отвечает за ручную конфигурацию authorization endpoint и token endpoint? Какие параметры передаём дополнительно: prompt, login_hint, audience, acr_values? Нужен ли OpenID Connect id_token или приложению достаточно OAuth access token? Где хранится состояние авторизации: в памяти, secure storage, отдельный session manager? Пример конфигурации AppAuth для Android на Kotlin: kotlin package com.appfox.demo.auth import android.net.Uri import net.openid.appauth.AuthorizationRequest import net.openid.appauth.AuthorizationServiceConfiguration import net.openid.appauth.ResponseTypeValues class OAuthRequestFactory { private val serviceConfig = AuthorizationServiceConfiguration( Uri.parse("https://auth.example.org/oauth/authorize"), Uri.parse("https://auth.example.org/oauth/token«) ) fun createLoginRequest(): AuthorizationRequest { return AuthorizationRequest.Builder( serviceConfig, «mobile-app», ResponseTypeValues.CODE, Uri.parse("com.appfox.demo:/oauth2redirect") ) .setScopes("openid«, «profile», «offline_access») .setCodeVerifier(null) .build() } } В AppAuth PKCE создаётся библиотекой, если не передавать собственный code_verifier. На практике вокруг этого класса ещё нужен слой, который сохраняет AuthState, обрабатывает AuthorizationException, пишет только безопасные логи и синхронизирует обновление токена между параллельными API-запросами. Logout — это не только очистить экран Logout часто описывают слишком коротко: «Пользователь выходит из аккаунта». Для OAuth этого мало. Нужно разделить минимум три состояния: 1. Локальная сессия в приложении. 2. Токены, сохранённые на устройстве. 3. Сессия у authorization server в браузере. Вопросы: При logout удаляем только локальные токены или ещё вызываем revocation endpoint? Нужно ли закрывать SSO-сессию у провайдера? Что происходит, если revocation endpoint недоступен? Пользователь после logout должен увидеть экран выбора аккаунта или сразу войти по старой браузерной сессии? Должны ли push-токены отвязываться от аккаунта? Что делать с локальным кэшем пользовательских данных? Самый неприятный сценарий простой и очень жизненный: пользователь нажал «Выйти», передал телефон другому человеку, тот нажал «Войти» и без ввода пароля попал в прежний аккаунт из-за активной SSO-сессии в браузере. Иногда это ожидаемое поведение, иногда — критичный баг. Его лучше обсудить до реализации. Ошибки и пограничные сценарии OAuth-интеграция проверяется не только успешным входом. На ревью требований мы обычно прогоняем вот такой список. Что должно произойти, если пользователь: отменил авторизацию в браузере; ввёл неправильный пароль; не прошёл MFA; отказался выдавать нужный scope; вернулся в приложение по старому redirect; удалил приложение и установил заново; сменил пароль на другом устройстве; отозвал доступ в настройках провайдера; открыл приложение без интернета; восстановил устройство из backup. Что должно произойти, если сервер: вернул 401 на API-запрос; вернул 403, потому что scope недостаточен; вернул invalid_grant при refresh; вернул temporarily_unavailable; изменил JWKS ключи, если API валидирует JWT; недоступен дольше таймаута клиента; вернул HTML-страницу ошибки вместо JSON. Для QA полезно отдельно фиксировать ожидаемый UI. Например, invalid_grant — это не «что-то пошло не так», а нормальная причина отправить пользователя на повторный вход. А таймаут token endpoint — не повод стирать refresh token. Несколько устройств и смена аккаунта Мобильное приложение редко живёт только на одном устройстве. Пользователь может войти на телефоне и планшете, сменить аккаунт, продать устройство, восстановиться из backup. Вопросы: Разрешены ли одновременные сессии на нескольких устройствах? Видит ли пользователь список активных сессий? Можно ли отозвать конкретное устройство? Что происходит с refresh token при смене пароля? Сохраняются ли push-настройки отдельно для каждого устройства? Как приложение отличает «сменили аккаунт» от «протух токен»? Нужно ли очищать локальные данные при входе другим пользователем? На одном проекте мы поймали баг именно на смене аккаунта: токены обновились, а локальный кэш профиля остался от предыдущего пользователя. API работало корректно, авторизация тоже. Ошибка была в том, что logout не был описан как очистка всех пользовательских слоёв хранения. Биометрия и PIN Face ID, Touch ID и Android BiometricPrompt не заменяют OAuth. Они решают другую задачу: подтверждают локальное действие пользователя на устройстве. Что стоит уточнить: Биометрия открывает приложение или защищает конкретное действие? Что происходит после изменения биометрических настроек на устройстве? Можно ли войти по PIN, если биометрия недоступна? Требуется ли повторный OAuth-login после перезагрузки устройства? Какой fallback нужен для пользователей без биометрии? Безопасная модель обычно такая: OAuth выдаёт токены, токены лежат в secure storage, а биометрия используется как дополнительный локальный барьер перед чтением секрета или выполнением чувствительной операции. Но если backend требует повторную аутентификацию для платежа или изменения пароля, локальной биометрии может быть недостаточно. Социальный логин Соцлогин через Google, Apple или другого провайдера всё равно остаётся OAuth / OpenID Connect-интеграцией со своими нюансами. Вопросы: Какой идентификатор пользователя считаем стабильным? Что делаем, если провайдер не вернул email? Что делаем, если email изменился? Как связываем социальный аккаунт с уже существующим аккаунтом приложения? Можно ли отвязать провайдера, если это единственный способ входа? Как обрабатываем отказ пользователя от нужных разрешений? Для Apple Sign in есть отдельный практический риск: имя пользователя может быть доступно только при первом успешном входе. Если приложение не сохранило его на backend, потом получить эти данные тем же способом может не получиться. Минимальный чек-лист перед разработкой Перед тем как заводить задачи в спринт, полезно иметь короткую таблицу решений. | Вопрос | Решение должно быть зафиксировано | |—-|—-| | Flow | Authorization Code + PKCE | | User-agent | Системный браузер, Custom Tabs или ASWebAuthenticationSession | | Redirect | Custom scheme или HTTPS App Links / Universal Links | | Token storage | Keychain на iOS, Keystore-backed storage на Android | | Refresh | Срок жизни, rotation, поведение при invalid_grant | | Logout | Локальная очистка, revocation, SSO-сессия | | Ошибки | UI и логика для cancel, timeout, 401, 403, invalid token | | Логи | Запрет на запись токенов и auth headers | | Окружения | Разные client_id и redirect URI для dev/stage/prod | | Тесты | Устройства, версии ОС, плохая сеть, несколько аккаунтов | Ограничения и компромиссы OAuth 2.0 не делает мобильное приложение безопасным автоматически. Public client не может хранить настоящий client_secret. Если сервер требует секрет от мобильного приложения как от confidential client, это слабое место архитектуры. PKCE защищает authorization code от перехвата, но не защищает устройство с malware, debug build с логированием токенов или скомпрометированный backend. Secure storage снижает риск утечки токенов, но не отменяет revocation, короткий срок жизни access token и аккуратную обработку refresh token. Системный браузер безопаснее WebView для OAuth, но хуже контролируется визуально. Пользователь может увидеть другой интерфейс, cookie-сессию или выбор аккаунта, и это нужно учитывать в UX. Refresh token rotation повышает безопасность, но усложняет клиент: нужно атомарное сохранение и защита от гонок при параллельных API-запросах. Logout редко бывает одинаковым для всех провайдеров. Где-то есть revocation endpoint, где-то есть end session endpoint, где-то можно очистить только локальную сессию приложения. Главное, что мы не решаем одной мобильной интеграцией: политику управления сессиями, серверную валидацию токенов, MFA, риск-модель, аудит и реакцию на компрометацию. Это общая зона ответственности клиента, backend и команды безопасности. Что в итоге OAuth 2.0 в мобильном приложении — это не одна кнопка и не один SDK. Это сценарии: вход, возврат из браузера, хранение токенов, обновление доступа, отмена, плохая сеть, смена аккаунта, logout и отзыв доступа. Если пройтись по этим вопросам до разработки, часть багов просто не появится. А те, что останутся, будут выглядеть не как «авторизация иногда ломается», а как конкретные проверяемые случаи: invalid_grant ведёт на логин, timeout не стирает сессию, refresh token не попадает в логи, redirect URI разделён между окружениями. Источники RFC 6749: The OAuth 2.0 Authorization Framework — https://www.rfc-editor.org/rfc/rfc6749 RFC 7636: Proof Key for Code Exchange by OAuth Public Clients — https://www.rfc-editor.org/rfc/rfc7636 RFC 8252: OAuth 2.0 for Native Apps — https://www.rfc-editor.org/rfc/rfc8252 RFC 7009: OAuth 2.0 Token Revocation — https://www.rfc-editor.org/rfc/rfc7009 OAuth 2.0 Security Best Current Practice — https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics AppAuth for Android — https://github.com/openid/AppAuth-Android AppAuth for iOS — https://github.com/openid/AppAuth-iOS Apple ASWebAuthenticationSession — https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession Android App Links — https://developer.android.com/training/app-links Habr: «Шпаргалка по тестированию требований к мобильным приложениям» — https://habr.com/ru/companies/mobileup/articles/336992/ Habr: «Как мы разрабатывали „Спецкор“ — супер-кастомное мобильное приложение для гражданских репортеров» — https://habr.com/ru/companies/true_engineering/articles/222945/ Style adaptation note Исходный текст был переписан из общего маркетингового обзора в практическую Habr-шпаргалку для мобильной команды. Я убрал неподтверждённые обещания про «удобную и надёжную авторизацию», добавил конкретные решения для iOS и Android, вопросы для ревью требований, failure modes, пример token refresh, AppAuth-конфигурацию, ограничения OAuth 2.0 и ссылки на RFC/AppAuth-документацию. Тон и структура адаптированы под наиболее релевантный Habr-референс: короткие прикладные разделы, вопросы «что будет, если...?» и связь каждого технического решения с пользовательским или командным последствием.

0
В избр. Сохранено
Авторизуйтесь
Вход с паролем