Ask
88
@openkey_owen ·

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.

9 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @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.

    34
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    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.

      15
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      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.

      7
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @quietpackets · 2h ago

    Something that makes this much more confusing than it needs to be: native iOS sign in through the system flow can keep working while web and Android break, because they do not all go through the same client id and exchange.

    So the support queue looks platform specific, someone says "it is an Android bug", and you spend the morning in the wrong repo. If your failures are split by platform, check the secret before you check the platform.

    12
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @oncall_omar · 2h ago

    Since there is no notification from Apple and no gradual degradation, you find out from users. So make it not be users:

    1. A scheduled job that decodes the configured client secret and alerts when exp is less than 30 days away. Fifteen lines, and it turns a silent cliff into a ticket.
    2. A synthetic login that runs the real token exchange every few minutes from outside your network and alerts on a non-200. That catches this and roughly ten other things.

    Both are cheap. The reason nobody has them is that this failure happens once every six months, which is exactly long enough to forget.

    18
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @flat_rate_finn · 2h ago

    The other common cause of invalid_client is a sub mismatch: using the bundle ID where the flow needs the Services ID, or the reverse. Easy to tell the two apart:

    • expired secret: everything using that secret fails, all at once, starting at a specific minute, and it worked yesterday
    • wrong sub: only the flow using that client id ever failed, and it never worked in the first place

    "It worked yesterday" is doing almost all the diagnostic work in your post.

    9
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @chmod_confused · 2h ago · 2 replies

    Pretty sure invalid_client is about the client identifier, not expiry. An expired secret gives you invalid_grant. So if you are seeing invalid_client I would look at the Services ID first.

    6
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @integral_ines · 2h ago

      Other way round. invalid_grant is about the authorization code: already used, expired, or exchanged with a different redirect than it was issued for. A client secret that is expired, malformed, signed with the wrong key or carrying the wrong claims comes back as invalid_client.

      This is worth getting right because the wrong version costs you an hour. Decoding the exp of the secret you have configured takes about ten seconds and rules the whole class in or out before you start editing identifiers that were working yesterday. Change nothing until you have read that number.

      21
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @indent_error_ivy · 2h ago

    Small practical note: set the rotation reminder at five months, not six. If you set it at six you are already down when it fires, and you will be doing the rotation under pressure with users complaining, which is when people paste the wrong Team ID.

    5
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report