Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the Golang version of the existing CPP Wasm plugin: OAuth #633

Open
johnlanni opened this issue Nov 13, 2023 · 7 comments · May be fixed by #663
Open

Implement the Golang version of the existing CPP Wasm plugin: OAuth #633

johnlanni opened this issue Nov 13, 2023 · 7 comments · May be fixed by #663

Comments

@johnlanni
Copy link
Collaborator

johnlanni commented Nov 13, 2023

No description provided.

@Uncle-Justice
Copy link
Contributor

你好,我想尝试做这个issue。

  1. 我对OAuth-cpp的理解是,它主要分为两个阶段

一阶段:申请token,请求可以是GET,把id直接放在header,或者POST,id等信息放在body,这个阶段不涉及路由问题,我一定是到一个特定的路径比如/token,才能得到token
二阶段:body携带token访问某些服务,这里涉及路由问题,我的request携带token可能会访问任意的路由下的服务,这里涉及到token在不同路由下的权限,可能会通过rule来做实际判断。

  1. 如果要转为go-wasm 的形式,我打算按照以下的计划来实现:

参考go-wasm 中已经完成的basic-authjwt-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的有效性

  1. 我预计提交的结果包括
  • 参照wasm-go/extentions/jwt-auth 路径下的内容,完成go相关源代码、wasm二进制文件以及文档
  • e2e测试代码,含测试用例及测试环境设计

@johnlanni
Copy link
Collaborator Author

@Uncle-Justice 手动点赞👍

@johnlanni johnlanni removed the help wanted Extra attention is needed label Nov 17, 2023
@Uncle-Justice
Copy link
Contributor

参照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做后续验证。

@johnlanni
Copy link
Collaborator Author

公私钥对格式是有要求的,不像现在secret是可以任意字符串。
保持功能一致性是用go改写的前提,不可以为了改写简单而改变功能。
jwt除了签名部分其他都是明文的,了解编码规则,解析是很简单的。

@Uncle-Justice
Copy link
Contributor

好的,我明白了

@Uncle-Justice
Copy link
Contributor

关于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是空的,从而触发测试失败。

我的疑问有

  1. 我试了一下,比如basic-auth的一些403的测试用例,cReq也是空的,因此按照oauth的token签发逻辑,在经过插件之后,也应该捕获到空的cReq?这个现象是正常的?
  2. 我就算自己在测试用例中手动设定ExpectedRequest 所有的属性为空似乎也不对,它应该不是这样用的
  3. 我也看到test/e2e/conformance/utils/http/http.go/CompareRequest() 中的if分支是关于200和ExpectedResponseNoRequest 的,但是在那个if分支下,除了检查cReq之外也做了ExpectedResponse 的检查,如果我的测试用例置ExpectedResponseNoRequest为true ,那如果我想设计一些ExpectedResponse 也会全部无效?

@Uncle-Justice Uncle-Justice linked a pull request Dec 3, 2023 that will close this issue
@Uncle-Justice
Copy link
Contributor

我个人认为正确的写法是,视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验证。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

3 participants