Matt Raible
Matt Raible

Reputation: 8614

How do I force HTTPS for a Spring Boot app on Heroku?

I tried using FORCE_HTTPS=true and SECURITY_REQUIRE_SSL=true as config variables, but neither works. I know that the former is supported by Cloud Foundry, but I've confirmed with Heroku that they do not support it. The SECURITY_REQUIRE_SSL property is supported by Spring Boot, but maybe only for basic auth?

Upvotes: 4

Views: 2836

Answers (3)

If you're using spring boot, you have to add dependency to your project

for maven

<dependency>
  <groupId>io.github.createam-labs</groupId>
  <artifactId>spring-boot-starter-heroku</artifactId>
  <version>1.1</version>
</dependency>

or for gradle

compile('io.github.createam-labs:spring-boot-starter-heroku:1.1')

and then enable https enforcing in application.properties file by adding following line

heroku.enforceHttps=true

For more information you can visit my project site https://github.com/createam-labs/spring-boot-starter-heroku

Upvotes: 2

Stefan Falk
Stefan Falk

Reputation: 25387

According to the docs on heroku this should be all you need:

@Configuration
public class WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.requiresChannel()
      .requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
      .requiresSecure();
  }
}

Force the use of HTTPS

Unless you have very specific needs, your app should be using HTTPS for all requests. Heroku provides an HTTPS URL (in the form https://.herokuapp.com) for every app, as well as free tools for adding your own domains and certificates.

You can enforce the use of HTTPS when your app is running on Heroku by adding the following configuration to your Spring Boot app.

Upvotes: 3

Matt Raible
Matt Raible

Reputation: 8614

I was able to fix this by creating an HttpEnforcer filter:

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HttpsEnforcer implements Filter {

    private FilterConfig filterConfig;

    public static final String X_FORWARDED_PROTO = "x-forwarded-proto";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        if (request.getHeader(X_FORWARDED_PROTO) != null) {
            if (request.getHeader(X_FORWARDED_PROTO).indexOf("https") != 0) {
                String pathInfo = (request.getPathInfo() != null) ? request.getPathInfo() : "";
                response.sendRedirect("https://" + request.getServerName() + pathInfo);
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // nothing
    }
}

And registering it in an existing @Configuration class.

@Bean
public Filter httpsEnforcerFilter(){
    return new HttpsEnforcer();
}

This is different from the solution I posted in the comments above because of the null check for pathInfo. Without this, it still works, but the Location does show up with null at the end.

$ curl -i http://www.21-points.com
HTTP/1.1 302 Found
Server: Cowboy
Connection: keep-alive
Expires: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
X-Xss-Protection: 1; mode=block
Pragma: no-cache
Location: https://www.21-points.comnull
Date: Tue, 31 Oct 2017 14:33:26 GMT
X-Content-Type-Options: nosniff
Content-Length: 0
Via: 1.1 vegur

Upvotes: 11

Related Questions