feat: google login
CI / 🧪 Tests Laravel (push) Failing after 9s
CI / 🐳 Build & Push Images (push) Has been skipped

This commit is contained in:
2026-08-17 10:06:35 +02:00
parent c0688044e8
commit ddc80956a0
22 changed files with 1886 additions and 6 deletions
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace App\Actions;
use App\Enums\SocialProvider;
use App\Models\SocialAccount;
use App\Models\User;
use App\Services\AppleOAuthTokenClient;
use App\Services\SocialIdentityResolver;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialiteUser;
class AuthenticateSocialUser
{
public function __construct(
private SocialIdentityResolver $identityResolver,
private AppleOAuthTokenClient $appleOAuthTokenClient,
) {}
/**
* @param array<string, mixed> $data
*/
public function execute(array $data): User
{
$provider = SocialProvider::from((string) $data['provider']);
$identity = $this->identityResolver->resolve(
$provider,
(string) $data['idToken'],
isset($data['nonce']) ? (string) $data['nonce'] : null,
);
$this->ensureUsableIdentity($identity);
$email = strtolower(trim((string) $identity->getEmail()));
$providerRefreshToken = $provider === SocialProvider::Apple
? $this->appleOAuthTokenClient->exchangeAuthorizationCode(
(string) ($data['authorizationCode'] ?? ''),
(string) $identity->getId(),
)
: null;
try {
return Cache::lock('social-auth:user:'.hash('sha256', $email), 10)
->block(5, fn (): User => DB::transaction(function () use ($data, $email, $provider, $providerRefreshToken, $identity): User {
$providerUserId = (string) $identity->getId();
$socialAccount = SocialAccount::query()
->where('provider', $provider)
->where('provider_user_id', $providerUserId)
->lockForUpdate()
->first();
if ($socialAccount) {
$user = $socialAccount->user;
abort_if(! $user, 409, 'SOCIAL_ACCOUNT_CONFLICT');
if ($providerRefreshToken !== null) {
$socialAccount->update([
'provider_refresh_token' => $providerRefreshToken,
]);
}
return $this->prepareUserForLogin($user, $data);
}
$user = User::query()
->where('email', $email)
->lockForUpdate()
->first();
if (! $user) {
abort_if($data['intent'] !== 'register', 422, 'SOCIAL_ACCOUNT_NOT_FOUND');
abort_if(($data['termsAccepted'] ?? false) !== true, 422, 'TERMS_ACCEPTANCE_REQUIRED');
$user = User::create([
'email' => $email,
'email_verified_at' => now(),
'locale' => $data['locale'] ?? 'en',
'name' => $this->uniqueUsername($this->preferredName($provider, $identity, $data), $email),
'password' => null,
'terms_accepted_at' => now(),
]);
} else {
abort_if($user->isSuspended(), 403, 'ACCOUNT_SUSPENDED');
if (! $user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
}
}
$user->socialAccounts()->create([
'provider' => $provider,
'provider_refresh_token' => $providerRefreshToken,
'provider_user_id' => $providerUserId,
]);
return $this->prepareUserForLogin($user, $data);
}, 3));
} catch (LockTimeoutException) {
abort(409, 'SOCIAL_AUTH_IN_PROGRESS');
}
}
private function ensureUsableIdentity(SocialiteUser $identity): void
{
$providerUserId = $identity->getId();
$email = $identity->getEmail();
$rawIdentity = $identity->getRaw();
$emailVerified = filter_var(
$rawIdentity['email_verified'] ?? $rawIdentity['verified_email'] ?? false,
FILTER_VALIDATE_BOOL,
);
abort_if(! is_string($providerUserId) || trim($providerUserId) === '', 401, 'SOCIAL_TOKEN_INVALID');
abort_if(! is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false, 401, 'SOCIAL_TOKEN_INVALID');
abort_if($emailVerified !== true, 401, 'SOCIAL_EMAIL_NOT_VERIFIED');
}
/**
* @param array<string, mixed> $data
*/
private function prepareUserForLogin(User $user, array $data): User
{
abort_if($user->isSuspended(), 403, 'ACCOUNT_SUSPENDED');
if (isset($data['locale']) && $user->locale !== $data['locale']) {
$user->forceFill(['locale' => $data['locale']])->save();
}
return $user->fresh();
}
/**
* @param array<string, mixed> $data
*/
private function preferredName(
SocialProvider $provider,
SocialiteUser $identity,
array $data,
): ?string {
if ($provider === SocialProvider::Apple && isset($data['name']) && is_string($data['name'])) {
return $data['name'];
}
return $identity->getName();
}
private function uniqueUsername(?string $preferredName, string $email): string
{
$source = filled($preferredName) ? $preferredName : Str::before($email, '@');
$base = Str::of((string) $source)
->ascii()
->lower()
->replaceMatches('/[^a-z0-9_.-]+/', '.')
->trim('.-_')
->limit(24, '')
->toString();
if (mb_strlen($base) < 3) {
$base = 'user';
}
if (! User::withTrashed()->where('name', $base)->exists()) {
return $base;
}
do {
$candidate = $base.'-'.Str::lower(Str::random(6));
} while (User::withTrashed()->where('name', $candidate)->exists());
return $candidate;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Actions;
use App\Enums\SocialProvider;
use App\Models\User;
use App\Services\AppleOAuthTokenClient;
use Illuminate\Contracts\Encryption\DecryptException;
class RevokeSocialAuthorizations
{
public function __construct(private AppleOAuthTokenClient $appleOAuthTokenClient) {}
public function execute(User $user): void
{
$appleAccount = $user->socialAccounts()
->where('provider', SocialProvider::Apple)
->first();
if (! $appleAccount) {
return;
}
try {
$refreshToken = $appleAccount->provider_refresh_token;
} catch (DecryptException) {
abort(503, 'SOCIAL_REVOCATION_FAILED');
}
if (! is_string($refreshToken) || $refreshToken === '') {
abort(409, 'SOCIAL_REVOCATION_TOKEN_MISSING');
}
$this->appleOAuthTokenClient->revoke($refreshToken);
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum SocialProvider: string
{
case Google = 'google';
case Apple = 'apple';
}
+9 -3
View File
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Actions\RevokeSocialAuthorizations;
use App\Http\Requests\EmailVerificationNotificationRequest;
use App\Http\Requests\ForgotPasswordRequest;
use App\Http\Requests\LoginRequest;
@@ -295,8 +296,11 @@ class AuthController extends Controller
]);
}
public function destroy(Request $request, RevenueCatService $revenueCat): JsonResponse
{
public function destroy(
Request $request,
RevenueCatService $revenueCat,
RevokeSocialAuthorizations $revokeSocialAuthorizations,
): JsonResponse {
$user = $request->user();
$identityVerificationRequest = $user->identityVerificationRequest()->first();
$storagePaths = collect([$user->avatar_url])
@@ -305,6 +309,8 @@ class AuthController extends Controller
->values()
->all();
$revokeSocialAuthorizations->execute($user);
try {
$revenueCat->deleteCustomer($user);
} catch (Throwable $exception) {
@@ -388,7 +394,7 @@ class AuthController extends Controller
{
$user = User::where('email', strtolower((string) $data['email']))->first();
if (! $user || ! Hash::check((string) $data['password'], $user->password)) {
if (! $user || ! is_string($user->password) || ! Hash::check((string) $data['password'], $user->password)) {
return response()->json([
'code' => 'INVALID_CREDENTIALS',
], 401);
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers;
use App\Actions\AuthenticateSocialUser;
use App\Enums\SocialProvider;
use App\Http\Requests\MobileSocialLoginRequest;
use App\Http\Requests\MobileSocialNonceRequest;
use App\Http\Resources\UserResource;
use App\Services\SocialAuthenticationNonce;
use Illuminate\Http\JsonResponse;
class MobileSocialAuthController extends Controller
{
public function nonce(
MobileSocialNonceRequest $request,
SocialAuthenticationNonce $socialAuthenticationNonce,
): JsonResponse {
$provider = SocialProvider::from($request->validated('provider'));
return response()->json([
'nonce' => $socialAuthenticationNonce->issue($provider),
]);
}
public function store(
MobileSocialLoginRequest $request,
AuthenticateSocialUser $authenticateSocialUser,
): JsonResponse {
$data = $request->validated();
$user = $authenticateSocialUser->execute($data);
$deviceName = $data['device_name'] ?? null;
$tokenName = is_string($deviceName) && trim($deviceName) !== ''
? trim($deviceName)
: 'mobile';
return response()->json([
'token' => $user->createToken($tokenName)->plainTextToken,
'user' => new UserResource($user),
]);
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Requests;
use App\Enums\SocialProvider;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class MobileSocialLoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'authorizationCode' => [
'nullable',
Rule::requiredIf($this->input('provider') === SocialProvider::Apple->value),
'string',
'max:4096',
],
'deviceName' => ['sometimes', 'string', 'max:255'],
'device_name' => ['sometimes', 'string', 'max:255'],
'idToken' => ['required', 'string', 'max:10000'],
'intent' => ['required', Rule::in(['login', 'register'])],
'locale' => ['sometimes', 'string', Rule::in(config('app.supported_locales', ['fr', 'en']))],
'name' => ['sometimes', 'nullable', 'string', 'max:255'],
'nonce' => ['required', 'string', 'size:64', 'regex:/\A[a-f0-9]{64}\z/'],
'provider' => ['required', Rule::enum(SocialProvider::class)],
'termsAccepted' => ['required', 'boolean'],
];
}
protected function prepareForValidation(): void
{
if ($this->has('locale')) {
$locale = strtolower(str_replace('_', '-', (string) $this->input('locale')));
$this->merge([
'locale' => explode('-', $locale)[0],
]);
}
if ($this->has('deviceName') && ! $this->has('device_name')) {
$this->merge([
'device_name' => $this->input('deviceName'),
]);
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use App\Enums\SocialProvider;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class MobileSocialNonceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'provider' => ['required', Rule::enum(SocialProvider::class)],
];
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use App\Enums\SocialProvider;
use Database\Factories\SocialAccountFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SocialAccount extends Model
{
/** @use HasFactory<SocialAccountFactory> */
use HasFactory;
/**
* @var list<string>
*/
protected $fillable = [
'provider',
'provider_refresh_token',
'provider_user_id',
'user_id',
];
/**
* @var list<string>
*/
protected $hidden = [
'provider_refresh_token',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'provider' => SocialProvider::class,
'provider_refresh_token' => 'encrypted',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+7 -1
View File
@@ -7,6 +7,7 @@ use App\Enums\PhysicalActivityLevel;
use App\Enums\UserRole;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Panel;
@@ -29,7 +30,7 @@ use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocalePreference, MustVerifyEmail
{
/** @use HasFactory<\Database\Factories\UserFactory> */
/** @use HasFactory<UserFactory> */
use HasApiTokens, HasFactory, HasUlids, Notifiable, SoftDeletes;
protected $attributes = [
@@ -125,6 +126,11 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
return $this->hasMany(DeviceToken::class);
}
public function socialAccounts(): HasMany
{
return $this->hasMany(SocialAccount::class);
}
public function mealPosts(): HasMany
{
return $this->hasMany(MealPosts::class);
+192
View File
@@ -0,0 +1,192 @@
<?php
namespace App\Services;
use App\Enums\SocialProvider;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Laravel\Socialite\Two\User as SocialiteUser;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
class AppleIdentityTokenVerifier
{
private const JWKS_CACHE_KEY = 'social-auth:apple:jwks';
public function __construct(private SocialAuthenticationNonce $socialAuthenticationNonce) {}
public function userFromToken(string $identityToken, string $nonce): SocialiteUser
{
$claims = $this->verifiedClaims($identityToken);
$this->validateNonce($claims, $nonce);
return (new SocialiteUser)->setRaw($claims)->map([
'id' => Arr::get($claims, 'sub'),
'nickname' => null,
'name' => null,
'email' => Arr::get($claims, 'email'),
'avatar' => null,
]);
}
public function subjectFromToken(string $identityToken): string
{
$subject = Arr::get($this->verifiedClaims($identityToken), 'sub');
if (! is_string($subject) || $subject === '') {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
return $subject;
}
/**
* @return array<string, mixed>
*/
private function verifiedClaims(string $identityToken): array
{
try {
$claims = (array) JWT::decode(
$identityToken,
JWK::parseKeySet($this->jwkSetForToken($identityToken)),
);
$this->validateStandardClaims($claims);
return $claims;
} catch (HttpExceptionInterface $exception) {
throw $exception;
} catch (ConnectionException) {
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
} catch (Throwable) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
}
/**
* @return array<string, mixed>
*/
private function jwkSetForToken(string $identityToken): array
{
$keyId = $this->tokenKeyId($identityToken);
$jwkSet = $this->jwkSet();
if ($this->jwkSetContains($jwkSet, $keyId)) {
return $jwkSet;
}
Cache::forget(self::JWKS_CACHE_KEY);
return $this->jwkSet();
}
/**
* @return array<string, mixed>
*/
private function jwkSet(): array
{
return Cache::remember(
self::JWKS_CACHE_KEY,
now()->addHours(6),
function (): array {
$url = (string) config('services.apple.jwks_url');
if ($url === '') {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
$response = Http::acceptJson()
->connectTimeout(3)
->timeout(10)
->retry([100, 500], throw: false)
->get($url);
$jwkSet = $response->successful() ? $response->json() : null;
if (! is_array($jwkSet) || ! isset($jwkSet['keys']) || ! is_array($jwkSet['keys'])) {
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
}
return $jwkSet;
},
);
}
private function tokenKeyId(string $identityToken): string
{
$segments = explode('.', $identityToken);
if (count($segments) !== 3) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($segments[0]));
$keyId = $header->kid ?? null;
if (! is_string($keyId) || $keyId === '') {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
return $keyId;
}
/**
* @param array<string, mixed> $jwkSet
*/
private function jwkSetContains(array $jwkSet, string $keyId): bool
{
return collect($jwkSet['keys'] ?? [])->contains(
fn (mixed $key): bool => is_array($key) && ($key['kid'] ?? null) === $keyId,
);
}
/**
* @param array<string, mixed> $claims
*/
private function validateStandardClaims(array $claims): void
{
$issuer = (string) config('services.apple.issuer');
$tokenIssuer = Arr::get($claims, 'iss');
if (! is_string($tokenIssuer) || $issuer === '' || ! hash_equals($issuer, $tokenIssuer)) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
$allowedClientIds = collect(explode(',', (string) config('services.apple.client_ids')))
->map(fn (string $clientId): string => trim($clientId))
->filter()
->values()
->all();
$tokenAudiences = array_filter(
Arr::wrap(Arr::get($claims, 'aud')),
fn (mixed $audience): bool => is_string($audience),
);
if ($allowedClientIds === []) {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
if (array_intersect($allowedClientIds, $tokenAudiences) === []) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
}
/**
* @param array<string, mixed> $claims
*/
private function validateNonce(array $claims, string $nonce): void
{
if (! $this->socialAuthenticationNonce->consume(
SocialProvider::Apple,
$nonce,
Arr::get($claims, 'nonce'),
)) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace App\Services;
use Firebase\JWT\JWT;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
class AppleOAuthTokenClient
{
public function __construct(private AppleIdentityTokenVerifier $identityTokenVerifier) {}
public function exchangeAuthorizationCode(string $authorizationCode, string $expectedSubject): string
{
try {
$response = $this->request()
->post($this->configuredUrl('token_url'), [
'client_id' => $this->clientId(),
'client_secret' => $this->clientSecret(),
'code' => $authorizationCode,
'grant_type' => 'authorization_code',
]);
} catch (HttpExceptionInterface $exception) {
throw $exception;
} catch (ConnectionException) {
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
}
if (! $response->successful()) {
$error = $response->json('error');
if ($error === 'invalid_grant') {
abort(401, 'SOCIAL_AUTHORIZATION_CODE_INVALID');
}
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
}
$refreshToken = $response->json('refresh_token');
$identityToken = $response->json('id_token');
if (! is_string($refreshToken) || $refreshToken === '' || ! is_string($identityToken)) {
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
}
$returnedSubject = $this->identityTokenVerifier->subjectFromToken($identityToken);
if (! hash_equals($expectedSubject, $returnedSubject)) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
return $refreshToken;
}
public function revoke(string $refreshToken): void
{
try {
$response = $this->request()
->retry(
[100, 500],
when: fn (Throwable $exception): bool => $exception instanceof ConnectionException
|| ($exception instanceof RequestException && $exception->response->serverError()),
throw: false,
)
->post($this->configuredUrl('revoke_url'), [
'client_id' => $this->clientId(),
'client_secret' => $this->clientSecret(),
'token' => $refreshToken,
'token_type_hint' => 'refresh_token',
]);
} catch (HttpExceptionInterface $exception) {
throw $exception;
} catch (Throwable) {
abort(503, 'SOCIAL_REVOCATION_FAILED');
}
if (! $response->successful()) {
abort(503, 'SOCIAL_REVOCATION_FAILED');
}
}
private function clientSecret(): string
{
$teamId = (string) config('services.apple.team_id');
$keyId = (string) config('services.apple.key_id');
$privateKey = $this->privateKey();
$issuer = (string) config('services.apple.issuer');
if ($teamId === '' || $keyId === '' || $issuer === '' || $privateKey === '') {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
try {
$issuedAt = now()->timestamp;
return JWT::encode([
'aud' => $issuer,
'exp' => $issuedAt + 300,
'iat' => $issuedAt,
'iss' => $teamId,
'sub' => $this->clientId(),
], $privateKey, 'ES256', $keyId);
} catch (Throwable) {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
}
private function clientId(): string
{
$clientId = (string) config('services.apple.client_id');
if ($clientId === '') {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
return $clientId;
}
private function configuredUrl(string $key): string
{
$url = (string) config('services.apple.'.$key);
if ($url === '') {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
return $url;
}
private function privateKey(): string
{
$base64PrivateKey = (string) config('services.apple.private_key_base64');
if ($base64PrivateKey !== '') {
$decodedPrivateKey = base64_decode($base64PrivateKey, true);
return is_string($decodedPrivateKey) ? $decodedPrivateKey : '';
}
return str_replace('\\n', "\n", (string) config('services.apple.private_key'));
}
private function request(): PendingRequest
{
return Http::asForm()
->acceptJson()
->connectTimeout(3)
->timeout(10);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Services;
use App\Enums\SocialProvider;
use Illuminate\Support\Facades\Cache;
class SocialAuthenticationNonce
{
public function issue(SocialProvider $provider): string
{
$nonce = bin2hex(random_bytes(32));
Cache::put($this->cacheKey($provider, $nonce), true, now()->addMinutes(10));
return $nonce;
}
public function consume(SocialProvider $provider, string $nonce, mixed $tokenNonce): bool
{
$nonceWasIssued = Cache::pull($this->cacheKey($provider, $nonce), false);
return is_string($tokenNonce)
&& hash_equals($nonce, $tokenNonce)
&& $nonceWasIssued === true;
}
private function cacheKey(SocialProvider $provider, string $nonce): string
{
return 'social-auth:'.$provider->value.':nonce:'.hash('sha256', $nonce);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Services;
use App\Enums\SocialProvider;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Arr;
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
use Laravel\Socialite\Contracts\User as SocialiteUser;
use Laravel\Socialite\Two\AbstractProvider;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
class SocialIdentityResolver
{
public function __construct(
private SocialiteFactory $socialite,
private AppleIdentityTokenVerifier $appleIdentityTokenVerifier,
private SocialAuthenticationNonce $socialAuthenticationNonce,
) {}
public function resolve(SocialProvider $provider, string $identityToken, ?string $nonce): SocialiteUser
{
try {
return match ($provider) {
SocialProvider::Google => $this->googleUserFromToken($identityToken, $nonce ?? ''),
SocialProvider::Apple => $this->appleIdentityTokenVerifier->userFromToken(
$identityToken,
$nonce ?? '',
),
};
} catch (HttpExceptionInterface $exception) {
throw $exception;
} catch (GuzzleException) {
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
} catch (Throwable) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
}
private function googleUserFromToken(string $identityToken, string $nonce): SocialiteUser
{
if (! config('services.google.client_id')) {
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
}
/** @var AbstractProvider $provider */
$provider = $this->socialite->driver(SocialProvider::Google->value);
$identity = $provider->userFromToken($identityToken);
$rawIdentity = method_exists($identity, 'getRaw') ? $identity->getRaw() : [];
if (! is_array($rawIdentity) || ! $this->socialAuthenticationNonce->consume(
SocialProvider::Google,
$nonce,
Arr::get($rawIdentity, 'nonce'),
)) {
abort(401, 'SOCIAL_TOKEN_INVALID');
}
return $identity;
}
}