Introduction
Single Sign-On (SSO) has become essential for modern architectures. It allows users to authenticate once and access multiple applications without re-entering credentials. In 2026, security requirements mandate standard protocols such as OpenID Connect (OIDC) and SAML. This tutorial guides you step by step through implementing a production-ready SSO with Keycloak. You will learn to configure a secure realm, manage clients, and integrate token validation in a Next.js application. Each step includes complete, functional code.
Prerequisites
- Keycloak 26+ installed (Docker or standalone)
- Node.js 20+ and TypeScript
- Advanced knowledge of JWT and OAuth2
- Access to a terminal and code editor
Starting Keycloak
version: '3.8'
services:
keycloak:
image: quay.io/keycloak/keycloak:26.0
command: start-dev
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: StrongPass2026!
ports:
- "8080:8080"This Docker Compose file launches Keycloak in development mode with a secure admin account. Use a strong password in production.
Creating the Realm via CLI
#!/bin/bash
kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password StrongPass2026!
kcadm.sh create realms -s realm=entreprise-sso -s enabled=true -s displayName="SSO Entreprise 2026"This script configures the main realm. The realm is the isolated management space for your SSO.
OIDC Client Configuration
{
"clientId": "nextjs-app",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "super-secret-2026",
"redirectUris": ["http://localhost:3000/api/auth/callback"],
"webOrigins": ["http://localhost:3000"],
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
}JSON configuration for the OIDC client. Disable direct flows to strengthen security in production.
Authentication Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import jwt from 'jsonwebtoken';
export function middleware(request: NextRequest) {
const token = request.cookies.get('kc-token')?.value;
if (!token) return NextResponse.redirect(new URL('/login', request.url));
try {
jwt.verify(token, process.env.KEYCLOAK_PUBLIC_KEY!);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = { matcher: ['/dashboard/:path*'] };Next.js middleware that validates the Keycloak JWT token on every protected request. Avoids unnecessary network calls.
OIDC Callback Route
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get('code');
const tokenRes = await fetch('http://localhost:8080/realms/entreprise-sso/protocol/openid-connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'nextjs-app',
client_secret: 'super-secret-2026',
code,
redirect_uri: 'http://localhost:3000/api/auth/callback'
})
});
const { access_token } = await tokenRes.json();
const response = NextResponse.redirect(new URL('/dashboard', request.url));
response.cookies.set('kc-token', access_token, { httpOnly: true, secure: true });
return response;
}This route exchanges the authorization code for a JWT and stores it in a secure cookie.
Best Practices
- Always use HTTPS and HttpOnly Secure cookies
- Configure minimal audiences and scopes
- Implement Keycloak public key rotation
- Log authentication events without sensitive data
- Regularly test flows with tools like Postman
Common Errors
- Forgetting to configure exact redirect URIs (CORS or invalid redirect error)
- Storing client secrets on the frontend
- Ignoring audience (aud) validation in the token
- Failing to handle refresh tokens before expiration
Going Further
Deepen your skills with our advanced authentication and security courses.