socialiteproviders/apple
Apple
composer require socialiteproviders/apple
Installation & Basic Usage
Please see the Base Installation Guide, then follow the provider specific instructions below.
Add configuration to config/services.php
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'),
'redirect' => env('APPLE_REDIRECT_URI')
],
See Configure Apple ID Authentication
Note: the client secret used for "Sign In with Apple" is a JWT token that can have a maximum lifetime of 6 months. The article above explains how to generate the client secret on demand and you'll need to update this every 6 months. To generate the client secret for each request, see Generating A Client Secret For Sign In With Apple On Each Request
If you don't have secret token, or you don't want to it do manually, you can use a private key (see official docs). Add lines to the configuration as follows:
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'), // Required. Bundle ID from Identifier in Apple Developer.
'client_secret' => env('APPLE_CLIENT_SECRET'), // Empty. We create it from private key.
'key_id' => env('APPLE_KEY_ID'), // Required. Key ID from Keys in Apple Developer.
'team_id' => env('APPLE_TEAM_ID'), // Required. App ID Prefix from Identifier in Apple Developer.
'private_key' => env('APPLE_PRIVATE_KEY'), // Required. Must be absolute path, e.g. /var/www/cert/AuthKey_XYZ.p8
'passphrase' => env('APPLE_PASSPHRASE'), // Optional. Set if your private key have a passphrase.
'signer' => env('APPLE_SIGNER'), // Optional. Signer used for Configuration::forSymmetricSigner(). Default: \Lcobucci\JWT\Signer\Ecdsa\Sha256
'redirect' => env('APPLE_REDIRECT_URI'), // Required.
'jwt_issued_time_leeway' => env('APPLE_JWT_ISSUED_TIME_LEEWAY'), // Optional. Set this to add a leeway to your JWT issued_time value. See section below
],
If you receive error 400 Bad Request {"error":"invalid_client"} , a possible solution is to use another Signer (Asymmetric algorithms), see Asymmetric algorithms.
Add provider event listener
Laravel 11+
In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your AppServiceProvider boot method.
- Note: You do not need to add anything for the built-in socialite providers unless you override them with your own providers.
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('apple', \SocialiteProviders\Apple\Provider::class);
});
Add the event to your listen[] array in app/Providers/EventServiceProvider. See the Base Installation Guide for detailed instructions.
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... other providers
\SocialiteProviders\Apple\AppleExtendSocialite::class.'@handle',
],
];
Usage
You should now be able to use the provider like you would regularly use Socialite (assuming you have the facade installed):
return Socialite::driver('apple')->redirect();
Callback state and nonce
Apple posts the callback to your redirect URL as a cross-site POST
(response_mode=form_post, which Apple requires whenever scopes are
requested). On the callback the provider checks the state
against the session, and checks the identity token's nonce against the one
issued on redirect, so a callback the app did not start is rejected with
InvalidStateException.
Both checks need the session cookie to arrive with that POST. Laravel's
default SameSite=lax cookie is not sent on a cross-site POST, so a
default install sees an empty session and rejects every callback. You have
to let the cookie cross the site boundary:
SESSION_SAME_SITE=none
SESSION_SECURE_COOKIE=true
SameSite=none loosens every cookie the app sets, not just Apple's. If you
would rather keep lax elsewhere, run the flow stateless with
cookieNonce() instead (see below).
Without a session (stateless)
If you cannot keep the session on the callback, run the flow stateless with
cookieNonce() and the provider handles the nonce for you:
// Redirect. The provider generates the nonce, sends it to Apple, and sets it
// on the browser in an encrypted cookie.
return Socialite::driver('apple')->stateless()->cookieNonce()->redirect();
// Callback. The provider reads the nonce back from the cookie and verifies it
// against the identity token.
$user = Socialite::driver('apple')->stateless()->cookieNonce()->user();
The nonce travels in a socialite_apple_nonce cookie (Secure, HttpOnly,
SameSite=none), encrypted with your APP_KEY so the client cannot forge
it. Only the browser that started the login carries it, which binds the flow
to that browser as RFC 9700 section 2.1 requires: a callback
captured and replayed in another browser has no cookie and is rejected, and
so is a tampered one. No session, cache, or server-side state is involved.
Because the cookie is SameSite=none it needs HTTPS, the callback must be
served over TLS or the browser drops it. This is one purpose-built cookie
rather than loosening every session cookie, so lax stays in force for the
rest of the app.
Set nonce_ttl (seconds, default 600) to change how long the cookie,
and so the login attempt, stays valid:
'apple' => [
// ...
'nonce_ttl' => env('APPLE_NONCE_TTL', 600),
],
To hold the nonce yourself instead, call setNonce() with a value you
generate, send to Apple, and hand back. Calling stateless() without either
throws InvalidStateException rather than accepting the callback
unprotected.
For native iOS and Android clients that already hand you an identity token,
use userByIdentityToken() (below) instead.
Native apps (identity token)
Native iOS and Android clients hand your server an identity token directly. Pass the nonce from the authorization request to have it verified, as Apple requires:
$user = Socialite::driver('apple')->userByIdentityToken($identityToken, $nonce);
The nonce is optional for backwards compatibility, but omitting it means a token can be replayed until it expires.
Apple issues an identity token for whichever client requested it, so one API serving several clients sees several audiences:
| Client | aud |
|---|---|
| Web, Android (Sign in with Apple JS / REST) | Services ID |
| Native iOS, macOS | App ID (bundle ID) of that app |
Keep client_id as the Services ID, which the web flow needs, and list the
others in audiences:
'apple' => [
// ...
'audiences' => ['com.example.app', 'com.example.app.macos'],
],
userByIdentityToken() and userFromToken() accept a token issued for
client_id or any of audiences. The web callback only accepts client_id.
Returned User fields
idnameemail
name comes from the user field of the callback POST, not from the
signed identity token, and Apple only sends it on the first authorization.
Treat it as user input: validate and sanitise it before storing.
Known Issues
JWT Issued_at
Sometimes the plugin may throw an exception due to a mismatch in time - See #1354. Use config('services.apple.jwt_issued_time_leeway') to 'rewind' the time. Default value is 3 seconds (PT3S).
Examples of possible values are PT3S -> 3 seconds, PT1M -> 1 Minute etc ...
The thrown exception may look like this:
[object] (Laravel\\Socialite\\Two\\InvalidStateException(code: 0): The token violates some mandatory constraints, details: - The token was issued in the future at /vendor/socialiteproviders/apple/Provider.php:207) [stacktrace]
Invalid audience
The identity token's aud claim must match config('services.apple.client_id')
or one of config('services.apple.audiences'). Native apps send their bundle ID
as the audience rather than the Services ID used for the web flow - see
Native apps.
The thrown exception looks like this:
Laravel\Socialite\Two\InvalidStateException: The token violates some mandatory constraints, details:
- The token is not allowed to be used by this audience