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

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>

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 15 (14 by maintainers)

Most upvoted comments

@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 https://github.com/spring-projects/spring-security/issues/12971 is released (or to those stuck with an earlier version).

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?