I've been assigned to overhaul an app that relies on Keycloak. Currently, when a user logs into the app, their session only lasts for 5 minutes before their access token expires. In the JavaScript code, each function checks if there's an updated access token in the response and if so then stores it. That means if the user is just browsing around the app without making a request, then they'll be logged out. I'm wanting to fix it so that if there's user activity of any sort (even just moving the mouse or browsing through pages), then the access token can be updated.
All of the other requests work fine, and the login request returns a refresh token in its response. However, when I make a POST request to get a new access token using the refresh token, then I get a 401 error. I've tried making this request inside the app and also using Postman and Insomnia, and I always get a 401 error.
Here's the request I'm making:
I've read that a client_secret is required when the client is confidential. However, 'admin-cli' is not confidential and doesn't have a client secret set up. Also, I've checked in the Keycloak Admin UI that refresh tokens are enabled.
Thanks for any help, it's much appreciated!
UPDATE: JWTs decoded
Here's the payloads of the access token and refresh token from BenchVue's answer:
ACCESS TOKEN
{
"exp": 1707230290,
"iat": 1707229990,
"jti": "41833ad2-d6a3-45d6-9ac3-fd4793c749a5",
"iss": "http://localhost:8080/realms/my-realm",
"sub": "f273bb9e-7a2a-49c3-b7f5-241efd4a4afd",
"typ": "Bearer",
"azp": "admin-cli",
"session_state": "114d3130-4502-4e63a448-9fc96e464879",
"acr": "1",
"scope": "profile email",
"sid": "114d3130-4502-4e63-a448-9fc96e464879",
"email_verified": true,
"preferred_username": "user1"
}
REFRESH TOKEN
{
"exp": 1707231790,
"iat": 1707229990,
"jti": "ecfc896f-93a8-4aa3-a2f9-c323d91c66ef",
"iss": "http://localhost:8080/realms/my-realm",
"aud": "http://localhost:8080/realms/my-realm",
"sub": "f273bb9e-7a2a-49c3-b7f5-241efd4a4afd",
"typ": "Refresh",
"azp": "admin-cli",
"session_state": "114d3130-4502-4e63-a448-9fc96e464879",
"scope": "profile email",
"sid": "114d3130-4502-4e63-a448-9fc96e464879"
}
And here's the access and refresh token from company app:
ACCESS TOKEN
{
"exp": 1707230135,
"iat": 1707229835,
"jti": "54942e59-4d25-4233-aefd-6c6c9a972c0a",
"iss": "http://localhost:8080/realms/[redacted]",
"sub": "8dd3a5a5-b467-4d65-9b2b-95da87d8bb36",
"typ": "Bearer",
"azp": "admin-cli",
"session_state": "558057da-dd11-43e6-ab10-aa35aa4a7235",
"acr": "1",
"scope": "email profile",
"sid": "558057da-dd11-43e6-ab10-aa35aa4a7235",
"email_verified": true,
"name": "[redacted]",
"preferred_username": "[redacted]",
"given_name": "[redacted]",
"family_name": "[redacted]",
"email": "[redacted]@test.com"
}
REFRESH TOKEN
{
"exp": 1707231635,
"iat": 1707229835,
"jti": "8dc3d5c1-636a-4645-b653-070a267da710",
"iss": "http://localhost:8080/realms/[redacted]",
"aud": "http://localhost:8080/realms/[redacted]",
"sub": "8dd3a5a5-b467-4d65-9b2b-95da87d8bb36",
"typ": "Refresh",
"azp": "admin-cli",
"session_state": "558057da-dd11-43e6-ab10-aa35aa4a7235",
"scope": "email profile",
"sid": "558057da-dd11-43e6-ab10-aa35aa4a7235"
}
Will this setting can get new tokens
In tests
tab
var jsonData = JSON.parse(responseBody);
postman.setEnvironmentVariable("access-token", jsonData.access_token);
postman.setEnvironmentVariable("refresh-token", jsonData.refresh_token);
Input Body with x-www-form-urlencoded
format
client_id: 'admin-cli'
grant_type: 'password'
username: {user name}
password: {user password}
Input Body with x-www-form-urlencoded
format
client_id: 'admin-cli
grant_type: 'password'
username: {user name}
password: {user password}
grant_type: 'refresh_token'
refresh_token: {{refresh-token}} <- Step 1's refresh-token
This URL and body data will get new tokens
URL
POST ${keycloakUrl}/realms/${realmName}/protocol/openid-connect/token
Input Body with x-www-form-urlencoded
format
client_id: {id}
client_secret: {secret}
grant_type: 'refresh_token'
scope: {scope}
refresh_token: {previous refresh_token}
I will demo the whole process from user logging to get the refresh token
by API in your local PC.
Save as docker-compose.yml
version: '3.7'
services:
postgres:
image: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: quay.io/keycloak/keycloak:latest # Update to the latest Keycloak image
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KC_HTTP_ENABLED: true # Enable HTTP if you're not using HTTPS
KC_HEALTH_ENABLED: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- 8080:8080
restart: always
depends_on:
- postgres
volumes:
postgres_data:
driver: local
docker compose up
It will launch Keycloak version 23.0.3
Step 1
Create 'my_realm'
Step 2
Create 'my_client'
Step 3
Add redirect URI 'http://localhost:3000/auth/callback'
Step 4
setting my_client
configuration
Step 5
copy Client Secret for demo(server.js)
Step 6
create user1 and set password by '1234'
Save as 'server.js'
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const crypto = require('crypto');
const session = require('express-session');
// TODO: Replace these with your actual configuration values
const clientId = 'my_client';
const clientSecret = 'MplDSOhQoiNwjjmA4w1YkBh5YteV8CJx';
const redirectUri = 'http://localhost:3000/auth/callback';
const realmName = 'my_realm';
const keycloakUrl = 'http://localhost:8080';
const responseType = 'code';
// Express setup
const app = express();
const port = 3000;
// Set up the session middleware
app.use(session({
secret: 'top-secret-key',
resave: false,
saveUninitialized: true,
}));
app.use(cors()); // Add CORS middleware
// Function to generate a code verifier for PKCE
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Exchange Authorization Code For Tokens
async function exchangeAuthorizationCodeForTokens(authorizationCode, clientId, redirectUri, realmName, keycloakUrl) {
const tokenEndpoint = `${keycloakUrl}/realms/${realmName}/protocol/openid-connect/token`;
const codeVerifier = generateCodeVerifier();
const data = {
grant_type: 'authorization_code',
client_id: clientId,
client_secret: 'MplDSOhQoiNwjjmA4w1YkBh5YteV8CJx',
redirect_uri: redirectUri,
code: authorizationCode,
code_verifier: codeVerifier
};
try {
const response = await axios.post(tokenEndpoint, new URLSearchParams(data), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }), // Ignore self-signed certificate
});
console.log('Token exchange successful');
console.log(response.data);
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
};
} catch (error) {
console.error('Token exchange failed:', error.response ? error.response.data : error.message);
return false;
}
}
// Get New Tokens by old Refresh Token
async function getRefreshToken(clientId, clientSecret, refresh_token, realmName, keycloakUrl) {
const tokenEndpoint = `${keycloakUrl}/realms/${realmName}/protocol/openid-connect/token`;
const codeVerifier = generateCodeVerifier();
const data = {
grant_type: 'refresh_token',
client_id: clientId,
client_secret: clientSecret,
scope: 'openid email',
refresh_token: refresh_token
};
try {
const response = await axios.post(tokenEndpoint, new URLSearchParams(data), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }), // Ignore self-signed certificate
});
console.log('Token refresh successful');
console.log(response.data);
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
};
} catch (error) {
console.error('Token exchange failed:', error.response ? error.response.data : error.message);
return false;
}
}
app.get('/login', (req, res) => {
// Construct the Keycloak login URL
const keycloakLoginUrl = `${keycloakUrl}/realms/${realmName}/protocol/openid-connect/auth?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=${encodeURIComponent(responseType)}&scope=openid`;
// Redirect the user to the Keycloak login page
res.redirect(keycloakLoginUrl);
});
app.get('/auth/callback', async (req, res) => {
const authorizationCode = req.query.code;
if (!authorizationCode) {
return res.status(400).send('Authorization code is required');
}
// Exchange authorization code for tokens
const tokens = await exchangeAuthorizationCodeForTokens(authorizationCode, clientId, redirectUri, realmName, keycloakUrl);
if (tokens) {
req.session.accessToken = tokens.access_token;
req.session.refresh_token = tokens.refresh_token;
res.send(JSON.stringify(`{'Access Token': ${tokens.access_token}, 'Refresh Token': ${tokens.refresh_token}}`, null, 4));
} else {
res.status(500).send('Failed to exchange authorization code for tokens');
}
});
app.get('/refresh_token', async (req, res) => {
const tokens = await getRefreshToken(clientId, clientSecret, req.session.refresh_token, realmName, keycloakUrl);
if (tokens) {
req.session.accessToken = tokens.access_token;
req.session.refresh_token = tokens.refresh_token;
res.send(JSON.stringify(`{'new Access Token': ${req.session.accessToken}, 'new Refresh Token': ${req.session.refresh_token}}`, null, 4));
} else {
res.status(500).send('Failed to exchange authorization code for tokens');
}
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
npm install express axios cripto cors express-session
node server.js
Open Browser
http://localhost:3000/login
After login, the will be displayed token in the Browser
http://localhost:3000/refresh_token
Copy the refresh_token from Browser to Postman
Then click the Send
button, the other setting is to follow the top explain.