Overview
SSO lets users authenticate once and access multiple systems without re-entering credentials. Two dominant protocols: SAML 2.0 (enterprise, XML-based) and OAuth 2.0 + OpenID Connect (web/mobile, JSON/JWT-based). Modern systems use OAuth/OIDC.
Key terms:
- Identity Provider (IdP): the authority that authenticates users (Google, Okta, Auth0)
- Service Provider (SP): the app that trusts the IdP (Gmail, Slack, your app)
- Access Token: short-lived credential for API access (15 min)
- Refresh Token: long-lived credential to get new access tokens (days/weeks)
- ID Token: JWT with user identity claims (for SSO/login, not API access)
Architecture
SSO LOGIN FLOW (OAuth 2.0 Authorization Code + PKCE)
════════════════════════════════════════════════════════
User Your App Auth Server (IdP) Resource API
│ │ │ │
│── "Login" ─────▶│ │ │
│ │── redirect ──────────▶│ │
│ │ ?client_id=app1 │ │
│ │ &redirect_uri=... │ │
│ │ &code_challenge=... │ (PKCE) │
│ │ │ │
│◀── login page ──────────────────────── │ │
│── credentials ─────────────────────────▶│ │
│ │ │ │
│◀── redirect to app?code=AUTH_CODE ──── │ │
│ │ │ │
│ │── POST /token ───────▶│ │
│ │ code=AUTH_CODE │ │
│ │ code_verifier=... │ (PKCE verify) │
│ │◀─ access_token ───────│ │
│ │ refresh_token │ │
│ │ id_token (JWT) │ │
│ │ │ │
│◀── logged in ───│ │ │
│ │── GET /api/profile ──────────────────────▶│
│ │ Authorization: Bearer <access_token> │
│◀── profile ─────────────────────────────────────────────────│
MULTI-APP SSO SESSION:
════════════════════════════════════════════════════════
User already logged in to App A (session cookie from IdP)
│
│── visits App B ──────────▶│ (not logged in)
│ App B ──▶ IdP redirect
│ IdP sees session cookie → user already authenticated
│ IdP issues tokens to App B immediately
│◀── logged in to App B without entering credentials ──────────
JWT STRUCTURE:
════════════════════════════════════════════════════════
eyJhbGciOiJSUzI1NiJ9 .eyJzdWIiOiJ1c2VyMSIsInJvb .SflKxwRJSMeKKF2QT4f
└─── Header (Base64) ──────── Payload (Base64) ─────── Signature ───────┘
Header: { "alg": "RS256", "typ": "JWT" }
Payload: {
"sub": "user_id_123", // subject
"iss": "https://auth.company.com", // issuer
"aud": "api.company.com", // audience
"exp": 1714608000, // expiry (Unix timestamp)
"iat": 1714604400, // issued at
"email": "user@company.com",
"roles": ["admin", "user"]
}
Signature: RS256(base64(header) + "." + base64(payload), privateKey)
Technical Implementation
JWT Validation in Node.js
// src/middleware/auth.ts
import jwt from 'jsonwebtoken';
import { createRemoteJWKSet, jwtVerify } from 'jose';
// Fetch IdP's public keys for signature verification
const JWKS = createRemoteJWKSet(
new URL('https://auth.company.com/.well-known/jwks.json')
);
export async function verifyToken(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.company.com',
audience: 'api.company.com',
});
if (!payload.sub) throw new Error('Missing subject claim');
if ((payload.exp ?? 0) < Date.now() / 1000) throw new Error('Token expired');
return payload;
}
// Express middleware
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
try {
req.user = await verifyToken(authHeader.slice(7));
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
JWT Validation in Spring Boot (Java)
// Spring Security OAuth2 Resource Server
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthConverter())
)
);
return http.build();
}
}
// application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.company.com
# Spring auto-fetches JWKS from /.well-known/openid-configuration
Token Refresh Flow
// Client-side token refresh
export class AuthService {
private refreshTokenKey = 'refresh_token'; // httpOnly cookie in production
async getValidAccessToken(): Promise<string> {
const accessToken = this.getStoredAccessToken();
// Check if about to expire (within 60 seconds)
if (accessToken && !this.isExpiringSoon(accessToken)) {
return accessToken;
}
// Refresh
const refreshToken = this.getRefreshToken();
if (!refreshToken) throw new AuthError('Not authenticated');
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
const { access_token, refresh_token } = await response.json();
this.storeTokens(access_token, refresh_token);
return access_token;
}
}
Interview Preparation
Q: What is the difference between OAuth 2.0 and OpenID Connect?
OAuth 2.0 is an authorization framework — it defines how to grant access to APIs on behalf of a user (access tokens). It says nothing about who the user is. OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0 — it adds an ID token (JWT) that contains user identity claims (email, name, sub). Use OAuth for API access; use OIDC for login/authentication.
Q: Where should you store JWTs on the client?
httpOnlycookie: preferred for web apps. Cannot be accessed by JavaScript (XSS-safe). Automatically sent on every request. Vulnerable to CSRF → mitigate withSameSite=Strictor CSRF tokens.localStorage: easy to use but accessible to JavaScript → any XSS can steal the token. Not recommended for access tokens.- In-memory (React state): most secure (no persistence), but lost on page refresh → refresh token needed in httpOnly cookie.
Q: Why use short-lived access tokens + long-lived refresh tokens?
Access tokens are sent on every API request — if stolen, they're valid until expiry. Short TTL (15 min) limits the damage window. Refresh tokens are only sent to the auth server to get new access tokens — less exposure. Refresh tokens can be revoked server-side (add to revocation list). If you had long-lived access tokens, a stolen token gives persistent access.
Q: How do you implement logout in SSO?
Two types: local logout (clear local session/cookies) and single logout (SLO — notify IdP to invalidate the global session across all apps). Local logout is easy. SLO is hard — the IdP must notify all registered apps via back-channel or front-channel logout. Many systems skip SLO and rely on short TTL access tokens instead.
Learning Resources
- OAuth 2.0 RFC 6749 — the spec
- OpenID Connect Core
- Auth0 Docs — well-explained auth flows
- JWT.io — JWT decoder and library list
- PKCE RFC 7636 — for public clients