Meet Soni

What is SSO and How to Implement It?

The Problem Nobody Likes

Imagine your company uses:

  • Jira
  • Confluence
  • GitHub
  • Slack
  • AWS
  • Internal HR Portal

Without SSO, every application needs its own username and password.

Now imagine an employee leaves the company.

The IT team has to manually revoke access from every single application.

Miss one application and the employee may still have access.

As organizations grow, managing identities becomes harder, security risks increase, and employees get frustrated juggling multiple passwords.

This is exactly the problem Single Sign-On (SSO) was created to solve.

What is Single Sign-On (SSO)?

Single Sign-On (SSO) allows users to authenticate once with a trusted identity provider and then access multiple applications without signing in again.

In simple terms:

Login Once → Access Everything

Instead of every application managing passwords independently, applications delegate authentication to a central identity provider.

A typical flow looks like this:

User


Microsoft / Okta / Keycloak

 ├── Jira
 ├── GitHub
 ├── Confluence
 └── Gothryve

Once the user is authenticated by the Identity Provider (IdP), every connected application trusts that authentication.

Why Companies Use SSO

Better User Experience

Users no longer need to remember multiple passwords.

One login gives access to all connected applications.

Better Security

Authentication is centralized.

Organizations can enforce:

  • MFA (Multi-Factor Authentication)
  • Password Policies
  • Access Reviews
  • Login Restrictions

from a single place.

Easier Employee Management

When an employee joins:

  • Create account once

When an employee leaves:

  • Disable account once

Access disappears everywhere.

Important Terms Before We Start

Before implementing SSO, let’s understand the actors involved.

Identity Provider (IdP)

The system responsible for verifying who you are.

Examples:

  • Google
  • Microsoft Entra ID
  • Okta
  • Keycloak

The IdP stores user credentials and performs authentication.

Service Provider (SP)

The application the user wants to access.

Examples:

  • Gothryve
  • Cloudflare
  • GitHub
  • Atlassian

The Service Provider trusts the Identity Provider to authenticate users.

OAuth vs OpenID Connect

Many developers hear OAuth and OpenID Connect (OIDC) and assume they are the same thing.

They are not.

OAuth

OAuth is an authorization protocol.

It answers:

“What can this application access?”

OpenID Connect (OIDC)

OIDC is an identity layer built on top of OAuth.

It answers:

“Who is the user?”

When implementing SSO, you will almost always use OpenID Connect.

What Actually Happens During an SSO Login?

Let’s say Meet from Behale wants to sign in to Gothryve.

Here’s what happens:

  1. Meet enters his email address.
  2. Gothryve identifies Behale’s SSO configuration.
  3. Gothryve redirects Meet to Behale’s Identity Provider.
  4. Meet authenticates.
  5. The Identity Provider sends back an authorization code.
  6. Gothryve exchanges that code for user identity information.
  7. Meet is logged in.

That’s it.

Everything else is implementation detail.

Implementing SSO

For this tutorial we’ll use:

  • Keycloak as Identity Provider
  • OpenID Connect (OIDC)

Step 1: Run Keycloak

Create a simple docker compose file:

services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: start-dev
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    ports:
      - "8081:8080"

Run it:

docker compose -f docker-compose.sso.yaml up -d keycloak

Access Keycloak:

http://localhost:8081

Default credentials:

admin / admin

Step 2: Configure Keycloak

Create a Realm

Create a realm named:

sso-test

A Realm represents an isolated authentication space.

Think of it as a tenant.

Create a Client

Navigate to:

Clients → Create Client

Configuration:

Client Type: OpenID Connect
Client ID: sso-local
Client Authentication: Enabled

Add redirect URI:

http://localhost:3000/login/sso/callback

Add Web Origin:

http://localhost:3000

Save the client.

Copy the generated Client Secret.

We’ll need it later.

Create a Test User

Create a user:

Username: meet@behale.in
Email: meet@behale.in
First Name: Meet
Last Name: Soni

Enable:

Email Verified

Set a password and disable temporary password mode.

Step 3: Why We Need an SSO Configuration Table

Every organization may use a different Identity Provider.

Example:

OrganizationIdentity Provider
BehaleKeycloak
Example CorpOkta
Acme CorpMicrosoft Entra

Our application needs a way to determine:

  • Which organization the user belongs to
  • Which Identity Provider is configured
  • Which client credentials should be used

A dedicated table solves this problem.

model OrganizationSsoConfig {
  id String @id @default(uuid())

  organizationId String @unique

  organization Organization
    @relation(
      fields: [organizationId],
      references: [id],
      onDelete: Cascade
    )

  encryptionType EncryptionType @default(AES_GCM)

  issuerUrl String?
  clientId String?
  clientSecret String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@map("organization_sso_config")
}

Step 4: Finding the Right SSO Configuration

When a user clicks:

Continue with SSO

we need to determine:

  • Is SSO enabled?
  • Which Identity Provider should be used?

We start by asking for the user’s email.

meet@behale.in

Extract the domain:

behale.in

Then:

Domain

Organization

SSO Config

The organization table should store allowed domains.

Example:

OrganizationDomains
Behalebehale.in
Exampleexample.com

Once we identify the organization, we can retrieve the SSO configuration.

Step 5: Generate the Login Redirect

Install OpenID Client:

pnpm add openid-client

Why do we need the openid-client library?

Implementing OpenID Connect from scratch is possible, but it involves a lot more than simply redirecting users to a login page.

A compliant OIDC implementation needs to:

  • Discover Identity Provider metadata
  • Build authorization URLs correctly
  • Generate and validate PKCE challenges
  • Exchange authorization codes for tokens
  • Verify token signatures
  • Validate claims such as state, nonce, and issuer

Doing all of this manually is error-prone and can introduce security vulnerabilities.

The openid-client library is a widely used OpenID Connect client implementation for Node.js that handles these protocol details for us. Instead of worrying about the low-level OIDC specification, we can focus on our application’s business logic.

Think of it as the Prisma of OpenID Connect.

Just like Prisma abstracts SQL queries while still following database standards, openid-client abstracts OIDC protocol details while still following the OpenID Connect specification.

We’ll use it throughout this tutorial to communicate with Keycloak and handle the authentication flow securely.

Import:

import * as oidc from "openid-client";

Generate security parameters:

const state = oidc.randomState();
const nonce = oidc.randomNonce();

const codeVerifier = oidc.randomPKCECodeVerifier();

const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);

Why Do We Need These Values?

state

Protects against CSRF attacks.

Ensures the authentication response belongs to the request we initiated.

Think of it as a tracking number.

nonce

Protects against replay attacks.

Ensures old authentication responses cannot be reused.

PKCE

PKCE prevents stolen authorization codes from being reused.

Even if an attacker captures the authorization code, it is useless without the original verifier.

scope

Defines what information we are requesting.

openid email profile

This tells the Identity Provider:

Give me:
- User identity
- User email
- User profile

Step 6: Build Authorization URL

const idp = oidc.discovery(issuerUrl, config.clientId, config.clientSecret);

const url = oidc.buildAuthorizationUrl(idp, {
  redirect_uri: redirectUri,
  scope: "openid email profile",
  state,
  nonce,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
});

This URL points to the Identity Provider login page.

The frontend redirects the user there.

Step 7: Store Login State

Before redirecting:

sessionStorage.setItem(
  "sso_in_flight",
  JSON.stringify({
    state,
    nonce,
    codeVerifier,
    organizationId,
  }),
);

Then:

window.location.href = authorizationUrl;

The user is now redirected to Keycloak.

Step 8: Authentication Happens

The user authenticates in Keycloak.

After successful authentication:

/login/sso/callback

receives:

code
state
iss

Example:

/login/sso/callback?
code=abc123
&state=xyz

Step 9: Verify the Response

Frontend validates the response.

if (stateFromUrl !== inFlight.state) {
  throw new Error("State verification failed");
}

This prevents malicious login responses from being accepted.

Step 10: Exchange Authorization Code

Frontend sends:

interface SsoCallbackRequest {
  code: string;
  state: string;
  iss?: string;
  nonce: string;
  codeVerifier: string;
  organizationId: string;
}

to the backend.

Backend exchanges the code for tokens.

const claims = await this.oidcClient.exchangeCode(oidcConfig, callbackUrl, {
  state: callback.state,
  nonce: callback.nonce,
  codeVerifier: callback.codeVerifier,
});

Step 11: Create or Find User

Extract identity information.

const email = claims.email;

Validate:

Does email belong
to the expected organization?

Then:

const user = await this.findOrCreateUser(email, claims);

Step 12: Issue Application Tokens

At this point authentication is complete.

Your application can continue using its normal authentication flow.

Example:

const jwt = await AuthService.login(user);

Return:

{
  "accessToken": "...",
  "user": {
    "id": "...",
    "email": "meet@behale.in"
  }
}

The user is now logged in.

Complete Login Flow

sequenceDiagram
    autonumber
    actor User as Meet
    participant FE as Frontend
    participant BE as Backend
    participant IDP as Keycloak

    User->>FE: Enter email
    FE->>BE: Lookup SSO config
    BE-->>FE: Authorization URL
    FE->>IDP: Redirect
    User->>IDP: Authenticate
    IDP->>FE: Authorization Code
    FE->>BE: Callback
    BE->>IDP: Exchange Code
    IDP-->>BE: User Claims
    BE-->>FE: JWT

What We Built

By the end of this implementation we have:

✅ Domain-based organization discovery

✅ Organization-specific SSO configuration

✅ OpenID Connect authentication

✅ PKCE protection

✅ State verification

✅ Nonce validation

✅ Automatic user provisioning

✅ JWT-based application login

This is fundamentally the same architecture used by most enterprise SaaS products.

Should Every Application Implement SSO?

Not necessarily.

For:

  • Side projects
  • Consumer applications
  • Small internal tools

Google Login may be sufficient.

SSO becomes valuable when:

  • Multiple employees use the product
  • Organizations need centralized access control
  • Security requirements increase
  • IT teams need automated onboarding and offboarding

That’s why SSO is commonly found in enterprise software.

Hope you have learned something new!