Как безопасно хранить и обновлять OAuth-токены соцсетей
Храните OAuth-токен только на сервере, в зашифрованном виде (Fernet или KMS) и отдельной строкой на каждого клиента и аккаунт. Следите за сроком по expires_at и обновляйте фоновым воркером до истечения. Платформы различаются: access token TikTok живёт 24 часа, Instagram Login и LinkedIn — 60 дней, токен страницы Facebook — бессрочно. Отзыв доступа видно по AuthError и вебхукам.
Обновлено:
Где и как хранить OAuth-токены?
Токен равен паролю. Не отдавайте его в браузер, мобильное приложение или логи: храните в зашифрованной колонке базы, а ключ шифрования — отдельно (переменная окружения, secret manager или KMS). Для каждого токена сохраняйте expires_at, фактически выданные scope и статус. Обновлением занимается планировщик, отзыв доступа выявляется через AuthError и вебхуки платформ.
SDK Socialift ничего не хранит: exchange_code() и refresh_token() возвращают TokenBundle (access_token, refresh_token, expires_in, expires_at), а куда это записать — решает ваше приложение. В разделе «Ulanishlar» (Подключения) кабинета Socialift токены шифруются на сервере и не отправляются в браузер — следуйте тому же правилу в своём бэкенде.
Сколько живёт access token: таблица по платформам
По официальной документации на 2026-09-17. Всегда считайте срок из expires_in в ответе — не зашивайте цифры из таблицы в код.
| Платформа / токен | Срок | Обновление | SDK |
|---|---|---|---|
| Токен пользователя Facebook (краткосрочный) | Несколько часов | На сервере обменивается на долгосрочный через fb_exchange_token | fb.exchange_code(...) возвращает долгосрочный токен |
| Токен пользователя Facebook (долгосрочный) | Около 60 дней | Истёкшим токеном новый не получить — пользователь логинится заново | fb.granted_scopes(token) |
| Токен страницы Facebook (из долгосрочного токена пользователя) | Без даты истечения | Не нужно; в определённых случаях аннулируется | fb.pages(token) |
| Токен system user Meta | Бессрочный или 60 дней | Meta рекомендует 60-дневный: создайте новый, разверните, отзовите старый | нет |
| Instagram Login | Краткосрочный — 1 час, долгосрочный — 60 дней | Обновляется токен старше 24 часов и ещё не истёкший — ещё на 60 дней | ig.refresh_token(token) |
| TikTok | Access — 24 часа, refresh — 365 дней | Refresh token в ответе может отличаться — используйте только новый | tt.refresh_token(refresh_token) |
| Access — 60 дней | Refresh token только у одобренных партнёров MDP: 365 дней, обновление срок не продлевает | li.refresh(...), li.introspect(token) | |
| Токен Telegram-бота | Срок в документации не указан | В BotFather можно в любой момент отозвать и получить новый | нет |
| Аккаунт Telegram (MTProto) | Пока сеанс не завершён в списке активных сеансов | Повторный вход (телефон → код → 2FA) | Строка export_session() равна паролю |
| amoCRM OAuth | Access — в примере документации 24 часа; refresh — 3 месяца | Refresh token обменивается один раз | нет |
| Bitrix24 OAuth | Access — 1 час, refresh — 180 дней | Через refresh_token | нет |
естьнетТаблицу можно прокрутить вбок
Примечания и ограничения
У Meta есть отдельный «срок доступа к данным»: если пользователь не активен 90 дней, данные не отдаются даже при валидном токене (некоторые бизнес-разрешения исключены). Статус показывает debug_token.
Стратегия refresh: когда и как обновлять токены
Храните срок
При каждом обновлении записывайте
expires_atи (если есть)refresh_expires_at. Они уже есть вTokenBundle.Планируйте по окну
Например, раз в час: TikTok — осталось меньше 4 часов, Instagram Login — меньше 10 дней (токен не моложе 24 часов), LinkedIn с refresh token — меньше 7 дней.
Один токен — один обновляющий
Ставьте блокировку на строку. В TikTok и amoCRM refresh token меняется: если два воркера обновят параллельно, второй получит ошибку со старым токеном.
Пишите атомарно
Новые access и refresh сохраняйте в одной транзакции. Если запись упала, новый refresh token потерян, и аккаунт отключится.
Разделяйте ошибки
AuthError— ставьте статусreauth_requiredи отправляйте пользователю ссылку на переподключение.RateLimitErrorилиTransportError— повторите позже.Где нет refresh — предупреждайте заранее
Для токена пользователя Facebook или LinkedIn без refresh напоминайте о переподключении за 7–14 дней до истечения.
Воркер обновления (Socialift SDK)
На провайдера достаточно одного клиента: токен передаётся аргументом в каждый метод и в клиенте не хранится.
from socialift import ApiError, AuthError, Instagram, LinkedIn, RateLimitError, TikTok
async def refresh_row(row, tt: TikTok, ig: Instagram, li: LinkedIn) -> None:
try:
if row.provider == "tiktok":
bundle = await tt.refresh_token(decrypt(row.refresh_token_enc))
elif row.provider == "instagram":
bundle = await ig.refresh_token(decrypt(row.access_token_enc))
else:
bundle = await li.refresh(decrypt(row.refresh_token_enc))
except RateLimitError:
raise # повторить позже
except ApiError as exc:
if not isinstance(exc, AuthError) and exc.status != 400:
raise
await db.set_status(row.id, "reauth_required")
return
await db.save_bundle( # одна транзакция
row.id,
access_token_enc=encrypt(bundle.access_token),
refresh_token_enc=encrypt(bundle.refresh_token) if bundle.refresh_token else None,
expires_at=bundle.expires_at,
)По стандарту OAuth отклонённый refresh token (invalid_grant) обычно возвращается с HTTP 400, а не 401 — поэтому такой случай тоже отправляем на переподключение.
Пусть db.save_bundle оставляет старый refresh token, если пришёл None: в ответе обновления Instagram refresh token нет. encrypt/decrypt — функции Fernet ниже.
Шифрование токенов: Fernet и ротация ключей
cryptography — собственная зависимость SDK. Fernet даёт аутентифицированное шифрование (AES-128-CBC + HMAC-SHA256), MultiFernet позволяет менять ключи.
import os
from cryptography.fernet import Fernet, MultiFernet
# TOKEN_KEYS="новый_ключ,старый_ключ" — шифруем первым
_keys = MultiFernet([Fernet(k) for k in os.environ["TOKEN_KEYS"].split(",")])
def encrypt(token: str) -> bytes:
return _keys.encrypt(token.encode())
def decrypt(blob: bytes) -> str:
return _keys.decrypt(blob).decode()
def reencrypt(blob: bytes) -> bytes:
"""Фоновая миграция после смены ключа: перешифровывает запись новым ключом."""
return _keys.rotate(blob)Ротация ключа: добавьте новый ключ в начало списка, прогоните все строки через reencrypt, затем уберите старый ключ. С KMS — envelope encryption: ключ данных шифруется в KMS, токены — ключом данных.
Как спроектировать таблицу токенов для multi-tenant?
| Колонка | Тип | Зачем |
|---|---|---|
| tenant_id | uuid | Фильтр в каждом запросе; токены клиентов не смешиваются (row-level security) |
| provider | text | facebook, instagram, tiktok, linkedin, telegram_bot, telegram_account |
| account_id | text | Page ID, IG user ID, TikTok open_id, LinkedIn sub; UNIQUE (tenant_id, provider, account_id) |
| token_kind | text | user, page, system_user, bot, session — от этого зависит правило обновления |
| access_token_enc, refresh_token_enc | bytea | Только зашифрованные значения |
| key_version | int | Каким ключом зашифровано — для ротации |
| expires_at, refresh_expires_at | timestamptz | Планировщик выбирает по индексу |
| scopes | text[] | Фактически выданные разрешения (не запрошенные) |
| status | text | active, reauth_required, revoked |
| last_refreshed_at, last_error | timestamptz, text | Диагностика и алерты (без самого токена) |
App secret, client secret и ключи шифрования в эту таблицу не пишутся — они живут в secret manager или переменных окружения.
Что такое appsecret_proof и зачем он нужен?
appsecret_proof — HMAC-SHA256 от access token с app secret в качестве ключа. Он добавляется к запросу Graph API параметром. Даже если токен украдут, без app secret proof не построить.
Если в App Dashboard включить «Require App Secret» (App Settings → Advanced → Security), вызовы без proof отклоняются — то есть клиентские запросы должны идти через бэкенд. SDK Socialift автоматически добавляет proof к запросам Meta (Facebook, Instagram, Ads); без SDK считайте его через socialift.crypto.appsecret_proof(access_token, app_secret) или hmac.
Как понять, что доступ отозван?
Meta: код 190 (истёкший или отозванный токен) приходит в SDK как AuthError. Подкоды объясняют причину: 458 — приложение удалено, 460 — сменён пароль, 463 — срок истёк, 467 — недействительный токен. Если пользователь удалил приложение на самом Facebook, вызывается deauthorize callback URL из App Dashboard; signed_request из него проверяет parse_signed_request(signed_request, app_secret).
TikTok при отзыве доступа шлёт вебхук authorization.removed (проверяйте через verify_tiktok_request). В LinkedIn li.introspect(token) возвращает статус токена (active, expired, revoked), а 401 превращается в AuthError. Если токен Telegram-бота отозван в BotFather, старый токен перестаёт работать — ответ Unauthorized SDK превращает в AuthError.
Когда пользователь жмёт «Отключить» в вашем приложении, сначала отзовите доступ у провайдера (fb.revoke(token), tt.revoke(token)), потом удалите строку. Одного удаления из базы мало: токен Meta продолжит действовать.
Минимальные разрешения и управление секретами
- Запрашивайте только нужные функции scope — App Review и аудит проверяют то же самое.
- Пользователь может отклонить часть разрешений — сохраняйте фактически выданные:
fb.granted_scopes(token), в LinkedInli.introspect(token). - Отдельные приложения и ключи для каждой среды (dev, staging, prod); продовые токены не должны попадать в dev-базу.
- Заранее продумайте смену app secret: от него зависят appsecret_proof и проверка подписи вебхуков — обновляйте все сервисы одновременно.
- Не пишите токены в логи, Sentry и аналитику; маскируйте их и в текстах ошибок.
- Строку сеанса Telegram-аккаунта (
export_session()) шифруйте так же, как токены. - Меняйте ключ шифрования минимум раз в год и при уходе сотрудника (MultiFernet или KMS).
- Показывайте в кабинете аккаунты, которым нужно переподключение — тихий обрыв обходится дороже всего.
Частые вопросы
Можно ли хранить OAuth-токены в базе в открытом виде?
Не рекомендуется. Токен, утёкший через бэкап базы или SQL-инъекцию, используется сразу. Храните токен в зашифрованной колонке, а ключ — вне базы: тогда одной базы злоумышленнику не хватит.
Когда обновлять токен Instagram?
Долгосрочный токен Instagram Login действует 60 дней и обновляется, если ему не меньше 24 часов и он ещё не истёк — ещё на 60 дней. На практике удобно обновлять фоновым воркером за 7–10 дней до истечения. Токен страницы, полученный через Facebook Login, обновлять не нужно.
Почему refresh token TikTok возвращает ошибку?
Чаще всего из-за повторного использования старого refresh token. По документации TikTok refresh token в ответе может отличаться, и использовать нужно новый. Сохраняйте его атомарно и не давайте двум воркерам обновлять один токен параллельно.
Почему LinkedIn не выдаёт refresh token?
Программные refresh token LinkedIn выдаёт только одобренным партнёрам Marketing Developer Platform. У остальных приложений access token живёт 60 дней, после чего пользователь проходит авторизацию заново — предупреждайте его заранее.
Истекает ли токен страницы Facebook?
У токена страницы, полученного из долгосрочного токена пользователя, нет даты истечения, но в определённых случаях он аннулируется — например, если пользователь сменил пароль или удалил приложение. Поэтому отслеживайте AuthError и храните статус.
Обязателен ли appsecret_proof?
Если в App Dashboard включён «Require App Secret» — да, вызовы без proof отклоняются. Даже без этой настройки добавлять proof к серверным запросам рекомендуется. SDK Socialift добавляет его к запросам Meta автоматически.