$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['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 $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 $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; } }