Keycloak API:使用刷新令牌获取更新的访问令牌返回 401

问题描述 投票:0回答:1

大家。我最近开始在一家公司实习,并被指派检修他们的一个使用 Keycloak 的 Web 应用程序。我之前没有任何使用 Keycloak 的经验。目前,当用户登录应用程序时,其会话仅持续 5 分钟,然后访问令牌就会过期。在 JavaScript 代码中,每个函数都会检查响应中是否有更新的访问令牌,如果有则存储它。这意味着如果用户只是浏览应用程序而不发出请求,那么他们将被注销。当然,这不是一个好的实现,我想修复它,以便如果存在任何类型的用户活动(即使只是移动鼠标或浏览页面),那么访问令牌都会更新。

所有其他请求都工作正常,并且登录请求在其响应中返回刷新令牌。但是,当我发出 POST 请求以使用刷新令牌获取新的访问令牌时,我收到 401 错误。我尝试在应用程序内使用 Postman 和 Insomnia 发出此请求,但总是收到 401 错误。

这是我提出的要求:

  • POST方法
  • URL:http://localhost:8080/admin/realms/{realm-name}/protocol/openid-connect/token
  • 表格:
    • client_id:admin-cli
    • grant_type:refresh_token
    • refresh_token:[此处刷新令牌]
  • 标题:
    • Content-Type:使用Insomnia时为“application/x-www-form-urlencoded”;在应用程序中,它是“application/json”

我读到,当客户端保密时需要 client_secret。但是,“admin-cli”不是机密的,也没有设置客户端机密。另外,我已经在 Keycloak 管理 UI 中检查了刷新令牌已启用。

我已经向 ChatGPT 询问过这个问题,但它的答案没有帮助。感谢您的帮助,非常感谢!

keycloak http-status-code-401
1个回答
0
投票

此 URL 和正文数据将获得新的令牌

网址

POST ${keycloakUrl}/realms/${realmName}/protocol/openid-connect/token

x-www-form-urlencoded
格式输入正文

client_id: {id}
client_secret: {secret}
grant_type: 'refresh_token'
scope: {scope}
refresh_token: {previous refresh_token}

我将演示从用户登录到通过本地 PC 中的 API 获取

refresh token
的整个过程。

演示要求

另存为

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

即将推出Keycloak 23.0.3版本

设置钥匙斗篷

Step 1
创建“my_realm”

Step 2
创建“my_client”

Step 3
添加重定向 URI 'http://localhost:3000/auth/callback'

Step 4
设置
my_client
配置

Step 5
复制客户端密钥用于演示(server.js)

Step 6
创建用户1并设置密码“1234”

演示代码

另存为“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}`);
});

运行server.js

node server.js

登录用户1

打开浏览器

http://localhost:3000/login

登录后,浏览器中会显示token

获取新代币

http://localhost:3000/refresh_token

通过 Postman 获取新代币

将refresh_token从浏览器复制到Postman 然后点击

Send
按钮,其他设置按照上面的说明。

© www.soinside.com 2019 - 2024. All rights reserved.