Hi all,
I am developing a microservice application, using spring security on the spring cloud gateway, with an oidc authentication process based on keycloak. We need to specify a custom user-info-uri, different from the keycloak provider.
Sometimes it happends that the called service returns an error, or has a timeout; in this case, the default spring security seems laking a proper management of the application and keycloak sessions.
I try to explain: the default behavior is rendering a page with "Login with Oauth 2.0 - Invalid credentials" and an url, but that condition is quite bad: the gateway hasn't yet created the security context, but the keycloak has its own sessions that are already authenticated. Trying to go back to the login page is impossible (unless to close all the browser windows), because of the keycloak session.
Thinking that the correct way to manage this could be to use the same logic that is used for the logout (OidcClientInitiatedServerLogoutSuccessHandler), I tried to create a custom handler (ReactiveOidcUserServiceFailureHandler) but this can't be done directly, because the callback to the end_session_endpoint needs also an id_token_hint, that in the logout handler is given by the Authentication in the onLogoutSuccess methot, but that can't be decoded from the OAuth2AuthenticationException. In fact, in this exception there is a reference (in the cause attribute) to the WebClientException thrown by the web client, but from that only the Bearer JWT token can be extracted, while we need the ID JWT token (using the bearer for the call to keycloak is not a valid value for the parameter).
I solved this using two other customizations:
OidcAuthenticationExceptionthat extendsOAuth2AuthenticationException, that has theOidcIdTokenas an attributeCustomOidcReactiveOAuth2UserServicethat adds to theOidcReactiveOAuth2UserServiceloadUsermethod the management of theOAuth2AuthenticationExceptionthat is thrown by thegetUserInfomethod:
return getUserInfo(userRequest)
.onErrorMap((ex) -> (ex instanceof OAuth2AuthenticationException), (ex) -> {
throw new OidcAuthenticationException((OAuth2AuthenticationException) ex, userRequest.getIdToken());
})
.map((userInfo) -> new OidcUserAuthority(userRequest.getIdToken(), userInfo))
With these customization, I am able to get, in the exception, also the informations that are needed to effectively close the session on keycloak, and get rid of the warning page.
I don't know if this need for managing the errors that may occur on the "user info" phase is alredy covered by some other handlers, but in the analysis that I did in the last days I found a number of classes that manages the first steps of the communication between the client and the oidc provider, but nothing that fits the needs of cleaning effectively the sessions when an exception in the get user info calls.
Let me know if these ideas can be useful, I can share some of my code.
Comment From: jgrandja
@polifr There is a lot going on in the scenario you described and I'm having a hard time following. Can you put together a minimal sample that reproduces the issue along with the customizations you added that helped resolve it? This will help me in troubleshooting this issue further. Thanks.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: polifr
I'm working on the example; it has been a very busy week, sorry.
Comment From: polifr
I've uploaded the code needed for reproducing the described behavior, at https://github.com/polifr/gateway-example. Tomorrow I will write the README for usage and also the workaround that I implemented to manage correctly the errors in a specific project.
Comment From: polifr
Hi, on the https://github.com/polifr/gateway-example repository there are the modules to reproduce the described case; the README.md contains instruction on how set up the applications and how to get the current behavior. I also uploaded the implemented solution that i created to make the gateway manage correctly the error, but I will describe it in details tomorrow. Let me know if you manage to run the example and reproduce the case. Thanks in advance.
Comment From: jgrandja
@polifr Thanks for providing the sample and instructions.
As an FYI, we ask that the sample have the minimum amount of code and dependencies and is easy to reproduce as specified in minimal reproducible sample.
The sample you provided has many dependencies - docker, redis, spring cloud gateway, nginx.
Either way, I took a look at it in detail and the issue is that the UserInfo Endpoint is not being used as per specification.
We need to specify a custom
user-info-uri, different from the keycloak provider.
The UserInfo Endpoint is specified by the OpenID Connect 1.0 specification and it acts as a protected resource residing at the provider. However, your solution is a custom one as you are supplying the UserInfo endpoint via my-userinfo-provider. The usage here is incorrect as OidcReactiveOAuth2UserService is designed to support the UserInfo Endpoint residing at the Provider (Keycloak) as detailed in the spec.
I'm going to close this as the usage is incorrect and you should be using the UserInfo Endpoint provided by Keycloak.
Comment From: polifr
Let me say that I don't really agree with the analysis that you gave.
First, I think that the use case that i described needs to have a minimum set of dependencies to be reproducible (an oauth2 / oidc provider, a spring security oauth2 client, a session management and something that has to give both user info and some resources that need to be inquired). Because of all this needs, in my opinion building a docker compose that gives in a single shot the 3 needed backend services was simple. Sorry if I can't find a simpler option to mock all this stuff and yet made it functional to the scope of the example.
Moreover, the fact that I am using an overriden (but supported by specifying the user-info-uri property in the application.yml) changes nothing about the issue, that lies in the managing of the session after the authentication and on the potential error that may occurr inquiring the user info endpoint. Wherever this endpoint service may be (on keycloak or on another service) it is susceptible to an error, and in that case the application end in a situation where the provider has an authenticated session, but the spring security context is not initialized - and this leads to the case that I have described before. I believe that if you try to run the example, you will understand this more clearly.
Finally: I read the specifications that you give (the UserInfo Endpoint, paragraph 5.3): nowhere it is said that the endpoint has to be on the same provider that gave the authentication, but has to be a protected resource server that has to be accessed using the jwt access token released by the provider (keycloak, in this case), as is done in the given example. In the application architecture, this endpoint is implemented as a microservice that lies beyond the gateway, not accessible by the external network.
Anyway, if you believe that all these considerations are wrong, never mind. I just wanted to contribute to put the light on a behavior that, in my opinion, is managed incorrectly by the framework, also in the case of an user info extraction done on the oidc provider on which the user has authenticated himself that ended in an error.
Have a nice day.