Summary

• Running behind a reverse proxy that performs SSL termination, and adds the X-Forwarded-... headers • The OAuth2 redirect URI uses HTTP instead of HTTPS • This is a bummer specially considering that Facebook only accepts HTTPS for redirect URLs.

Actual Behavior

Hitting: https://somedomain/oauth2/authorization/facebook

Redirects to Facebook sending the redirect_uri

redirect_uri=http%3A%2F%2Fsomedomain%3A443%2Flogin%2Foauth2%2Fcode%2Ffacebook

Unencoded

redirect_uri=http://somedomain:443/login/oauth2/code/facebook

Expected Behavior

Should redirect to this instead

redirect_uri=https://somedomain:443/login/oauth2/code/facebook

With the difference in the scheme • expected: https • but was: http

Configuration

application.yml

...
spring:
  security:
    oauth2:
      client:
        registration:
          login-facebook:
            provider: facebook
            client-id: facebook-client-id
            client-secret: facebook-client-secret
            scope: public_profile, email
            client-name: Login using Facebook
server:
  use-forward-headers: true
...

Version

• Spring Boot 2.0.3 • Spring Security 5.0.6 • Spring Security OAuth2 Client 5.0.6

Analysis

I tracked this down to: org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter

...
    private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) {
        int port = request.getServerPort();
        if (("http".equals(request.getScheme()) && port == 80) || ("https".equals(request.getScheme()) && port == 443)) {
            port = -1;      // Removes the port in UriComponentsBuilder
        }
...

request.getScheme() does not provide the original scheme correctly, as it does not take into account the X-Forwarded-Proto header

One possible fix is to use the following instead of request.getScheme()

            HttpRequest httpRequest = new ServletServerHttpRequest(request);
            UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build();
            String scheme = uriComponents.getScheme();

Comment From: rwinch

Thanks for the report!

Have you read through Proxy Server Configuration portion of the reference documentation? In short you need to ensure that your application server is properly configured for use with a proxy server.

Using X-Forwarded- headers directly within Spring Security is not preferred because it means that a malicious user can use header injection to attack an application. Using the headers directly also means that there is no way for users to opt out of this feature. Using an opt in model means that applications are required to take an explicit step to support X-Forwarded headers which means they are aware that they should prevent the headers from being sent past the first proxy server.

I'm going to go ahead and close this report, but if you have additional questions, comments, or concerns please feel free to reopen or create a new ticket.

Comment From: mragab

I read the Running Behind a Front-end Proxy Server Which states:

If the proxy adds conventional X-Forwarded-For and X-Forwarded-Proto headers (most proxy servers do so), the absolute links should be rendered correctly, provided server.use-forward-headers is set to true in your application.properties.

I still find this to be a defect in Spring Security for the following reasons:

(a) As per the documentation, I did take an explicit step to enable support for X-Forwarded headers as included in the application.yml snippet in the defect report, which follows the "an opt in model" criteria you described. Not wanting an "opt out mode" by default is one thing, and it not working in "opt in mode" when configured explicitly is another thing.

(b) The behavior of Spring Security is not consistent. The redirect URL contains: the public hostname, the public port number, but not the public scheme. It should be either all or nothing.

Comment From: rwinch

Spring Security uses whatever the HttpServletRequest reports, so if you do not see the correct scheme that means that there is something wrong in your configuration.

If the scheme is not being consumed, it sounds to me that your setup is incorrect. What X-Forwareded headers are you using? Are you using Tomcat? If so, what is the IP Address of your proxy? From the boot reference documentation:

Tomcat is also configured with a default regular expression that matches internal proxies that are to be trusted. By default, IP addresses in 10/8, 192.168/16, 169.254/16 and 127/8 are trusted. You can customize the valve’s configuration by adding an entry to application.properties, as shown in the following example:

server.tomcat.internal-proxies=192\\.168\\.\\d{1,3}\\.\\d{1,3}

You can also test to see if the ip address is the problem by setting

server.tomcat.internal-proxies=.*

Comment From: rwinch

FYI this is related https://github.com/spring-projects/spring-security/issues/3115

Comment From: mragab

Thanks for the tip, this changed the behavior

server.tomcat.internal-proxies=.*

With this configured: • The redirect URL uses HTTPS as expected. • request.getScheme() now correctly reports https

Thank you, now I understand where this is coming from, and I understand the reasoning behind it.

I am not sure I agree though - consider this as food for thought Running behind a reverse proxy that performs SSL termination is a very common case: • The current restriction of trusted proxy addresses only restricted the Scheme. While the public address and port went through just fine. • The current approach means users need to configure things differently based on which container they are using. • Tracing the inconsistency I experienced even though it might be considered a Tomcat issue is inconvenient to say the least. • The machinery is already there in Spring Web's UriComponentsBuilder • The current approach is limited with regards to adding more related features. For example: limiting the acceptable forwarded hosts not based on the proxy address but based on a dynamic list of allowed forwarded hosts. It would be great if I can simply implement some interface, register a bean and I am done.

Thanks again and have a great day

Comment From: rwinch

@mragab Thanks for the feedback

Running behind a reverse proxy that performs SSL termination is a very common case:

Agreed.

The current restriction of trusted proxy addresses only restricted the Scheme. While the public address and port went through just fine.

This is a Tomcat specific implementation detail. Unfortunately, there are quite a few differences in how each of the container specific proxy support works. I have requested Spring Boot switch to ForwardedHeaderFilter to make this more consistent, but this was rejected. I'd encourage you to let the Boot team know about your problems. Perhaps if enough people complain, then we can find a change that makes everyone's lives easier and be in aligned with what the Boot team wants for the project.

The machinery is already there in Spring Web's UriComponentsBuilder

Spring Framework's preferred approach is to centralize the support using ForwardedHeaderFilter as well. See https://jira.spring.io/browse/SPR-16668

It's worth mentioning there are multiple standards (i.e. PROXY protocol) for supporting proxies and we would need the code to transparently support each mechanism (in lots of places throughout the code base). Instead, we can leverage a Filter that is applied as an aspect (very spring like) to opt into whatever proxy mode you are using.

Updating the HttpServletRequest makes a lot of sense (what ForwardedHeaderFilter does and what Tomcat's RemoteIpValve does). It is already a well defined interface, and depending on your proxy settings can report different values. Ultimately, the values on it are a fundamental contract within the Servlet APIs, so if it reports the wrong values you are going to get incorrect behavior.

For example: limiting the acceptable forwarded hosts not based on the proxy address but based on a dynamic list of allowed forwarded hosts. It would be great if I can simply implement some interface, register a bean and I am done.

Honestly, I don't think the restriction on the proxy address (or other headers) buys you much. If the user can spoof X-Forwarded-Proto, then it is quite likely that they can spoof the headers stating which proxy is being used. Tomcat is the only container that performs these checks so using a wildcard as I described puts you in the same scenario as if you were using Jetty or Undertow.

As far as a custom interface, this is a ticket you would need to log with the Boot team.

Comment From: mragab

Thanks for the insights, much appreciated. I will see how to possibly bring this forward with the Spring Boot team.

Comment From: songokudbz

Spring Boot: 2.1.3.RELEASE Spring Security OAuth2 Client: 2.1.3.RELEASE

Adding server.use-forward-headers=true and server.tomcat.internal-proxies=.* does not work :(

Comment From: rwinch

@songokudbz That doesn't provide very many details. Please create a ticket at https://github.com/spring-projects/spring-security-oauth/issues/new that provides a sample and details on how to reproduce.

Comment From: songokudbz

@rwinch I managed to make it work. I am deploying my Spring Boot 2 application as a WAR file in a Tomcat Docker Image. Inside I needed to configure server.xml with a "Valve" to support the X-Forwarded-Porto logic. Thanks

Comment From: yemanasyan

@rwinch I've tried both server.tomcat.internal-proxies=.* as well and it didn't work. I'm using Spring Boot 2.2.4.RELEASE with Spring Security OAuth2 Client. I'm using the Spring Boot default embedded Tomcat container. I've deployed my application in AWS where I have Application Load Balancer (which is proxy in this case). Like @mragab 's case the domain name is correct, but schema not.

Could you please help me?

Comment From: yemanasyan

I solved this issue by setting this property: server.forward-headers-strategy=FRAMEWORK

It's available starting from Spring Boot 2.2.X

Comment From: superzjn

For those who are NOT using Spring 2.2.x, you can add these to application.yml. Verified it works.

server:
    use-forward-headers: true
    tomcat:
         use-relative-redirects: true
         protocol-header: x-forwarded-proto
         remote-ip-header: x-forwarded-for

Comment From: pawoua22

For those who are NOT using Spring 2.2.x, you can add these to application.yml. Verified it works.

server: use-forward-headers: true tomcat: use-relative-redirects: true protocol-header: x-forwarded-proto remote-ip-header: x-forwarded-for greater or less than v 2.2.x?

Comment From: superzjn

For those who are NOT using Spring 2.2.x, you can add these to application.yml. Verified it works. server: use-forward-headers: true tomcat: use-relative-redirects: true protocol-header: x-forwarded-proto remote-ip-header: x-forwarded-for

greater or less than v 2.2.x?

<2.2

Comment From: pawoua22

For those who are NOT using Spring 2.2.x, you can add these to application.yml. Verified it works. server: use-forward-headers: true tomcat: use-relative-redirects: true protocol-header: x-forwarded-proto remote-ip-header: x-forwarded-for

greater or less than v 2.2.x?

<2.2

OK. I'm on 2.1.3 and it does not work.

Comment From: PatelVishalJ

For those who are NOT using Spring 2.2.x, you can add these to application.yml. Verified it works.

server: use-forward-headers: true tomcat: use-relative-redirects: true protocol-header: x-forwarded-proto remote-ip-header: x-forwarded-for

Works for me. I am no spring boot 2.1.4