← All posts
·7 min read·GuardLayer

Supabase Apple "Secret key should be a JWT": the fix

SupabaseAuthAppleSecretsNext.js

"Secret key should be a JWT." means you pasted the raw Apple .p8 private key (or something else that isn't a token) into the Supabase Apple provider's Secret Key field. That field takes a signed ES256 JWT built from the .p8, not the key itself. If your app only uses native Sign in with Apple, leave the field empty.

You enable Apple in Supabase, download AuthKey_XXXXXXXXXX.p8 from the Apple Developer Console, paste its contents into Secret Key (for OAuth), and the dashboard refuses to save. The error is right, and the way most people "fix" it is where the security trouble starts.

What does "Secret key should be a JWT" mean in Supabase?

It means the value in the Apple Secret Key field doesn't have the shape of a JWT: three base64url segments separated by dots. Studio checks this before it saves anything. The validation lives in AuthProvidersFormValidation.tsx in the supabase/supabase repo, and the first check is a regex for exactly that shape, with the message 'Secret key should be a JWT.'

A .p8 file starts with -----BEGIN PRIVATE KEY-----, contains newlines, and has no dots, so it fails on the first line. The same goes for the Key ID, the Team ID or the Services ID.

Two more checks follow in the same chain, and they produce errors you may hit next:

  • 'Secret key is not a correctly generated JWT.' The token decodes, but the header alg isn't ES256 or the payload aud isn't https://appleid.apple.com.
  • 'Secret key expires in less than 7 days!' The token's exp is less than a week away.

So Studio wants a JWT you signed with the .p8, using Apple's client-secret format. That JWT is what Apple calls the client secret.

Do you even need the secret key?

Only if users sign in with Apple through the web OAuth redirect. Supabase's Apple guide is direct about the native case: "If you're building a native app only, you do not need to configure the OAuth settings." It also says "Native-only implementations don't require secret key rotation."

Studio agrees. With Apple enabled, each of the three checks above starts by returning true when the value is empty, so an empty Secret Key passes validation. The Client IDs field is the required one, and it holds your bundle IDs for native sign-in.

If you're on iOS with signInWithIdToken, clear the field and stop there. You never need the .p8 anywhere near your Supabase project.

If you're building a Next.js app with a "Sign in with Apple" button, you're on the OAuth flow and you need the JWT:

"use client";
import { createClient } from "@/lib/supabase/client";

export function AppleButton() {
  const supabase = createClient();
  return (
    <button
      onClick={() =>
        supabase.auth.signInWithOAuth({
          provider: "apple",
          options: { redirectTo: `${location.origin}/auth/callback` },
        })
      }
    >
      Sign in with Apple
    </button>
  );
}

That redirect goes to Apple with your Services ID, and Supabase Auth then exchanges the code using the client secret you saved. No valid secret, no web login.

The fix: generate the JWT, then save only the JWT

Apple's client secret is a JWT with an ES256 header carrying your Key ID (kid), and a payload with your Team ID as iss, your Services ID as sub, aud set to https://appleid.apple.com, and an exp. Apple's "Creating a client secret" page says it's an error to request an expiration more than 15777000 seconds (six months) in the future.

You have two safe ways to make one.

Supabase's in-page generator. The Apple guide includes a tool that builds the secret from your .p8, and states "No keys leave your browser!" It also warns that the tool "does not currently work in Safari", so use Firefox or a Chromium browser.

A local script. Keep the .p8 outside the repo and point a one-off script at it:

// scripts/apple-secret.mts: run locally, never deployed
import { readFileSync } from "node:fs";
import jwt from "jsonwebtoken";

const key = readFileSync(process.env.APPLE_P8_PATH!, "utf8"); // e.g. ~/keys/AuthKey_ABC123DEFG.p8
const now = Math.floor(Date.now() / 1000);

console.log(
  jwt.sign(
    {
      iss: process.env.APPLE_TEAM_ID,
      iat: now,
      exp: now + 180 * 24 * 60 * 60, // a little under Apple's six-month cap
      aud: "https://appleid.apple.com",
      sub: process.env.APPLE_SERVICES_ID,
    },
    key,
    { algorithm: "ES256", keyid: process.env.APPLE_KEY_ID }
  )
);

Run it, paste the printed token into Secret Key (for OAuth), save. The token goes in the dashboard and nowhere else. It isn't an env var for your Next.js app, because your app never talks to Apple's token endpoint. Supabase Auth does.

Where this goes wrong

The error message nudges people toward three bad moves.

Pasting the .p8 into a chat or a random generator site. Searches for an "Apple client secret generator" turn up plenty of third-party pages that ask you to upload the key. That .p8 doesn't expire. Anyone holding it, plus your Team ID and Key ID (both visible in the JWT header and payload), can mint valid client secrets for your Services ID until you revoke it. Use Supabase's in-browser tool or your own machine.

Committing the key to make a signer work. The usual shape is a helper that signs the secret at runtime with the PEM pasted inline:

import jwt from "jsonwebtoken";

const APPLE_PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
ThisIsNotARealKeyJustAFakeBodyForThisBlogPost2026AppleExample
-----END PRIVATE KEY-----`;

export function appleClientSecret() {
  const now = Math.floor(Date.now() / 1000);
  return jwt.sign(
    { iss: "TEAMID1234", iat: now, exp: now + 15552000, aud: "https://appleid.apple.com", sub: "com.example.web" },
    APPLE_PRIVATE_KEY,
    { algorithm: "ES256", keyid: "KEYID12345" }
  );
}
guardlayer scan · lib/apple-secret.tsLive engine output
Check failed
75/100 · B
  • Criticallib/apple-secret.ts:3

    Hardcoded secret in source

    Move the secret to an environment variable, purge it from git history (e.g. git filter-repo / BFG), and rotate it at the provider.

Your Next.js app doesn't need this file at all, since Supabase holds the secret. And once a key is in a commit, deleting the line doesn't help: it stays in git history until you rotate and purge it. Supabase's guide is explicit: if the .p8 is lost or compromised, "immediately revoke it in the Apple Developer Console and create a new one". A pre-commit check for keys catches the PEM block before it ships. The same applies to APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----..." sitting in a committed .env.local. GuardLayer flags a PEM private key inside code or .env files as general/hardcoded-secret.

Forgetting the six-month clock. Studio shows an alert on the Apple provider: "A new secret should be generated every 6 months, otherwise users on the web will not be able to sign in." Nothing fails at deploy. Web sign-in just stops on the day the token expires. Supabase's guide suggests "a recurring calendar reminder for every 6 months to rotate your secret key." Set it for five months, so you're not rotating under pressure.

Quick self-check

# Any Apple key material in the repo?
git ls-files | grep -iE '\.p8$|AuthKey_'
grep -rn "BEGIN PRIVATE KEY" --exclude-dir=node_modules --exclude-dir=.next .

# Was a .p8 ever committed, even if deleted since?
git log --all --oneline -- '*.p8'

Any hit on the first two means moving the key out and rotating it. A hit on the third means the key is in history: revoke it in the Apple Developer Console and generate a new one.

While you're in the Apple settings, if you self-host Supabase Auth, check your version against CVE-2026-31813, a session-forgery bug with crafted ID tokens when Apple or Azure sign-in is enabled, fixed in Auth 2.185.0.

FAQ

Can I paste the .p8 file contents into the Supabase Apple Secret Key field? No. The field expects a JWT signed with the .p8, and Studio rejects anything that isn't three dot-separated segments with "Secret key should be a JWT." Generate the JWT first and paste only that.

My app only uses native Sign in with Apple on iOS. What goes in Secret Key (for OAuth)? Nothing. Supabase's docs say native-only apps don't need the OAuth settings, and Studio's validation accepts an empty value. Put your bundle ID in Client IDs.

What does "Secret key is not a correctly generated JWT" mean? The value is JWT-shaped, but its header alg isn't ES256 or its aud claim isn't https://appleid.apple.com. Regenerate it with the Supabase tool or a script that sets both.

How long does the Apple client secret last? At most six months. Apple rejects an exp more than 15777000 seconds out, and Studio refuses to save a secret that expires within 7 days. Rotate it on a schedule.

Should the Apple client secret go in my Next.js environment variables? No. signInWithOAuth({ provider: 'apple' }) only redirects. Supabase Auth exchanges the code with Apple using the secret stored in your project settings, so neither the JWT nor the .p8 belongs in your app.

Check your Supabase Auth settings and tables, free

Connect your Supabase project read-only. GuardLayer checks your Auth settings (leaked passwords, email confirmation, secure email change, token rotation, CAPTCHA, MFA) and your RLS, policies and grants, and says how to fix each problem. It never reads your rows and can't change anything.

Check my Supabase project

Prefer to check code instead? Scan your repo, no signup.

Keep reading