Skip to content
Learni
View all tutorials
Authentification

How to Implement SSO with Keycloak in 2026

Lire en français

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

docker-compose.yml
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

setup-realm.sh
#!/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

client-config.json
{
  "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

middleware.ts
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

app/api/auth/callback/route.ts
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