NativePHP Mobile Social Auth#
Native Apple Sign-In and Google Sign-In for NativePHP mobile apps. Uses native platform SDKs (not browser-based redirects) for a seamless sign-in experience.
App Store Requirement: If your app offers any third-party sign-in (Google, Facebook, etc.), Apple requires you to also offer Sign in with Apple. Apps that don't comply will be rejected during App Store review. (Apple Guideline 4.8)
Features#
- Apple Sign-In -- Native
ASAuthorizationControlleron iOS with Face ID / Touch ID - Google Sign-In -- Native Credential Manager on Android, Google Sign-In SDK on iOS
- Identity tokens -- JWT tokens for server-side verification
- User info -- Name, email, profile photo
- Nonce support -- Replay protection for both providers
- Credential state -- Check if an Apple credential is still valid
- Events -- Livewire
#[OnNative]and JS event listeners
Platform Support#
| Feature | iOS | Android |
|---|---|---|
| Apple Sign-In | Yes | No (Apple limitation) |
| Google Sign-In | Yes | Yes |
| Credential State Check | Yes (Apple) | No |
| Sign Out | Yes (Google) | Yes (Google) |
Requirements#
- PHP 8.3+
- Laravel 11, 12, or 13
- NativePHP Mobile 3.x
- iOS 18.0+ / Android API 29+ (see Installation -- the Android minimum is not NativePHP's default)
- A paid Apple Developer Program membership for Apple Sign-In. The
com.apple.developer.applesigninentitlement cannot be provisioned by a free Personal Team, and the iOS Simulator refuses to launch a build carrying it without a provisioning profile. Google Sign-In needs no Apple account.
Installation#
composer require ikromjon/nativephp-mobile-social-auth
On Laravel 13 add -W: NativePHP Mobile 3.x pins guzzlehttp/guzzle ^7.9 while Laravel 13 ships
Guzzle 8, so Composer needs permission to downgrade it.
The service provider and facade are auto-discovered by Laravel. Auto-discovery is not enough on its
own -- NativePHP will not compile a plugin into a build unless it is also listed in your
NativeServiceProvider:
php artisan vendor:publish --tag=nativephp-plugins-providerphp artisan native:plugin:register ikromjon/nativephp-mobile-social-auth
Skip these and the app still builds, but every bridge call silently returns null. Confirm with:
php artisan native:plugin:list # should list 4 registered bridge functionsphp artisan native:plugin:validate # should report OK
Android: raise the minimum SDK#
This plugin requires Android API 29, and NativePHP defaults to 26. If you skip this, the build aborts before compiling.
The build error tells you to set NATIVEPHP_ANDROID_MIN_SDK in .env -- that does not work.
Nothing in NativePHP Mobile 3.3.x reads that variable; the value is read only from
config('nativephp.android.min_sdk'), and the shipped config never defines that key. Copy the
package config into your app and add it:
cp vendor/nativephp/mobile/config/nativephp.php config/nativephp.php
// config/nativephp.php'android' => [ 'min_sdk' => 29, // ... leave the rest of the array as shipped],
Then build the native projects:
php artisan native:install --force
Configuration#
1. Google Cloud Console Setup#
You need two OAuth client IDs from the same Google Cloud project:
Step 1: Create a project & consent screen
- Go to Google Cloud Console
- Create a new project (or select existing)
- Go to APIs & Services > OAuth consent screen
- Choose External, fill in app name and email
- Add scopes:
email,profile - Add your test email under Test users (required while in testing mode)
Step 2: Create an Android OAuth client
- Go to Credentials > Create Credentials > OAuth client ID
- Application type: Android
- Package name: your
NATIVEPHP_APP_IDfrom.env(e.g.com.yourcompany.yourapp) - SHA-1 fingerprint -- get it with:
Copied!cd nativephp/android && ./gradlew signingReport
- Click Create (you won't use this client ID directly -- Google uses it to verify your app's signing key)
Step 3: Create a Web OAuth client
- Credentials > Create Credentials > OAuth client ID
- Application type: Web application
- No redirect URIs needed
- Click Create
- Copy the Client ID -- this is your
GOOGLE_SERVER_CLIENT_ID
Step 4: Create an iOS OAuth client (if targeting iOS)
- Credentials > Create Credentials > OAuth client ID
- Application type: iOS
- Bundle ID: your
NATIVEPHP_APP_IDfrom.env - Click Create
- Copy the Client ID -- this is your
GOOGLE_IOS_CLIENT_ID - (Optional) Copy the iOS URL scheme shown below it (the client ID reversed,
com.googleusercontent.apps.123456789-abc) -- this isGOOGLE_IOS_REVERSED_CLIENT_ID. It is registered as the OAuth callback URL scheme, without which Google Sign-In cannot start. You only need to set it if your reversed ID differs from the default form; otherwise the plugin derives it fromGOOGLE_IOS_CLIENT_ID.
Why three client IDs? The Android client verifies your app's signing key. The Web client ID is used by Android Credential Manager and for backend token verification. The iOS client ID configures the Google Sign-In SDK on iOS.
Step 5: Add credentials to your .env
GOOGLE_IOS_CLIENT_ID=123456789-abc.apps.googleusercontent.com# Optional -- derived from GOOGLE_IOS_CLIENT_ID when omitted:# GOOGLE_IOS_REVERSED_CLIENT_ID=com.googleusercontent.apps.123456789-abcGOOGLE_SERVER_CLIENT_ID=123456789-xyz.apps.googleusercontent.com
The plugin picks up GOOGLE_SERVER_CLIENT_ID from your .env out of the box — it is read through the plugin's own social-auth config, so it keeps working after php artisan config:cache — and passes it to the native SDK automatically. No manual Android string resources needed.
To customize, you can optionally publish the config file:
php artisan vendor:publish --tag=social-auth-config
2. Apple Sign-In Setup#
The com.apple.developer.applesignin entitlement is automatically added by this plugin. You need to:
- Log in to Apple Developer Portal
- Go to Certificates, Identifiers & Profiles > Identifiers
- Select your App ID (matching
NATIVEPHP_APP_ID) - Enable Sign in with Apple capability
- Save
No .env configuration needed for Apple -- it uses the native iOS SDK directly.
Usage#
Important: Platform Behavior Differences#
| iOS | Android | |
|---|---|---|
| Apple Sign-In | Returns AuthResult directly |
Returns null (unsupported) |
| Google Sign-In | Returns AuthResult directly |
Returns null; result arrives via event |
On iOS, bridge calls block until the user completes or cancels sign-in, then return the result synchronously. The same result is also dispatched as an AppleSignInCompleted / GoogleSignInCompleted event — the synchronous return is a convenience only.
On Android, Google Sign-In is asynchronous -- the call returns immediately, and the result is delivered via GoogleSignInCompleted or SignInFailed events.
Recommended pattern: Handle results via event listeners as the single handling path — events fire on both platforms. Do not handle the return value AND register listeners for the same sign-in, or your handler runs twice on iOS:
Livewire (Recommended)#
<?php namespace App\Livewire; use Ikromjon\NativePHP\SocialAuth\Data\AuthResult;use Ikromjon\NativePHP\SocialAuth\Events\AppleSignInCompleted;use Ikromjon\NativePHP\SocialAuth\Events\GoogleSignInCompleted;use Ikromjon\NativePHP\SocialAuth\Events\SignInFailed;use Ikromjon\NativePHP\SocialAuth\Facades\SocialAuth;use Livewire\Component;use Native\Mobile\Attributes\OnNative; class LoginScreen extends Component{ public ?string $error = null; public function signInWithApple() { $rawNonce = bin2hex(random_bytes(16)); session(['auth_nonce' => $rawNonce]); // The result is handled by the #[OnNative] listeners below -- // identically on iOS and Android. (On iOS the call also returns // the result synchronously; it is intentionally unused here.) SocialAuth::appleSignIn( scopes: ['email', 'fullName'], nonce: hash('sha256', $rawNonce), ); } public function signInWithGoogle() { $nonce = bin2hex(random_bytes(16)); session(['auth_nonce' => $nonce]); // The result is handled by the #[OnNative] listeners below -- // identically on iOS and Android. (On iOS the call also returns // the result synchronously; it is intentionally unused here.) SocialAuth::googleSignIn(nonce: $nonce); } // Event handlers use NAMED PARAMETERS matching the event payload keys. // Do NOT use a single $data array — Livewire dispatches each key as a named argument. #[OnNative(AppleSignInCompleted::class)] public function onAppleSignIn( string $userId = '', ?string $identityToken = null, ?string $authorizationCode = null, ?string $email = null, ?string $givenName = null, ?string $familyName = null, ?string $displayName = null, ?string $state = null, ?string $realUserStatus = null, ) { if (!empty($userId)) { $this->handleSignIn([ 'provider' => 'apple', 'userId' => $userId, 'identityToken' => $identityToken, 'email' => $email, 'givenName' => $givenName, 'familyName' => $familyName, ]); } } #[OnNative(GoogleSignInCompleted::class)] public function onGoogleSignIn( string $userId = '', ?string $identityToken = null, ?string $email = null, ?string $displayName = null, ?string $givenName = null, ?string $familyName = null, ?string $photoUrl = null, ?string $accessToken = null, ?string $authorizationCode = null, ) { if (!empty($userId)) { $this->handleSignIn([ 'provider' => 'google', 'userId' => $userId, 'identityToken' => $identityToken, 'email' => $email, 'displayName' => $displayName, 'givenName' => $givenName, 'familyName' => $familyName, 'photoUrl' => $photoUrl, ]); } } #[OnNative(SignInFailed::class)] public function onSignInFailed( string $provider = '', string $error = '', ?string $errorCode = null, ) { if ($errorCode !== 'CANCELED') { $this->error = !empty($error) ? $error : 'Sign-in failed.'; } } private function handleSignIn(array $data) { // Verify identity token server-side, then create/find user // IMPORTANT: Apple only sends email/name on FIRST sign-in! // You must persist this data immediately. return $this->redirect('/dashboard'); } public function render() { return view('livewire.login-screen'); }}
{{-- resources/views/livewire/login-screen.blade.php --}}<div class="flex flex-col gap-4 p-6"> @if($error) <div class="bg-red-100 text-red-700 p-3 rounded">{{ $error }}</div> @endif <button wire:click="signInWithApple" class="flex items-center justify-center gap-2 bg-black text-white rounded-lg py-3 px-6 font-medium" > Sign in with Apple </button> <button wire:click="signInWithGoogle" class="flex items-center justify-center gap-2 bg-white text-gray-700 border border-gray-300 rounded-lg py-3 px-6 font-medium" > Sign in with Google </button></div>
JavaScript (Vue / React / Inertia)#
import { On } from '#nativephp';import socialAuth from 'vendor/ikromjon/nativephp-mobile-social-auth/resources/js/social-auth'; // Generate nonce client-sidefunction generateNonce() { const array = new Uint8Array(16); crypto.getRandomValues(array); return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');} async function sha256Hex(value) { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); return Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join('');} // Google Sign-Inasync function handleGoogleSignIn() { const nonce = generateNonce(); // The result is handled by the On(...) listeners below -- identically // on iOS and Android. (On iOS the promise also resolves with the // result; it is intentionally unused here.) await socialAuth.googleSignIn(nonce);} // Apple Sign-Inasync function handleAppleSignIn() { const rawNonce = generateNonce(); // Apple expects the SHA-256 hash of the nonce -- keep rawNonce for server-side verification await socialAuth.appleSignIn(['email', 'fullName'], await sha256Hex(rawNonce));} // Single handling path: these events fire on both platformsOn('Ikromjon\\NativePHP\\SocialAuth\\Events\\GoogleSignInCompleted', (payload) => { sendTokenToBackend(payload.identityToken);}); On('Ikromjon\\NativePHP\\SocialAuth\\Events\\AppleSignInCompleted', (payload) => { sendTokenToBackend(payload.identityToken);}); On('Ikromjon\\NativePHP\\SocialAuth\\Events\\SignInFailed', (payload) => { if (payload.errorCode !== 'CANCELED') { alert(`Sign-in failed: ${payload.error}`); }});
API Reference#
SocialAuth::appleSignIn(array $scopes, ?string $nonce, ?string $state): ?AuthResult#
Initiates native Apple Sign-In. Returns AuthResult on iOS, null on Android.
$scopes-- Requested scopes:['email', 'fullName'](default: both)$nonce-- SHA256-hashed nonce for replay protection$state-- Optional state string echoed back in response
Important: Apple only returns
givenName/familyNameon the first sign-in. Subsequent sign-ins return onlyuserIdandidentityToken. You must persist user info on first authentication.
SocialAuth::googleSignIn(?string $nonce): ?AuthResult#
Initiates native Google Sign-In. Returns AuthResult on iOS, null on Android (result via event).
$nonce-- Optional nonce for replay protection (raw string, not hashed). Supported on both platforms: Android via Credential Manager, iOS via GoogleSignIn-iOS 9.x. The nonce comes back as thenonceclaim inside the ID token -- verify it server-side.
SocialAuth::checkAppleCredentialState(string $userId): string#
Checks if an Apple credential is still valid. iOS only.
Returns: 'authorized', 'revoked', 'not_found', 'transferred', or 'unknown'
SocialAuth::signOut(): bool#
Signs out from Google and clears credential state. Apple has no sign-out API.
AuthResult#
| Property | Type | Apple | |
|---|---|---|---|
provider |
string |
'apple' |
'google' |
userId |
?string |
Unique Apple user ID | Google user ID |
identityToken |
?string |
JWT | JWT |
authorizationCode |
?string |
One-time code | Server auth code |
accessToken |
?string |
-- | OAuth access token |
email |
?string |
First sign-in only | Always |
givenName |
?string |
First sign-in only | Always |
familyName |
?string |
First sign-in only | Always |
displayName |
?string |
First sign-in only | Always |
photoUrl |
?string |
-- | Profile photo URL |
nonce |
?string |
Returned as nonce claim inside identityToken |
Returned as nonce claim inside identityToken |
state |
?string |
Echoed | -- |
realUserStatus |
?string |
'likelyReal' / 'unknown' |
-- |
Events#
| Event | Payload |
|---|---|
AppleSignInCompleted |
userId, identityToken, authorizationCode, email, givenName, familyName, displayName, state, realUserStatus |
GoogleSignInCompleted |
userId, identityToken, email, displayName, givenName, familyName, photoUrl, accessToken, authorizationCode (iOS only -- Android Credential Manager issues neither) |
Event payloads carry every field of AuthResult except provider and nonce (the nonce is inside identityToken). Fields the platform did not return arrive as empty strings, not null -- check with !empty() / filled(), not !== null.
| SignInFailed | provider, error, errorCode |
Error codes: CANCELED, FAILED, INVALID_RESPONSE, NOT_HANDLED, NOT_INTERACTIVE, NO_AUTH_IN_KEYCHAIN, NO_CREDENTIAL, SCOPES_ALREADY_GRANTED, UNSUPPORTED_PLATFORM, MISSING_CONFIG, PARSE_ERROR, UNKNOWN
Server-Side Token Verification#
Identity tokens are JWTs that must be verified server-side before trusting the user's identity.
Google ID tokens from both platforms carry aud = your GOOGLE_SERVER_CLIENT_ID (Android sets it via setServerClientId, iOS via the GIDServerClientID Info.plist key), so the single aud check below covers both.
Migration note: On iOS, plugin versions ≤ 1.0.2 issued Google ID tokens with
aud= yourGOOGLE_IOS_CLIENT_ID. If you have existing installs, temporarily accept both audiences server-side until all clients are updated.
use Firebase\JWT\JWT;use Firebase\JWT\JWK; // Google verification//// Note on `azp`: a real token from this plugin carries `aud` = your server// client ID and `azp` = the *platform* client ID (the iOS or Android client// that requested it). Check `aud`; do not compare `azp` against the server// client ID, or every mobile sign-in will be rejected.$googleKeys = json_decode( file_get_contents('https://www.googleapis.com/oauth2/v3/certs'), true);$decoded = JWT::decode($identityToken, JWK::parseKeySet($googleKeys));// Verify: $decoded->aud === your GOOGLE_SERVER_CLIENT_ID// Verify: $decoded->iss === 'https://accounts.google.com'// If you passed a nonce to googleSignIn():// Verify: $decoded->nonce === session('auth_nonce') // Apple verification$appleKeys = json_decode( file_get_contents('https://appleid.apple.com/auth/keys'), true);$decoded = JWT::decode($identityToken, JWK::parseKeySet($appleKeys));// Verify: $decoded->aud === your app's bundle ID// Verify: $decoded->iss === 'https://appleid.apple.com'// If you passed a nonce to appleSignIn() (SHA-256 of the raw nonce):// Verify: $decoded->nonce === hash('sha256', session('auth_nonce'))
Install the JWT library: composer require firebase/php-jwt
Known issues#
System::isIos() / System::isAndroid() return false inside the app
Not a fault of this plugin, but it affects any platform-conditional code written around it:
Device::getInfo() returns null on the iOS simulator, so both helpers report false and code
silently takes its "not on a device" branch. Read env('NATIVEPHP_PLATFORM') instead -- the native
runtime exports it into $_SERVER before Laravel boots, so it also survives config:cache.
How the iOS URL scheme is registered#
GoogleSignIn-iOS will not start unless the reversed client ID is registered in CFBundleURLTypes,
and it reports a missing scheme by raising an uncaught NSException -- which terminates the app
rather than returning an error.
The plugin manifest cannot express this. url_schemes is not a key NativePHP Mobile reads
(neither 3.3.x nor 4.x), and the supported info_plist route only handles flat strings and flat
arrays of strings, not the array-of-dicts CFBundleURLTypes requires.
So the plugin registers it from a post_compile hook instead
(social-auth:register-url-scheme), which runs after NativePHP has finished rewriting the
Info.plist and before Xcode builds. This is automatic -- there is nothing to configure. For
reference, it:
- writes its own entry, tagged
CFBundleURLName = ikromjon.social-auth.google, so it never collides with NativePHP's deeplink entry (which is refilled fromNATIVEPHP_DEEPLINK_SCHEMEeach build); - patches every Info.plist in the generated project -- the device target builds against
NativePHP/Info.plistand the simulator target againstNativePHP-simulator-Info.plist; - updates its entry in place on rebuilds rather than duplicating it, and rewrites it if the client ID changes;
- derives the reversed ID from
GOOGLE_IOS_CLIENT_IDwhenGOOGLE_IOS_REVERSED_CLIENT_IDis absent.
As a second line of defence, the Swift bridge checks CFBundleURLTypes before calling
GIDSignIn. Swift cannot catch an Objective-C NSException, so this cannot be wrapped in
do/catch; if the scheme is missing the plugin dispatches SignInFailed with MISSING_CONFIG
instead of letting the app die.
Troubleshooting#
iOS build fails: error: extra arguments at positions #4, #5 in call in SocialAuthFunctions.swift
- Plugin versions up to 1.0.1 pinned GoogleSignIn-iOS
~> 8.0while calling the nonce sign-in overload, which only exists in GoogleSignIn-iOS 9.0+. Upgrade the plugin (composer update ikromjon/nativephp-mobile-social-auth), then runphp artisan native:install --forceso the regenerated Podfile resolves GoogleSignIn~> 9.0. If CocoaPods then reports a dependency conflict, another pod in your project is pinning AppAuth 1.x / GTMAppAuth 4.x -- update that dependency, since GoogleSignIn 9.x requires AppAuth 2.x and GTMAppAuth 5.x.
"Developer console is not set up correctly" (Android)
- Ensure you have BOTH an Android client AND a Web client in the same Google Cloud project
- The Android client must have the correct package name and SHA-1 fingerprint
"MISSING_CONFIG" error
- Check that
GOOGLE_SERVER_CLIENT_IDis set in your.envfile
Build aborts: "Missing required plugin secrets" although the values are in .env
- Run
php artisan config:clearbefore building. Onceconfig:cachehas run, Laravel stops loading.env, so everyenv()call returns null -- and NativePHP reads plugin secrets throughenv(). This does not affect the built app: values reach it through config, which is whyGOOGLE_SERVER_CLIENT_IDstill resolves at runtime with the config cached.
Build aborts: "Plugin ... requires Android API level 29, but your min SDK is 26"
- Setting
NATIVEPHP_ANDROID_MIN_SDKin.envas the message suggests has no effect -- nothing reads it. Defineandroid.min_sdkin a publishedconfig/nativephp.phpinstead; see Installation.
Every bridge call returns null and no events fire
- The plugin is installed but not registered. Run
php artisan native:plugin:list-- if it appears under "Unregistered Plugins", runphp artisan native:plugin:register ikromjon/nativephp-mobile-social-authand rebuild.
Apple Sign-In fails with AuthorizationError error 1000 and no sheet appears
- The entitlement is not in the built binary. Check with
codesign -d --entitlements - /path/to/YourApp.app; empty output means it was dropped. This happens whenNATIVEPHP_DEVELOPMENT_TEAMis unset, because the app is then ad-hoc signed. A free Personal Team is not sufficient -- see Requirements.
Code changes do not appear after native:run
- The app can keep serving the previous build's files. Force a clean extraction:
xcrun simctl uninstall <udid> <your.app.id>(iOS) oradb uninstall <your.app.id>(Android) before re-running.
Google Sign-In returns null on Android
- This is expected. On Android, Google Sign-In is async. Use
#[OnNative(GoogleSignInCompleted::class)]to receive the result.
Apple email/name are null
- Apple only provides email and name on the first sign-in. After that, only
userIdandidentityTokenare returned. To reset during development: Settings > Apple ID > Sign-In & Security > Sign in with Apple > Your App > Stop Using Apple ID.
App Store rejection for missing Apple Sign-In
- If your app offers Google (or any third-party) sign-in, you must also offer Apple Sign-In. This plugin handles both.
Support#
- Issues: GitHub Issues
- Email: [email protected]
License#
This is a commercial plugin distributed through the official NativePHP plugin marketplace:
👉 nativephp.com/plugins/ikromjon/nativephp-mobile-social-auth
Use is governed by the End User License Agreement. Redistribution or resale is not permitted.