Keycloak:访问令牌验证端点

27

在独立模式下运行Keycloak,并使用Node.js适配器创建微服务,用于认证API调用。

来自Keycloak的JWT令牌将随每个API调用一起发送。仅当发送的令牌是有效的时,它才会响应。

  • 我该如何验证微服务中的访问令牌?
  • Keycloak是否提供任何令牌验证?
6个回答

36

进一步解释troger19的回答:

问题1: 如何验证来自微服务的访问令牌?

实现一个函数来检查每个请求是否有承载令牌,并将该令牌发送到您的keycloak服务器的userinfo端点进行验证,然后再将其传递给您的API路由处理程序。

您可以通过请求well-known configuration来查找您的keycloak服务器的特定端点(如userinfo路由)。

如果您在node api中使用expressjs,则可能如下所示:

const express = require("express");
const request = require("request");

const app = express();

/*
 * additional express app config
 * app.use(bodyParser.json());
 * app.use(bodyParser.urlencoded({ extended: false }));
 */

const keycloakHost = 'your keycloak host';
const keycloakPort = 'your keycloak port';
const realmName = 'your keycloak realm';

// check each request for a valid bearer token
app.use((req, res, next) => {
  // assumes bearer token is passed as an authorization header
  if (req.headers.authorization) {
    // configure the request to your keycloak server
    const options = {
      method: 'GET',
      url: `https://${keycloakHost}:${keycloakPort}/auth/realms/${realmName}/protocol/openid-connect/userinfo`,
      headers: {
        // add the token you received to the userinfo request, sent to keycloak
        Authorization: req.headers.authorization,
      },
    };

    // send a request to the userinfo endpoint on keycloak
    request(options, (error, response, body) => {
      if (error) throw new Error(error);

      // if the request status isn't "OK", the token is invalid
      if (response.statusCode !== 200) {
        res.status(401).json({
          error: `unauthorized`,
        });
      }
      // the token is valid pass request onto your next function
      else {
        next();
      }
    });
  } else {
    // there is no token, don't process request further
    res.status(401).json({
    error: `unauthorized`,
  });
});

// configure your other routes
app.use('/some-route', (req, res) => {
  /*
  * api route logic
  */
});


// catch 404 and forward to error handler
app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});

问题2:Keycloak提供任何令牌验证吗?
向Keycloak的用户信息端点发出请求是验证您的令牌是否有效的简单方法。
来自有效令牌的用户信息响应:
状态:200 OK
{
    "sub": "xxx-xxx-xxx-xxx-xxx",
    "name": "John Smith",
    "preferred_username": "jsmith",
    "given_name": "John",
    "family_name": "Smith",
    "email": "john.smith@example.com"
}

来自无效的有效令牌的用户信息响应:

状态:401 未经授权

{
    "error": "invalid_token",
    "error_description": "Token invalid: Token is not active"
}

附加信息:

Keycloak提供了自己的npm包keycloak-connect。文档描述了路由上的简单身份验证,需要用户登录才能访问资源:

app.get( '/complain', keycloak.protect(), complaintHandler );

我发现使用仅限bearer的身份验证方法无法正常工作。根据我的经验,在路由上实现这种简单的身份验证方法会导致“拒绝访问”的响应。这个问题还询问了如何使用Keycloak访问令牌对rest api进行身份验证。被接受的答案也建议使用keycloak-connect提供的简单身份验证方法,但正如Alex在评论中所述:

“keyloak.protect()函数(不)从标题中获取承载者令牌。我仍在寻找此解决方案以进行仅限承载者的身份验证-alex Nov 2 '17 at 14:02


在每个请求之前向Keycloak服务器发出请求,这样做不会降低响应速度,从而影响应用的性能吗? - vishal munde
你所指的“well-known-configuration”是什么? - Linkx_lair
@Linkx_lair https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig,例如Keycloak可能是https://URL/auth/realms/REALM/.well-known/openid-configuration。 - DavidS
在我看来,Markus的回答中描述的离线令牌验证是一种更好的实践。我使用过的每个安全库(Node、Java、C#、Drupal、Moodle)都支持它。调用userinfointrospection端点会创建不必要的流量,JWT身份验证的主要设计目标之一是减轻服务器端会话管理的扩展问题。我曾经见过真实的集群Keycloak(RedHat SSO)系统受到在线令牌验证误用的影响。 - DavidS
1
你好,我已经尝试了你的答案。如果我没有在头部提供access_token,它会抛出未授权的响应,但没有错误消息。如果我在头部授权中提供有效的令牌,它会抛出禁止的错误,但没有错误消息。你能告诉我为什么即使令牌是有效的,它也会抛出禁止吗? - Bennison J
显示剩余3条评论

22

两种方法来验证令牌:

  • 在线验证
  • 离线验证

上面描述的是在线验证方式。这种方法成本相当高,因为每个验证都需要进行另一次http/往返。

离线验证更加高效:JWT令牌是一个base64编码的JSON对象,其已经包含了所有要在离线验证中使用的信息(声明)。您只需要公钥并验证签名(以确保内容是“有效的”):

有几个库(例如keycloak-backend)可以在没有任何远程请求的情况下对令牌进行离线验证。离线验证就是这么简单:

token = await keycloak.jwt.verifyOffline(someAccessToken, cert);
console.log(token); //prints the complete contents, with all the user/token/claim information...

为什么不使用官方的keycloak-connect Node.js库(而是使用keycloak-backend)?官方库更专注于将Express框架作为中间件,(据我所见)没有直接公开任何验证函数。或者您可以使用任意JWT / OICD库,因为验证是标准化的过程。


9
是的,在线验证成本很高,但如果您仅使用离线验证,我们如何知道令牌是否已通过注销无效化? - Ala Abid
3
你好alabid,你说得非常正确。这是一个需要做出决定和权衡的问题。JWTs应该尽可能短暂。另一种选择是将某种类型的“注销事件”推送到内存失效存储中:因此您可以检查每个令牌,但不需要连接远程服务,而只需连接包含推送失效信息的进程/系统内部缓存。但我不知道任何实现这种功能的库。 - Markus
2
我正在使用keycloak-connect库来构建我的node-rest-api,但是当我从react-app注销或在keycloak管理控制台中关闭所有会话之前,令牌过期,我仍然可以使用先前在登录时生成的令牌调用rest api后端(例如,使用postman)。keycloak库中是否有一些方法可以验证令牌? - Hector
1
@ala 典型的 JWT 生命周期约为3-5分钟。我必须声明,大多数系统并不是那么关键,以至于他们需要少于3分钟的确定性来确认用户没有在其他地方退出登录。 - DavidS
1
如果用户在Keycloak中不是管理员,则以下API会抛出403错误: https://${keycloakHost}:${keycloakPort}/auth/realms/${realmName}/protocol/openid-connect/userinfo - Bennison J
显示剩余2条评论

8

2

您必须使用introspect令牌服务

以下是使用CURL和Postman的示例

curl --location --request POST 'https://HOST_KEYCLOAK/realms/master/protocol/openid-connect/token/introspect' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=oficina-virtual' \
--data-urlencode 'client_secret=4ZeE2v' \
--data-urlencode 

'token=eyJhbGciKq4n_8Bn2vvy2WY848toOFxEyWuKiHrGHuJxgoU2DPGr9Mmaxkqq5Kg'

或者使用Postman

enter image description here

注意:使用变量expiatactive来验证访问令牌。


1
为了解决这个问题,Keycloak 实现了 JWKS
以下是一个使用此功能的 TypeScript 代码片段示例。

import jwt, { JwtPayload, VerifyCallback } from 'jsonwebtoken';
import AccessTokenDecoded from './AccessTokenDecoded';
import jwksClient = require('jwks-rsa');

function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback): void {
  const jwksUri = process.env.AUTH_SERVICE_JWKS || '';
  const client = jwksClient({ jwksUri, timeout: 30000 });

  client
    .getSigningKey(header.kid)
    .then((key) => callback(null, key.getPublicKey()))
    .catch(callback);
}

export function verify(token: string): Promise<AccessTokenDecoded> {
  return new Promise(
    (
      resolve: (decoded: AccessTokenDecoded) => void,
      reject: (error: Error) => void
    ) => {
      const verifyCallback: VerifyCallback<JwtPayload | string> = (
        error: jwt.VerifyErrors | null,
        decoded: any
      ): void => {
        if (error) {
          return reject(error);
        }
        return resolve(decoded);
      };

      jwt.verify(token, getKey, verifyCallback);
    }
  );
}

"import AccessTokenDecoded from './AccessTokenDecoded';"只是解码令牌的接口。
"AUTH_SERVICE_JWKS=http://localhost:8080/realms/main/protocol/openid-connect/certs"是一个环境变量,其中包含由Keycloak提供的证书的URI。"

1

@kfrisbie感谢您的回复,通过您的示例,我可以使用keycloak连接适配器重构您的代码:

// app.js
app.use(keycloakConfig.validateTokenKeycloak); // valid token with keycloak server

// add routes
const MyProtectedRoute = require('./routes/protected-routes'); // routes using keycloak.protect('some-role')
app.use('/protected', MyProtectedRoute);

当授权头被发送时,我可以验证令牌是否仍然有效,并针对keycloak服务器进行验证,因此,在管理员控制台或前端spa在令牌到期之前注销的情况下,我的rest api会抛出401错误,在其他情况下,keycloak保护方法将被使用。
// keycloak.config.js
let memoryStore = new session.MemoryStore();
let _keycloak = new Keycloak({ store: memoryStore });

async function validateTokenKeycloak(req, res, next) {
    if (req.kauth && req.kauth.grant) {        
        console.log('--- Verify token ---');
        try {
            var result = await _keycloak.grantManager.userInfo(req.kauth.grant.access_token);
            //var result = await _keycloak.grantManager.validateAccessToken(req.kauth.grant.access_token);
            if(!result) {
                console.log(`result:`,  result); 
                throw Error('Invalid Token');
            }                        
        } catch (error) {
            console.log(`Error: ${error.message}`);
            return next(createError.Unauthorized());
        }
    }
    next();  
}

module.exports = {
    validateTokenKeycloak
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接