Mike Roberts
Mike Roberts

Reputation: 550

How to force Play framework 2 to always use SSL?

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?

Upvotes: 16

Views: 4792

Answers (5)

Al-Mothafar
Al-Mothafar

Reputation: 8219

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.

Upvotes: 2

grannygearGA
grannygearGA

Reputation: 1

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.

Upvotes: 0

Dimitry
Dimitry

Reputation: 4593

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)
        }
    }
}

Upvotes: 12

plade
plade

Reputation: 541

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);
}

Upvotes: 2

johanandren
johanandren

Reputation: 11479

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.

Upvotes: 4

Related Questions