OpenLiberty / open-liberty

Open Liberty is a highly composable, fast to start, dynamic application server runtime environment
https://openliberty.io
Eclipse Public License 2.0
1.15k stars 590 forks source link

way to intercept the JsonWebToken creation? #12915

Open mdagit opened 4 years ago

mdagit commented 4 years ago

The article at https://openliberty.io/blog/2019/08/29/securing-microservices-social-login-jwt.html accurately describes my scenario, and I'm sure it is not unusual. I want to support external authentication systems, but only to get the Principal; I don't want to rely on them for any other information (such as my application-specific roles/groups). The solution suggested in that article is rather heavyweight -- create a whole additional proxy server which receives the REST requests, gets the external JWT, then builds a new JWT with roles added for a backend request to the "real" microservice.

I would like to do something similar, but without the overhead of a whole separate proxy server.

I have tried to do this with OpenLiberty using either a PreMatching ContainerRequestFilter or by using a javax servlet HttpFilter. Neither has worked. Looking at the OpenLiberty source code, this isn't surprising, because it appears that com/ibm/ws/security/mp/jwt/tai/TAIRequestHelper.java is using an immutable low-level request object that could care less about anything i attempt to change at higher levels.

I could imagine a solution that works at either of two levels:

  1. the low-level HTTP request level (manipulating the Authorization header) before the MP container sees it.
  2. A mechanism to directly interact with the OpenLiberty feature implementation: ... container looks at Authorization header and creates a JWT ... container calls my jwt filter: JsonWebToken filterJsonWebToken(JsonWebToken jwt); ... container uses the return value to inject via CDI into Resources, and to enforce RolesAllowed security on endpoints I'm not sure exactly how that interception API would look or how it would be configured, but hopefully you get the idea.
teddyjtorres commented 4 years ago

It is not safe to allow modification of the JWT since it is signed by a provider. Allowing modification of the received JWT introduces a vulnerability where the receiver is able to change the claims the provider already signed.

If a modification is required, then the receiver must process the JWT and create a new signed JWT to send downstream. This will keep the integrity of the original JWT.

mdagit commented 4 years ago

I'm aware of that -- what I'm asking for is a way to do replacement in my single application, without the overhead and complexity of introducing a whole separate proxy server.

I suggested two levels that could be done at -- either the ability for me to replace the Authorization header that OpenLiberty looks at (before it looks at it), or the ability for me to replace a verified JWT with another one (that i create), between when OpenLiberty verifies the incoming JWT, and it then supplies it to CDI and verifies the appSecurity RolesAllowed annotations.

On Tue, Jul 7, 2020, at 9:33 AM, Teddy J. Torres wrote:

It is not safe to allow modification of the JWT since it is signed by a provider. Allowing modification of the received JWT introduces a vulnerability where the receiver is able to change the claims the provider already signed.

If a modification is required, then the receiver must process the JWT and create a new signed JWT to send downstream. This will keep the integrity of the original JWT.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/OpenLiberty/open-liberty/issues/12915#issuecomment-654981252, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABQZW6WHIQEBFAJPEVXNT4DR2NE5VANCNFSM4OS6POFQ.

teddyjtorres commented 3 years ago

The https://tools.ietf.org/html/rfc8693 RFC is not relevant to the MP JWT specification. Allowing switching the verified JWT for MP JWT would introduce a security vulnerability.

On the other hand, even if the RFC were to be implemented, it would affect the RP only since it does not apply to MP JWT.

NottyCode commented 3 years ago

@mdagit Can you clarify what your requirement is? Talking to @teddyjtorres we seem to have slightly different interpretations of the requirement. I think you are wanting to use the JWT to identify the user, but you want the Java EE/Jakarta EE authorization roles to use a mapping unrelated to what is in the JWT. @teddyjtorres is more concerned about the implications of having the MP JWT Principle claiming to be signed by the issuer without it matching what the issuer signed.

If you are looking to just have the RolesAllowed annotation processed based on groups found via another mechanism than reading from the JWT I think we have ways to do that without changing the JWT Principal.

mdagit commented 3 years ago

Neither actually.... I don't want to modify the JWT that is already injected. I agree that it would be messy and ugly and unexpected -- to allow adding/modifying claims in a JWT object that was already signed. The Microprofile JsonWebToken is currently immutable (getters only) and I wouldn't want to change that.

Rather, I would like more access to layers before the Microprofile JWT/RBAC stuff kicks in. I mentioned two possible layers where this might be done, the servlet api's HttpFilter, and a JAX-RS ContainerRequestFilter. To my view both of those approaches should work, but neither does with OpenLiberty. OL seems to be getting its Authorization header from some place untouchable by any of those standards-based mechanisms for request alteration.

Scenarios include: I might want to find the bearer token in some other place in the request (cookie, query param, etc). Or I might want to accept some other kind of non-JWT identity token and inject a signed Bearer in the Authorization header, to then take advantage of the downstream out-of-the-box Microprofile support.

These are all kinds of things that are frequently done with front-end proxies like Envoy -- and in fact it is also accomplished with a separate frontend proxy in the openliberty blog article I linked to at the top of this issue. It is just my feeling that it is completely reasonable to expect to be able to do such things in-process -- and I can in some other platforms, just not in OpenLiberty.

Oh and another subtle issue that doesn't get discussed much is that externally signed JWTs might have a lifetime that expires sooner than a user session might last. So then there is a problem using that externally provided JWT directly as a session token. There is legitimate debate about whether such tokens should be used as a session, but regardless a solution must be had -- through refresh tokens, or by an application specific exchange protocol, or something. And such solutions are currently rather hard to do with the way OL walls off its implementation.

The need for hooking is particularly important as apps want to start separating their authentication provider from their authorization provider. The OAuth2/OIDC standards kind of mush them together, but there are lots of scenarios where they need to be separated. The identity token and the access token both will be signed JWTs but from different providers. I'm by no means asking for OIDC to be rolled in to MP JWT. But it would be nice if OL could enable friendly interoperation with other providers/plugins that want to get involved along the request lifecycle. As just one example of a somewhat related request, see: https://github.com/OpenLiberty/open-liberty/issues/11225

To deal with the above issues I've had to exclude the use of MP JWT in my app using OpenLiberty, and implement it all myself (including looking at @RolesAllowed annotations etc). I really dislike having to implement something that is part of the container, particularly when other containers will let me do what I want.

Hope this helps....

chunlongliang-ibm commented 3 years ago

Can you check if following solution works for you?

  1. add useAuthenticationDataForUnprotectedResource="false" into <webAppSecurity ...> in server.xml <webAppSecurity useAuthenticationDataForUnprotectedResource="false" .... />
  2. use pre-match ContainerRequestFilter to inject JWT header
mdagit commented 3 years ago

@chunlongliang-ibm I tried a pre-match ContainerRequestFilter last year and it doesn't work in OpenLiberty. As I pointed out in the top of this issue I even dug into the openliberty source code to investigate, and it is pretty clear that the implementation in TAIRequestHelper.java is based on its direct access to the original HttpServletRequest, and it is totally unaffected to anything I might try to do in my prematch provider at a higher level.

chunlongliang-ibm commented 3 years ago

@mdagit if you add useAuthenticationDataForUnprotectedResource="false" into <webAppSecurity ...> in server.xml <webAppSecurity useAuthenticationDataForUnprotectedResource="false" .... /> I guess Liberty might defer JWT authentication to JAX-RS container from web container. Which may give you chance to change request header in ContainerRequestFilter

mdagit commented 3 years ago

I don't see how that could possibly make a difference. The setting for useAuthenticationDataForUnprotectedResource has to do with what to do if someone supplies authentication data for a resource that is not protected.

I'm trying to hook the the request before JWT RBAC looks at it for resources that are protected (by @RolesAllowed etc).

And if you look at dev/com.ibm.ws.security.mp.jwt/src/com/ibm/ws/security/mp/jwt/tai/TAIRequestHelper.java it should be clear why it doesn't work to use a PreMatching ContainerRequestFilter -- the implementation is looking directly at its private copy of the original HttpServletRequest. Changing the MultivaluedMap Headers object in a PreMatching filter won't do anything.

-mda