I'm using Keycloak 21 for authentication, and I’m having an issue where the nonce value is not included in the id_token returned after i invoke /token. I’m passing the nonce in the /auth request,
You can get by this method
URL
GET ${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/auth?
query parameters
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid',
nonce: nonce,
Then get ID token at callback URL
version: '3.9'
services:
keycloak:
image: quay.io/keycloak/keycloak:21.1.2
container_name: keycloak
command:
- start-dev
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_DB: postgres
KC_DB_URL_HOST: keycloak-db
KC_DB_URL_DATABASE: keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloakpassword
KC_HOSTNAME: localhost
ports:
- "8080:8080"
depends_on:
- keycloak-db
volumes:
- keycloak-data:/opt/keycloak/data
keycloak-db:
image: postgres:15
container_name: keycloak-db
environment:
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloakpassword
POSTGRES_DB: keycloak
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
keycloak-data:
postgres-data:
more detail in here
Call back URL
http://localhost:3000/callback
Client Secret copy from UI
username
is User1 and password
is 1234
install dependencies
npm install express axios querystring jwt-decode@3.1.2 crypto
demo.js
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const querystring = require('querystring');
const jwtDecode = require('jwt-decode');
const app = express();
const PORT = 3000; //replace your Redirect PORT
// Keycloak Configuration
const KEYCLOAK_URL = 'http://localhost:8080'; // Replace with your Keycloak server URL
const REALM = 'my-realm'; // Replace with your realm name
const CLIENT_ID = 'my-client'; // Replace with your client ID
const CLIENT_SECRET = '[your my-client password]'; // Replace if your client is confidential
const REDIRECT_URI = 'http://localhost:3000/callback'; // Replace with your app's redirect URI
// Generate a random nonce
const generateNonce = () => crypto.randomBytes(16).toString('base64');
// Store the nonce temporarily (use a proper session store in production)
let storedNonce;
// Routes
// 1. Start Authorization
app.get('/login', (req, res) => {
const nonce = generateNonce();
storedNonce = nonce; // Save the nonce for validation later
const authUrl = `${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/auth?` + querystring.stringify({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid',
nonce: nonce,
});
res.redirect(authUrl); // Redirect user to Keycloak login
});
// Step 2. Handle Callback for get tokens
app.get('/callback', async (req, res) => {
const { code } = req.query;
if (!code) {
return res.status(400).send('Authorization code not found!');
}
try {
const tokenResponse = await axios.post(
`${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token`,
querystring.stringify({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
const { id_token } = tokenResponse.data;
// Decode and validate the ID token
const decodedToken = jwtDecode(id_token);
if (decodedToken.nonce !== storedNonce) {
return res.status(401).send('Invalid nonce!');
}
res.send({
message: 'Login successful!',
id_token: decodedToken,
});
} catch (error) {
console.error('Error exchanging code for tokens:', error);
res.status(500).send('Error exchanging code for tokens.');
}
});
// Start the Express server
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
run it
node demo.js
http://localhost:3000/login