Keycloak API: Getting updated access token using refresh token returns 401

379 views Asked by At

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:

  • POST method
  • URL: http://localhost:8080/admin/realms/{realm-name}/protocol/openid-connect/token
  • Form:
    • client_id: admin-cli
    • grant_type: refresh_token
    • refresh_token: [refresh token here]
  • Headers:
    • Content-Type: when using Insomnia, it's "application/x-www-form-urlencoded"; in the app, it's "application/json"

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!

enter image description here

enter image description here

enter image description here

enter image description here

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"
}
1

There are 1 answers

15
Bench Vue On BEST ANSWER

Two cases

Case 1 - Client authentication OFF

enter image description here

Will this setting can get new tokens enter image description here

Step 1 get tokens

In tests tab

var jsonData = JSON.parse(responseBody);
postman.setEnvironmentVariable("access-token", jsonData.access_token);
postman.setEnvironmentVariable("refresh-token", jsonData.refresh_token);

enter image description here

Input Body with x-www-form-urlencoded format

client_id: 'admin-cli'
grant_type: 'password'
username: {user name}
password: {user password}

enter image description here

Step 2 get new tokens by refresh token

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

enter image description here


Case 2 - Client authentication ON

enter image description here

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}

enter image description here

I will demo the whole process from user logging to get the refresh token by API in your local PC.

Requirement for Demo

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

Run it

docker compose up

It will launch Keycloak version 23.0.3

enter image description here

Setting Keycloak

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'

enter image description here

Demo code

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}`);
});

Install server dependencies

npm install express axios cripto cors express-session

run server.js

node server.js

login user1

Open Browser

http://localhost:3000/login

enter image description here

After login, the will be displayed token in the Browser

enter image description here

Get new Tokens

http://localhost:3000/refresh_token

enter image description here

Get new Tokens by Postman

Copy the refresh_token from Browser to Postman Then click the Send button, the other setting is to follow the top explain.

enter image description here