string('tenant')->toString(); $returnUrl = $request->string('return_url')->toString(); $tenant = Tenant::query()->where('codigo', $tenantCode)->first(); if (! $tenant || ! $this->isTenantReturnUrl($returnUrl, $tenant)) { throw ValidationException::withMessages([ 'tenant' => __('api.auth.invalid_tenant_or_return_url'), ]); } $state = (string) Str::uuid(); Cache::put("google-oauth-context:{$state}", [ 'tenant_codigo' => $tenant->codigo, 'return_url' => rtrim($returnUrl, '/'), ], now()->addMinutes(10)); return Socialite::driver('google') ->scopes(['openid', 'profile', 'email']) ->stateless() ->with(['state' => $state]) ->redirect(); } public function callback(Request $request): RedirectResponse { $state = $request->string('state')->toString(); /** @var array{tenant_codigo?: string, return_url?: string}|null $context */ $context = Str::isUuid($state) ? Cache::pull("google-oauth-context:{$state}") : null; if (! is_array($context) || ! isset($context['tenant_codigo'], $context['return_url'])) { abort(400, __('api.auth.request_expired')); } $tenant = Tenant::query()->where('codigo', $context['tenant_codigo'])->first(); if (! $tenant || ! $this->isTenantReturnUrl($context['return_url'], $tenant)) { abort(400, __('api.auth.invalid_return_url')); } /** @var SocialiteUser $googleUser */ $googleUser = Socialite::driver('google')->stateless()->user(); $user = $this->resolveUser($googleUser, $tenant); $token = $user->createToken( 'google-oauth', ['*'], now()->addMinutes((int) config('sanctum.expiration')), )->plainTextToken; $exchangeCode = (string) Str::uuid(); Cache::put("google-oauth-exchange:{$exchangeCode}", [ 'user_id' => $user->id, 'token' => $token, 'tenant_codigo' => $tenant->codigo, ], now()->addMinutes(5)); return redirect()->to($context['return_url'].'/login?'.http_build_query([ 'oauth_code' => $exchangeCode, ])); } /** @return array{user: User, token: string, tenant_codigo: string} */ public function exchange(string $exchangeCode, string $tenantCodigo): array { /** @var array{user_id: int, token: string, tenant_codigo?: string}|null $authentication */ $authentication = Cache::pull("google-oauth-exchange:{$exchangeCode}"); if (! $authentication) { throw ValidationException::withMessages([ 'oauth_code' => __('api.auth.oauth_code_expired'), ]); } if (($authentication['tenant_codigo'] ?? null) !== $tenantCodigo) { throw ValidationException::withMessages([ 'tenant_codigo' => __('api.auth.tenant_mismatch'), ]); } return [ 'user' => User::query()->findOrFail($authentication['user_id']), 'token' => $authentication['token'], 'tenant_codigo' => $tenantCodigo, ]; } private function resolveUser(SocialiteUser $googleUser, Tenant $tenant): User { $googleId = $googleUser->getId(); $email = $googleUser->getEmail(); if (! is_string($googleId) || $googleId === '' || ! is_string($email) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { throw ValidationException::withMessages([ 'google' => __('api.auth.google_email_required'), ]); } $rawUser = $googleUser instanceof \Laravel\Socialite\Two\User ? $googleUser->getRaw() : []; $emailVerified = $rawUser['email_verified'] ?? $rawUser['verified_email'] ?? false; if (! in_array($emailVerified, [true, 'true', 1, '1'], true)) { throw ValidationException::withMessages([ 'google' => __('api.auth.google_email_unverified'), ]); } $user = User::query()->where('google_id', $googleId)->first(); if ($user) { return $user; } $user = User::query()->where('email', $email)->first(); if ($user) { $user->forceFill(['google_id' => $googleId])->save(); return $user; } $name = $googleUser->getName(); $user = User::query()->create([ 'nombre_apellido' => is_string($name) && $name !== '' ? $name : $email, 'email' => $email, 'email_verified_at' => now(), 'google_id' => $googleId, 'password' => Str::password(64), ]); UserRegistered::dispatch($user, $tenant->codigo); return $user; } private function isTenantReturnUrl(string $returnUrl, Tenant $tenant): bool { $parts = parse_url($returnUrl); if (! is_array($parts) || ! isset($parts['scheme'], $parts['host']) || isset($parts['user'], $parts['pass'], $parts['query'], $parts['fragment']) || ($parts['path'] ?? '') !== '') { return false; } $scheme = strtolower($parts['scheme']); $host = TenantDomainNormalizer::normalize($parts['host']); $tenantDomain = TenantDomainNormalizer::normalize($tenant->dominio); if ($host === null || $tenantDomain === null || $host !== $tenantDomain) { return false; } return $scheme === 'https' || ($scheme === 'http' && in_array($host, ['localhost', '127.0.0.1'], true)); } }