Back to Blog

Google Workspace SSO for SaaS in NestJS

Published: June 29, 2026
Google Workspace SSO for SaaS in NestJS

There's a moment in every B2B sale where the prospect's security team sends a one-line email that stalls the whole deal: "Do you support SSO?" Google Workspace SSO is how you answer yes — it lets enterprise IT manage access to your app from their own admin console, and it's table stakes for selling to any company big enough to have a procurement process. Build it wrong, though, and you ship something that looks like enterprise SSO while quietly letting any @gmail.com address walk in.

The single control that makes the difference: validate the hosted-domain (hd) claim from Google's verified ID token, and map it to a registered tenant. Everything else — OIDC setup, just-in-time provisioning, role mapping — is plumbing around that one check. Skip it and you don't have enterprise SSO; you have a login button with a security hole. Here's the production build in NestJS.

Google Workspace SSO centralizes access the way one badge opens the right doors

Why OIDC Over SAML

Google Workspace speaks both SAML 2.0 and OpenID Connect, but OIDC is the better fit for modern SaaS:

  • SAML — older, XML payloads POSTed through the browser, heavier to parse and configure in a REST API. Some enterprises still mandate it, so keep it on the roadmap.
  • OIDC — built on OAuth 2.0, issues compact signed JWTs you verify with a standard signature check. Clean for SPAs and backends alike, and it's the same OAuth flow as any social-login integration, just pointed at a Workspace domain.

Start with OIDC; reach for SAML when a contract specifically requires it. Google's own OpenID Connect docs are the reference for the token shape.

Configure Google as the Identity Provider

In the Google Cloud Console, before any code:

  1. Create a dedicated project for your app.
  2. Configure the OAuth consent screen with scopes openid, profile, email.
  3. Generate OAuth 2.0 web client credentials — your Client ID and Client Secret.
  4. Set the authorized redirect URI, e.g. http://localhost:3000/api/auth/google/callback for local dev.

Keep the client secret in encrypted runtime config, never in the repo.

The NestJS OIDC Strategy (and the hd Check)

A Passport strategy handles the callback. The security lives in validate, where you read the hosted domain:

TypeScript
1// src/auth/strategies/google-oidc.strategy.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, Profile } from 'passport-google-oauth20';
5
6@Injectable()
7export class GoogleOidcStrategy extends PassportStrategy(Strategy, 'google') {
8  constructor() {
9    super({
10      clientID: process.env.GOOGLE_CLIENT_ID!,
11      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
12      callbackURL: process.env.GOOGLE_CALLBACK_URL!,
13      scope: ['openid', 'email', 'profile'],
14    });
15  }
16
17  async validate(_accessToken: string, _refreshToken: string, profile: Profile) {
18    const email = profile.emails?.[0]?.value;
19    const hostedDomain = (profile._json as { hd?: string }).hd; // present only for Workspace accounts
20
21    if (!email) throw new UnauthorizedException('Google profile missing email.');
22    if (!hostedDomain) throw new UnauthorizedException('Personal Google accounts are not allowed.');
23
24    return {
25      email,
26      firstName: profile.name?.givenName,
27      lastName: profile.name?.familyName,
28      domain: hostedDomain, // used to resolve the tenant
29    };
30  }
31}

One hardening note: passport-google-oauth20 fetches the profile from Google over TLS, but for defense in depth verify the hd from the signed ID token (via openid-client or by validating the id_token) rather than trusting a profile field alone. The point is the same — no hd, no entry.

No hosted-domain claim, no entry — that check is what makes Google Workspace SSO actually enterprise-grade

Just-In-Time Provisioning

Don't make IT pre-create accounts. Once the domain checks out, upsert the user and bind them to the tenant on first login:

TypeScript
1// src/auth/auth.service.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { PrismaService } from '../prisma/prisma.service';
4
5@Injectable()
6export class AuthService {
7  constructor(private readonly prisma: PrismaService) {}
8
9  async loginWithGoogle(p: { email: string; firstName?: string; lastName?: string; domain: string }) {
10    // Resolve the tenant from the verified hosted domain.
11    const tenant = await this.prisma.tenant.findUnique({ where: { registeredDomain: p.domain } });
12    if (!tenant) {
13      throw new UnauthorizedException('Your domain is not registered for this workspace.');
14    }
15
16    // Create on first login; update on subsequent ones.
17    return this.prisma.user.upsert({
18      where: { email: p.email },
19      update: { lastLoginAt: new Date() },
20      create: {
21        email: p.email,
22        firstName: p.firstName,
23        lastName: p.lastName,
24        tenantId: tenant.id, // scopes the user to the right company
25        isActive: true,
26      },
27    });
28  }
29}

The tenant.findUnique on registeredDomain is what stops a valid Google user from a different company logging into a tenant they don't belong to — the hd check and the tenant lookup work as a pair.

Map Workspace Groups to Roles

You can map Google Workspace groups to your internal roles, but be honest about the cost: reading group membership needs the Admin SDK Directory API with admin-granted scopes (often domain-wide delegation), not just the login scopes. When it's worth it, the mapping is straightforward:

  • dev-leads@enterprise.comAdmin
  • staff@enterprise.comUser

For many products it's simpler to provision users via SSO and manage elevation inside your own role-based access control, syncing groups only for the enterprises that ask. Don't take the heavy Admin SDK scopes you don't need — every extra scope is something the customer's security review will question.

Offboarding: Honor the Off Switch Fast

SSO's whole promise to IT is one place to cut access. When they disable a terminated employee's Google account, your app has to notice quickly. Keep access tokens short (~15 minutes) and re-check account status on every refresh-token rotation against Redis, so a disabled user is locked out within minutes, not whenever a long-lived token expires. Stolen or stale credentials drive about 24% of breaches (Verizon DBIR 2024); fast revocation is exactly the mitigation enterprise buyers are checking for.

Offboarding in Google Workspace SSO must cut access in minutes, like revoking a badge

Test Google Workspace SSO Without a Live Tenant

You don't need a paid enterprise tenant to test the logic — mock the strategy and assert the domain handling:

TypeScript
1// test/auth.e2e-spec.ts
2describe('Google Workspace SSO', () => {
3  it('accepts a user whose hosted domain matches a registered tenant', async () => {
4    const strategy = {
5      validate: jest.fn().mockResolvedValue({
6        email: 'engineer@targetcorp.com',
7        domain: 'targetcorp.com',
8      }),
9    };
10    expect(await strategy.validate()).toHaveProperty('domain', 'targetcorp.com');
11  });
12});

Build It or Buy It?

Rolling your own with openid-client gives you full control and no per-user fees — sensible if SSO is core and you have the security depth to verify tokens correctly. Managed providers like WorkOS, Auth0, or Clerk handle multi-tenant routing, SAML, and the security maintenance for you, which gets you to an enterprise "yes" faster. Default to managed unless you have a concrete reason to own the flow — this is auth, and you don't roll your own crypto without a good reason.

LayerConsumer defaultEnterprise practice
ProtocolPlain OAuth grantOIDC ID tokens, verified
Domain scopeAny Google emailhd claim + tenant lookup
ProvisioningManual accountsJust-in-time on first login
OffboardingLong sessionsShort tokens + Redis status check

Get the hd check right and Google Workspace SSO turns a stalled security review into a signature. Skip it and you've built the convenience without the control — which the customer's IT team will find, usually in the demo, usually right before they were going to say yes.

Frequently Asked Questions

The hd (hosted domain) claim is a value in Google's verified ID token that names the Workspace domain a user belongs to, like company.com. It's the control that turns 'sign in with Google' into real enterprise SSO: without checking it, anyone with a personal @gmail.com account could log into an enterprise tenant. Verify hd from the signed ID token, confirm it matches a registered tenant domain, and reject everything else.

OIDC for most modern SaaS. It's built on OAuth 2.0, issues compact signed JWTs your server verifies with a standard signature check, and fits REST APIs and SPAs cleanly. SAML is the older XML-over-browser-POST protocol — still required by some enterprise buyers, but heavier to parse and configure. Start with OIDC, and add SAML only when a specific deal demands it.

JIT provisioning creates the user record on their first successful SSO login instead of making an admin pre-create accounts. When the token's domain matches a registered tenant, you upsert the user and link them to that tenant. It removes onboarding friction — IT enables your app in their Google admin console and employees just log in — while the hd check keeps provisioning scoped to the right company.

Keep access tokens short (around 15 minutes) and re-check account status on every refresh against a fast store like Redis. When IT disables the employee's Google account, your next refresh fails and they're locked out within minutes rather than whenever a long-lived token happens to expire. SSO centralizes the off switch — your job is to honor it quickly.

Building with openid-client or Passport gives you full control, no per-user fees, and no third-party dependency — reasonable if SSO is core and you have the security depth. Managed providers (WorkOS, Auth0, Clerk) handle multi-tenant routing, SAML, and security updates out of the box, which gets you to an enterprise 'yes' faster. Default to managed unless you have a concrete reason to own it.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio