keycloakhttp-status-code-401

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


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!

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

Solution

  • 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