What is SSO and How to Implement It?
21 Jun 2026
9 min read
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:
- 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:
- Meet enters his email address.
- Gothryve identifies Behale’s SSO configuration.
- Gothryve redirects Meet to Behale’s Identity Provider.
- Meet authenticates.
- The Identity Provider sends back an authorization code.
- Gothryve exchanges that code for user identity information.
- 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:
| Organization | Identity Provider |
|---|---|
| Behale | Keycloak |
| Example Corp | Okta |
| Acme Corp | Microsoft 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:
| Organization | Domains |
|---|---|
| Behale | behale.in |
| Example | example.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, andissuer
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!