Open gfrankliu opened 1 year ago
$
is a special char in yaml so you may need to quote or escape it in your yaml.
As long as the header_name
is not authorization
I think you can set the scheme
attribute to empty string ""
and it should work (https://github.com/travisghansen/external-auth-server/blob/master/PLUGINS.md?plain=1#L157)
I tried quoting the string: query: "$.req.headers.my-jwt-header"
but got the same error.
Is below ok to check if the header doesn't exist?
- type: jwt
header_name: my-jwt-header
scheme: ""
pcb:
skip:
- query_engine: jp
query: "$.req.headers.my-jwt-header"
rule:
method: regex
value: /./
negate: true
Actually, I just tested it and you've found a bug in the library (https://github.com/dchester/jsonpath) the problem has nothing to do with the $
it's the -
in the query. Based on my research this should work $.req.headers."my-jwt-header"
but does not. Further it appears that library has fallen into bitrot so it seems unlikely to get fixed anytime soon :(
As an alternative use another query engine. For example if you allow eval you can use the js
engine:
query_engine: js
query: "try { return data.req.headers[\"my-jwt-header\"]; } catch (e) { return undefined; }"
Glad you were able to reproduce and thanks for the alternative way. I tried it and got:
{"level":"error","message":"cannot use potentially unsafe query_engine 'js' unless env variable 'EAS_ALLOW_EVAL' is set","service":"external-auth-server","stack":"Error: cannot use potentially unsafe query_engine 'js' unless env variable 'EAS_ALLOW_EVAL' is set\n at Object.json_query (/home/eas/app/src/utils.js:425:15)\n at Assertion.query (/home/eas/app/src/assertion/index.js:63:29)\n at Assertion.assert (/home/eas/app/src/assertion/index.js:78:28)\n at Function.assertSet (/home/eas/app/src/assertion/index.js:18:36)\n at processPipeline (/home/eas/app/src/server.js:392:42)\n at /home/eas/app/src/server.js:483:7\n at new Promise (<anonymous>)\n at _verifyHandler (/home/eas/app/src/server.js:313:12)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async verifyHandler (/home/eas/app/src/server.js:97:12)","timestamp":"2023-07-29T04:25:28.859Z"}
Then I set env EAS_ALLOW_EVAL to 1, but got another error when sending a request without jwt header:
{"level":"error","message":"Cannot read properties of undefined (reading 'case_insensitive')","service":"external-auth-server","stack":"TypeError: Cannot read properties of undefined (reading 'case_insensitive')\n at Assertion.assert (/home/eas/app/src/assertion/index.js:83:14)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async Function.assertSet (/home/eas/app/src/assertion/index.js:18:20)\n at async processPipeline (/home/eas/app/src/server.js:392:26)","timestamp":"2023-07-29T04:27:45.564Z"}
Finally sending a request with real jwt header:
{"level":"error","message":"Cannot read properties of undefined (reading 'case_insensitive')","service":"external-auth-server","stack":"TypeError: Cannot read properties of undefined (reading 'case_insensitive')\n at Assertion.assert (/home/eas/app/src/assertion/index.js:83:14)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async Function.assertSet (/home/eas/app/src/assertion/index.js:18:20)\n at async processPipeline (/home/eas/app/src/server.js:392:26)","timestamp":"2023-07-29T04:39:11.117Z"}
(seems same error as if I were to not send jwt header at all.
Did you remove the rule
block?
Yes I did:
- type: jwt
header_name: x-goog-iap-jwt-assertion
scheme: ""
config:
secret: https://www.gstatic.com/iap/verify/public_key-jwk
options:
audience: /projects/1111111111/global/backendServices/222222
issuer: https://cloud.google.com/iap
pcb:
skip:
- query_engine: js
query: "try { return data.req.headers[\"x-goog-iap-jwt-assertion\"]; } catch (e) { return undefined; }"
BTW, the bug you found in jsonpath doesn't seem to exist in jsonata, so if I use query_engine: jsonata
, I am able to use $.req.headers."my-jwt-header"
like you suggested. Now the question is how I can have a pcb rule like:
rule:
method: eq
value: undefined
so I can skip if the header doesn't exist. I tried but the engine seems to treat "undefined" as literal string match :(
You still need the rule with the js engine as well FYI. I think the regex syntax you had may work if you simply to /.*/
but I would need to test to be sure..
Tried but didn't work.
I’ll look at this later in the day and get you a working example.
I think this will work with the existing codebase:
- query_engine: js
query: "try { return data.req.headers[\"my-jwt-header\"]; } catch (e) { return ''; }"
rule:
method: regex
value: /\w/i
negate: true
Give it a try and let me know. I have some minor fixes to commit to make this work better due to javascript oddities but the above should do what you want.
It doesn't seem to work. When I send a request without jwt header, I expect to see "skipping plugin due to pcb", something like:
{"level":"info","message":"starting verify for plugin: jwt","service":"external-auth-server","timestamp":"2023-08-02T05:11:39.347Z"}
{"level":"info","message":"skipping plugin due to pcb assertions: jwt","service":"external-auth-server","timestamp":"2023-08-02T05:11:39.348Z"}
but I saw below instead:
{"level":"info","message":"starting verify for plugin: jwt","service":"external-auth-server","timestamp":"2023-08-02T05:26:56.251Z"}
{"level":"warn","message":"failed assertion: {\"query_engine\":\"js\",\"query\":\"try { return data.req.headers[\\\"my-jwt-header\\\"]; } catch (e) { return ''; }\",\"rule\":{\"method\":\"regex\",\"value\":\"/\\\\w/i\",\"negate\":true}} against value: undefined","service":"external-auth-server","timestamp":"2023-08-02T05:26:56.251Z"}
Maybe I don't need this pcb rule. I assume the default jwt plugin will already do the same: skip to next plugin if jwt header doesn't exist.
Yes it will. I typically would use the skip also with a stop (this helps ensure the response to applicable clients is more focused and relevant to the client). For example you don’t really want to redirect a machine to an oauth endpoint.
I think I know why you’re getting that, let me try 1 more variation and send it your way.
Yeah, update the query
:
query: "try { return data.req.headers[\"my-jwt-header\"] || ''; } catch (e) { return ''; }"
Yes it will. I typically would use the skip also with a stop (this helps ensure the response to applicable clients is more focused and relevant to the client). For example you don’t really want to redirect a machine to an oauth endpoint.
Can you clarify the skip/stop? My understanding is: "skip" will go to next plugin without running the current plugin, but "stop" will NOT stop running the current plugin, it will still run the current plugin, just stop AFTER it finishes and won't go to next plugin.
In my test case, since "skip" is implicit behavior of jwt plugin (it will directly go to next plugin if the jwt header doesn't exist), I only need a "stop" pcb where jwt header exists?
"try { return data.req.headers[\"my-jwt-header\"] || ''; } catch (e) { return ''; }"
Yes, this works! Thanks!
A stop
without the skip
will result in the 2nd plugin never getting executed. Basically you want to skip
if the header is not present at all and stop
if the header is present regardless of success/failure. stop
will end the pipeline even if the result is a failure.
I tried below pcb "stop", without "skip":
pcb:
stop:
- query_engine: js
query: "try { return data.req.headers[\"my-jwt-header\"] || ''; } catch (e) { return ''; }"
rule:
method: regex
value: /\w/i
Now if I send a request without the jwt header, it actually goes to the next plugin:
{"level":"info","message":"starting verify for plugin: jwt","service":"external-auth-server","timestamp":"2023-08-02T16:58:50.394Z"}
{"level":"warn","message":"failed assertion: {\"query_engine\":\"js\",\"query\":\"try { return data.req.headers[\\\"my-jwt-header\\\"] || ''; } catch (e) { return ''; }\",\"rule\":{\"method\":\"regex\",\"value\":\"/\\\\w/i\"}} against value: \"\"","service":"external-auth-server","timestamp":"2023-08-02T16:58:50.395Z"}
{"level":"info","message":"starting verify for plugin: oidc","service":"external-auth-server","timestamp":"2023-08-02T16:58:50.395Z"}
If I send the request with jwt header, it will stop at jwt plugin, whether the header is valid or not. In case an invalid jwt header, the plugin returns 401 and stops there. The problem is ingress-nginx will trigger the oauth redirect anyway when it gets 401 from "external-auth-server". I am using this nginx ingress config example. Client side gets 302 location: https://eas.example.com/auth/nginx/auth-signin?rd=
from nginx where "rd=" is empty.
Oh right, that should work. The performance gains of using skip
in this scenario are likely minimal.
Regarding the empty rd
value maybe nginx snippet syntax has changed? I actually don't use nginx much so not entirely sure on that one :(
The empty rd in this case is expected because we put a stop at the jwt plugin and rd only makes sense for oauth/oidc plugins. jwt plugin returns 401 but without a redirect URL to nginx (expected since jwt doesn't have a redirect URL).
What I tried to say in last post was the "stop" didn't prevent a redirect in this case :(
nginx
doesn't allow for a redirect per-se in how it handles forward auth. So I have to respond with a 401 and then the extra config kicks in. It's rather stupid how nginx
handles the pattern instead of just passing the non-2xx responses back to the client directly :(
Wondering if there is anything we can learn from how oauth2-proxy did it with nginx as documented here, with two annotations:
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://$host/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://$host/oauth2/start?rd=$escaped_request_uri"
They didn't do any nginx.ingress.kubernetes.io/configuration-snippet:
Give it a try and see what you get.
I can't try it until external-auth-server has the proper signin endpoint like oauth2-proxy does. Since external-auth-server supports multiple profiles, we will need additional query params to the signin endpoint, unlike oauth2-proxy where only ?rd=$escaped_request_uri
is needed.
Well I do have an endpoint. Are you using ingress nginx or nginx ingress? Is eas exposed behind ingress nginx?
I am using ingress-nginx and already got it working using nginx.ingress.kubernetes.io/configuration-snippet
as suggested in your example. Just trying to see if we can have a simplified ngnix signin endpoint that doesn't need nginx.ingress.kubernetes.io/configuration-snippet
like what oauth2-proxy signin endpoint does.
I tried below yaml token config:
When I test to send a curl request with header my-jwt-header, external-auth-server throws below errors about json:
Is query_engine jp not supported in yaml format?
BTW, our jwt header doesn't have "bearer" in it. I manually added it in my manual curl test. If the real client sends the jwt header without "bearer", will that be a problem for external-auth-server? Is there a query rule that can check the existence of a header?