Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Apple Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

socialiteproviders/apple is a thin Socialite driver — nothing more. Read SocialiteProviders\Apple\Provider first: it implements only getAuthUrl, getTokenFields, getUserByToken, and mapUserToObject. Everything else (signing the client secret, JWKS verification, refresh, revocation) is yours. Knowing that up front saves you the afternoon you'd otherwise spend looking for methods that don't exist.

Apple Developer prerequisites. You need an App ID (native) or Services ID (web), the Sign in with Apple capability enabled, a .p8 private key, your Team ID, Key ID, and a registered Return URL. The Return URL must be HTTPS and byte-match your config — a trailing slash in redirect()->route(...) that differs from the console entry produces a redirect-URI mismatch. Use ngrok, expose, or a Cloudflare Tunnel locally; http://localhost will not work.

Install and register:

composer require socialiteproviders/apple socialiteproviders/manager

Laravel 10 and earlier, in app/Providers/EventServiceProvider.php:

protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        \SocialiteProviders\Apple\AppleExtendSocialite::class.'@handle',
    ],
];

Laravel 11/12 ship without an EventServiceProvider, so use AppServiceProvider::boot():

use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;

public function boot(): void
{
    Event::listen(function (SocialiteWasCalled $event) {
        $event->extendSocialite('apple', \SocialiteProviders\Apple\Provider::class);
    });
}

Config plus the secret you have to build yourself:

'apple' => [
    'client_id' => env('APPLE_CLIENT_ID'),      // Services ID for web
    'client_secret' => env('APPLE_CLIENT_SECRET'), // signed ES256 JWT — see below
    'redirect' => env('APPLE_REDIRECT_URI'),
],

Apple expects an ES256 JWT with iss = Team ID, sub = client ID, aud = https://appleid.apple.com, kid in the header:

use Firebase\JWT\JWT;

$secret = JWT::encode([
    'iss' => config('services.apple.team_id'),
    'iat' => now()->timestamp,
    'exp' => now()->addDays(180)->timestamp, // Apple's maximum
    'aud' => 'https://appleid.apple.com',
    'sub' => config('services.apple.client_id'),
], file_get_contents(storage_path('apple/AuthKey_ABC123.p8')), 'ES256', config('services.apple.key_id'));

Routes — the callback is POST:

Route::get('/auth/apple/redirect', [AppleController::class, 'redirect']);
Route::post('/auth/apple/callback', [AppleController::class, 'callback']);

Apple uses response_mode=form_post whenever name/email scopes are requested, and the provider sets that for you. Getting the verb wrong is the most common first-day failure.

First end-to-end use case — web login persisting the sub:

public function redirect()
{
    return Socialite::driver('apple')->scopes(['name', 'email'])->redirect();
}

public function callback(Request $request)
{
    $appleUser = Socialite::driver('apple')->user();

    // Name arrives only on first consent, in the POST body — not the id_token.
    $name = null;
    if ($request->filled('user')) {
        $decoded = json_decode($request->input('user'), true);
        $name = trim(($decoded['name']['firstName'] ?? '').' '.($decoded['name']['lastName'] ?? ''));
    }

    $user = User::firstOrCreate(
        ['apple_id' => $appleUser->getId()],
        ['email' => $appleUser->getEmail(), 'name' => $name ?: null]
    );

    Auth::login($user, true);

    return redirect()->intended('/dashboard');
}

Implementation Patterns

Keep the callback controller boring. Call user(), resolve by the Apple sub, never by email. Wrap in try/catch for InvalidStateException and \Throwable from the token exchange so a failed login redirects back with a flash message instead of a 500.

First-login profile capture. Apple returns name and email only on the user's first authorization. The id_token carries email (and email_verified, sometimes the string "true"), but the name only exists in the user form field of the callback POST. Use firstOrCreate, not updateOrCreate — later logins send null and you'll wipe a stored name.

Native/API clients. If an iOS app already holds an id_token:

$user = Socialite::driver('apple')->stateless()->userFromToken($idToken);

Treat this as an identity lookup, not authentication. The provider decodes the JWT payload; that is not signature verification. Verify against Apple's JWKS (https://appleid.apple.com/auth/keys) with firebase/php-jwt and check iss, exp, and aud yourself.

Multiple audiences (6.1.0). When one backend serves both a Services ID (web) and a bundle ID (native), 6.1.0 accepts multiple identity-token audiences. Read the provider source in your pinned version to confirm how the audience list is consumed before wiring it into config — don't assume a single client_id string is still the only validated value.

Capturing refresh tokens for account deletion. App Store rules require revocation on account deletion, and user() hides the raw token response. Do the exchange explicitly:

$response = Socialite::driver('apple')->getAccessTokenResponse($request->input('code'));
$user = Socialite::driver('apple')->userFromToken($response['id_token']);
// store $response['refresh_token'] encrypted

Revoke later with a POST to https://appleid.apple.com/auth/revoke carrying client_id, a freshly signed client_secret, token, and token_type_hint=refresh_token.

Rotate the secret on a schedule. Six-month expiry is easy to forget until it breaks login. Generate on demand and cache:

$secret = Cache::remember('apple.client_secret', now()->addDays(30), fn () => $this->signAppleJwt());
config(['services.apple.client_secret' => $secret]);

Better: an php artisan apple:secret command that writes to cache, called from a scheduler entry. Never drop a static JWT in .env and walk away.

Wrap the provider behind an internal boundary. A small SignInWithApple service exposing redirect(), userFromCallback(Request), and revoke(string $refreshToken) keeps the Socialite surface in one class, gives you a test seam, and makes an eventual exit from the package a one-file change.

Subclass when you need more. Custom token exchange, refresh capture, JWKS verification, or extra audience validation — extend and register your class instead of the base:

class AppleProvider extends \SocialiteProviders\Apple\Provider
{
    protected function getUserByToken($token) { /* ... */ }
}

// in the SocialiteWasCalled listener:
$event->extendSocialite('apple', AppleProvider::class);

Testing. Socialite has no built-in fake; mock the driver in feature tests:

Socialite::shouldReceive('driver->user')->andReturn(
    (new \Laravel\Socialite\Two\User)->map(['id' => '001234.abcd', 'email' => '[email protected]'])
);

For the callback route, include a user form field in the POST so the first-login name path is actually exercised, and assert the CSRF exclusion is in place.

Gotchas and Tips

  • The callback is POST. This is the number-one source of "invalid state" and 419s. Register with Route::post and exclude it from CSRF: Laravel 11+ $middleware->validateCsrfTokens(except: ['auth/apple/callback']), Laravel 10 VerifyCsrfToken::$except.
  • SameSite cookies. Apple posts cross-site. With Laravel's default SESSION_SAME_SITE=lax, the session cookie isn't sent on the POST and the state check fails. Set SESSION_SAME_SITE=none (requires HTTPS) or go stateless.
  • client_secret is not generated by the package. invalid_client from Apple is nearly always a bad JWT: wrong sub (must be the client ID, not the Team ID), wrong kid, stale exp, or mangled newlines in the .p8. Store the file path and file_get_contents it; if you must use env vars, replace literal \n.
  • Scopes are required for email/name. Without ->scopes(['name','email']) Apple may return only sub — and scoped requests are what trigger the form_post behaviour above.
  • Name is not in the id_token. It's $request->input('user'), a JSON string, present only on first consent.
  • Emails can be private relay addresses (@privaterelay.appleid.com) and email_verified may arrive as a string. Normalise rather than naively type-casting.
  • aud and iss checks. Valid issuer is https://appleid.apple.com. Supporting both web and native means configuring all expected audiences (see 6.1.0) or you'll reject legitimate tokens.
  • Driver [apple] is not supported means the SocialiteWasCalled listener isn't registered — check the service provider, run php artisan config:clear after config changes.
  • The package does not implement revocation, refresh, or JWKS verification. Those are your integration responsibilities. Keep the refresh_token; build the revoke call.
  • stateless() disables the state check by design. Use it only for API clients carrying their own id_token, never for the browser flow.
  • Debugging: log the raw token response outside production, paste the id_token into jwt.io (it is not encrypted), and verify aud, iss, exp, sub. Watch for clock skew on exp.
  • Return URL must byte-match the Apple developer console entry — trailing slashes and query strings both break it.
  • Pin the version and watch upstream. 58 stars and 0 listed dependents means you are relying on the SocialiteProviders/Providers monorepo to keep this moving. Read the changelog for provider-behaviour changes (6.1.0's audience handling is exactly the kind of change that alters validation semantics), and keep the internal abstraction so replacement is cheap.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core