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

Back-Channel Logout should use localhost for internal logout request #14553

Closed
ch4mpy opened this issue Feb 6, 2024 · 10 comments
Closed

Back-Channel Logout should use localhost for internal logout request #14553

ch4mpy opened this issue Feb 6, 2024 · 10 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: bug A general bug
Milestone

Comments

@ch4mpy
Copy link
Contributor

ch4mpy commented Feb 6, 2024

Describe the bug
Given I have:

  • a Spring OAuth2 client configured with Back-Channel Logout
  • an authorization server with Back-Channel Logout configured to call the Spring OAuth2 client

When I try to visit a path requiring to be authorized on the Spring client, then I am redirected to login.

Once authenticated, I can access the protected resource.

When I visit the end_session_endpoint of the authorization server, then I can see a TRACE log on the Spring client displaying: Found and removed 1 session(s) from mapping of 1 session(s)

However, if I refresh the tab pointing to the protected resource, I can still access it.

There are two log lines after the one stating that the session was removed:

ExchangeFunctions : [1c75356f] HTTP POST http://host.docker.internal:7080/logout, headers={masked}
ExchangeFunctions : [1c75356f] Cancel signal (to close connection)

Is the client trying to call its own /logout endpoint but using the wrong hostname? host.docker.internal is the hostname used by the authorization server to initiate the Back-Channel Logout (which is running in Docker), but this hostname is unknown from the Spring client.

Expected behavior
If the Spring client needs to call itself to actually destroy the user session, shouldn't it call localhost?

Sample

https://github.com/ch4mpy/spring-security-14553

run docker compose up to create a new Keycloak instance (admin / admin to access http://localhost:8080/realms/master)

create a new realm by importing spring-security-realm.json

create a user

start the spring boot app

visit http://localhost:7080/

the end_session_endpoint is at http://localhost:8080/realms/spring-security/protocol/openid-connect/logout

@ch4mpy ch4mpy added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Feb 6, 2024
@jzheaux
Copy link
Contributor

jzheaux commented Feb 12, 2024

@ch4mpy, good observation. I think we can change this:

String logout = UriComponentsBuilder.fromHttpUrl(url)
			.replacePath(this.logoutEndpointName)
			.build()
			.toUriString();

to:

String logout = UriComponentsBuilder.fromHttpUrl(url)
+			.host("localhost")
			.replacePath(this.logoutEndpointName)
			.build()
			.toUriString();

to address the issue.

@jzheaux jzheaux self-assigned this Feb 13, 2024
@jzheaux jzheaux added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 13, 2024
@jzheaux jzheaux added this to the 6.2.2 milestone Feb 13, 2024
@jzheaux jzheaux changed the title Back-Channel Logout logs a removed session but user remains logged-in (using the wrong hostname for subsequent logout?) Back-Channel Logout should use localhost for internal logout request Feb 13, 2024
@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 13, 2024

@jzheaux thank you.

@koyama-tagbangers
Copy link

koyama-tagbangers commented Feb 16, 2024

In the production environment, the logout endpoint is accessed via https.
With the current implementation, requests to https://localhost will fail due to a different default port.

e.g. https://example.com/logout/connect/back-channel/my-registration -> https://localhost/logout

So it seems like you need to change the schema and port to your own values.

Or rewrite hostname to localhost only host.docker.internal.

@lmorocz
Copy link

lmorocz commented Feb 27, 2024

@jzheaux

Rewriting only the host name to localhost is not correct when the app (client) is behind a reverse proxy with TLS termination. In this case schema (HTTPS vs. HTTP) and/or port number is often different for localhost and the original hostname, as @koyama-tagbangers mentioned earlier.

The application context-path is another thing to consider here, see #14181.

One could use the internal host name/address and port number of the client in the server backchannel logout configuration, but only if the IDP (e.g. Keycloak) can access these internal hosts at all, which is often not possible.

Could we replace the POST request for the local logout resource (LogoutFilter) with something else?

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 27, 2024

One could use the internal host name/address and port number of the client in the server backchannel logout configuration, but only if the IDP (e.g. Keycloak) can access these internal hosts at all

I think this is a wrong assumption: a server should always be able to call itself with its internal scheme, host and port. The idea here was not to modify the hostname to which the OP sends the requests, just to change what the RP uses to call itself after it received the Back-Channel Logout request. Also, the path used for this additional call(s) is not the one of the Back-Channel Logout endpoint, it is the path of the RP "standard" logout.

Note that in the case of a reverse proxy rewriting the path the previous impl could be broken too: if the request goes through the reverse proxy, the logoutEndpointName (set around line 118 of OidcBackChannel(Server)LogoutHandler) can be altered and the request fail.

Sure that if one finds a way to perform the logout without this second network call, that would eliminate the problem, but I think it was discussed in another ticket already (couldn't remember which one).

Updating the scheme, port and context path with the values we see in the logs after startup (Tomcat started on port 8080 (https) with context path '') would improve the situation, but what if the /logout endpoint is configured to something non-standard? Maybe should we just make this other "internal" logout endpoint entirely configurable?

@jzheaux maybe would it be worth to:

  • rollback the modification I asked in this ticket (my use-case is certainly less frequent in prod than having the OP call the RP through a reverse proxy with a different scheme or port). An alternative being to keep localhost, but to also set scheme, port and context path (this would probably be a better default in the case of path rewriting between a reverse proxy and the OAuth2 client, but is more work).
  • on the OidcBackChannelServerLogoutHandler, make all of the logout endpoint URI configurable (not only the path)
  • open a bit the BackChannelLogoutConfigurer to give a hand on the OidcBackChannelServerLogoutHandler (for now, everything in BackChannelLogoutConfigurer is private, which makes it a relatively useless configurer)

Should I propose a PR for that ?

@kmeyer-mbs
Copy link

I just ran into the problem with "localhost". I use a let's encrypt SSL certificate directly in the embedded Tomcat in the local development environment. Due to the host name localhost the handshake fails.

2024-02-29T18:09:35.256+01:00 DEBUG 37790 --- [nio-7443-exec-4] c.a.w.c.o.c.OidcBackChannelLogoutHandler : Failed to invalidate session

org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://localhost:7443/logout": No subject alternative DNS name matching localhost found.
	at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:915) ~[spring-web-6.1.4.jar:6.1.4]
...

I would also welcome a configuration option.

Many thanks and best regards!

@dalbani
Copy link

dalbani commented Mar 16, 2024

I confirm that the change in 2702a64 broke the use of back-channel logout in my application.

As mentioned above, in a situation where the application sits behind a reverse proxy, the connection to, in my case, http://localhost/logout [*] fails.

This is really a blocker for us to be able to upgrade to 3.2.3.

jzheaux added a commit that referenced this issue Mar 22, 2024
jzheaux added a commit that referenced this issue Mar 22, 2024
@daliborfilus
Copy link

daliborfilus commented May 6, 2024

I just wanted to implement OIDC back channel logout and got to the issue with "localhost" and found this ticket. In my experience, the app is never run on the same host and port as it is seen from the outside, there's always some proxying going on, be it on local nginx/haproxy or external nginx/haproxy/F5/whatever. Changing just the host to localhost may work in a very small minority of production environments. (Also note that the hostname is used for routing on the reverse proxy even in the case of local nginx; it may be used for other apps and relies on the hostname for routing to the correct one.)
In my case, the app is run in a container on port 8080, the https is terminated on the edge (k8s ingress controller) somewhere else in the cluster. Not only it would not work trying to contact "localhost", but it also would require different port, different scheme.

Let it at least be configurable...

But I don't understand why it needs to do such internal requests against itself at all. The backchannel request came to the app via controllers, yes, it's a different session at that point, but we are inside the app and have access to the session registry. We are looking into it in the code to get the session ID for that token anyway, so it's not like it would be required for Spring Gateway or something. Why not just delete it right at that moment, why does it need to do the request? Some of you mentioned it was discussed somewhere, I'd like to read that discussion.

@dalbani
Copy link

dalbani commented May 6, 2024

I'm interested to know more as well.
To be honest, I was quite surprised to see this change included in a patch release (3.2.2), pretty much breaking back-channel logout, without any workaround.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Jun 11, 2024

The internal logout URI was made configurable back in March:

http.oidcLogout(ol -> {
    ol.backChannel(bc -> {
        bc.logoutUri("http://localhost:8080/logout");
    });
});

This will work even behind a reverse proxy having different scheme and port.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: bug A general bug
Projects
Status: Done
Development

No branches or pull requests

7 participants