在独立模式下运行Keycloak,并使用Node.js适配器创建微服务,用于认证API调用。
来自Keycloak的JWT令牌将随每个API调用一起发送。仅当发送的令牌是有效的时,它才会响应。
- 我该如何验证微服务中的访问令牌?
- Keycloak是否提供任何令牌验证?
进一步解释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);
});
{
"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 );
“keyloak.protect()函数(不)从标题中获取承载者令牌。我仍在寻找此解决方案以进行仅限承载者的身份验证-alex Nov 2 '17 at 14:02
有两种方法来验证令牌:
上面描述的是在线验证方式。这种方法成本相当高,因为每个验证都需要进行另一次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库,因为验证是标准化的过程。
http://localhost:8081/auth/realms/demo/protocol/openid-connect/userinfo
您必须使用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
注意:使用变量exp、iat和active来验证访问令牌。
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。"@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.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
};
userinfo
或introspection
端点会创建不必要的流量,JWT身份验证的主要设计目标之一是减轻服务器端会话管理的扩展问题。我曾经见过真实的集群Keycloak(RedHat SSO)系统受到在线令牌验证误用的影响。 - DavidS