Вход через Apple сломался с invalid_client
Работало месяцами, ничего не выкатывали, и внезапно никто не может войти. Если вашему входу через Apple около полугода — это почти наверняка ваш случай.
Как это выглядит
Сервер Apple перестаёт принимать ваше приложение и отвечает коротким JSON. На вашей стороне при этом не менялось ничего:
HTTP 400 · https://appleid.apple.com/auth/token
{"error":"invalid_client"}
Пользователь видит другое: кнопка Apple не делает ничего либо возвращает на экран входа без сообщения. В логах — тот же invalid_client.
Почему так происходит
В отличие от всех остальных OAuth-провайдеров, у Apple нет долгоживущего секрета. То, что вы вставили в бэкенд или в Supabase, — это JWT, который вы подписали сами, и Apple ограничивает его срок примерно шестью месяцами: 15777000 секунд — задокументированный максимум.
Предупреждений Apple не присылает. Ни письма, ни баннера в консоли, ни отметки в дашборде. Секрет просто перестаёт быть валидным, и первыми об этом узнают ваши пользователи.
Официальная документация Supabase прямо советует «поставить повторяющееся напоминание в календаре раз в полгода». Это текущее состояние индустрии.
Убедиться, что дело именно в этом
Ваш client secret — это JWT: три блока base64 через точку. Раскодируйте средний и посмотрите на поле exp, это метка времени Unix.
- Возьмите секрет из конфига бэкенда или из настроек провайдера Apple в Supabase
- Раскодируйте payload: любым декодером JWT или base64 по среднему сегменту
- exp в прошлом — это ваш случай. exp в будущем — причина другая: не тот Services ID, не тот Team ID или несовпадение redirect URI.
Что Apple ждёт внутри этого JWT
Пригодится, когда будете пересобирать секрет руками: неверное поле даёт ровно ту же ошибку invalid_client, из-за чего её так тяжело диагностировать.
| alg | ES256, другой алгоритм Apple не примет |
| kid | Key ID вашего ключа .p8 |
| iss | ваш Apple Team ID |
| sub | Services ID, а не bundle ID приложения |
| aud | https://appleid.apple.com |
| exp | не дальше чем iat + 15777000 секунд |
Починить прямо сейчас, руками
- Откройте портал разработчика Apple → Certificates, Identifiers & Profiles → Keys и найдите ключ, созданный для Sign in with Apple. Нужны его Key ID и файл .p8. Если .p8 потерян — создайте новый ключ: Apple даёт скачать файл только один раз.
- Team ID возьмите в правом верхнем углу портала, Services ID — в разделе Identifiers. Он выглядит как домен наоборот и это не bundle ID вашего приложения.
- Соберите новый JWT с алгоритмом ES256 и полями из таблицы выше. exp ставьте не дальше полугода.
- Вставьте новый секрет туда, где лежал старый: конфиг бэкенда либо Supabase → Authentication → Providers → Apple → Secret Key.
- Войдите один раз для проверки. Ошибка исчезает сразу, ждать распространения и чистить кеш не нужно.
Это вся починка. Занимает минут десять и покупает вам полгода.
Через полгода повторится
Ровно то же самое: та же тишина со стороны Apple, те же растерянные пользователи. Напоминание в календаре работает ровно до того дня, когда человек в отпуске, сменил работу или просто смахнул уведомление.
Либо перестать делать это руками
Я напоролся на это на своём проекте и написал бесплатный GitHub Action. По расписанию он пересобирает JWT из вашего .p8 и записывает его в Supabase через Management API. Не Supabase? Он просто отдаёт свежий секрет как output — заберите куда нужно. MIT, ноль зависимостей, вся логика в одном читаемом файле.
Частые вопросы
Можно поставить срок побольше и забыть?
Нет. Полгода — жёсткий потолок Apple. JWT с большим exp отклоняется сразу, и вы получите тот же invalid_client не через полгода, а немедленно.
Перевыпуск секрета разлогинит пользователей?
Нет. Client secret удостоверяет ваш сервер перед Apple, а не пользователя перед вами. Действующие сессии не затрагиваются.
Я потерял файл .p8. Это фатально?
Нет, но восстановить его нельзя: Apple разрешает одно скачивание. Создайте новый ключ в портале, запишите его новый Key ID и пересоберите секрет с новой парой.
Почему invalid_client вылезает и на свежем секрете?
Потому что Apple отдаёт одну и ту же ошибку на несколько разных причин: bundle ID вместо Services ID в поле sub, не тот Team ID в iss, алгоритм отличный от ES256, несовпадение redirect URI. Поэтому и стоит начинать с проверки exp — она сразу отсекает половину вариантов.