How to force Play framework 2 to always use SSL?
Asked Answered
S

5

16

I have a Play Framework app running on Heroku, using Heroku's SSL endpoint.

I would like to make all pages available via SSL only.

What's the best way to that?

So far, my best solution is to use onRouteRequest in my GlobalSettings and route non-SSL requests to a special redirect handler:

override def onRouteRequest(request: RequestHeader): Option[Handler] = {
  if (Play.isProd && !request.headers.get("x-forwarded-proto").getOrElse("").contains("https")) {
    Some(controllers.Secure.redirect)
  } else {
    super.onRouteRequest(request)
  }
}

and

package controllers

import play.api.mvc._

object Secure extends Controller {

  def redirect = Action { implicit request =>
    MovedPermanently("https://" + request.host + request.uri)
  }
}

Is there a way to do this entirely from within GlobalSettings? Or something even better?

Sobriquet answered 2/10, 2013 at 21:28 Comment(3)
You can turn off non-ssl by starting with -Dhttp.port=disabled if that's what you're looking for: playframework.com/documentation/2.2.x/ConfiguringHttpsEgghead
From Play's point of view, I'm not using SSL. Heroku's load balancer is proxying the SSL to straight HTTP for the app.Sobriquet
was there any update on this? I ended up doing something similar but I would've preffered the disabled http but I couldn't get it to work.Generalist
P
4

We have done that much like you but with a play filter that generates a MovedPermanently instead of a controller method.

I don't think there is a better way with heroku, or at least we couldn't find any feature to disable unencrypted HTTP.

Powered answered 4/10, 2013 at 12:49 Comment(2)
How do you force a redirect from a filter?Sobriquet
Just return a Result (or Future[SimpleResult] in play 2.2) instead of invoking the next action/essentialactionPowered
V
12

Here is another way using filter. This also uses strict transport security to make sure that future requests go to https.

object HTTPSRedirectFilter extends Filter with Logging {

    def apply(nextFilter: (RequestHeader) => Future[SimpleResult])(requestHeader: RequestHeader): Future[SimpleResult] = {
        //play uses lower case headers.
        requestHeader.headers.get("x-forwarded-proto") match {
            case Some(header) => {
                if ("https" == header) {
                    nextFilter(requestHeader).map { result =>
                        result.withHeaders(("Strict-Transport-Security", "max-age=31536000"))
                    }
                } else {
                    Future.successful(Results.Redirect("https://" + requestHeader.host + requestHeader.uri, 301))
                }
            }
            case None => nextFilter(requestHeader)
        }
    }
}
Vday answered 19/5, 2014 at 13:43 Comment(2)
Why did you add the result.withHeaders(("Strict-Transport-Security", "max-age=31536000"))? Is this a mandatory header?Jit
You can read about it more on my blogVday
P
4

We have done that much like you but with a play filter that generates a MovedPermanently instead of a controller method.

I don't think there is a better way with heroku, or at least we couldn't find any feature to disable unencrypted HTTP.

Powered answered 4/10, 2013 at 12:49 Comment(2)
How do you force a redirect from a filter?Sobriquet
Just return a Result (or Future[SimpleResult] in play 2.2) instead of invoking the next action/essentialactionPowered
C
2

Here is the solution for the Java version of Play Framework.

Add the following to the Global.java file:

@Override
public Handler onRouteRequest(RequestHeader request) {
    String[] x = request.headers().get("X-Forwarded-Proto");
    if (Play.isProd() && (x == null || x.length == 0 || x[0] == null || !x[0].contains("https")))
        return controllers.Default.redirect("https://" + request.host() + request.uri());
    return super.onRouteRequest(request);
}
Cayser answered 18/3, 2014 at 19:49 Comment(2)
Unfortunatelly, I am not getting anything in X-Forwarded-Proto. Probably due to the front HTTP server setup.Froghopper
This cannot work as controllers.Default.redirect() is a non-static method.Manipulator
C
2

In Play 2.5.x Java (I think should work in play 2.4.x as well but using Promise.F instead of CompletionStage), I added a filter:

public class TLSFilter extends Filter {
    @Inject
    public TLSFilter(Materializer mat) {
        super(mat);
    }

    @Override
    public CompletionStage<Result> apply(Function<Http.RequestHeader, CompletionStage<Result>> next, Http.RequestHeader rh) {
        if (Play.current().isProd()) {
            String[] httpsHeader = rh.headers().getOrDefault(Http.HeaderNames.X_FORWARDED_PROTO, new String[]{"http"});
            if (Strings.isNullOrEmpty(httpsHeader[0]) || httpsHeader[0].equalsIgnoreCase("http")) {
                return CompletableFuture.completedFuture(Results.movedPermanently("https://".concat(rh.host().concat(rh.uri()))));
            }
        }
        return next.apply(rh).toCompletableFuture();
    }
}

And then add it to the list of filters to use it:

public class AppFilters extends DefaultHttpFilters {

    @Inject
    public AppFilters(TLSFilter tlsFilter, GzipFilter gzipFilter) {
        super(tlsFilter, gzipFilter);
    }
}

And then to use your filters add the following inside application.conf :

play.http.filters = "filters.AppFilters"

And please note, if you have a request handler enabled (look for play.http.requestHandler inside the application.conf file), filters will not work, I suggest to handle requests using filters and remove your current requestHandler.

Coauthor answered 3/1, 2017 at 8:12 Comment(0)
D
0

I had a similar requirement using Play 2.2.3. I'm running my application behind a load balancer which is doing the SSL termination, and wanted all HTTP requests to be redirected to SSL. In my case, my load balancer (Amazon ELB) adds the X-Forwarded-Proto header, so I keyed off it as follows. In Global.java (or whatever class you've got that extends GlobalSettings), add this method:

@SuppressWarnings("rawtypes")
@Override
public Action onRequest(Request actionRequest, Method actionMethod) {

  String forwardedProtocol = actionRequest.getHeader("X-Forwarded-Proto");

  if (StringUtils.equalsIgnoreCase(forwardedProtocol, "http")) {

    // Redirect to HTTPS
    final String secureURL = "https://" + actionRequest.host() + actionRequest.uri();
    return new Action.Simple() {
      @Override
      public Promise<play.mvc.SimpleResult> call(Context ctx) throws Throwable {
        return Promise.pure(Results.movedPermanently(secureURL));
      }
    };

  } else {

    return super.onRequest(actionRequest, actionMethod);

  }
}

This doesn't handle custom ports, so some might implementations might require a little extra coding.

Demagoguery answered 16/7, 2014 at 9:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.