Skip to content

Backend

SSO for SaaS — Supabase Auth + Google Workspace + Okta SAML in 2026

Enterprise customers won't use your app without SSO. Supabase Auth handles Google Workspace and Microsoft natively. For SAML—Okta, OneLogin, Azure AD—pair Supabase with WorkOS or enterprise SAML. Real code: password + Google + Microsoft + SAML in one login form, plus JIT user provisioning and role mapping.

🔐 👥 ⚙️

SSO is the enterprise unlock. Your SaaS can be perfect, but if a CTO at a Fortune 500 company can't log in via their corporate identity provider, they will not use it. Velocity X dashboard supports password login, Google Workspace, Microsoft 365, and SAML-based Okta. The moment you add those, deal size goes up, sales cycle goes down, and you stop losing pilots to "we can't get IT to approve a password manager entry." Supabase Auth handles the Google / Microsoft integrations natively. For SAML (Okta, OneLogin, Azure AD as SAML, others), you pair Supabase with WorkOS or configure enterprise SAML at the Supabase level if you're on the Pro plan. The real complexity isn't the OAuth dance—it's JIT (just-in-time) user provisioning, mapping enterprise roles to your app's permission model, and handling scenarios like "user logs in via Google for the first time but we already created their account via SCIM." Here's the full pattern: OAuth provider setup, Supabase Auth config, SAML via WorkOS, JIT provisioning on first login, role mapping, and the edge cases that trip up most teams.

Why SSO is the Enterprise Requirement, Not a Nice-to-Have

Enterprise IT departments demand SSO for three reasons: security (single sign-off), audit (all logins logged to their IdP), and password hygiene (one less password for employees to lose). Without it, you're locked out of the entire mid-market and enterprise motion. Even self-serve SaaS grows faster with SSO—the moment you offer it, upgrade rates jump. The OAuth2 / OIDC handshake is standard; Supabase Auth abstracts most of it. But the hard parts are elsewhere: detecting when a new IdP user logs in for the first time and provisioning them as a new user in your org, mapping their enterprise roles to your app's roles, handling email domain verification (so anyone with @company.com can't just sign up), and the SAML flow which is more rigid than OAuth. Most tutorials show a single provider. Production needs all of them: password for testing, Google for SMBs, Microsoft for teams already on 365, and SAML for enterprises that demand it.

Supabase Auth: Native Google and Microsoft Support

Supabase Auth ships with pre-built integrations for Google, GitHub, Discord, Apple, and Microsoft. Setting up Google Workspace and Microsoft 365 is a five-minute dashboard config.

Google Workspace Setup

Go to Google Cloud Console, create a new project, enable Google+ API, and generate OAuth2 credentials. Supabase gives you a redirect URI; paste it into Google. You'll get a client ID and secret.

// In Supabase dashboard: Authentication → Providers → Google
// Paste Client ID and Secret
// That's it. Your users can now login with Google.

// In your app:
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://yourdomain.com/auth/callback',
    // Optional: restrict to Google Workspace domain
    queryParams: {
      hd: 'company.com', // Restrict to this domain
    },
  },
});

The `hd` (hosted domain) parameter restricts login to @company.com addresses. Without it, anyone with a personal Google account can sign up. For enterprise, always set this.

Microsoft 365 Setup

Microsoft setup is nearly identical: Azure Portal → App registrations → New registration. You'll generate a client secret and paste credentials into Supabase.

const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'azure',
  options: {
    redirectTo: 'https://yourdomain.com/auth/callback',
    // Optional: tenant restriction for enterprise
    scopes: 'openid profile email',
  },
});

For Microsoft, you can restrict to a specific tenant (Azure Entra tenant ID) instead of domain. This prevents cross-tenant login and is mandatory if you're selling into enterprise.

Combining All Providers in One Login Form

Here's a real login component that supports password, Google, Microsoft, and a SAML button (SAML is handled separately below):

// src/components/LoginForm.tsx
import { useState } from 'react';
import { supabase } from '@/lib/supabase';

export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handlePasswordLogin = async () => {
    setLoading(true);
    setError('');
    const { error: err } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    if (err) setError(err.message);
    setLoading(false);
  };

  const handleOAuth = async (provider: 'google' | 'azure') => {
    setLoading(true);
    const { error: err } = await supabase.auth.signInWithOAuth({
      provider,
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
        queryParams: provider === 'google' ? { hd: 'company.com' } : undefined,
      },
    });
    if (err) setError(err.message);
    setLoading(false);
  };

  return (
    
{error &&
{error}
} {/* Password login */} setEmail(e.target.value)} className="w-full px-3 py-2 border rounded" /> setPassword(e.target.value)} className="w-full px-3 py-2 border rounded" /> {/* OAuth buttons */}
{/* SAML button (see next section) */}
); }

SAML via WorkOS: Okta, OneLogin, Azure AD

SAML is enterprise-standard but different from OAuth. Your app doesn't get a client secret; instead, you configure a service provider (your app) in the IdP's console. SAML requests are XML assertions. Supabase Pro supports SAML natively, but if you're on the free or standard plan, use WorkOS—they abstract SAML into a simple API.

WorkOS Setup (Fastest Path for Most SaaS)

Sign up at workos.com, create an organization, and generate a client ID. You'll add this to your env and call WorkOS's OAuth-like endpoint.

// .env
WORKOS_CLIENT_ID=your_client_id
WORKOS_API_KEY=your_api_key

// src/lib/workos.ts
import { WorkOS } from '@workos-inc/node';

export const workos = new WorkOS(process.env.WORKOS_API_KEY);

// src/pages/api/auth/workos-callback.ts (Netlify Function)
import { workos } from '@/lib/workos';

export default async (event: any) => {
  const { code } = event.queryStringParameters;

  if (!code) {
    return { statusCode: 400, body: 'Missing code' };
  }

  try {
    // Exchange code for access token
    const response = await workos.sso.authenticateWithCode({
      code,
      clientId: process.env.WORKOS_CLIENT_ID!,
    });

    const { userId, email, firstName, lastName, organizationId } = response;

    // Now sign this user in with Supabase
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password: crypto.randomUUID(), // SAML users have no password
    });

    // ... (JIT provisioning below)
  } catch (error) {
    return { statusCode: 500, body: JSON.stringify(error) };
  }
};

Supabase Enterprise SAML (Pro Plan)

If you're on Supabase Pro, you can configure SAML directly. Go to Authentication → Providers → SAML, and you'll get metadata XML. Paste this into your customer's IdP (Okta, OneLogin, etc.) and you're done. Users will see a "Sign in with SAML" button on your login form.

// For Supabase SAML:
const { data, error } = await supabase.auth.signInWithSSO({
  domain: 'company.okta.com', // Customer's Okta domain
  redirectTo: `${window.location.origin}/auth/callback`,
});

JIT (Just-in-Time) User Provisioning: The Hard Part

When a user logs in via SSO for the first time, they don't exist in your database yet. You need to create them. This is JIT provisioning. It sounds simple; it's not, because you need to: detect new users, create them with the right org (if they're part of an enterprise account), map their roles, and handle the case where multiple team members from Company X all sign up on the same day.

JIT Flow: OAuth Callback Handler

// src/pages/api/auth/callback.ts
import { supabase } from '@/lib/supabase';

export default async (event: any) => {
  const { code } = event.queryStringParameters;

  // Exchange code for session
  const { data, error } = await supabase.auth.exchangeCodeForSession(code);

  if (error) {
    return { statusCode: 400, body: error.message };
  }

  const user = data.user;

  // Check if user already exists in your app
  const { data: existingUser } = await supabase
    .from('app_users')
    .select('id')
    .eq('email', user.email)
    .single();

  if (!existingUser) {
    // New user: provision them
    const { data: newUser, error: createError } = await supabase
      .from('app_users')
      .insert([
        {
          email: user.email,
          auth_id: user.id,
          name: user.user_metadata?.full_name || '',
          avatar_url: user.user_metadata?.avatar_url || null,
          organization_id: detectOrganizationFromEmail(user.email),
          role: 'member', // Default role
          created_at: new Date(),
        },
      ])
      .select()
      .single();

    if (createError) {
      return { statusCode: 500, body: createError.message };
    }
  }

  return {
    statusCode: 302,
    headers: {
      Location: '/dashboard',
    },
  };
};

// Helper: detect org from email domain
function detectOrganizationFromEmail(email: string) {
  const domain = email.split('@')[1];
  // Query organizations by domain
  // If found, return org ID; otherwise create new org for this user
  return '...'; // Implement based on your data model
}

Role Mapping: From IdP to App Roles

Enterprise SSO providers like Okta and Azure AD can send role information in the JWT. You need to map those roles to your app's permission model. For example, an "Engineering Manager" in Okta might map to "team_admin" in your app.

// Roles returned by IdP (in jwt.groups or custom claims)
const idpRoles = response.idp_metadata?.roles || [];

// Map to app roles
const appRole = mapIdpRoleToAppRole(idpRoles);

// Save to database
const { error } = await supabase
  .from('app_users')
  .update({ role: appRole })
  .eq('auth_id', user.id);

function mapIdpRoleToAppRole(idpRoles: string[]): string {
  const roleMap: Record = {
    'Engineering Manager': 'team_admin',
    'Finance Team': 'finance_viewer',
    'Executive': 'org_admin',
  };

  for (const role of idpRoles) {
    if (roleMap[role]) {
      return roleMap[role];
    }
  }

  return 'member'; // Default
}

Six Enterprise SSO FAQs

Do I need both Google and Microsoft SSO?

No, but you should offer both. Google Workspace is most common in tech/startups. Microsoft 365 dominates corporate. Offering both covers 95% of SaaS buyers. Add SAML if you're selling to mid-market or enterprise.

What if a user signs up with Google but later their company adds an email domain?

They have two identities: one Google, one SAML. This is called "account linking." Most SaaS merge them when the user logs into settings. Prompt: "We found your work email linked to your personal account. Merge them?" Then update the Supabase user's identities array to include both.

How do I prevent @gmail.com signups while allowing @company.com Google Workspace?

Use the `hd` parameter (hosted domain) in the Google OAuth call. This forces Google to reject personal accounts. In your callback, also check the email domain server-side before creating the user.

What if an employee leaves the company and Okta deactivates them?

You need a sync. Okta can send SCIM events (System for Cross-domain Identity Management) to your endpoint. When a user is deactivated in Okta, you receive a SCIM DELETE. Mark them as inactive in your app. Velocity X handles this by monitoring role changes: if a user's Okta role is empty, we revoke access.

Can I enforce MFA (multi-factor authentication) from the IdP level?

Yes. If the user's Okta account requires MFA, Okta enforces it before sending SAML to you. Your app doesn't need to implement MFA separately—the IdP handles it. This is a major security win for enterprise: IT controls security policy, not your app.

How do I test SSO locally in development?

Use a staging Okta instance or Okta's free trial. For Google Workspace, use a Google Workspace sandbox. Point your localhost to a public URL using ngrok or Cloudflare Tunnel, then register that URL as the redirect URI in the IdP. Test callback flow, JIT provisioning, and role mapping before shipping.

The Bottom Line

SSO is not optional if you want enterprise deals. Supabase Auth + Google + Microsoft covers SMB and small enterprise. WorkOS or Supabase SAML adds Okta, OneLogin, and Azure AD—the final 20% of deals that unlock serious revenue. The real work is JIT provisioning (handling new users on first login) and role mapping (translating IdP roles to app permissions). Get these right, and you'll watch your deal size jump. For more on auth security patterns, see Supabase RLS for multi-tenant access control. Ready to build enterprise-grade SaaS? Check Aidxn Design's backend partnerships to ship auth that scales.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.