genealabs / laravel-sign-in-with-apple
Add Apple's new single-signon feature to your site with ease.
Package info
github.com/mikebronner/laravel-sign-in-with-apple
pkg:composer/genealabs/laravel-sign-in-with-apple
Fund package maintenance!
Requires
- php: ^8.2
- firebase/php-jwt: ^7.0
- illuminate/support: ^11.0|^12.0|^13.0
- laravel/socialite: ^5.6
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- orchestra/testbench-browser-kit: ^9.0|^10.0|^11.0
- orchestra/testbench-dusk: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.5.3|^12.5.12
- predis/predis: ^2.1|^3.4
Suggests
- genealabs/laravel-socialiter: Automatic user resolution and persistence for any Laravel Socialite driver.
- dev-master
- 13.0.0
- 12.0.0
- 0.8.0
- 0.7.0
- 0.6.0
- 0.5.0
- 0.4.4
- 0.4.3
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.1
- 0.3.0
- 0.2.1
- 0.2.0
- 0.1.2
- 0.1.1
- 0.1.0
- dev-feat/7-shorten-config-keys
- dev-feat/37-php-client-secret-generation
- dev-fix/53-invalid-client-error-handling
- dev-feat/50-handle-revoked-access
- dev-feat/57-add-php-8-5-support
- dev-feat/6-add-tests
- dev-fix/32-csrf-token-mismatch
- dev-feat/27-update-configuration
- dev-feat/26-package-routes-controllers
- dev-feat/45-fix-laravel-8-install
- dev-feat/56-add-laravel-13-support
- dev-fix/49-handle-apple-server-errors
- dev-fix/43-validate-redirect-url
- dev-fix/44-missing-authorization-code
- dev-feat/46-name-surname-handling
- dev-fix/38-invalid-client-error
- dev-feat/24-handle-invalid-grant-error
This package is auto-updated.
Last update: 2026-03-30 01:08:54 UTC
README
Supporting This Package
This is an MIT-licensed open source project with its ongoing development made possible by the support of the community. If you'd like to support this, and our other packages, please consider sponsoring us via the button above.
We thank the following sponsors for their generosity, please take a moment to check them out:
Table of Contents
Requirements
- PHP 8.2+
- Laravel 10.0+
- Socialite 5.0+
- Apple Developer Subscription
Version Support
| Laravel | PHP | Package |
|---|---|---|
| 10.x | 8.2+ | 5.x |
| 11.x | 8.2+ | 5.x |
| 12.x | 8.2+ | 5.x |
| 13.x | 8.3+ | 5.x |
Installation
-
Install the composer package:
composer require mikebronner/laravel-sign-in-with-apple
We also recommend using geneaLabs/laravel-socialiter to automatically manage user resolution and persistence:
composer require genealabs/laravel-socialiter
Configuration
- Create an
App IDfor your website (https://developer.apple.com/account/resources/identifiers/list/bundleId) with the following details:- Platform: iOS, tvOS, watchOS (I'm unsure if either choice has an effect for web apps)
- Description: (something like "example.com app id")
- Bundle ID (Explicit): com.example.id (or something similar)
- Check "Sign In With Apple"
- Create a
Service IDfor your website (https://developer.apple.com/account/resources/identifiers/list/serviceId) with the following details:- Description: (something like "example.com service id")
- Identifier: com.example.service (or something similar)
- Check "Sign In With Apple"
- Configure "Sign In With Apple":
- Primary App Id: (select the primary app id created in step 1)
- Web Domain: example.com (the domain of your web site)
- Return URLs: https://example.com/apple-signin (the route pointing to the callback method in your controller)
- Click "Save".
- Click the "Edit" button to edit the details of the "Sign In With Apple" configuration we just created.
- If you haven't verified the domain yet, download the verification file, upload it to https://example.com/.well-known/apple-developer-domain-association.txt, and then click the "Verify" button.
- Create a
Private Keyfor your website (https://developer.apple.com/account/resources/authkeys/list) with the following details:- Key Name:
- Check "Sign In With Apple"
- Configure "Sign In With Apple":
- Primary App ID: (select the primary app id created in step 1)
- Click "Save"
- Click "Continue"
- Click "Register"
- Click "Download"
- Rename the downloaded file to
key.txt
- Create your app's client secret:
-
Install the JWT Gem:
sudo gem install jwt
-
Create a file called
client_secret.rbto process the private key:require 'jwt' key_file = 'key.txt' team_id = '' client_id = '' key_id = '' ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file headers = { 'kid' => key_id } claims = { 'iss' => team_id, 'iat' => Time.now.to_i, 'exp' => Time.now.to_i + 86400*180, 'aud' => 'https://appleid.apple.com', 'sub' => client_id, } token = JWT.encode claims, ecdsa_key, 'ES256', headers puts token
-
Fill in the following fields:
team_id: This can be found on the top-right corner when logged into your Apple Developer account, right under your name.client_id: This is the identifier from the Service Id created in step 2 above, for example com.example.servicekey_id: This is the identifier of the private key created in step 3 above.
-
Save the file and run it from the terminal. It will spit out a JWT which is your client secret, which you will need to add to your
.envfile in the next step.ruby client_secret.rb
-
Alternative: Generate client_secret in PHP
Instead of using the Ruby script above, you can generate the client secret JWT directly in PHP using this package's built-in helper:
use GeneaLabs\LaravelSignInWithApple\Support\ClientSecretGenerator; // One-off generation $secret = ClientSecretGenerator::generate( teamId: 'YOUR_TEAM_ID', clientId: 'com.example.service', keyId: 'YOUR_KEY_ID', privateKey: file_get_contents(storage_path('keys/apple-auth-key.p8')), ttlDays: 180, // Max 180 days ); // Or use config/env values automatically $secret = ClientSecretGenerator::fromConfig();
You can use an Artisan command or scheduled task to auto-rotate the secret before it expires:
// In a scheduled command or service provider $secret = ClientSecretGenerator::fromConfig(ttlDays: 180); config(['services.sign_in_with_apple.client_secret' => $secret]);
Required env vars for fromConfig():
SIGN_IN_WITH_APPLE_TEAM_ID=your-team-id SIGN_IN_WITH_APPLE_KEY_ID=your-key-id SIGN_IN_WITH_APPLE_PRIVATE_KEY_PATH=/path/to/key.p8
-
Set the necessary environment variables in your
.envfile:APPLE_REDIRECT="/apple/login/controller/callback/action" APPLE_CLIENT_ID="your app's service id as registered with Apple" APPLE_CLIENT_SECRET="your app's client secret as calculated in step 4"
Note: The
APPLE_LOGINenvironment variable has been removed (previouslySIGN_IN_WITH_APPLE_LOGIN). Login routes should be defined in your application's route files instead. See the Migration Guide below if upgrading from an older version.
Redirect URL Requirements
Apple has strict requirements for the redirect (callback) URL:
- Must use HTTPS — HTTP is rejected in production. Only
http://localhostis allowed for local development. - Must exactly match the Return URL registered in your Apple Developer account under Services ID configuration.
- No query parameters — Apple will reject URLs with query strings.
- No fragments — Hash fragments are not supported.
The package validates your redirect URL at auth initiation and throws an InvalidRedirectUrlException with a clear error message if it doesn't meet these requirements.
Common mistakes:
- Using
http://instead ofhttps://in production - Having a trailing slash mismatch between config and Apple Developer Console
- Forgetting to add the URL to your Services ID in the Apple Developer portal
Implementation
Button
Add the following blade directive to your login page:
@signInWithApple($color, $hasBorder, $type, $borderRadius)
| Parameter | Definition |
|---|---|
| $color | String, either "black" or "white. |
| $hasBorder | Boolean, either true or false. |
| $type | String, either "sign-in" or "continue". |
| $borderRadius | Integer, greater or equal to 0. |
CSRF Exclusion
Apple sends the authorization response as a POST request to your callback URL. This would normally trigger a 419 | Page Expired (CSRF token mismatch) error. This package automatically excludes the configured callback route from CSRF verification, so no additional configuration is required.
This is safe because Apple callbacks are validated via the OAuth state parameter, not CSRF tokens. If you need to manually exclude the route for any reason, you can use one of these approaches:
Option A: Exclude the route in your VerifyCsrfToken middleware (Laravel 10 and earlier):
// app/Http/Middleware/VerifyCsrfToken.php protected $except = [ '/apple/callback', // or whatever your callback URL is ];
Option B: Use withoutMiddleware on the route (Laravel 11+):
Route::post('/apple/callback', [AppleSigninController::class, 'callback']) ->withoutMiddleware([\\Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken::class]);
Controller
This implementation uses Socialite to get the login credentials. The following is an example implementation of the controller:
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use GeneaLabs\LaravelSocialiter\Facades\Socialiter; use Laravel\Socialite\Facades\Socialite; class AppleSigninController extends Controller { public function __construct() { $this->middleware('guest')->except('logout'); } public function login() { return Socialite::driver("sign-in-with-apple") ->scopes(["name", "email"]) ->redirect(); } public function callback(Request $request) { // get abstract user object, not persisted $user = Socialite::driver("sign-in-with-apple") ->user(); // or use Socialiter to automatically manage user resolution and persistence $user = Socialiter::driver("sign-in-with-apple") ->login(); } }
Note that when processing the returned $user object, it is critical to know that the sub element is the unique identifier for the user, NOT the email address. For more details, visit https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple.
Missing Authorization Code
If you receive an error about a missing authorization code in the callback, check:
-
Your callback route must accept POST requests — Apple uses
response_mode=form_post, which means the authorization code is sent as a POST form parameter, not a URL query parameter. UseRoute::post(), notRoute::get(). -
CSRF protection must be disabled for the callback — Since Apple's POST doesn't include a CSRF token, Laravel will return a 419 error and the code will never reach your controller. See the CSRF Exclusion section above.
-
The redirect URL must exactly match — The URL in your
.env(APPLE_REDIRECT) must exactly match the Return URL configured in your Apple Developer account, including the protocol, domain, and path.
Handling Revoked Access
When a user revokes your app's access via Apple ID settings, Apple sends a server-to-server notification. This package provides an AppleNotificationController and AppleAccessRevoked event to handle this.
1. Register the notification route:
use GeneaLabs\LaravelSignInWithApple\Http\Controllers\AppleNotificationController; Route::post('/apple/notifications', [AppleNotificationController::class, 'handle']) ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
2. Listen for the revocation event:
// In EventServiceProvider or a listener use GeneaLabs\LaravelSignInWithApple\Events\AppleAccessRevoked; Event::listen(AppleAccessRevoked::class, function (AppleAccessRevoked $event) { // $event->sub — the Apple user ID // $event->eventType — 'consent-revoked' or 'account-delete' $user = User::where('apple_id', $event->sub)->first(); if ($user) { // Deactivate, log out, or clean up } });
3. Configure the endpoint in Apple Developer:
Add your notification URL (https://example.com/apple/notifications) in the Apple Developer portal under your Services ID configuration.
Important: Apple only provides the user's name and email on the first authorization. If a user revokes access and re-authenticates, Apple treats it as a new sign-in but may not provide the name again. Always store the user's name on first sign-in.
Handling re-authentication after revocation:
When a user revokes and re-authenticates, Apple may assign a new sub value. The Socialite user object includes an is_returning_user flag to help you detect this:
$appleUser = Socialite::driver('sign-in-with-apple')->user(); if ($appleUser['is_returning_user']) { // User re-authenticated after revocation — match by email $user = User::where('email', $appleUser->getEmail())->first(); } else { // First-time sign-in — name is available $user = User::firstOrCreate( ['apple_id' => $appleUser->getId()], ['name' => $appleUser->getName(), 'email' => $appleUser->getEmail()] ); }
Security: The notification endpoint verifies Apple's JWT signatures against Apple's public keys (fetched from https://appleid.apple.com/auth/keys). Keys are cached for 1 hour. Unsigned or forged notifications are rejected.
Testing
This package includes unit, feature, and browser tests. Unit and feature tests run without any additional dependencies:
vendor/bin/phpunit --testsuite=Unit,Feature
Browser Tests
Browser tests use Laravel Dusk via orchestra/testbench-dusk and require a Chrome-based browser installed on your machine.
Install Chrome or Chromium:
-
Google Chrome (recommended):
# macOS brew install --cask google-chrome # Ubuntu/Debian sudo apt-get install google-chrome-stable
-
Chromium (lighter alternative):
# macOS brew install --cask chromiumOn macOS, Chromium may be blocked by Gatekeeper. Remove the quarantine flag after installing:
xattr -dr com.apple.quarantine /Applications/Chromium.app
Install Chromedriver:
The package uses orchestra/dusk-updater (included as a dev dependency) to manage the Chromedriver binary. Run this to auto-detect your Chrome version and install the matching Chromedriver:
# If Google Chrome is installed (auto-detected): vendor/bin/dusk-updater detect --auto-update --no-interaction # If using Chromium on macOS (must specify the binary path): vendor/bin/dusk-updater detect --chrome-dir="/Applications/Chromium.app/Contents/MacOS/Chromium" --auto-update --no-interaction
Run browser tests:
vendor/bin/phpunit --testsuite=Browser
Run all tests:
vendor/bin/phpunit
Migration Guide
Upgrading from versions prior to the config update
The configuration has been simplified. The following changes were made:
| Old Key | New Behavior |
|---|---|
SIGN_IN_WITH_APPLE_LOGIN |
Removed. Define your login route in your application's route files instead of the config. |
SIGN_IN_WITH_APPLE_REDIRECT |
Renamed to APPLE_REDIRECT. Old name still works as fallback. |
SIGN_IN_WITH_APPLE_CLIENT_ID |
Renamed to APPLE_CLIENT_ID. Old name still works as fallback. |
SIGN_IN_WITH_APPLE_CLIENT_SECRET |
Renamed to APPLE_CLIENT_SECRET. Old name still works as fallback. |
Steps to upgrade:
- Remove
SIGN_IN_WITH_APPLE_LOGINfrom your.envfile. - Rename env vars:
SIGN_IN_WITH_APPLE_REDIRECT→APPLE_REDIRECT,SIGN_IN_WITH_APPLE_CLIENT_ID→APPLE_CLIENT_ID,SIGN_IN_WITH_APPLE_CLIENT_SECRET→APPLE_CLIENT_SECRET. The old names continue to work as fallbacks. - If you relied on the
loginconfig key for the@signInWithAppleBlade directive button URL, define the route in your application's routes file and update the directive or link accordingly. - The package will emit a
E_USER_DEPRECATEDnotice if the oldloginkey is still present, giving you time to migrate before it is fully removed.
Credits
- https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
- https://developer.apple.com/sign-in-with-apple/get-started
Commitment to Quality
During package development I try as best as possible to embrace good design and development practices, to help ensure that this package is as good as it can be. My checklist for package development includes:
- ✅ Achieve as close to 100% code coverage as possible using unit tests.
- ✅ Eliminate any issues identified by SensioLabs Insight and Scrutinizer.
- ✅ Be fully PSR1, PSR2, and PSR4 compliant.
- ✅ Include comprehensive documentation in README.md.
- ✅ Provide an up-to-date CHANGELOG.md which adheres to the format outlined at http://keepachangelog.com.
- ✅ Have no PHPMD or PHPCS warnings throughout all code.
Troubleshooting
invalid_client Error
This means Apple rejected your credentials. Common causes:
- Wrong Client ID:
APPLE_CLIENT_IDmust be your Services ID (not your App ID or Team ID). - Wrong Client Secret: The JWT must be signed with the correct private key, team ID, and key ID.
- Key revoked: Check that your key is still active in your Apple Developer account under Keys.
invalid_grant Error
This means the authorization code was rejected. Common causes:
- Code already used: Apple authorization codes are single-use. If you refresh the callback page, the code will be invalid.
- Code expired: Codes are valid for a short time (approximately 5 minutes).
- Redirect URI mismatch: The
APPLE_REDIRECTmust exactly match the URL registered in your Apple Developer account. - Client secret expired: Apple client secret JWTs are valid for up to 6 months. Regenerate if expired.
Missing Configuration
If you see Sign In With Apple is missing required config, ensure you have set the following in your .env:
APPLE_CLIENT_ID=your-services-id
APPLE_CLIENT_SECRET=your-generated-jwt
APPLE_REDIRECT=https://your-app.com/callback
Contributing
Please observe and respect all aspects of the included Code of Conduct.
Reporting Issues
When reporting issues, please fill out the included template as completely as possible. Incomplete issues may be ignored or closed if there is not enough information included to be actionable.
Submitting Pull Requests
Please review the Contribution Guidelines. Only PRs that meet all criterium will be accepted.
If you ❤️ open-source software, give the repos you use a ⭐️.
We have included the awesome symfony/thanks composer package as a dev dependency. Let your OS package maintainers know you appreciate them by starring the packages you use. Simply run composer thanks after installing this package. (And not to worry, since it's a dev-dependency it won't be installed in your live environment.)

