IvanJosipovic / ingress-nginx-validate-jwt

Enables Kubernetes ingress-nginx to validate JWT tokens
MIT License
42 stars 8 forks source link

Extract claims into response headers? #26

Closed philomory closed 1 year ago

philomory commented 1 year ago

Is there any way to extract the values of JWT claims and set them as response headers, so that the ingress controller can then pass them along to the actual backend using auth_request_set and add_header? With oauth2-proxy you can do this using the --set-xauthrequest flag for basic configuration, or using the injectResponseHeaders.*.values.*.claim alpha configuration for advanced configuration.

I'd love to see similar functionality here, because this tool seems much easier to use when different Ingress objects need different configurations; maybe something along the lines of:

nginx.ingress.kubernetes.io/auth-url: http://ingress-nginx-validate-jwt.ingress-nginx-validate-jwt.svc.cluster.local:8080/auth?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222&aud=33333333-3333-3333-3333-333333333333&inject-claims=user,groups

IvanJosipovic commented 1 year ago

Hey,

Thank you for your feedback. My goal was to make a simple oauth-proxy that can be used to protect all the apps in the cluster as opposed to 1:1.

In terms of this feature, I plan to look into this in the next week to two.

IvanJosipovic commented 1 year ago

This feature was released in 1.7.0. Give it a try and let me know if it works for you.

philomory commented 1 year ago

Sounds great I'll give it a shot for sure!

philomory commented 1 year ago

So, this seems great, although I've run into one minor issue: we use Auth0 as our OAuth2 provider, and Auth0 recommends using "namespaced" custom claims, of the form http://example.com/foo rather than just foo. We've followed their recommendations, so, e.g. our custom groups claim is of the form "http://magicmemories.com/groups":[...]. Unfortunately, : characters are not valid characters in HTTP header names (/ also seems like it might be disallowed, although opinions and implementations seem to vary).

I can think of three ways this could be addressed:

  1. We could change our custom claims to not include a colon in the namespace name (perhaps namespacing them as just magicmemories.com|groups, etc).
  2. ingress-nginx-validate-jwt could automatically convert disallowed characters in header names to e.g. -
  3. ingress-nginx-validate-jwt could include a mechanism for specifying how to map claim names to header names, e.g. replacing &inject-claims=http%3A%3F%3Fmagicmemories.com%3Fgroups with &inject-claims=http%3A%3F%3Fmagicmemories.com%3Fgroups%3Dgroups, might map the custom claim http://magicmemories.com/groups to the header groups.

On a separate note, this doesn't affect us, but if you don't go for option 3, it's also possible that some users might have JWT claims whose names match "standard" headers like Expires or Age (I mean, it doesn't seem very likely, but, I've seen weirder things); if option 3 above is chosen, then it can be left to the user to decide what these should map to, but otherwise you may want to consider prepending a prefix not used by any standard headers, e.g. mapping a claim foo to jwt-claim-foo or similar.

IvanJosipovic commented 1 year ago

I like option 3. I'll code this in the next couple days.

IvanJosipovic commented 1 year ago

Version 1.8.0 has a new parameter, see here https://github.com/IvanJosipovic/ingress-nginx-validate-jwt#inject-claims-as-headers-with-custom-name

Let me know how this works out.

philomory commented 1 year ago

The new setup works pretty well, although I do still run into a few issues:

  1. Asking to have a claim injected as a header implicitly also marks that claim as required. I would have expected that if a claim was absent, the associated header would simply be omitted, but currently an exception is raised (System.InvalidOperationException: Sequence contains no matching element). This mostly matters in the case where an API secured by JWT has both human and machine users; in our case authorization of human users if often controlled by a "groups" claim, while authorization of machine users is handled by the default scope claim (and typically machine users will not have any groups claim in their token).
  2. Currently the system doesn't seem to handle very well claims whose values are arrays; for example, the following is notionally valid as the payload portion of a JWT: {"sub":"foo","iss":"...","groups":["foo","bar","baz"]}. Currently, if you request the injection of the groups claim from such a token, you get only groups=foo as your header, without any retention of bar or baz. I'm not a C# or .NET developer, but I did some digging and it appears to me that System.IdentityModel.Tokens.Jwt claims are always string-valued; I'm not sure if that means that the framework turns "groups":["foo","bar","baz"] into three separate claims (["key":"groups","value":"foo"},{"key":"groups","value":"bar"},{"key":"groups","value":"baz"}]), or if it simply discards any value from a multi-valued claim beyond the first, and expects users to encode such claims as strings rather than using the "native" multi-valued claims specified by the various RFCs.

Either or both of the above issues can be worked around on our end, of course, but I thought they were worth pointing out. At the very least, both limitations should probably be documented.

IvanJosipovic commented 1 year ago

Thanks for the awesome feedback. I have pushed v1.9.0 which should address all the issues above. I have removed "inject-claims" in favor of "inject-claim" and made it not required. I also added support for arrays.

philomory commented 1 year ago

Beautiful, that's basically perfect for our use-case. Thanks!

philomory commented 1 year ago

If you want to include an example of usage of the inject-claims feature in an Ingress, something like this should be appropriate:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: emojivoto-2
  annotations:
    nginx.ingress.kubernetes.io/auth-url: http://ingress-nginx-validate-jwt.ingress-nginx-validate-jwt.svc.cluster.local:8080/auth?aud=11111111-11111-1111111111&inject-claim=https%3A%2F%2Fexample.com%2Fgroups,groups&inject-claim=scope
    nginx.ingress.kubernetes.io/configuration-snippet: |
      auth_request_set $groups $upstream_http_groups;
      auth_request_set $scope $upstream_http_scope;
      proxy_set_header JWT-Claim-Groups $groups;
      proxy_set_header JWT-Claim-Scope $scope;

(and, contrary to my original, the appropriate nginx directives are auth_request_set and proxy_set_header, not auth_request_set and add_header)