Sign in with Apple Implementation for Websites
Sign in with Apple is required by Apple for native iOS/macOS apps: if an app offers any social authorization, Apple ID must be present. For web it's not formally mandatory, but needed where the audience is Apple device users and for web components of iOS-based apps.
Apple ID has unique features compared to Google OAuth and other providers:
- User can hide their real email—Apple issues a relay address like
[email protected] -
id_tokenreturns only on first auth with user name - Subsequent logins don't return name—must save on first login
- No refresh token in standard OAuth2 sense
Registering App in Apple Developer
- Certificates, Identifiers & Profiles → Identifiers → create App ID with Sign In with Apple enabled
- Create Services ID (web component)—specify domain and Redirect URL
- Create Key with Sign In with Apple enabled—download
.p8file (store securely, can download only once) - Note: Team ID, Client ID (= Services ID), Key ID
Generating client_secret
Apple doesn't use static secrets. client_secret is a JWT signed with the private .p8 key:
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
function generateAppleClientSecret(): string
{
$config = Configuration::forAsymmetricSigner(
new Sha256(),
InMemory::file(storage_path('keys/apple_auth.p8')),
InMemory::empty()
);
return $config->builder()
->issuedBy(config('services.apple.team_id')) // iss: Team ID
->permittedFor('https://appleid.apple.com') // aud
->relatedTo(config('services.apple.client_id')) // sub: Services ID
->issuedAt(new \DateTimeImmutable())
->expiresAt(new \DateTimeImmutable('+6 months'))
->withHeader('kid', config('services.apple.key_id'))
->getToken($config->signer(), $config->signingKey())
->toString();
}
Expires up to 6 months. Recreate token in advance via cron.
OAuth2 Flow
1. User redirect:
GET https://appleid.apple.com/auth/authorize
?client_id=com.example.web
&redirect_uri=https://example.com/auth/apple/callback
&response_type=code id_token
&response_mode=form_post
&scope=name email
&state=<random_string>
&nonce=<random_nonce>
2. Apple POSTs to redirect_uri with:
- code
- id_token
- state
- user (JSON with name—only on first login!)
Important: response_mode=form_post—Apple does POST, not GET. Redirect URI must accept POST.
Callback Handling
public function handleCallback(Request $request): RedirectResponse
{
// Verify state
abort_unless($request->state === session('apple_state'), 422);
// Decode id_token (without signature verification yet)
$idToken = $this->decodeIdToken($request->id_token);
// user comes only on first login
$appleUser = $request->has('user')
? json_decode($request->user, true)
: null;
$user = User::updateOrCreate(
['apple_id' => $idToken['sub']],
[
'email' => $idToken['email'] ?? null,
'email_verified_at' => $idToken['email_verified'] ? now() : null,
// Save name only if received (first login)
'name' => $appleUser
? trim(($appleUser['name']['firstName'] ?? '') . ' ' . ($appleUser['name']['lastName'] ?? ''))
: null,
]
);
// Update name only if not set before
if ($appleUser && !$user->name) {
$user->update(['name' => ...]);
}
Auth::login($user);
return redirect()->intended('/dashboard');
}
Verify id_token
Apple publishes public keys at https://appleid.apple.com/auth/keys. Verify with JWT:
// composer require firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
$keys = Cache::remember('apple_public_keys', 3600, function () {
return Http::get('https://appleid.apple.com/auth/keys')->json();
});
$payload = JWT::decode($idToken, JWK::parseKeySet($keys));
// Check: iss = appleid.apple.com, aud = client_id, exp, nonce
Relay Email and Limitations
If user hides email, Apple issues relay address @privaterelay.appleid.com. Emails only reach it if domain registered in Apple Developer Console → More → Configure Sign in with Apple for Email Communication.
Laravel Socialite
composer require laravel/socialite socialiteproviders/apple
// config/services.php
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'), // generated JWT
'redirect' => env('APPLE_REDIRECT_URI'),
],
Socialite Apple provider handles most details, but client_secret needs periodic refresh.
Work Timeline
| Stage | Time |
|---|---|
| Apple Developer registration | 0.5 day |
| client_secret generator + cron | 1 day |
| OAuth callback + id_token verification | 1.5 days |
| Relay email handling, name processing | 0.5 day |
| Tests + real device verification | 1 day |
Total: 4–5 days.







