📚 Full tutorial: Read the step-by-step guide here →
https://natasatm.netlify.app/articles/nextjs-keycloak-spring-boot-bff
This project demonstrates a clean integration of Next.js 15 with NextAuth.js using Keycloak as an OIDC provider, following a secure BFF (Backend-for-Frontend) pattern.
- 🔐 Login with Keycloak via NextAuth
- 🍪 HTTP-only cookie sessions (JWT strategy)
- 🚧 Middleware protection for
apppages andapiroutes - 🔁 Secure token refresh handled server-side
- 🧰 BFF proxy routes to a Spring Boot backend (no tokens on the client)
- 🐳 Complete Docker setup with Keycloak + Mailpit
- 🎨 Custom Keycloak login theme included
- 🤝 Pairs with the Spring Boot backend
- Overview
- Tech stack
- Project structure
- Getting started
- Docker setup (Keycloak + Mailpit)
- Environment variables
- Keycloak configuration
- Custom Keycloak theme
- Authentication flow
- Middleware protection
- API routes (BFF)
- Scripts
- Deployment notes
- Useful links
- License
The app uses NextAuth with the Keycloak provider. Users hitting protected routes are redirected to a minimal relay page at /auth/login that immediately calls signIn("keycloak") and then returns them to the original page via callbackUrl.
JWT tokens from Keycloak are stored server-side (in the encrypted NextAuth JWT). BFF routes under src/app/api/* forward requests to the Spring Boot backend with the server-only access token so the browser never sees bearer tokens.
- Next.js 15 (App Router, Turbopack)
- NextAuth.js 5 (Keycloak provider, JWT sessions)
- React 19
- TypeScript
- Tailwind CSS 4
- Keycloak 24.x (via Docker)
- Mailpit (local email testing)
web-next/
src/
app/
app/
page.tsx # Protected dashboard page
api/
auth/
[...nextauth]/route.ts # NextAuth core routes
kc-logout/route.ts # Revokes Keycloak session
login/page.tsx # Client relay to trigger signIn
me/route.ts # BFF proxy to backend /api/me
update/route.ts # BFF proxy to backend /api/update
admin/only/route.ts # BFF proxy to backend /api/admin/only
page.tsx # Public landing page
layout.tsx # Root layout
components/
Providers.tsx # NextAuth SessionProvider wrapper
middleware.ts # Protects /app/* and /api/*
docker/
docker-compose.yml # Keycloak + Mailpit setup
themes/
my-theme/
login/ # Custom Keycloak theme
- Node.js 18+
- Docker Desktop
- Java 21 and Maven (for the Spring Boot backend)
git clone https://github.com/NatasaTM/nextauth-keycloak-demo.git
cd nextauth-keycloak-demo/web-nextcd docker
docker compose up -dThis starts:
- Keycloak on http://localhost:8080 (admin:
admin/admin) - Mailpit on http://localhost:8025 (email testing UI)
Create a .env.local file in the web-next directory:
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=<generate-a-random-32-byte-string>
AUTH_KEYCLOAK_ISSUER=http://localhost:8080/realms/demo
AUTH_KEYCLOAK_ID=next-app
AUTH_KEYCLOAK_SECRET=<your-client-secret-from-keycloak>
BACKEND_URL=http://localhost:8082Generate a secure secret:
macOS/Linux:
openssl rand -base64 32Windows PowerShell:
[System.Convert]::ToBase64String((1..32 | ForEach-Object {Get-Random -Maximum 256}))Follow the full tutorial or see the Keycloak configuration section below.
npm install
npm run devOpen http://localhost:3000 in your browser.
In a separate terminal, clone and run the Spring Boot backend:
git clone https://github.com/NatasaTM/springboot-keycloak-bff.git
cd springboot-keycloak-bff
export KC_ISSUER_URI=http://localhost:8080/realms/demo
export ALLOWED_ORIGINS=http://localhost:3000
mvn spring-boot:runThe backend will be available at http://localhost:8082.
This project includes a complete Docker Compose setup in the docker/ folder that runs both Keycloak and Mailpit.
- Keycloak 24.0.2 with:
- Admin console on port 8080
- Persistent data volume (
kc_data) - Custom theme auto-mounted from
themes/folder - Cache disabled for theme development
- Mailpit for local email testing:
- SMTP server on port 1025
- Web UI on port 8025
cd docker
docker compose up -ddocker compose downdocker compose down -vThis removes the kc_data volume, so you'll need to reconfigure Keycloak.
docker compose logs -f keycloak
docker compose logs -f mailpit- Keycloak admin console: http://localhost:8080 (login:
admin/admin) - Mailpit web UI: http://localhost:8025
| Variable | Description | Example |
|---|---|---|
NEXTAUTH_URL |
Your application URL | http://localhost:3000 |
NEXTAUTH_SECRET |
Random secret for session encryption | Generate with openssl rand -base64 32 |
AUTH_KEYCLOAK_ISSUER |
Keycloak realm issuer URL | http://localhost:8080/realms/demo |
AUTH_KEYCLOAK_ID |
Keycloak client ID | next-app |
AUTH_KEYCLOAK_SECRET |
Keycloak client secret | From Keycloak Clients → Credentials |
BACKEND_URL |
Spring Boot backend URL | http://localhost:8082 |
- Open the Keycloak admin console at http://localhost:8080
- Click the dropdown next to "master" → Create realm
- Name:
demo
- Go to Realm roles → Create role
- Create two roles:
USERandADMIN
- Realm roles → Default roles
- Click Add roles
- Select
USERand add it
- Go to Realm settings → Login tab
- Enable:
- ✅ User registration
- ✅ Forgot password
- ✅ Verify email
- Save changes
- Realm settings → Email tab
- Fill in:
| Field | Value |
|---|---|
| From | admin@demo.local |
| From display name | Keycloak Demo |
| Host | mailpit |
| Port | 1025 |
| Enable SSL | ❌ |
| Enable StartTLS | ❌ |
| Authentication | Disabled |
- Click Test connection and check Mailpit UI at http://localhost:8025
- Go to Clients → Create client
- Settings:
- Client ID:
next-app - Client type: OpenID Connect
- Client authentication: ON
- Client ID:
- Capability config:
- Standard flow: ON
- Direct access grants: OFF
- Login settings:
- Valid redirect URIs:
http://localhost:3000/* - Valid post logout redirect URIs:
http://localhost:3000/* - Web origins:
http://localhost:3000
- Valid redirect URIs:
- Advanced settings:
- Proof Key for Code Exchange (PKCE): S256 Required
- Save
- Go to Credentials tab and copy the Client secret
This project includes a custom Keycloak login theme in the themes/my-theme/ folder.
- Custom colors and branding
- "Back to site" link on login page
- Responsive design
- Email verification and password reset pages
- Make sure Docker Compose is running (theme is auto-mounted)
- In Keycloak admin panel:
- Go to Realm settings → Themes
- Under Login theme, select
my-theme - Click Save
- Refresh the login page
The theme files are in themes/my-theme/login/:
theme.properties- Theme metadata and configurationresources/css/login.css- Custom stylesresources/img/logo.png- Your logo*.ftlfiles - Freemarker templates (HTML structure)
Since caching is disabled in Docker Compose, changes apply immediately after page refresh.
- User requests a protected route (e.g.,
/app) middleware.tschecks auth and redirects unauthenticated users to/auth/login/auth/loginimmediately callssignIn("keycloak", { callbackUrl })- After Keycloak login, NextAuth receives tokens in
[...nextauth]/route.ts jwtcallback storesaccess_token,refresh_token, andexpires_atin the NextAuth JWT- On subsequent requests, the
jwtcallback refreshes the access token when it's close to expiry
See src/middleware.ts:
- Protects
"/app/:path*"and"/api/:path*" - Always allows
"/api/auth/*"(NextAuth internal endpoints) - Unauthenticated users are redirected to
"/auth/login"
export const config = {
matcher: ["/app/:path*", "/api/:path*"],
};These routes read the server-side NextAuth JWT and forward requests to the Spring Boot backend using the Bearer token. The browser never sees the token.
GET /api/me→ forwards to${BACKEND_URL}/api/mePOST /api/update→ forwards JSON body to${BACKEND_URL}/api/updateGET /api/admin/only→ forwards to${BACKEND_URL}/api/admin/only(requires ADMIN role)POST /api/auth/kc-logout→ revokes Keycloak refresh token (call beforesignOut())
const token = await getToken({ req });
const response = await fetch(`${process.env.BACKEND_URL}/api/me`, {
headers: {
Authorization: `Bearer ${token.access_token}`
},
cache: "no-store",
});{
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
}- Run Docker Compose for Keycloak and Mailpit
- Configure environment variables in
.env.local - Run
npm run dev - Start the Spring Boot backend
When deploying to production:
- Use HTTPS for both frontend and Keycloak
- Update
NEXTAUTH_URLto your production domain - Update
AUTH_KEYCLOAK_ISSUERto your production Keycloak URL - Configure proper SMTP instead of Mailpit (e.g., SendGrid, AWS SES)
- Use a production-ready Keycloak setup with external database
- Set secure environment variables in your hosting platform
- Update Keycloak client redirect URIs to match production URLs
- Push your code to GitHub
- Import project to Vercel
- Add environment variables in Vercel dashboard
- Deploy
- Update Keycloak client settings with Vercel URLs
- Full tutorial: https://natasatm.netlify.app/articles/nextjs-keycloak-spring-boot-bff
- Backend companion (Spring Boot): https://github.com/NatasaTM/springboot-keycloak-bff
- Next.js documentation
- NextAuth.js (Auth.js) documentation
- Keycloak documentation
- Mailpit documentation
MIT © 2025 Natasa Todorov Markovic
Feel free to fork and adapt.