higress
higress copied to clipboard
Implement the Golang version of the existing CPP Wasm plugin: OAuth
你好,我想尝试做这个issue。
- 我对OAuth-cpp的理解是,它主要分为两个阶段
一阶段:申请token,请求可以是GET,把id直接放在header,或者POST,id等信息放在body,这个阶段不涉及路由问题,我一定是到一个特定的路径比如/token,才能得到token 二阶段:body携带token访问某些服务,这里涉及路由问题,我的request携带token可能会访问任意的路由下的服务,这里涉及到token在不同路由下的权限,可能会通过rule来做实际判断。
- 如果要转为
go-wasm
的形式,我打算按照以下的计划来实现:
参考:go-wasm
中已经完成的basic-auth
和jwt-auth
数据结构:consumer,token响应模板,auth的参数配置&规则(包括consumer的id到token的映射)
接口:
main
: 注册使用的接口的上下文
parseGlobalConfig
: 从manifest中读取OAuth的全局参数配置
parseOverrideRuleConfig
:更新全局参数配置
onHttpRequestHeaders
: 如果类型为GET,则在header里取用户id,就直接生成token并直接返回一个Continue的header状态,如果为POST,就需要交由onHttpRequestBody。如果分析头部发现是二阶段,就根据rules做鉴定,调用ParseTokenValid
,涉及路由匹配和域名匹配,以及token过期或者token格式错误等问题,根据鉴定结果生成response
onHttpRequestBody
: 提取用户id并生成token,返回Continue的header状态
generateToken
: 涉及到对consumer和路由等设置的字段有效性校验,然后根据校验成功的consumer和路由生成jwt,这个过程打算使用https://github.com/golang-jwt/jwt
这个库来完成。
ParseTokenValid
:对应cpp版本中的checkPlugin
,对token进行解码,根据路由、域名规则,consumer的哈希映射,超时等条件判断tolen的有效性
- 我预计提交的结果包括
- 参照
wasm-go/extentions/jwt-auth
路径下的内容,完成go相关源代码、wasm二进制文件以及文档 - e2e测试代码,含测试用例及测试环境设计
@Uncle-Justice 手动点赞👍
参照cpp版本时,我发现它的Oauth的写法和我以为的oauth有一些不一样,我想向你确认一下
cpp版本生成一个jwt,使用的签名是client_secret,导致在jwt的验证环节时,它先直接将这个jwt解码,提取出client_id,然后再去全局config里找对应的client_secret,再用这个client_secret去验证jwt,cpp版本做jwt验证的部分代码plugins/wasm-cpp/extensions/oauth/plugin.cc
:
bool PluginRootContext::checkPlugin(){
// .......
// 直接把jwt未经验证解码成字符串
auto token = jwt::decode(token_str);
CLAIM_CHECK(token, client_id, jwt::json::type::string);
CLAIM_CHECK(token, iss, jwt::json::type::string);
CLAIM_CHECK(token, sub, jwt::json::type::string);
CLAIM_CHECK(token, aud, jwt::json::type::string);
CLAIM_CHECK(token, exp, jwt::json::type::integer);
CLAIM_CHECK(token, iat, jwt::json::type::integer);
auto client_id = token.get_payload_claim("client_id").as_string();
auto it = rule.consumers.find(client_id);
if (it == rule.consumers.end()) {
LOG_DEBUG(absl::StrFormat("client_id not found:%s", client_id));
goto failed;
}
// 拿着直接解码的结果去config查到了consumer的结果之后,又回过头去做jwt的验证
auto consumer = it->second;
auto verifier =
jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{consumer.client_secret})
.with_issuer(rule.issuer)
.with_subject(consumer.name)
.with_type(TypeHeader)
.leeway(rule.clock_skew);
std::error_code ec;
verifier.verify(token, ec);
并且从实现的角度而言,我使用的是github.com/golang-jwt/jwt/v5
这个库,它似乎也并不提供直接解码的接口,而都是密钥验证+解码一体。虽然jwt应该是可以直接解码的。
我个人认为正确的写法是,视client_secret为私钥,与client_id一起存进jwt中,使用一个服务器保留的固定公钥进行签发,验证时,使用固定公钥验证,再将jwt解码,对内部的client_id与client_secret做后续验证。
公私钥对格式是有要求的,不像现在secret是可以任意字符串。 保持功能一致性是用go改写的前提,不可以为了改写简单而改变功能。 jwt除了签名部分其他都是明文的,了解编码规则,解析是很简单的。
好的,我明白了
关于token的测试我有一些疑问。按照cpp版本的逻辑,生成token的时候直接发送一个httpresponse,状态码200,内部含token内容,然后ActionContinue,然后onHttpRequestBody
发现body为空就ActionPause
我在写测试代码的时候,比如我只简单的测试一个响应码为200的生成token的用例,但是在test/e2e/conformance/utils/http/http.go/CompareRequest()
中,它会因为状态码是200去做cReq的检查,然而因为token逻辑是发出token之后就不继续到backend了,导致cReq是空的,从而触发测试失败。
我的疑问有
- 我试了一下,比如basic-auth的一些403的测试用例,cReq也是空的,因此按照oauth的token签发逻辑,在经过插件之后,也应该捕获到空的cReq?这个现象是正常的?
- 我就算自己在测试用例中手动设定
ExpectedRequest
所有的属性为空似乎也不对,它应该不是这样用的 - 我也看到
test/e2e/conformance/utils/http/http.go/CompareRequest()
中的if分支是关于200和ExpectedResponseNoRequest
的,但是在那个if分支下,除了检查cReq之外也做了ExpectedResponse
的检查,如果我的测试用例置ExpectedResponseNoRequest
为true ,那如果我想设计一些ExpectedResponse
也会全部无效?
我个人认为正确的写法是,视client_secret为私钥,与client_id一起存进jwt中,使用一个服务器保留的固定公钥进行签发,验证时,使用固定公钥验证,再将jwt解码,对内部的client_id与client_secret做后续验证。
这种想法是错误的,当时我没有足够理解jwt。原因:jwt中只有signature是加密的,header和payload都只做了base64编码,因此将client_secret作为私钥存进payload,等验证后再与服务器内的consumer比较的这种做法根本不成立,client_secret必须加密传输。
所以cpp版本的写法才更符合jwt的设计逻辑,先解码payload,再做signature验证。