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

OAuth2 client: default redirection to login page is done on wrong socket when SSL is enabled (authorization-server instead of client) #12307

Closed
ch4mpy opened this issue Nov 27, 2022 · 15 comments
Assignees
Labels
status: feedback-provided Feedback has been provided type: bug A general bug

Comments

@ch4mpy
Copy link
Contributor

ch4mpy commented Nov 27, 2022

Describe the bug
SSL is enabled by default for my spring-boot apps (I have set SERVER_SSL_KEY_PASSWORD, SERVER_SSL_KEY_STORE and SERVER_SSL_KEY_STORE_PASSWORD environement variables).

I configured a very simple OAuth2 client with oauth2Login. Spring client application runs on port 8080 and authorization-server (Keycloak) on port 8443. When trying to first access a secured page, I'm redirected to spring-boot generated login page on wrong socket: authorization-server socket instead of client socket (https://localhost:8443/oauth2/authorization/spring-addons-public when I'd expect https://localhost:8080/oauth2/authorization/spring-addons-public).

If I manually visit https://localhost:8080/oauth2/authorization/spring-addons-public, authorization-code flow is successful but I'm last redirected to the wrong socket again (sent to authorization-server index instead of client's one).

Interestingly enough, redirections are done on the right port if I explicitly disable SSL (server.ssl.enabled=false).

To Reproduce

  • start an authorization-server. Sample configuration below expects a Keycloak instance running on port 8443 with SSL and a spring-addons-public client (with authorization-code flow enabled and no client secret)
  • start a spring-boot OAuth2 client servlet on port 8080 with ssl enabled and oauth2Login (detailed configuration below)
  • visit https://localhost:8080/secured.html

Expected behavior
First redirection should be to https://localhost:8080/oauth2/authorization/spring-addons-public to initiate authorization-code flow from generated login page (instantly followed by another redirection to https://localhost:8443/realms/master/protocol/openid-connect/auth as there is only one provider in my conf)

Workarounds

When using a port that is not one of the two registered in PortMapperImpl default constructor, there is no alteration. So, the easiest solution, by far, is to use any port but 80 or 8080 when SSL is enabled. PortMapperImpl is initialised with 80 -> 443 and 8080 -> 8443 and components like LoginUrlAuthenticationEntryPoint alter the request port using this mapper, and even if you define your own port mapper in the application context, it is not picked => you'll have to configure it explicitly as done below by @sjohnr (but 1. it gets funny when you have several client registrations, and 2. reconfiguring just the authentication entry-point is not enough as there are other places where the port is altered by PortMapperImpl).

Port mapper being ignored with absolute URIs, the second option is to use absolute URIs for login page, post login redirection URIs and inside authorization request resolver.

Warning: explicitly configuring the loginPage also requires to implement a controller to handle it.

scheme: http
hostname: localhost
base-uri: ${scheme}://${hostname}:${server.port}

server:
  port: 8080
  ssl:
    enabled: false
---
spring:
  config:
    activate:
      on-profile: ssl

server:
  ssl:
    enabled: true

scheme: https

Then in the OAuth2 client configuration, I can use this ${base-uri} like that:

@Order(Ordered.HIGHEST_PRECEDENCE + 1)
@Bean
SecurityFilterChain springAddonsClientFilterChain(
        HttpSecurity http,
        OAuth2AuthorizationRequestResolver authorizationRequestResolver,
        @Value("${base-uri}") URI base-uri) throws Exception {

    http.oauth2Login(login -> {
        login.loginPage(UriComponentsBuilder.fromUri(base-uri).path("/login").build().toString());
        login.authorizationEndpoint().authorizationRequestResolver(authorizationRequestResolver);
    });
    login.defaultSuccessUrl(UriComponentsBuilder.fromUri(base-uri).path("/home").build().toString(), true);

    ...

    return httpPostProcessor.process(http).build();
}

@ConditionalOnMissingBean
@Bean
OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
    return new SpringAddonsOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
}

public class SpringAddonsOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private final OAuth2AuthorizationRequestResolver defaultResolver;

    public SpringAddonsOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
                OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        return toAbsolute(defaultResolver.resolve(request), request);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        return toAbsolute(defaultResolver.resolve(request, clientRegistrationId), request);
    }

    private OAuth2AuthorizationRequest toAbsolute(OAuth2AuthorizationRequest defaultAuthorizationRequest,
            HttpServletRequest request) {
        final var clientUriString = request.getRequestURL();
        if (defaultAuthorizationRequest == null || clientUriString == null) {
            return defaultAuthorizationRequest;
        }
        final var clientUri = URI.create(clientUriString.toString());
        final var redirectUri = UriComponentsBuilder.fromUriString(defaultAuthorizationRequest.getRedirectUri())
                .scheme(clientUri.getScheme()).host(clientUri.getHost())
                .port(clientUri.getPort()).build().toUriString();
        return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest).redirectUri(redirectUri).build();
    }
}

This is lot of configuration code for a Boot application, but I isolated it in alternate starters wrapping spring-boot-starter-oauth2-client which makes my OAuth2 clients "bootiful" enough (configurable from properties with little to no Java conf). For the curious, the starters are here for servlets and there for reactive apps. Both come with additional "worthless" auto-configuration controlled with properties (0 Java conf):

  • CSRF with the right request handler and a filter to actually set the cookie when cookie repo is chosen, because this is so much easier and saves so much time wasted to sort this CSRF errors with security 6 and JS frontends like Angular (stopped counting Stackoverflow questions and, even more serious, blog posts, with sessions enabled and CSRF protection disabled)
  • authorities mapping (source claims, prefix and case transformation), so that I don't have to provide with a user service or a GrantedAuthoritiesMapper in each app
  • logout success handler for OIDC Providers not strictly following the RP-Initiated Logout standard (exotic parameter names or missing end_session_endpoint in OpenID configuration). Auth0 and Amazon Cognito are samples of such OPs
  • basic access control: permitAll for a list of path matchers and authenticated as default (to be fine tuned with method security or a configuration post-processor bean)
  • an implementation for the client side of the Back-Channel Logout (remove corresponding authorized client from the repository and invalidate user session if it was its last authorized client with authorization-code)
  • fine grained CORS configuration (per path matcher), so that I can provide allowed origins as environment variable when switching from localhost to dev or prod environments

To Reproduce (the issue, not the workarounds)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.0</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<artifactId>demo</artifactId>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
server:
  ssl:
    enabled: true
spring:
  security:
    oauth2:
      client:
        registration:
          spring-addons-public:
            client-id: "spring-addons-public"
            client-secret: ""
            client-name: "spring-addons-public"
            provider: "keycloak"
            scope: 
              - "openid"
              - "profile"
            client-authentication-method: "none"
            authorization-grant-type: "authorization_code"

        provider:
          keycloak:
            issuer-uri: "https://localhost:8443/realms/master"
            authorization-uri: "https://localhost:8443/realms/master/protocol/openid-connect/auth"
            token-uri: "https://localhost:8443/realms/master/protocol/openid-connect/token"
@Configuration
@EnableWebSecurity
public class SecurityConfig {
	@Bean
	SecurityFilterChain uiFilterChain(HttpSecurity http) throws Exception {
		// @formatter:off
		http.authorizeHttpRequests()
			.requestMatchers("/login/**").permitAll()
			.requestMatchers("/oauth2/**").permitAll()
			.anyRequest().authenticated();
		// @formatter:on
		http.oauth2Login();

		return http.build();
	}
}

src/main/resources/static/index.html:

<!DOCTYPE html>
<head>
	<title>secured</title>
</head>
<body>
	<h1>Secured</h1>
</body>
@ch4mpy ch4mpy added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Nov 27, 2022
@ch4mpy ch4mpy changed the title OAuth2 client: default login page on wrong socket (authorization-server instead of client) OAuth2 client: default login page configured on wrong socket (authorization-server instead of client) Nov 27, 2022
@ch4mpy ch4mpy changed the title OAuth2 client: default login page configured on wrong socket (authorization-server instead of client) OAuth2 client: default login page configured on wrong socket (authorization-server instead of client) when ssl is enabled Nov 27, 2022
@ch4mpy ch4mpy changed the title OAuth2 client: default login page configured on wrong socket (authorization-server instead of client) when ssl is enabled OAuth2 client: default redirection to login page is done on wrong socket when SSL is enabled (authorization-server instead of client) Nov 28, 2022
@sjohnr
Copy link
Member

sjohnr commented Jan 10, 2023

@ch4mpy sorry for the delay on this.

Does this issue only occur when both client/server run on localhost? Does the behavior change if you use 127.0.0.1 as the host for the client and configure the redirect uri accordingly in Keycloak?

Have you tried configuring http.portMapper()? I wonder if a setup with http port 8080 mapping to https port 8443 and configuring (the default) PortMapperImpl would improve the situation?

If that doesn't yield a result, is there an easier way to set up a reproducing test than manually configuring SSL for both client/server? Can you provide a minimal sample with a test SSL setup and a mock oauth2 server? I think demonstrating the first incorrect redirect would be enough to get started triaging the issue.

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 10, 2023
@ch4mpy
Copy link
Contributor Author

ch4mpy commented Jan 10, 2023

Does this issue only occur when both client/server run on localhost?

I have no easy access to a hosted server to make this test.

Does the behavior change if you use 127.0.0.1 as the host for the client and configure the redirect uri accordingly in Keycloak?

Not on my point of interest: I only get very inconvenient warnings when browsing over https by IP rather than hostname listed in the self-signed certificate altnames (certificate is added to my OS trusted root certificates)

Have you tried configuring http.portMapper()? I wonder if a setup with http port 8080 mapping to https port 8443 and configuring (the default) PortMapperImpl would improve the situation?

No. but as ports 8080 and 8443 are already used by respectively resource-server and authorization-server, I don't really understand the point :/

Can you provide a minimal sample with a test SSL setup and a mock oauth2 server?

The code source included in the description is a complete reproducer (but it is based on a real authorization-server)

is there an easier way to set up a reproducing test than manually configuring SSL for both client/server?

Not that I know of. But I think it is acceptable as, with the right tools, it can take less than 5 minutes to generate a new certificate and start Keycloak and Spring client with SSL...

Generating a self-signed certificate and adding it to JDK(s) cacerts file(s) (in one command)

Detailed instructions in the README of this repo of mine

By running this script, you can generate certificates valid for a year and add it to your JREs / JDKs cacerts files.

All required setup is setting 2 environment variables (3 if JAVA_HOME is not set, 4 if you set SERVER_SSL_KEY_STORE to have boot app start over https by default).

The README also contains instructions for adding the certificate to trusted root OS authorities on Windows or OS X.

Running a local Keycloak instance with SSL on port 8443

Provided that

  • SERVER_SSL_KEY_STORE_PASSWORD is set as environment variable (as per the setup to generate the certificates with the script above)
  • SERVER_SSL_KEY_STORE is set as environment or shell variable and is pointing to the self-signed .jks (something like file:C:/Users/ch4mp/.ssh/bravo-ch4mp_self_signed.jks):
# On OS X, sed is not POSIX. Work around that.
if [[ "$OSTYPE" == "darwin"* ]]; then
  SED="sed -i '' -e"
else
  SED="sed -i -e"
fi

# Download and unpack a fresh Keycloak instance
curl https://github.com/keycloak/keycloak/releases/download/20.0.3/keycloak-20.0.3.zip -O -J -L
unzip ./keycloak-20.0.3.zip
rm -f ./keycloak-20.0.3.zip

# Update Keycloak config
# Prevent port conflict (both Keycloak and Spring default to 8080)
echo "http-port=8442" >> ./keycloak-20.0.3/conf/keycloak.conf
# SSL config
echo "https-port=8443" >> ./keycloak-20.0.3/conf/keycloak.conf
echo "https-key-store-file=$SERVER_SSL_KEY_STORE" >> ./keycloak-20.0.3/conf/keycloak.conf
$SED "s/file://g" "./keycloak-20.0.3/conf/keycloak.conf"
echo "https-key-store-password=$SERVER_SSL_KEY_STORE_PASSWORD" >> ./keycloak-20.0.3/conf/keycloak.conf

# Start Keycloak
if [[ "$OSTYPE" == "msys" ]]; then
  ./keycloak-20.0.3/bin/kc.bat start-dev
else
  bash ./keycloak-20.0.3/bin/kc.sh start-dev
fi

Depending on the OS, subsequent Keycloak starts are done with just kc.bat start-dev or bash ./kc.sh start-dev from Keycloak's bin directory.

Declare a client

  • browse to https://localhost:8443
  • create an admin account for "Administration Console"
  • browse to https://localhost:8443/admin/master/console/#/master/clients and click "Create client"
  • set spring-addons-public as client name and ensure "Standard flow" is enabled in next step
  • set https://localhost:8080/* as "Valid redirect URIs" and "Valid post logout redirect URIs". Also set https://localhost:8080 as "Web origins"
  • save

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 10, 2023
@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 3, 2023

@sjohnr I updated my previous comment to better answer your questions

@sjohnr
Copy link
Member

sjohnr commented Feb 4, 2023

@ch4mpy, I've looked into this and set up an OAuth2 client configured for SSL and an authorization server also configured for SSL. When configuring the client to run on port 8080, I do see the behavior you've outlined.

However, this behavior would exist in any application with Spring Security. Consider the following security config:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.formLogin(Customizer.withDefaults());

		return http.build();
	}

}

When the application is configured to run on port 8080 with SSL enabled, visiting https://localhost:8080 I am redirected to https://localhost:8443/login. The reason for this is the default PortMapper and PortResolver configured with Spring Security assume that an SSL-enabled app will use port 8443.

I don't seem to find much information on this topic in the reference, so this is a topic that could use some better documentation.

If we don't wish to accept the defaults, we do have to configure Spring Security to use port 8080. You would do so by defining that you use port 8080 as the SSL port, like so:

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.portMapper((ports) -> ports
				.http(8080).mapsTo(8080)
			);

		return http.build();
	}

I would expect this to be enough to configure redirects to work properly. However, it appears this portMapper() configuration is not honored by the LoginUrlAuthenticationEntryPoint, which also uses a PortResolver that internally uses another PortMapper. I'm not sure the exact history behind this, but this has been in place in Spring Security since at least 2009.

I think the solution would be to allow the above configuration to configure 8080 as the SSL port, if you really want to do so. It should be used by both the PortMapper and the PortResolver.

However, 8443 is the default for SSL and nothing would tell Spring Security you wish to use 8080 unless you configure it. I believe this makes sense since an application could be serving traffic on both 8080 and 8443 simultaneously and the defaults make sense in this (very common) case.

In the meantime, you can work around this with the following configuration:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.oauth2Login(Customizer.withDefaults())
			.exceptionHandling((exceptions) -> exceptions
				.authenticationEntryPoint(authenticationEntryPoint())
			);

		return http.build();
	}

	private AuthenticationEntryPoint authenticationEntryPoint() {
		PortMapperImpl portMapper = new PortMapperImpl();
		portMapper.setPortMappings(Map.of("8080", "8080"));

		PortResolverImpl portResolver = new PortResolverImpl();
		portResolver.setPortMapper(portMapper);

		LoginUrlAuthenticationEntryPoint authenticationEntryPoint =
				new LoginUrlAuthenticationEntryPoint(
						"/oauth2/authorization/spring-addons-public");
		authenticationEntryPoint.setPortMapper(portMapper);
		authenticationEntryPoint.setPortResolver(portResolver);

		return authenticationEntryPoint;
	}

}

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 4, 2023

When I configure server.port=8080, I expect it to be picked even if app is served with https (this is the case for server port binding already) and I'd expect it to be picked by spring-security as default.

At minimum when using Spring-boot, couldn't auto-configuration pick server host and port and set spring-security defaults accordingly?

@sjohnr
Copy link
Member

sjohnr commented Feb 4, 2023

@ch4mpy

At minimum when using Spring-boot, couldn't auto-configuration pick server host and port and set spring-security defaults accordingly?

I think that would be a spring boot question though given everything else we’re discussing I wouldn’t expect that to be a feature request that would be considered.

When I configure server.port=8080, I expect it to be picked even if app is served with https (this is the case for server port binding already) and I'd expect it to be picked by spring-security as default.

Keep in mind that just specifying port 8080 is not enough in an SSL scenario because you also need to redirect from an http port to an https port for browser based apps (which an OAuth client is). Your configuration does not account for this, which makes it a non-standard setup.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 4, 2023

Unless I want spring apps to be served with SSL only (end-to-end encryption), case where I am happy with boot default binding to a single port, even with https enabled. I let the Middleware handle redirections.

However, 8443 is the default for SSL

When starting a spring-boot app with SSL enabled and no port specified, the app is bound to 8080 as usual.

However, 8443 is the default for SSL and nothing would tell Spring Security you wish to use 8080 unless you configure it

Wouldn't ServerProperties::getPort tell which port the application is actually bound to (it being the default one or explicitly configured in properties)? And actually, when client is configured with server.ssl.enabled=false and server.ssl.port=4200 (and a http provider) there is no redirection issue: spring-security builds client redirect URIs with port 4200, as I expect it to do, even if 4200 is not the default port

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 4, 2023

@sjohnr appart from the issue related to clients served with server.ssl.enabled=true, there might be another one when the providers are configured with https (client having server.ssl.enabled=false). Consider the following configuration:

server:
  port: 8080
  ssl:
    enabled: false

spring:
  security:
    oauth2:
      client:
        registration:
          spring-addons-public:
            client-id: "spring-addons-public"
            client-secret: ""
            client-name: "spring-addons-public"
            provider: "keycloak"
            scope: 
              - "openid"
              - "profile"
            client-authentication-method: "none"
            authorization-grant-type: "authorization_code"

        provider:
          keycloak:
            issuer-uri: "http://localhost:8442/realms/master"
            authorization-uri: "http://localhost:8442/realms/master/protocol/openid-connect/auth"
            token-uri: "http://localhost:8442/realms/master/protocol/openid-connect/token"

Things work as expected

Now, if I switch provider URIs to https://localhost:8443, things go wrong after user is redirected back to client with authorization-code:

  1. http://localhost:8080/ is redirected to http://localhost:8080/oauth2/authorization/spring-addons-public
  2. http://localhost:8080/oauth2/authorization/spring-addons-public is redirected to https://localhost:8443/realms/master/protocol/openid-connect/auth with redirect_uri=http://localhost:8080/login/oauth2/code/spring-addons-public
  3. after authentication, I am redirected to http://localhost:8080/login/oauth2/code/spring-addons-public with state, session_state and code parameters
  4. http://localhost:8080/login/oauth2/code/spring-addons-public redirects to itself with exact same parameters
  5. a request is issued to https://localhost:8080/login/oauth2/code/spring-addons-public with the same parameters as in step 3 and 4 and fails

I'd expect step 4. to be a redirect to http://localhost:8080/?continue (with tokens fetched by the client and attached to the user session between 3. and 4.)

@sjohnr
Copy link
Member

sjohnr commented Feb 5, 2023

@ch4mpy

When starting a spring-boot app with SSL enabled and no port specified, the app is bound to 8080 as usual.

Spring security does not know about Spring Boot configuration as it can be used with and without Spring Boot. Security only knows that 8080 is used for SSL based on configuration as discussed above.

Wouldn't ServerProperties::getPort tell which port the application is actually bound to

ServerProperties is a Spring Boot class which isn’t part of spring-security. It sounds like you are asking for features like the one discussed above that would be part of Boot auto configuration. If so, please open an issue on the boot issue tracker.

If you believe you’ve found another bug/issue, please open a separate issue.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 20, 2023

It is interesting to note that this "feature" of rewriting the port of client URIs involved in login process from 80 to 443 and 8080 to 8443 exists on the WebMVC side of spring applications only (without any explicit configuration, only for those two ports, and only when the original request was sent over https). I couldn't find anything like that in WebFlux...

And honestly, I don't understand why the port should be modified when building redirection URIs: if the current request is being processed on the client with an HttpServletRequest URL having https scheme and 8080 port, why should a different combination be used to reach that client again during login process?

8443 is the default for SSL

I don't agree with that (no more than 8080 is default for http). To me, defaults are 443 for https and 80 for http.

8443 is only a frequent usage for web apps started as user process and served with https (specially those historically based on Tomcat), and this does not apply to Spring Boot apps which are bound to 8080 by default, no matter if server.ssl.enabled is true or false, but, I understood, spring-boot apps are not the subject here.

I'm not sure spring-security should make any assumption on the ports my Spring applications bind to (it being Boot or not), and It feels unnatural to me that spring-security is not configured by Boot with the server port (again, I understood this is not the place to complain about the second point).

please open an issue on the boot issue tracker

What would be the point, considering that you (I mean spring-security team) decide what is done regarding security configuration in boot, and:

I think that would be a spring boot question though given everything else we’re discussing I wouldn’t expect that to be a feature request that would be considered.

Let's save Spring Boot team some time and post here 2 workarounds for spring-security assumptions on the ports used by servlets.

@rwinch rwinch self-assigned this Apr 3, 2023
@rwinch
Copy link
Member

rwinch commented Apr 6, 2023

Hello @ch4mpy!

I'd like to start by apologizing for the difficulties you are having and thanking you for your thorough feedback & posting your workarounds. The behavior you are seeing is indeed confusing and is an unfortunate result of a long history around redirects and ports. Given how long this logic has been in place, @sjohnr reached out to me to help since I have been working on Spring Security longer.

Absolute Redirects vs Relative Redirects

A large part of the problem is that Spring Security was written to conform with RFC2616 which requires that the location header be an absolute URI. In order to create an absolute URI, Spring Security must lookup the port to redirect to. At the time the APIs were created (and for quite some time afterwards), IE had a bug which caused ServletRequest.getServerPort() to return the incorrect port when performing redirects between http and https and vice versa.

To work around the IE bug, the PortResolver was created. PortResolver is used when https is the scheme and the port is a typical http port because this is how the IE bug manifested itself and how it was possible to work around. This explains why you do not see the issue you are reporting unless you are using https and port 8080. The problem has not been prioritized because not many users leverage port 80 or 8080 for https. Those that come across the problem you are experiencing typically just switch the https port to not be 80 or 8080.

When Absolute URIs Are Required

There are places where absolute URIs are still necessary. This is typically an absolute URL needed for an exchange between an Identity Provider (IdP) so that after the user is authenticated with the IdP, the IdP is able to redirect back to the originating application. For consistency within the Servlet APIs, the PortResolver is used in absolute URL construction for these exchanges.

WebFlux

You won’t find the same problems in WebFlux because, whenever possible, it uses relative redirects which do not require a port. When an absolute URI is required, the actual port can be used because the IE bug has been fixed, IE is not nearly as popular as it once was, and it is consistent with the rest of WebFlux.

It's also important to point out that in WebFlux the Spring team is in control of how the ServerWebExchange is resolved. This means that any problems with resolving attributes related to the request (including the port) should be fixed in the creation of ServerWebExchange rather than in isolation using something like PortResolver. This is in stark contrast to Servlets where the HttpServletRequest is managed (in often very different ways) by various Servlet containers (e.g. Tomcat, Jetty, etc) making a centralized fix infeasible.

What to do?

All that said, the issue you describe is still a problem. I acknowledge your comment that PortResolver is not well documented (the explanation is found only in the javadoc). It is possible we could better document PortResolver. It is also possible we could enhance Spring Boot to understand how to configure the PortResolver / PortMapper.

However, I’d prefer to spend the time migrating to an approach that does not require PortResolver by using relative redirects when possible and APIs that do not use PortResolver (or use no op implementation with the member deprecated). Given how long PortResolver has been in use with servlets, it will be a bit difficult to switch to relative URLs but I think it is worth pursuing. I’ve created gh-12971 to track this effort.

I hope this answers your questions around what is happening and why, and more importantly how we can ensure others do not have the same problems you are having. If you are agreeable to it, I'd like to close this issue out in favor of gh-12971 because it should fully address the issue throughout the portfolio.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Apr 6, 2023

@rwinch thank you for your feedback.

I moved the workarounds section to the first comment to save the reading of all the intermediate comments to others facing the same issue as me. This could be useful until the fix to #12971 is released (or to those stuck with an earlier version).

@ch4mpy ch4mpy closed this as completed Apr 6, 2023
@ch4mpy
Copy link
Contributor Author

ch4mpy commented Apr 7, 2023

I wanted to apologize to @sjohnr too, it always takes me ages and dozens of edits to clarify my point. Sorry about that.

@sjohnr
Copy link
Member

sjohnr commented Apr 10, 2023

Thanks @ch4mpy! No problem.

@sandipchitale
Copy link

sandipchitale commented Nov 8, 2023

I think the issue is https://github.com/spring-projects/spring-security/blame/22000b42e9e84d2a8b5479b076dbdf1998a91e17/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java#L565 which simply creates a LoginUrlAuthenticationEntryPoint which is not accessible. If it were to be allowed to be post processed, then it would have been easy to configure PortMapper/Resolver on it. This PortMapper business has caused us all kinds of grief and I am glad to hear it will be removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided type: bug A general bug
Projects
Archived in project
Development

No branches or pull requests

5 participants