Search code examples
spring-bootspring-securitykeycloak

As in keycloak version 26.0.6. how to enable Allow Token Exchange (to update tokens)


Good time to all, As in keycloak version 26.0.6. how to enable Allow token exchange (for token renewal)

I tried to turn it on through the console but it didn't help

I would be very grateful if you would send a link to the documentation where it is written about it


Solution

  • Exchange API call by curl

    curl --location 'http://localhost:8080/realms/master/protocol/openid-connect/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode 'client_id=[your client id]' \
    --data-urlencode 'client_secret=[your client secret]' \
    --data-urlencode 'audience=[your audience client]' \
    --data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:refresh_token' \
    --data-urlencode 'subject_token=[your client token]'
    

    Requirement

    By kc.sh

    bin/kc.sh build --features="preview,token-exchange"
    

    OR By environment variable in docker-compose.yml

    KC_FEATURES: preview,token-exchange
    

    Detail

    docker-compose.yml

    version: '3.9'
    
    services:
      keycloak_web:
        image: quay.io/keycloak/keycloak:26.0.6
        container_name: keycloak_web
        environment:
          KC_DB: postgres
          KC_DB_URL: jdbc:postgresql://keycloak_db:5432/keycloak
          KC_DB_USERNAME: keycloak
          KC_DB_PASSWORD: password
    
          KC_HOSTNAME: localhost
          KC_HOSTNAME_PORT: 8080
          KC_HOSTNAME_STRICT: false
          KC_HOSTNAME_STRICT_HTTPS: false
    
          KC_LOG_LEVEL: info
          KC_METRICS_ENABLED: true
          KC_HEALTH_ENABLED: true
          KEYCLOAK_ADMIN: admin
          KEYCLOAK_ADMIN_PASSWORD: admin
          KC_FEATURES: preview,token-exchange
        command: 
          - start-dev
        depends_on:
          - keycloak_db
        ports:
          - 8080:8080
    
      keycloak_db:
        image: postgres:15
        container_name: keycloak_db
        volumes:
          - postgres_data:/var/lib/postgresql/data
        environment:
          POSTGRES_DB: keycloak
          POSTGRES_USER: keycloak
          POSTGRES_PASSWORD: password
    
    volumes:
      postgres_data:
    

    How to confirm features set token-exchange

    docker exec -it keycloak_web ./opt/keycloak/bin/kc.sh show-config
    

    Should be this shape

    kc.features =  preview,token-exchange
    
    Current Mode: development
    Current Configuration:
            kc.cache =  local (classpath keycloak.conf)
            kc.config.args =  show-config (SysPropConfigSource)
            kc.config.built =  true (SysPropConfigSource)
            kc.db =  postgres (ENV)
            kc.db-password =  ******* (ENV)
            kc.db-url =  jdbc:postgresql://keycloak_db:5432/keycloak (ENV)
            kc.db-username =  keycloak (ENV)
            kc.features =  preview,token-exchange (ENV)
            kc.health-enabled =  true (ENV)
            kc.hostname =  localhost (ENV)
            kc.hostname-port =  8080 (ENV)
            kc.hostname-strict =  false (ENV)
            kc.hostname-strict-https =  false (ENV)
            kc.http-enabled =  true (classpath keycloak.conf)
            kc.log-console-output =  default (classpath keycloak.conf)
            kc.log-level =  info (ENV)
            kc.metrics-enabled =  true (ENV)
            kc.run-in-container =  true (ENV)
            kc.spi-hostname-v2-hostname =  localhost (ENV)
            kc.spi-hostname-v2-hostname-strict =  false (ENV)
            kc.spi-theme-cache-templates =  false (classpath keycloak.conf)
            kc.spi-theme-cache-themes =  false (classpath keycloak.conf)
            kc.spi-theme-static-max-age =  -1 (classpath keycloak.conf)
            kc.version =  26.0.6 (SysPropConfigSource)
    

    Can see the permission tab

    When you create client

    enter image description here

    Create Client & Audience Client

    Example

    Client : `my-client`
    Audience: `target-client`
    

    Copy my-client's secret

    enter image description here

    Create Exchange Token Permission for my-client

    enter image description here

    enter image description here

    Get Exchange Token by curl

    Using bash terminal with jq If not have jq, download in here

    Replace my-client client secret in client_secret

    Get client token first

    CLIENT_TOKEN=$(curl --location 'http://localhost:8080/realms/master/protocol/openid-connect/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode 'client_id=my-client' \
    --data-urlencode 'client_secret=GpiPmEsiqvZbF0YV55Q7KxTrmN1vk7tI' | jq -r '.access_token')
    
    echo 'CLIENT_TOKEN = '$CLIENT_TOKEN
    

    enter image description here

    Exchange token with client token

    EXCHANGE_TOKEN=$(curl --location 'http://localhost:8080/realms/master/protocol/openid-connect/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode 'client_id=my-client' \
    --data-urlencode 'client_secret=GpiPmEsiqvZbF0YV55Q7KxTrmN1vk7tI' \
    --data-urlencode 'audience=target-client' \
    --data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:refresh_token' \
    --data-urlencode 'subject_token='$CLIENT_TOKEN | jq -r '.access_token')
    
    echo 'EXCHANGE_TOKEN = '$EXCHANGE_TOKEN
    

    enter image description here

    Exchange token by Postman

    enter image description here

    var jsonData = JSON.parse(responseBody);
    postman.setEnvironmentVariable("client_token", jsonData.access_token);
    
    grant_type : client_credentials
    client_id: my-client
    client_secret: [my-client's secret]
    

    enter image description here

    grant_type : client_credentials
    client_id: my-client
    client_secret: [my-client's secret]
    audience: [your audience client]
    requested_token_type: urn:ietf:params:oauth:token-type:refresh_token
    subject_token : [your client token]
    

    enter image description here

    Get Exchange Token by node.js

    demo.js

    // demo.js
    const axios = require('axios');
    const jwt = require('jsonwebtoken'); // Import jsonwebtoken
    
    // Utility function to delay execution
    const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    const decodeToken = (token) => {
        try {
            // Decode the token without verifying it
            const decoded = jwt.decode(token, { complete: true });
            return decoded;
        } catch (error) {
            console.error('Failed to decode token:', error.message);
            return null;
        }
    };
    
    
    const getClientToken = async (clientId, clientSecret) => {
        try {
            const resp = await axios.post(
                'http://localhost:8080/realms/master/protocol/openid-connect/token',
                new URLSearchParams({
                    'client_id': clientId,
                    'client_secret': clientSecret,
                    'grant_type': 'client_credentials'
                })
            );
            return resp.data.access_token;
        } catch (err) {
            console.error(err);
        }
    };
    
    const getExchangeToken = async (clientId, clientSecret, audience, subjectToken) => {
        try {
            const resp = await axios.post(
                'http://localhost:8080/realms/master/protocol/openid-connect/token',
                new URLSearchParams({
                    'grant_type': 'client_credentials',
                    'client_id': clientId,
                    'client_secret': clientSecret,
                    'audience': audience,
                    'requested_token_type': 'urn:ietf:params:oauth:token-type:refresh_token',
                    'subject_token': subjectToken
                }),
                {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    }
                }
            );
            return resp.data.access_token;
        } catch (err) {
            console.error('Error fetching exchange token:', err.response?.data || err.message);
        }
    };
    
    
    
    (async () => {
    
        const clientId = 'my-client';
        const clientSecret = 'replace your client secret';
        const audienceClientId = 'target-client';
    
        const client_token = await getClientToken(clientId, clientSecret);
    
    
        if (client_token) {
            const decodedToken = await decodeToken(client_token);
            if (decodedToken) {
                console.log('Decoded Client Token:', JSON.stringify(decodedToken, null, 2));
            }
    
            console.log('\nWaiting for 2 seconds before exchanging token...');
            await delay(2000); // Wait for 2 seconds
            const exchangeToken = await getExchangeToken(clientId, clientSecret, audienceClientId, client_token);
            if (exchangeToken) {
                const decodedExchangeToken = decodeToken(exchangeToken);
                if (decodedExchangeToken) {
                    console.log('Decoded Exchange Token:', JSON.stringify(decodedExchangeToken, null, 2));
                }
            }
        }
    
    })();
    

    Install dependencies

    npm install axios jsonwebtoken
    

    Result

    enter image description here

    Exchange Token documentation

    In here search by exchange keyword