In the B2B SaaS world, the ultimate feature request is "Enterprise SSO".
Your big clients (Acme Corp, Beta Inc) want to enforce security by managing their own users. They don't want their employees to have a password for your app; they want them to log in via their corporate Azure AD or Okta.
But here is the challenge: You have hundreds of clients. Acme uses Azure, Beta uses Okta, and Gamma uses Google Workspace. How do you route the user to the correct provider?
In this guide, we will implement the "Domain Discovery" pattern using beartropy/saml2.
🏢 The Architecture
We cannot just put a "Login with SSO" button, because we don't know which SSO to trigger. We need a two-step login flow:
- Step 1: Ask for the Email Address.
- Step 2: Check the domain (e.g.,
@acme.com).
- Step 3: If mapped to an IDP, redirect to SAML. If not, ask for a password.
🗄️ Phase 1: Mapping Domains to IDPs
First, we need to associate an IDP Key (from our package) with a specific email domain. You might add a sso_domain column to your teams table, or use a simple config array.
Let's assume we have configured our IDPs in the Beartropy Admin UI:
- Key:
acme-corp (Azure AD)
- Key:
beta-inc (Okta)
🚦 Phase 2: The Login Controller
We need a custom controller that intercepts the login attempt.
1namespace App\Http\Controllers\Auth;
2
3use Illuminate\Http\Request;
4use Beartropy\Saml2\Models\Idp;
5use App\Models\Team;
6
7class SsoLoginController extends Controller
8{
9 public function check(Request $request)
10 {
11 $request->validate(['email' => 'required|email']);
12
13 $domain = substr(strrchr($request->email, "@"), 1);
14
15 // 1. Check if this domain belongs to a Team with SSO enabled
16 $team = Team::where('sso_domain', $domain)->first();
17
18 if ($team && $team->sso_idp_key) {
19 // 2. FOUND! Redirect to the specific IDP route provided by the package
20 return redirect()->route('saml2.login', ['idp' => $team->sso_idp_key]);
21 }
22
23 // 3. Not found? Continue to standard password login
24 return redirect()->route('login.password', ['email' => $request->email]);
25 }
26}
🎨 Phase 3: The UI (The "Linear" Style Login)
Using Livewire and Beartropy UI, this feels seamless.
1
2<div class="max-w-md mx-auto mt-10">
3 <x-bt-card>
4 <h2 class="text-2xl font-bold mb-6 text-center">Sign in to Beartropy</h2>
5
6 @if(!$showPasswordInput)
7
8 <form wire:submit="checkEmail">
9 <x-bt-input
10 label="Work Email"
11 wire:model="email"
12 placeholder="name@company.com"
13 autofocus
14 />
15
16 <x-bt-button class="w-full mt-4" type="submit">
17 Continue
18 </x-bt-button>
19 </form>
20
21 @else
22
23 <form wire:submit="login">
24 <div class="mb-4">
25 <span class="text-sm text-gray-500">Signing in as {{ $email }}</span>
26 <button type="button" wire:click="resetFlow" class="text-xs text-indigo-600 ml-2">Change</button>
27 </div>
28
29 <x-bt-input
30 type="password"
31 label="Password"
32 wire:model="password"
33 autofocus
34 />
35
36 <x-bt-button class="w-full mt-4" type="submit">
37 Sign In
38 </x-bt-button>
39 </form>
40 @endif
41 </x-bt-card>
42</div>
🔄 Phase 4: Dynamic Tenant Assignment
When the user returns from Azure AD, we need to ensure they are added to the correct team. We use the event listener we published during installation.
1// app/Listeners/HandleSamlLogin.php
2public function handle(Saml2LoginEvent $event)
3{
4 // The 'key' tells us which IDP was used (e.g., 'acme-corp')
5 $idpKey = $event->getIdpKey();
6
7 $user = User::firstOrCreate(['email' => $event->getEmail()], [...]);
8
9 // Find the team linked to this IDP
10 $team = Team::where('sso_idp_key', $idpKey)->first();
11
12 if ($team && !$user->belongsToTeam($team)) {
13 $team->users()->attach($user);
14 }
15
16 Auth::login($user);
17}
Conclusion
With beartropy/saml2, Multi-Tenancy isn't a headache. The package handles the heavy lifting of the SAML protocol, allowing you to focus on the routing logic that makes sense for your business.
Ready to sell to the Enterprise? You are now.