Sign in with Apple suddenly returns invalid_client - the client secret is a JWT and it expires after 6 months
Spent this morning on this, so here it is in one place for whoever searches the error string next.
Symptom: Sign in with Apple, working fine for months, starts failing at the token exchange with
{"error": "invalid_client"}
No deploy. No config change. No email from Apple. Nothing on any status page. Every other provider fine. The failure is instant and identical every time, which already tells you it is not an outage.
Cause: the thing Apple calls a client secret is not a secret string you paste once. It is a JWT that you sign yourself with the .p8 key from the developer portal, and it carries its own expiry. Apple will not accept a client secret whose exp is more than 15,777,000 seconds, about six months, in the future, so six months is the longest one you can possibly issue. The day it lapses, every token exchange comes back invalid_client and nothing anywhere says why.
The claims, for anyone checking theirs right now:
- header: alg ES256, kid set to the key id of your .p8
- iss: your Team ID
- iat: now, in seconds, not milliseconds
- exp: in the future, in seconds, at most iat + 15777000
- aud: https://appleid.apple.com
- sub: your client_id, which is the Services ID for web and Android flows and the bundle ID for native
Fastest check by a mile: decode whatever secret you have configured and read exp. If it is in the past, that is your entire outage and everything else you were about to investigate is fine.
Posting it because the error name sends everyone straight to "my identifier must be wrong", and mine had been correct for two years.
@pgpolicy_nadia · 2h ago · 3 replies
The permanent fix, if you control the token exchange: generate the JWT per request instead of storing one. Signing an ES256 token is microseconds, it adds nothing measurable to the request, and the expiry problem stops existing because you issue it with a lifetime of a few minutes and never store it anywhere.
Keep the .p8 in your secret store, load it once at boot, sign on demand. The only thing you then have to remember is the key itself, which does not expire the way the secret does.
Everyone who has been bitten by this once ends up here. It is worth doing before it happens rather than after.
Reply
Report
@iris_tanaka · 2h ago
Important caveat: this only works if you own the exchange. If your auth is a managed provider, you paste a static secret into a dashboard field and you cannot sign per request at all.
Supabase is the obvious example. Their Apple provider takes a generated Secret Key and their own docs say plainly that Apple requires you to generate a new one every 6 months from the .p8, and that missing it causes authentication failures. So on that setup the fix is not clever signing, it is a calendar entry plus an alert that fires before the date, and knowing where the .p8 lives.
Reply
Report
@flat_rate_finn · 2h ago
And store the .p8 somewhere your future self can actually find. Revoking and reissuing the signing key because nobody knows where the file went is a considerably worse morning than the one being described here.
Reply
Report