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

Laravel Sign In With Apple Laravel Package

mikebronner/laravel-sign-in-with-apple

A Laravel package that adds Sign in with Apple authentication to your app. It handles Apple's token verification, user lookup and creation, and integrates cleanly with Laravel's existing auth scaffolding and Socialite-style flows.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require mikebronner/laravel-sign-in-with-apple
php artisan vendor:publish --tag=apple-config   # or the package's published tag

13.x consolidates configuration under config/services.php → services.apple. Set the credentials as env vars and reference them there:

// config/services.php
'apple' => [
    'client_id'   => env('APPLE_CLIENT_ID'),      // your Services ID, e.g. com.example.web
    'team_id'     => env('APPLE_TEAM_ID'),
    'key_id'      => env('APPLE_KEY_ID'),
    'private_key' => env('APPLE_PRIVATE_KEY'),    // contents of the .p8, or a path
    'redirect'    => env('APPLE_REDIRECT_URI'),
],

2. Where to look first

  • config/services.php → the apple block is the single source of truth in 13.x. Everything else reads from it.
  • The auto-registered routes (new in 13.0.0 via #66) — run php artisan route:list | grep -i apple to see exactly what was registered and under which middleware group.
  • The package's controller — the callback handler is where tokens are exchanged and where the new descriptive exceptions are thrown. Read it before you customize anything.
  • Apple's own requirements page for the redirect URL, since 13.0.0 now validates your redirect against them.

3. First use case

Drop a "Sign in with Apple" button on your login page that hits the package's redirect route, then handle the callback by finding-or-creating a user:

// routes/web.php
Route::get('/auth/apple/redirect', fn () => Socialite::driver('sign-in-with-apple')->redirect());

Route::get('/auth/apple/callback', function () {
    $appleUser = Socialite::driver('sign-in-with-apple')->user();

    $user = User::firstOrCreate(
        ['apple_id' => $appleUser->getId()],
        [
            'name'       => $appleUser->getName(),
            'first_name' => $appleUser->getFirstName(), // new in 13.0.0
            'last_name'  => $appleUser->getLastName(),  // new in 13.0.0
            'email'      => $appleUser->getEmail(),
        ]
    );

    Auth::login($user);

    return redirect()->intended('/dashboard');
})->middleware(['web']);

That's the whole happy path. Everything below is what you'll actually spend your time on.

Implementation Patterns

Prefer the auto-registered routes until you outgrow them. 13.0.0 registers the redirect and callback routes and controller for you. Use them for a first integration, then take over by pointing the callback at your own controller (or disabling route registration in config) once you need custom post-login redirects, tenant resolution, or logging. Don't half-override: pick one.

Treat the callback as a state machine. The reliable shape is:

  1. Exchange code → tokens via the driver.
  2. Look up apple_id (the sub claim). If found → log in, optionally refresh email.
  3. If not found → create the user, persisting first/last name immediately (see gotchas).
  4. Fire your own AppleSignedIn event; do account linking, onboarding, MFA prompts, etc. in listeners rather than in the controller.

Cache the client secret. The new PHP client_secret JWT generator (#68) removes the need for the Ruby/PHP helper script you previously ran out-of-band. Generate on demand and cache it:

$secret = Cache::remember('apple.client_secret', now()->addDays(150), fn () =>
    AppleClientSecret::generate(
        teamId: config('services.apple.team_id'),
        keyId:  config('services.apple.key_id'),
        clientId: config('services.apple.client_id'),
        privateKey: config('services.apple.private_key'),
    )
);

Regenerating per request is wasteful (ES256 signing + it's called on every token exchange and revocation check). Regenerating never is worse — Apple caps the lifetime at 6 months.

Wrap the callback in a typed catch. 13.0.0 adds descriptive exceptions for token errors (#65), invalid_client (#73), missing authorization code (#72), and 5xx upstream failures (#75). Map them explicitly instead of swallowing everything:

try {
    $appleUser = Socialite::driver('sign-in-with-apple')->user();
} catch (MissingAuthorizationCodeException $e) {
    return redirect()->route('login')->withErrors(['apple' => 'Sign in was cancelled.']);
} catch (InvalidClientException $e) {
    report($e); // your client_secret / team / key config is wrong
    return back()->withErrors(['apple' => 'Apple sign in is misconfigured.']);
} catch (AppleServerException $e) {
    report($e); // 5xx — transient, safe to retry
    return back()->withErrors(['apple' => 'Apple is unavailable, try again.']);
}

The distinction matters operationally: invalid_client is your bug and pages someone; a 5xx is Apple's and should not alarm.

Handle revocation as a real feature, not an afterthought. #74 adds handling for Apple's account-deletion / access-revocation notifications. Expose the endpoint Apple calls, verify the signature, and treat it as idempotent:

// job dispatched from the revocation handler
User::where('apple_id', $payload['sub'])
    ->update(['apple_id' => null, 'apple_revoked_at' => now()]);

Revoke sessions and any refresh tokens you store. Handle "user not found" silently — Apple will retry, and you don't want a 500 loop.

Test by faking the HTTP layer, not the driver. Http::fake() around the token endpoint plus a locally-signed JWT for the id_token gets you a fast, honest test of your callback without touching Apple. Build a trait that boots a fake private key and asserts both the success path and each new exception path — the descriptive exceptions are only useful if you actually handle them.

Config-driven per environment. Because everything routes through services.apple, you can define a distinct Services ID per environment (Apple requires separate IDs for staging vs production anyway). Keep redirect absolute HTTPS and environment-specific.

Gotchas and Tips

The config key move is the breaking change. 13.0.0 shortens config to services.apple (#64). If you're upgrading from 12.x, any config('services.apple_sign_in...') or package-namespaced key will silently return null — which surfaces as invalid_client at login time rather than an error at boot. Grep your entire codebase and every .env for the old keys, re-publish the config, and add a config assertion to your deploy checklist or a Pest test that config('services.apple.client_id') is non-null in each environment.

Apple sends first_name/last_name exactly once — ever. They arrive on the very first authorization only, in the user payload of the initial POST, not in the id_token. If you don't persist them on that request, they are gone forever. The new getFirstName()/getLastName() accessors (#71) are only useful if you write them to the database in the same request that reads them. Never treat them as refreshable profile data.

The callback is a form_post, not a GET. Apple POSTs to your redirect URI. That's the source of the CSRF mismatch fixed in #67 — but if you're on a custom controller, custom route, or non-standard middleware stack, you may still need the callback path excluded from VerifyCsrfToken and possibly treated as stateless. Symptom: 419 on the callback while everything else looks fine.

Redirect URL validation is now strict (#70). Apple requires HTTPS, a real domain (no localhost, no IPs), no query string, no fragment, and an exact match against what's registered in the Apple Developer portal. Trailing slash mismatches count. 13.0.0 throws early instead of failing opaquely later — read the exception message, it's telling you precisely which rule you violated.

invalid_client almost always means one of four things: rotated .p8 key not updated, team_id/key_id swapped, expired client secret, or a client_id that doesn't match the Services ID in the portal. Check them in that order. The new exception (#73) makes it loud, but it can't tell you which one.

Email may be a private relay address. Users can hide their email; @privaterelay.appleid.com aliases are expected, not a bug. Don't validate against your corporate domain allowlist or attempt to email from a different sending domain without registering it with Apple.

Don't retry on invalid_client or invalid_grant. Only the 5xx path (#75) is safe to retry. Blindly wrapping the exchange in retry(3) will hammer Apple with bad credentials and can get your Services ID rate-limited.

Log the raw Apple response on failure, never the tokens. The new exception messages include enough context to debug most issues; add the request ID Apple returns for support tickets. Redact id_token, access_token, and refresh_token from anything that hits your logs.

Extension points worth knowing: the config flag for route auto-registration (turn it off the moment you need a custom controller), the container binding for the controller if you only want to override one method, and the events the package fires during the flow — hook those rather than editing vendored code. On Laravel 13 / PHP 8.5, verify your whole dependency tree resolves before committing; a package pinned to an older Socialite major can block the upgrade.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console