socialiteproviders/apple
Apple provider for Laravel Socialite, enabling Sign in with Apple via a native OAuth provider. Install with Composer, configure your Apple client ID, team ID, key ID, and private key, then authenticate users with a single Socialite call. Maintained by the Socialite Providers community.
Architecture fit
socialiteproviders/apple is a driver plugin for Laravel Socialite, not an auth system. It is a subtree split of the SocialiteProviders/Providers monorepo, and the surface is deliberately thin: SocialiteProviders\Apple\Provider implements roughly four methods (getAuthUrl, getTokenFields, getUserByToken, mapUserToObject). It plugs into the Socialite driver registry via the SocialiteWasCalled event, so it inherits the same ergonomics, controller shape, and test seams as the Google/Facebook/GitHub drivers already in a typical Laravel app.
That is a good architectural fit if and only if the product's auth layer already speaks Socialite. If login is currently a bespoke OAuth implementation, this package introduces a second auth idiom rather than simplifying the first — a consolidation cost worth naming before adoption.
The critical fit boundary: the package does not sign the ES256 client secret, does not verify JWKS, and does not revoke tokens. Those are integration responsibilities. Architecturally this should sit behind an internal SignInWithApple service (redirect(), userFromCallback(Request), revoke(string $refreshToken)), so the Socialite-specific surface lives in one class. That boundary is what makes the vendor-risk story tolerable and an exit cheap.
Integration feasibility
Feasible in days for the happy path, weeks for the full lifecycle.
| Workstream | Effort | Owner |
|---|---|---|
Apple Developer provisioning (Services ID, .p8, Team/Key ID, byte-matched Return URL) |
Low, but blocking | TPM + Apple account holder |
| Composer install + provider registration | Hours | Backend |
config/services.php entry + .env |
Hours | Backend |
POST callback route + CSRF exemption + SameSite=none |
Hours, high footgun density | Backend |
| Client secret signing (ES256 JWT) + cache/scheduler | 1–2 days | Backend |
User model: apple_id column, firstOrCreate on sub |
Hours | Backend |
| First-consent name/email capture | Half day | Backend |
| Refresh-token capture + revocation endpoint (if App Store) | 2–4 days | Backend |
| Multi-audience config + security sign-off (6.1.0) | 1–3 days | Backend + Security |
The prerequisite chain is the schedule risk, not the code. The Return URL must be HTTPS and byte-match the Apple console entry — no trailing slash, no query string. http://localhost will not work, so local development requires ngrok/expose/Cloudflare Tunnel. If the Apple Developer account holder is not on the team or the Sign in with Apple capability is not enabled on the identifier, the sprint stalls before line one of code.
Technical risk
@atymic authored the 6.1.0 change). This is a real continuity risk for anything on the login critical path. Mitigation is not "find a better package" — there isn't a meaningfully better Laravel-native option — it's the internal adapter boundary plus version pinning.invalid state. SESSION_SAME_SITE=lax (Laravel's default) → session cookie absent on Apple's cross-site POST → state check fails. Both are one-line fixes that cost days if discovered in staging..env works through launch and breaks a quarter later with invalid_client. This must be a scheduled job from day one, not a follow-up ticket.userFromToken($idToken) decodes the payload; it does not verify the signature against https://appleid.apple.com/auth/keys, nor check iss/exp/aud. For a token originating from a client you don't control, treating that as authentication is a security defect. Budget firebase/php-jwt JWKS verification explicitly.Key questions
.p8 private key, and what is the rotation/escrow procedure if that individual leaves?user() — which hides the raw token response — cannot be the integration point.@privaterelay.appleid.com) make email an unreliable key; the sub claim must be the identity anchor.Stack fit
Laravel 10–12 with Socialite. Requires socialiteproviders/apple and socialiteproviders/manager (the manager is what dispatches SocialiteWasCalled). Add firebase/php-jwt for client-secret signing and, if any mobile flow exists, JWKS verification. Storage: one apple_id column on users (indexed, unique), plus an encrypted apple_refresh_token column if revocation is in scope. No migration of existing rows — Apple sign-in is purely additive.
Migration path
sub, never by email. Relay addresses and email changes make email unstable as a key. Decide the linking policy for an Apple login whose email collides with an existing password account — auto-link, or require an authenticated link step? Auto-linking on an unverified relay address is an account-takeover vector; default to explicit linking.EventServiceProvider::$listen. Laravel 11/12 ship without that file, so registration goes in AppServiceProvider::boot() via Event::listen(...) and $event->extendSocialite('apple', Provider::class). Pick the pattern matching the pinned framework version and document it — this is the source of Driver [apple] is not supported.Route::post, and the CSRF exemption is a hard requirement (Laravel 11+: $middleware->validateCsrfTokens(except: ['auth/apple/callback']); Laravel 10: VerifyCsrfToken::$except). If the product's web tier is behind a proxy or CDN, confirm SESSION_SAME_SITE=none (HTTPS-only) survives that layer.Compatibility
SESSION_SAME_SITE=none for the browser flow, or adopt stateless() — but stateless() disables the state check by design, so it is only legitimate for API clients carrying their own id_token, never the browser redirect flow.['name','email'] force Apple into response_mode=form_post, which is why the callback is POST. Dropping scopes silently changes the contract back to GET and quietly breaks the route.Sequencing
.p8 key, Return URL recorded byte-exact in config.composer require socialiteproviders/apple socialiteproviders/manager; register the SocialiteWasCalled listener for the target Laravel version.Cache::remember('apple.client_secret', now()->addDays(30), ...)) + an apple:secret artisan command wired to the scheduler. Do this before the first staging deploy, not after.SameSite configuration, deployed together as one atomic change.firstOrCreate(['apple_id' => ...]) resolution by sub.$request->input('user'), JSON-decode, concatenate firstName/lastName, persist once. Use firstOrCreate, not updateOrCreate, so later nulls don't clobber a stored name.SignInWithApple interface; controllers depend on the interface, not Socialite.Socialite::shouldReceive('driver->user')), including a POST with a user form field to exercise the first-login path, and an assertion that the CSRF exception is in place.refresh_token by calling getAccessTokenResponse() explicitly, store encrypted, and build the revoke call to https://appleid.apple.com/auth/revoke.Maintenance
invalid_client outage six months post-launch, on the login path, with no obvious cause.SocialiteProviders/Providers releases and reading provider-behavior changelogs (the 6.1.0 audience change is the template — a behavior change shipped as a minor version)..p8 file is a long-lived credential. It needs a documented location, a backup, an access restriction, and a procedure for the case where it is lost or the owning Apple account changes hands.Support
Support volume will concentrate in a small number of recognizable error strings. Publish a runbook mapping symptom → cause:
| Symptom | Cause | Fix |
|---|---|---|
invalid_client |
Bad client-secret JWT: wrong sub (must be client ID, not Team ID), wrong kid, stale exp, mangled newlines in the .p8 |
Regenerate secret; file_get_contents the key rather than env-embedding it |
Driver [apple] is not supported |
SocialiteWasCalled listener not registered |
Check service provider; php artisan config:clear |
invalid state / 419 |
Callback registered as GET, or CSRF not exempted, or SameSite=lax suppressing the session cookie |
POST route, CSRF exception, SameSite=none |
| Redirect-URI mismatch | Return URL differs by a trailing slash from the Apple console entry | Byte-match both sides |
Debugging guidance: log the raw token response outside production only, and inspect the id_token at jwt.io (it is unencrypted) checking aud, iss, exp, sub. Note clock skew on exp.
Scaling
The flow itself is cheap — Apple's endpoints are the only external hop, and user resolution is a single indexed lookup on apple_id. Two real scaling considerations:
https://appleid.apple.com in the critical path of every sign-in attempt. Apple-side degradation becomes your sign-in outage. For products where login is revenue-critical, surface this as a known dependency rather than discovering it in an incident review.Failure modes
invalid state affecting some browsers/environments only. The single highest-probability integration defect in this package.updateOrCreate, a returning user with null values overwrites a stored name. Product-visible data-quality bug that surfaces weeks later in support tickets and lifecycle email failures.@privaterelay.appleid.com addresses plus email_verified sometimes arriving as the string "true". Any naive casting or email-derived identity logic fails here. Decide before design whether marketing can rely on Apple-derived emails at all.Ramp-up
Low for a Laravel developer — the driver is idiomatic Socialite and the controller is boring by design. Realistic budget:
Frame the funding ask honestly with stakeholders: this package buys the OAuth2 adapter. It does not buy Apple auth. Secret signing, token verification, and revocation are engineering deliverables that exist whether or not this package is adopted — which makes the build-vs-buy decision narrow ("buy the adapter, build the lifecycle") rather than strategic.
How can I help you explore Laravel packages today?