# Autenticação SSO (Google + Microsoft) — Guia completo de implementação

Este documento descreve **em detalhes** como a autenticação social (Google e
Microsoft/Azure) foi implementada no projeto **nolapis** (Laravel), com todo o
código real, configurações e regras de negócio, para que seja possível
**replicar a mesma solução em outro sistema parecido**.

> Resumo da pilha: `laravel/socialite` (driver `google` nativo) +
> `socialiteproviders/microsoft-azure` (driver `azure`). A regra de negócio fica
> centralizada num service (`SsoAuthService`), o controller é "fino"
> (`SocialAuthController`), e os botões ficam num partial Blade reutilizável.

---

## 1. Visão geral da arquitetura

O fluxo é dividido em camadas bem separadas:

| Camada | Arquivo | Responsabilidade |
|--------|---------|------------------|
| Controller | `app/Http/Controllers/SocialAuthController.php` | Orquestra `redirect`/`callback` do OAuth e decide entre fluxo de **login** ou **cadastro** (via `intent`). |
| Service | `app/Services/SsoAuthService.php` | Toda a regra de negócio (vincular usuário, validar tipo de auth, concluir login, gerenciar sessão de cadastro SSO). |
| Config | `config/services.php` + `config/nolapis_auth.php` | Credenciais OAuth e rótulos/provedores. |
| Provider | `app/Providers/EventServiceProvider.php` | Registra o driver `azure` do pacote SocialiteProviders. |
| Migration | `database/migrations/2026_06_08_100000_add_sso_auth_fields.php` | Adiciona colunas de auth em `users` e `tenant`. |
| View | `resources/views/partials/sso-auth-buttons.blade.php` | Botões reutilizáveis (login e cadastro). |

**Detalhe crítico:** o "Microsoft" é implementado pelo driver **`azure`**
(Microsoft Azure AD / Entra ID), **não** pelo driver "microsoft" legado. No
service: `PROVEDOR_MICROSOFT = 'azure'`. Em todos os lugares (config, rotas,
banco) o provedor da Microsoft é identificado pela string `azure`.

### Conceitos de domínio

- **`auth_tipo`** (por usuário): `senha` | `sso` | `hibrido`.
  - `senha`: só login por senha.
  - `sso`: só login social (sem senha local).
  - `hibrido`: pode logar por senha **ou** SSO.
- **`sso_obrigatorio`** (por usuário): se `true`, bloqueia login por senha.
- **`auth_provedor`** (por usuário): `google` | `azure` — qual provedor a conta usa.
- **`auth_padrao`** (por tenant/empresa): método padrão de novos usuários.
- **`sso_provedores`** (por tenant, JSON): provedores SSO permitidos no tenant.
- **`permitir_senha_local`** (por tenant): se contas híbridas podem usar senha.

---

## 2. Dependências (Composer)

Em `composer.json`:

```json
"laravel/socialite": "^5.27",
"socialiteproviders/microsoft-azure": "^5.2"
```

Instalação num projeto novo:

```bash
composer require laravel/socialite socialiteproviders/microsoft-azure
```

- `laravel/socialite` → base OAuth, fornece o driver `google` embutido.
- `socialiteproviders/microsoft-azure` → adiciona o driver `azure` (Microsoft).

> Não há `league/oauth` direto nem o driver "microsoft" legado. O Google usa o
> driver nativo do Socialite; o Microsoft usa o provider extra via SocialiteProviders.

---

## 3. Configuração

### 3.1 `config/services.php`

```php
'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect' => env('GOOGLE_REDIRECT_URI', env('APP_URL').'/auth/google/callback'),
],

'azure' => [
    'client_id' => env('MICROSOFT_CLIENT_ID'),
    'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
    'redirect' => env('MICROSOFT_REDIRECT_URI', env('APP_URL').'/auth/azure/callback'),
    'tenant' => env('MICROSOFT_TENANT_ID', 'common'),
],
```

> A chave da Microsoft é **`azure`** (e não `microsoft`), com o parâmetro extra
> `tenant` (`common` por padrão, exigido pelo provider do Azure). `common` aceita
> contas de qualquer organização + contas pessoais; para restringir a uma única
> organização, use o ID do tenant do Azure.

### 3.2 `config/nolapis_auth.php` (config customizado do projeto)

```php
<?php

return [

    'sso_habilitado' => env('SSO_HABILITADO', true),

    'provedores' => [
        'google' => 'Google',
        'azure' => 'Microsoft',
    ],

    'auth_tipos' => [
        'senha' => 'Senha',
        'sso' => 'SSO',
        'hibrido' => 'Híbrido (senha ou SSO)',
    ],

];
```

Esse arquivo centraliza os rótulos exibidos na UI e a flag global `SSO_HABILITADO`.

### 3.3 Variáveis de ambiente (`.env` / `.env.example`)

```dotenv
SSO_HABILITADO=true
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/azure/callback"
MICROSOFT_TENANT_ID=common
```

---

## 4. Registro do provider Azure (Event Listener)

O driver `azure` **não** é registrado em `config/socialite.php` (esse arquivo não
existe). Ele é registrado via o evento `SocialiteWasCalled` no
`app/Providers/EventServiceProvider.php`:

```php
<?php

namespace App\Providers;

use App\Listeners\LogSentEmailListener;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        MessageSent::class => [
            LogSentEmailListener::class,
        ],
    ];

    public function boot()
    {
        Event::listen(function (SocialiteWasCalled $event) {
            $event->extendSocialite('azure', \SocialiteProviders\Azure\Provider::class);
        });
    }

    public function shouldDiscoverEvents()
    {
        return false;
    }
}
```

> O Google **não** precisa de registro (driver nativo). Apenas o Azure é
> estendido aqui. Em projetos Laravel 11+, lembre-se que o EventServiceProvider
> precisa estar registrado no bootstrap/`providers.php` (no nolapis ele é um
> provider clássico).

---

## 5. Rotas

Em `routes/web.php` (import no topo + rotas com middleware `guest`):

```php
use App\Http\Controllers\SocialAuthController;

Route::get('auth/{provedor}/redirect', [SocialAuthController::class, 'redirect'])
    ->middleware('guest')->name('auth.social.redirect');

Route::get('auth/{provedor}/callback', [SocialAuthController::class, 'callback'])
    ->middleware('guest')->name('auth.social.callback');
```

`{provedor}` é dinâmico e validado no controller (apenas `google` e `azure` são
aceitos). URLs efetivas:

- `/auth/google/redirect` e `/auth/google/callback`
- `/auth/azure/redirect` e `/auth/azure/callback`

> Essas URLs de callback são exatamente as que você deve cadastrar como
> **Redirect URIs autorizadas** no console do Google e no portal do Azure.

---

## 6. Controller — `SocialAuthController`

Arquivo `app/Http/Controllers/SocialAuthController.php`:

```php
<?php

namespace App\Http\Controllers;

use App\Models\Plano;
use App\Services\SsoAuthService;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;

class SocialAuthController extends Controller
{
    public function __construct(
        protected SsoAuthService $ssoAuthService
    ) {}

    public function redirect(Request $request, string $provedor)
    {
        $this->validarProvedor($provedor);

        $intent = $request->query('intent', 'login');

        session([
            'sso_intent' => $intent,
            'sso_plano_id' => $intent === 'cadastro' ? (int) $request->query('plano') : null,
            'sso_return_url' => $request->query('return'),
        ]);

        return Socialite::driver($provedor)->redirect();
    }

    public function callback(Request $request, string $provedor)
    {
        $this->validarProvedor($provedor);

        try {
            $oauthUser = Socialite::driver($provedor)->user();
        } catch (\Throwable $e) {
            return $this->falha($provedor, 'Não foi possível autenticar com '. $this->ssoAuthService->labelProvedor($provedor) .'. Tente novamente.');
        }

        $intent = session()->pull('sso_intent', 'login');

        if ($intent === 'cadastro') {
            return $this->callbackCadastro($provedor, $oauthUser);
        }

        return $this->callbackLogin($provedor, $oauthUser);
    }

    protected function callbackLogin(string $provedor, $oauthUser)
    {
        try {
            $user = $this->ssoAuthService->autenticarOAuth($oauthUser, $provedor);
            $this->ssoAuthService->concluirLogin($user);
        } catch (\RuntimeException $e) {
            return redirect()->route('login')->withErrors(['login' => $e->getMessage()]);
        }

        return redirect()->intended('/dashboard');
    }

    protected function callbackCadastro(string $provedor, $oauthUser)
    {
        $planoId = (int) session()->pull('sso_plano_id');
        $plano = $planoId > 0 ? Plano::find($planoId) : null;

        if (! $plano) {
            return redirect()->route('login')->withErrors([
                'login' => 'Plano de cadastro inválido. Reinicie o cadastro.',
            ]);
        }

        $email = strtolower(trim((string) $oauthUser->getEmail()));
        if ($email === '') {
            return redirect()->route('cadastro', ['plano' => $plano])
                ->withErrors(['email' => 'Não foi possível obter o e-mail da conta social.']);
        }

        if ($this->ssoAuthService->buscarUsuarioPorEmail($email)) {
            return redirect()->route('login')->withErrors([
                'login' => 'Já existe uma conta com este e-mail. Faça login.',
            ]);
        }

        $this->ssoAuthService->armazenarCadastroSsoNaSessao($provedor, $oauthUser, (int) $plano->idplano);

        $returnUrl = session()->pull('sso_return_url');
        if ($returnUrl) {
            return redirect($returnUrl)->with('sso_vinculado', $this->ssoAuthService->labelProvedor($provedor));
        }

        return redirect()->route('cadastro', ['plano' => $plano])
            ->with('sso_vinculado', $this->ssoAuthService->labelProvedor($provedor));
    }

    protected function validarProvedor(string $provedor): void
    {
        if (! in_array($provedor, [SsoAuthService::PROVEDOR_GOOGLE, SsoAuthService::PROVEDOR_MICROSOFT], true)) {
            abort(404);
        }

        if (! $this->ssoAuthService->provedorConfigurado($provedor)) {
            abort(503, 'Login social temporariamente indisponível.');
        }
    }

    protected function falha(string $provedor, string $mensagem)
    {
        $intent = session()->pull('sso_intent', 'login');

        if ($intent === 'cadastro') {
            $planoId = (int) session()->pull('sso_plano_id');
            $plano = $planoId > 0 ? Plano::find($planoId) : null;

            if ($plano) {
                return redirect()->route('cadastro', ['plano' => $plano])->withErrors(['email' => $mensagem]);
            }
        }

        return redirect()->route('login')->withErrors(['login' => $mensagem]);
    }
}
```

Pontos-chave:

- Usa `Socialite::driver($provedor)->redirect()` e `->user()` (fluxo **stateful**
  padrão — **não** usa `stateless()`, então depende da sessão).
- `intent` (`login` ou `cadastro`) e `plano`/`return` são guardados na sessão
  **antes** do redirect e recuperados no callback.
- O callback bifurca entre `callbackLogin` e `callbackCadastro`.
- `validarProvedor()` retorna 404 para provedor desconhecido e 503 se as
  credenciais não estiverem configuradas.

---

## 7. Service de regra de negócio — `SsoAuthService`

Arquivo `app/Services/SsoAuthService.php` (completo):

```php
<?php

namespace App\Services;

use App\Models\User;
use App\Models\UserHist;
use App\Scopes\TenantScope;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Contracts\User as SocialiteUser;

class SsoAuthService
{
    public const PROVEDOR_GOOGLE = 'google';

    public const PROVEDOR_MICROSOFT = 'azure';

    public const TIPO_SENHA = 'senha';

    public const TIPO_SSO = 'sso';

    public const TIPO_HIBRIDO = 'hibrido';

    public function ssoDisponivel(): bool
    {
        if (! config('nolapis_auth.sso_habilitado', true)) {
            return false;
        }

        return $this->provedorConfigurado(self::PROVEDOR_GOOGLE)
            || $this->provedorConfigurado(self::PROVEDOR_MICROSOFT);
    }

    public function provedorConfigurado(string $provedor): bool
    {
        $config = config("services.{$provedor}");

        return ! empty($config['client_id']) && ! empty($config['client_secret']);
    }

    public function provedoresDisponiveis(): array
    {
        $provedores = [];

        foreach (array_keys(config('nolapis_auth.provedores', [])) as $provedor) {
            if ($this->provedorConfigurado($provedor)) {
                $provedores[] = $provedor;
            }
        }

        return $provedores;
    }

    public function labelProvedor(?string $provedor): string
    {
        if (! $provedor) {
            return '—';
        }

        return config("nolapis_auth.provedores.{$provedor}", $provedor);
    }

    public function labelAuthTipo(?string $tipo): string
    {
        if (! $tipo) {
            return '—';
        }

        return config("nolapis_auth.auth_tipos.{$tipo}", $tipo);
    }

    public function podeLogarComSenha(User $user): bool
    {
        if ($user->sso_obrigatorio) {
            return false;
        }

        if (! in_array($user->auth_tipo, [self::TIPO_SENHA, self::TIPO_HIBRIDO], true)) {
            return false;
        }

        if ($user->auth_tipo === self::TIPO_HIBRIDO && $user->tenant && ! $user->tenant->permitir_senha_local) {
            return false;
        }

        return ! empty($user->password);
    }

    public function podeLogarComSso(User $user, string $provedor): bool
    {
        if (! in_array($user->auth_tipo, [self::TIPO_SSO, self::TIPO_HIBRIDO], true)) {
            return false;
        }

        if ($user->auth_provedor && $user->auth_provedor !== $provedor) {
            return false;
        }

        $tenant = $user->tenant;
        if ($tenant && $tenant->sso_provedores) {
            $permitidos = is_array($tenant->sso_provedores) ? $tenant->sso_provedores : [];
            if ($permitidos !== [] && ! in_array($provedor, $permitidos, true)) {
                return false;
            }
        }

        return true;
    }

    public function buscarUsuarioPorEmail(string $email): ?User
    {
        return User::withoutGlobalScope(TenantScope::class)
            ->where('email', $email)
            ->first();
    }

    public function autenticarOAuth(SocialiteUser $oauthUser, string $provedor): User
    {
        $email = strtolower(trim((string) $oauthUser->getEmail()));

        if ($email === '') {
            throw new \RuntimeException('Não foi possível obter o e-mail da conta social.');
        }

        $user = $this->buscarUsuarioPorEmail($email);

        if (! $user) {
            throw new \RuntimeException('Nenhuma conta cadastrada com este e-mail. Solicite acesso ao administrador.');
        }

        if ($user->status !== 'ATIVO') {
            throw new \RuntimeException('Sua conta está inativa. Entre em contato com o administrador.');
        }

        if (! $this->podeLogarComSso($user, $provedor)) {
            throw new \RuntimeException('Esta conta não está habilitada para login com '. $this->labelProvedor($provedor) .'.');
        }

        $user->provider_id = (string) $oauthUser->getId();
        $user->provider_email = $email;

        if (! $user->auth_provedor) {
            $user->auth_provedor = $provedor;
        }

        $user->save();

        return $user;
    }

    public function concluirLogin(User $user, bool $remember = true): void
    {
        Auth::login($user, $remember);
        session()->regenerate();

        $user->idsession = session()->getId();
        $user->last_login_at = Carbon::now();
        $user->save();

        UserHist::create([
            'iduser' => $user->id,
        ]);
    }

    public function armazenarCadastroSsoNaSessao(string $provedor, SocialiteUser $oauthUser, int $planoId): void
    {
        session([
            'sso_cadastro' => [
                'provedor' => $provedor,
                'provider_id' => (string) $oauthUser->getId(),
                'email' => strtolower(trim((string) $oauthUser->getEmail())),
                'nome' => trim((string) ($oauthUser->getName() ?: $oauthUser->getNickname() ?: '')),
                'idplano' => $planoId,
                'criado_em' => now()->timestamp,
            ],
        ]);
    }

    public function obterCadastroSsoDaSessao(?int $planoId = null): ?array
    {
        $dados = session('sso_cadastro');

        if (! is_array($dados) || empty($dados['email']) || empty($dados['provedor'])) {
            return null;
        }

        if ($planoId !== null && (int) ($dados['idplano'] ?? 0) !== $planoId) {
            return null;
        }

        $criadoEm = (int) ($dados['criado_em'] ?? 0);
        if ($criadoEm > 0 && (now()->timestamp - $criadoEm) > 3600) {
            $this->limparCadastroSsoDaSessao();

            return null;
        }

        return $dados;
    }

    public function limparCadastroSsoDaSessao(): void
    {
        session()->forget('sso_cadastro');
    }

    public function dadosAuthParaNovoUsuario(?string $authTipo = null, ?string $authProvedor = null, ?string $providerId = null, ?string $providerEmail = null): array
    {
        $tipo = $authTipo ?: self::TIPO_SENHA;
        $provedor = $tipo === self::TIPO_SSO ? $authProvedor : ($authProvedor ?: null);

        return [
            'auth_tipo' => $tipo,
            'auth_provedor' => $provedor,
            'sso_obrigatorio' => $tipo === self::TIPO_SSO,
            'provider_id' => $providerId,
            'provider_email' => $providerEmail,
        ];
    }

    public function dadosAuthPadraoTenant(?string $authTipo = null, ?string $authProvedor = null): array
    {
        $tipo = $authTipo ?: self::TIPO_SENHA;

        return [
            'auth_padrao' => $tipo,
            'sso_provedores' => $tipo === self::TIPO_SSO && $authProvedor
                ? [$authProvedor]
                : null,
            'permitir_senha_local' => $tipo !== self::TIPO_SSO,
        ];
    }
}
```

---

## 8. Banco de dados (Migration + Models)

### 8.1 Migration — `database/migrations/2026_06_08_100000_add_sso_auth_fields.php`

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('auth_tipo', 20)->default('senha')->after('status');
            $table->string('auth_provedor', 20)->nullable()->after('auth_tipo');
            $table->boolean('sso_obrigatorio')->default(false)->after('auth_provedor');
            $table->string('provider_id')->nullable()->after('sso_obrigatorio');
            $table->string('provider_email')->nullable()->after('provider_id');
        });

        DB::statement('ALTER TABLE users MODIFY password VARCHAR(255) NULL');

        Schema::table('tenant', function (Blueprint $table) {
            $table->string('auth_padrao', 20)->default('senha')->after('status');
            $table->json('sso_provedores')->nullable()->after('auth_padrao');
            $table->boolean('permitir_senha_local')->default(true)->after('sso_provedores');
        });
    }

    public function down(): void
    {
        Schema::table('tenant', function (Blueprint $table) {
            $table->dropColumn(['auth_padrao', 'sso_provedores', 'permitir_senha_local']);
        });

        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn([
                'auth_tipo',
                'auth_provedor',
                'sso_obrigatorio',
                'provider_id',
                'provider_email',
            ]);
        });

        DB::statement('ALTER TABLE users MODIFY password VARCHAR(255) NOT NULL');
    }
};
```

Colunas adicionadas:

- **`users`**: `auth_tipo` (senha|sso|hibrido), `auth_provedor` (google|azure),
  `sso_obrigatorio` (bool), `provider_id`, `provider_email`. Além disso,
  `password` passa a ser **NULL** (para contas SSO sem senha).
- **`tenant`**: `auth_padrao`, `sso_provedores` (JSON), `permitir_senha_local`.

> Importante: o projeto **não** usa colunas separadas `google_id`/`microsoft_id`.
> Usa um único par genérico `provider_id` + `provider_email`, e o provedor é
> identificado pela coluna `auth_provedor`. Isso simplifica suportar N provedores.

### 8.2 Model `User` — `app/Models/User.php`

```php
protected $fillable = [
    'name',
    'email',
    'password',
    'username',
    'idtenant',
    'idsession',
    'status',
    'auth_tipo',
    'auth_provedor',
    'sso_obrigatorio',
    'provider_id',
    'provider_email',
];

protected $hidden = [
    'password',
    'remember_token',
];

protected $casts = [
    'email_verified_at' => 'datetime',
    'last_login_at' => 'datetime',
    'sso_obrigatorio' => 'boolean',
];
```

Mutator de senha que aceita senha nula (essencial para contas SSO):

```php
public function setPasswordAttribute($password)
{
    if ($password === null || $password === '') {
        $this->attributes['password'] = null;

        return;
    }

    $this->attributes['password'] = bcrypt($password);
}
```

### 8.3 Model `Tenant` — `app/Models/Tenant.php`

```php
protected $fillable = [
    // ... outros campos ...
    'auth_padrao',
    'sso_provedores',
    'permitir_senha_local',
];

protected $casts = [
    // ... outros casts ...
    'sso_provedores' => 'array',
    'permitir_senha_local' => 'boolean',
];
```

---

## 9. Views / Blade

### 9.1 Partial reutilizável — `resources/views/partials/sso-auth-buttons.blade.php`

```blade
@php
    use Illuminate\Support\Facades\Route;

    $ssoService = app(\App\Services\SsoAuthService::class);
    $provedores = $ssoService->provedoresDisponiveis();
    $intent = $intent ?? 'login';
    $planoId = $planoId ?? null;
    $returnUrl = $returnUrl ?? null;
    $rotasSsoOk = Route::has('auth.social.redirect') && Route::has('auth.social.callback');
@endphp

@if ($rotasSsoOk && $ssoService->ssoDisponivel() && count($provedores) > 0)
    <div class="sso-auth-buttons my-3">
        @if (!empty($titulo))
            <p class="text-center text-muted text-sm mb-2">{{ $titulo }}</p>
        @endif

        @foreach ($provedores as $provedor)
            @php
                $params = ['intent' => $intent];
                if ($planoId) {
                    $params['plano'] = $planoId;
                }
                if ($returnUrl) {
                    $params['return'] = $returnUrl;
                }
                $url = route('auth.social.redirect', ['provedor' => $provedor]) . '?' . http_build_query($params);
                $label = $ssoService->labelProvedor($provedor);
                $icon = $provedor === 'google' ? 'fa-google' : 'fa-windows';
                $btnClass = $provedor === 'google' ? 'btn-outline-danger' : 'btn-outline-primary';
            @endphp
            <a href="{{ $url }}" class="btn {{ $btnClass }} w-100 mb-2 d-flex align-items-center justify-content-center gap-2">
                <i class="fa {{ $icon }}"></i>
                <span>{{ $botaoPrefixo ?? 'Entrar' }} com {{ $label }}</span>
            </a>
        @endforeach

        @if (!empty($mostrarSeparador))
            <div class="d-flex align-items-center my-3">
                <hr class="flex-grow-1 m-0">
                <span class="px-2 text-muted text-xs">ou</span>
                <hr class="flex-grow-1 m-0">
            </div>
        @endif
    </div>
@endif
```

O partial:
- Só renderiza se SSO estiver habilitado **e** houver ao menos um provedor configurado.
- Aceita parâmetros: `intent` (`login`/`cadastro`), `planoId`, `returnUrl`,
  `titulo`, `botaoPrefixo`, `mostrarSeparador`.
- Monta o link para `auth.social.redirect` com a query string apropriada.

### 9.2 Tela de login — `resources/views/sessions/create.blade.php`

```blade
<h4 class="mb-1">Login</h4>
<p class="text-muted mb-3">Acesse sua conta</p>

@include('partials.sso-auth-buttons', [
    'intent' => 'login',
    'mostrarSeparador' => true,
])
```

### 9.3 Tela de cadastro — `resources/views/pages/cadastro.blade.php`

```blade
@php
    $ssoVinculado = !empty($ssoCadastro);
    $ssoProvedorLabel = $ssoVinculado
        ? app(\App\Services\SsoAuthService::class)->labelProvedor($ssoCadastro['provedor'] ?? null)
        : null;
@endphp

@if (session('sso_vinculado') || $ssoVinculado)
    <div class="alert alert-success text-sm py-2 mb-3" role="alert">
        Conta {{ session('sso_vinculado') ?? $ssoProvedorLabel }} vinculada. Complete os dados da empresa abaixo.
    </div>
@endif

@if (!$ssoVinculado)
    @include('partials.sso-auth-buttons', [
        'intent' => 'cadastro',
        'planoId' => $plano->idplano,
        'botaoPrefixo' => 'Cadastrar',
        'mostrarSeparador' => true,
        'titulo' => 'Cadastro rápido',
    ])
@endif
```

Quando há vínculo SSO, o e-mail vem pré-preenchido/`readonly` e o campo de senha
é ocultado:

```blade
<div class="input-group input-group-floating mb-3" data-input-name='email'>
    <label class="form-label">E-mail</label>
    <input type="email" class="form-control" name="email" id="email" autocomplete="email"
           value="{{ $ssoCadastro['email'] ?? '' }}" {{ $ssoVinculado ? 'readonly' : '' }} required>
</div>
<div class="input-group input-group-floating mb-3 {{ $ssoVinculado ? 'd-none' : '' }}" data-input-name='senha' id="senha-wrapper">
    <label class="form-label">Crie sua senha</label>
    ...
</div>
```

E o JS pula direto para o passo 2 quando o SSO já está vinculado:

```js
@if (!empty($ssoCadastro))
currentStep = 2;
showStep(2);
updateProgressBar();
@endif
```

### 9.4 Telas administrativas (gestão de método de auth)

- **Por usuário** — `resources/views/admin/partials/auth-campos-usuario.blade.php`:
  select de `auth_tipo`, select de `auth_provedor` e switch `sso_obrigatorio`,
  com JS que desabilita o provedor quando o método é "senha" e oculta o campo de
  senha quando é "sso".
- **Por tenant/instância** — `resources/views/admin/tenants/_auth-form.blade.php`:
  `auth_padrao` (método padrão de novos usuários), checkboxes `sso_provedores[]`
  (provedores permitidos) e switch `permitir_senha_local`.

---

## 10. Regras de negócio (a parte mais importante para replicar)

### 10.1 Login social (`callbackLogin` → `SsoAuthService::autenticarOAuth`)

1. **Não cria usuário automaticamente no login.** Se o e-mail do provedor não
   existir em `users`, lança erro ("Nenhuma conta cadastrada com este e-mail.
   Solicite acesso ao administrador.").
2. Busca o usuário **ignorando o `TenantScope`** (escopo global multi-tenant),
   via `User::withoutGlobalScope(TenantScope::class)`.
3. Valida `status === 'ATIVO'`.
4. Valida elegibilidade SSO via `podeLogarComSso()`:
   - `auth_tipo` deve ser `sso` ou `hibrido`;
   - se `auth_provedor` já estiver definido, deve coincidir com o provedor usado;
   - se o tenant tiver `sso_provedores` restritos, o provedor precisa estar na lista.
5. Atualiza `provider_id` e `provider_email`; se `auth_provedor` estava vazio,
   fixa o provedor atual (**vinculação automática por e-mail**).
6. `concluirLogin()`: `Auth::login($user, remember=true)`, `session()->regenerate()`,
   grava `idsession` (controle de **sessão única**), `last_login_at`, e cria
   registro de histórico em `UserHist`.

### 10.2 Login por senha bloqueado para contas SSO

Em `app/Http/Controllers/SessionsController.php::store()`, antes do
`auth()->attempt()`:

```php
if ($user && ! $this->ssoAuthService->podeLogarComSenha($user)) {
    $provedor = $this->ssoAuthService->labelProvedor($user->auth_provedor);

    return back()->withErrors([
        'login' => $user->auth_tipo === SsoAuthService::TIPO_SSO
            ? "Esta conta utiliza login com {$provedor}. Use o botão correspondente abaixo."
            : 'Esta conta não permite login por senha.',
    ]);
}
```

`podeLogarComSenha()` bloqueia senha quando: `sso_obrigatorio` está ligado; ou
`auth_tipo` não é `senha`/`hibrido`; ou é `hibrido` mas o tenant tem
`permitir_senha_local = false`; ou não há senha definida.

Após sucesso por senha, também chama `concluirLogin()` para uniformizar o
pós-login (sessão única + histórico).

### 10.3 Cadastro via SSO (`callbackCadastro`)

1. Exige um `plano` válido (vindo da sessão `sso_plano_id`). Se não houver,
   redireciona ao login.
2. **Se já existir conta com aquele e-mail, bloqueia** o cadastro e manda fazer
   login ("Já existe uma conta com este e-mail. Faça login.").
3. Caso contrário, **não cria nada ainda** — apenas guarda os dados na sessão
   (`sso_cadastro`: provedor, provider_id, email, nome, idplano, criado_em) com
   validade de **1 hora**, e redireciona de volta ao formulário de cadastro
   pré-preenchido.

### 10.4 Conclusão do cadastro — `app/Http/Controllers/CadastroController.php`

- No `create()` e `store()`, recupera o SSO da sessão com
  `obterCadastroSsoDaSessao($plano->idplano)` (valida que o plano bate e que não
  expirou — 1 h).
- No `store()`, se for cadastro SSO, **valida que o e-mail enviado no form bate
  exatamente** com o e-mail da conta social; se não for SSO, exige senha:

```php
if ($isSsoCadastro) {
    $emailInformado = strtolower(trim((string) $request->input('email', '')));
    if ($emailInformado !== $ssoCadastro['email']) {
        return response()->json([
            'error' => 'O e-mail informado não corresponde à conta social vinculada. Refaça o cadastro com '. $this->ssoAuthService->labelProvedor($ssoCadastro['provedor']) .'.',
        ], 422);
    }
} elseif (trim((string) $request->input('password', '')) === '') {
    return response()->json(['error' => 'Informe uma senha ou cadastre-se com Google/Microsoft.'], 422);
}
```

- Cria o **Tenant** com `dadosAuthPadraoTenant()` e o **User** com
  `dadosAuthParaNovoUsuario()` (password null no SSO, com `provider_id`/
  `provider_email`), e atribui o papel `ADMIN`:

```php
$authDados = $isSsoCadastro
    ? $this->ssoAuthService->dadosAuthParaNovoUsuario(
        SsoAuthService::TIPO_SSO,
        $ssoCadastro['provedor'] ?? null,
        $ssoCadastro['provider_id'] ?? null,
        $ssoCadastro['email'] ?? $email
    )
    : $this->ssoAuthService->dadosAuthParaNovoUsuario(SsoAuthService::TIPO_SENHA);

$user = User::create(array_merge([
    'idtenant' => $newTenant->idtenant,
    'name' => $request['nome'],
    'email' => $email,
    'username' => $username,
    'status' => $v_status,
    'password' => $isSsoCadastro ? null : $request['password'],
    'api_token' => Str::random(80),
], $authDados))->assignRole('ADMIN');
```

- Após `DB::commit()`, chama `limparCadastroSsoDaSessao()`; em planos
  gratuitos/trial, faz `concluirLogin()` automático.

### 10.5 Gestão administrativa — `app/Http/Controllers/Admin/UserController.php`

- Validação: `auth_tipo => required|in:senha,sso,hibrido` e
  `auth_provedor => nullable|in:google,azure`.
- Regras: senha só é obrigatória se o tipo não for `sso`; `sso_obrigatorio` é
  forçado a `true` quando tipo = `sso`; ao voltar para `senha`, limpa
  `provider_id`/`provider_email`.

---

## 11. Passo a passo para replicar em outro sistema

1. **Instalar pacotes**
   ```bash
   composer require laravel/socialite socialiteproviders/microsoft-azure
   ```

2. **Registrar o provider Azure** no `EventServiceProvider::boot()`:
   ```php
   Event::listen(function (SocialiteWasCalled $event) {
       $event->extendSocialite('azure', \SocialiteProviders\Azure\Provider::class);
   });
   ```

3. **Configurar `config/services.php`** com os blocos `google` e `azure`
   (lembre do `tenant` no azure).

4. **Criar `config/nolapis_auth.php`** (ou nome equivalente) com `sso_habilitado`,
   `provedores` e `auth_tipos`.

5. **Adicionar as variáveis** ao `.env`/`.env.example` (`GOOGLE_*`, `MICROSOFT_*`,
   `SSO_HABILITADO`).

6. **Criar a migration** com as colunas em `users` (incluindo `password` nullable)
   e `tenant`. Atualizar `$fillable`/`$casts` dos models e o mutator
   `setPasswordAttribute`.

7. **Criar as rotas** `auth/{provedor}/redirect|callback` com middleware `guest`.

8. **Criar o controller fino** `SocialAuthController` e o **service**
   `SsoAuthService` com toda a regra.

9. **Criar o partial Blade** de botões e incluí-lo nas telas de login e cadastro.

10. **Ajustar o login por senha** (`SessionsController`) para bloquear contas SSO
    e chamar `concluirLogin()`.

11. **Ajustar o cadastro** (`CadastroController`) para consumir a sessão
    `sso_cadastro` e criar usuário/tenant com os dados de auth corretos.

### Cadastro das credenciais nos provedores

- **Google** — [Google Cloud Console](https://console.cloud.google.com/) → APIs &
  Services → Credentials → OAuth 2.0 Client ID (tipo Web application).
  - Authorized redirect URI: `https://SEU_DOMINIO/auth/google/callback`
  - Copie Client ID/Secret para `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`.
- **Microsoft** — [Azure Portal](https://portal.azure.com/) → Microsoft Entra ID →
  App registrations → New registration.
  - Redirect URI (Web): `https://SEU_DOMINIO/auth/azure/callback`
  - Em "Certificates & secrets" gere um Client Secret.
  - Copie Application (client) ID e o secret para `MICROSOFT_CLIENT_ID` /
    `MICROSOFT_CLIENT_SECRET`. Defina `MICROSOFT_TENANT_ID` (`common` para
    multi-tenant, ou o ID da sua organização para single-tenant).

---

## 12. Checklist final

- [ ] `composer require laravel/socialite socialiteproviders/microsoft-azure`
- [ ] `extendSocialite('azure', ...)` no `EventServiceProvider`
- [ ] Blocos `google` e `azure` em `config/services.php`
- [ ] `config/nolapis_auth.php` criado
- [ ] Variáveis no `.env` e `.env.example`
- [ ] Migration aplicada (`users` + `tenant`, `password` nullable)
- [ ] `User`: `$fillable`, `$casts`, `setPasswordAttribute`
- [ ] `Tenant`: `$fillable`, `$casts` (`sso_provedores` array)
- [ ] Rotas `auth/{provedor}/redirect|callback`
- [ ] `SocialAuthController` + `SsoAuthService`
- [ ] Partial `sso-auth-buttons` incluído em login e cadastro
- [ ] Login por senha bloqueia contas SSO
- [ ] Cadastro consome `sso_cadastro` da sessão
- [ ] Redirect URIs cadastradas no Google e no Azure
