How to Add Sign in with Apple and Google to a PHP App
Use the OpenID Connect authorisation code flow. Redirect to Apple or Google with a signed state and a nonce, exchange the returned code on your server, check the ID token's issuer, audience, expiry and nonce, then find or link the account by the provider's stable sub claim rather than the email address.
What you need from each provider
Google is quick to set up. In Google Cloud Console, create a project, configure the OAuth consent screen, then create an OAuth client of type Web application. You get a client ID and client secret, and you register your exact redirect URI, for example https://example.com/oauth/callback/google. Basic sign-in scopes (openid email profile) are non-sensitive.
Apple needs a paid Apple Developer Program membership and four pieces:
- An App ID with the Sign in with Apple capability enabled.
- A Services ID (for example
com.example.web). For web sign-in this is your OAuthclient_id; you register your domain and HTTPS return URL on it. - A private key (
.p8) with Sign in with Apple enabled, plus its Key ID. You can download it once, so store it safely outside the web root. - Your Team ID.
Apple has no static client secret. You create one: a short JWT signed with ES256 using the .p8 key, with your Team ID as iss, your Services ID as sub and https://appleid.apple.com as aud. Apple rejects an expiry more than six months ahead, so generate it per request or rotate it on a schedule.
The flow, step by step
- Generate a random nonce and a state value that you can later verify.
- Redirect to the provider's authorisation endpoint with
response_type=code, your client ID, redirect URI, scopes, state and nonce. Apple needsresponse_mode=form_postwhen you request name or email. - On the callback, check the state before anything else. Reject on mismatch or age.
- Exchange the
codeat the token endpoint from your server, sending the client secret. - Validate the
id_tokenclaims (next section). - Look up the identity by provider and
sub; create or link a user. - Call
session_regenerate_id(true)and sign the user in.
One detail catches many PHP apps: Apple returns to your site with a cross-site POST. A session cookie set with SameSite=Lax is not sent on that request, so a state stored only in $_SESSION appears to be missing. Either use a dedicated cookie for the round trip, or make the state self-verifying:
function make_state(array $data, string $secret): string {
$data['ts'] = time();
$payload = rtrim(strtr(base64_encode(json_encode($data)), '+/', '-_'), '=');
return $payload . '.' . hash_hmac('sha256', $payload, $secret);
}
function verify_state(string $state, string $secret, int $maxAge = 600): array {
[$payload, $sig] = array_pad(explode('.', $state, 2), 2, '');
if (!hash_equals(hash_hmac('sha256', $payload, $secret), $sig)) {
throw new RuntimeException('Bad state.');
}
$data = json_decode(base64_decode(strtr($payload, '-_', '+/')), true);
if (!is_array($data) || time() - (int) ($data['ts'] ?? 0) > $maxAge) {
throw new RuntimeException('Expired state.');
}
return $data; // carries the nonce to compare with the id_token
}
Keep the HMAC secret in .env, never in the repository.
Validating the ID token
Whatever route the token took, check these claims before trusting it:
iss:https://appleid.apple.comfor Apple;https://accounts.google.comoraccounts.google.comfor Google.aud: exactly your client ID (Services ID for Apple web, OAuth client ID for Google). Compare withhash_equalsand never accept an empty value.exp: in the future.nonce: equal to the nonce you issued. Native Apple SDKs typically put the SHA-256 hex digest of your raw nonce in the token, so native flows compare against the hash.
The signature question depends on where the token came from. Google's documentation says a token received directly from its token endpoint over HTTPS, authenticated with your client secret, can be trusted as coming from Google. A token that arrives from a browser or a mobile app has no such guarantee, so you must verify its RS256 signature against the provider's published keys (https://appleid.apple.com/auth/keys and https://www.googleapis.com/oauth2/v3/certs), matching the kid header and caching the key set. A maintained JWT library with JWK support is the safer choice than hand-written crypto. Verifying the signature on the server flow too is cheap defence in depth.
Linking accounts without a takeover hole
Store social identities in their own table keyed by (provider, sub). The sub is the stable identifier; Google explicitly advises against using email as the key because addresses can change.
- If
(provider, sub)exists, sign in that user. - Otherwise, if the token says
email_verifiedis true, link to an existing user with that email or create one. - If the email is missing or unverified, create a separate account rather than attaching to someone else's.
Linking on an unverified email is how account takeovers happen. Note that Apple may send email_verified as the string "true", so parse it with filter_var(..., FILTER_VALIDATE_BOOLEAN).
Apple users can hide their address behind a private relay. That address is real and stable for your app, but to email it you must register your sending domains with Apple's private email relay settings, and your SPF and DKIM must pass. Apple also sends the user's name only on the first authorisation, so save it then or do not depend on it. For the email side, see sending email with Amazon SES.
Common errors and platform rules
- redirect_uri_mismatch: the URI must match character for character, including scheme, path, query string and trailing slash.
- Testing locally: Apple will not redirect to
http://localhost. Use an HTTPS tunnel and register that URL, or test on staging. - invalid_client from Apple: usually a wrong Key ID, Team ID or Services ID in the client-secret JWT, or an expired secret.
- Native apps: the token's
audis the app's bundle ID or the platform client ID, not the web client ID, so keep a separate allow-list for app audiences instead of widening the web check. - App Store rules: guideline 4.8 says an iOS app that uses a third-party login such as Google for the primary account must also offer an equivalent privacy-focused option. Sign in with Apple meets that requirement.
If you are building the native side too, the mobile app backend guide covers app tokens and API design, and passwordless email login gives users a fallback that needs no provider account.
Sources: Google: OpenID Connect · Apple: Verifying a user · Apple: App Review Guidelines
How Simple Stack handles it
Simple Stack ships both providers, configured from .env and documented step by step in SOCIAL_LOGIN.md. The web flow lives in public/index.php; the native-app endpoints (app_auth_apple, app_auth_google) live in public/api.php.
- Apple and Google buttons appear only once their keys are set; AUTH_OAUTH_LOGIN=false hides both
- An HMAC-signed state carries the nonce, so Apple's cross-site form_post callback works
- The web callback exchanges the code server-side, then checks issuer, audience, expiry and nonce
- Native app tokens are signature-checked against the provider JWKS before any claim is read
- Identities link by provider and sub; only a provider-verified email joins an existing account
- Signed-in users can link Apple or Google to their account from the account page
Questions people ask
Skip the plumbing. Build the product.
Sign-in, Stripe, emails, an admin and a CMS — already built and tested in plain PHP. £49, once, for unlimited projects.