diff --git a/README.md b/README.md index 0a89d7533..3472dad4f 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,8 @@ The following configuration properties are supported. Please refer to the [Wiki] | -------- | ----------- | ------- | | `broccoli.auth.mode` | Authentication and authorization mode (`none` or `conf`). | `none` | | `broccoli.auth.conf.accounts` | User accounts when running in `conf` mode. | `[{username:administrator, password:broccoli, role:administrator, instanceRegex:".*"}]` | -| `broccoli.auth.session.timeout` | HTTPS session timeout (in seconds). | `3600` | +| `broccoli.auth.session.timeout` | Cookie session timeout (in seconds). | `3600` | +| `broccoli.auth.cookie.secure` | Whether to mark the cookie as secure (switch off if running on HTTP). | `true` | ### Web Server diff --git a/app/de/frosner/broccoli/conf/package.scala b/app/de/frosner/broccoli/conf/package.scala index bc487df1c..c69b27ff0 100644 --- a/app/de/frosner/broccoli/conf/package.scala +++ b/app/de/frosner/broccoli/conf/package.scala @@ -41,6 +41,9 @@ package object conf { val AUTH_SESSION_TIMEOUT_KEY = "broccoli.auth.session.timeout" val AUTH_SESSION_TIMEOUT_DEFAULT = 3600 + val AUTH_COOKIE_SECURE_KEY = "broccoli.auth.cookie.secure" + val AUTH_COOKIE_SECURE_DEFAULT = true + val AUTH_MODE_KEY = "broccoli.auth.mode" val AUTH_MODE_NONE = "none" val AUTH_MODE_CONF = "conf" diff --git a/app/de/frosner/broccoli/controllers/AuthConfigImpl.scala b/app/de/frosner/broccoli/controllers/AuthConfigImpl.scala index a087bfa20..5f1c97e35 100644 --- a/app/de/frosner/broccoli/controllers/AuthConfigImpl.scala +++ b/app/de/frosner/broccoli/controllers/AuthConfigImpl.scala @@ -27,6 +27,8 @@ trait AuthConfigImpl extends AuthConfig with Logging { val sessionTimeoutInSeconds = securityService.sessionTimeoutInSeconds + val cookieSecure = securityService.cookieSecure + def resolveUser(id: Id)(implicit ctx: ExecutionContext): Future[Option[User]] = Future.successful(securityService.getAccount(id)) def loginSucceeded(request: RequestHeader)(implicit ctx: ExecutionContext): Future[Result] = @@ -52,7 +54,7 @@ trait AuthConfigImpl extends AuthConfig with Logging { override lazy val tokenAccessor = new CookieTokenAccessor( cookieName = AuthConfigImpl.CookieName, - cookieSecureOption = play.api.Play.maybeApplication.exists(app => play.api.Play.isProd(app)), + cookieSecureOption = play.api.Play.maybeApplication.exists(app => play.api.Play.isProd(app) && cookieSecure), cookieHttpOnlyOption = true, cookieDomainOption = None, cookiePathOption = "/", diff --git a/app/de/frosner/broccoli/services/SecurityService.scala b/app/de/frosner/broccoli/services/SecurityService.scala index 666974690..65920f52e 100644 --- a/app/de/frosner/broccoli/services/SecurityService.scala +++ b/app/de/frosner/broccoli/services/SecurityService.scala @@ -51,6 +51,18 @@ case class SecurityService @Inject() (configuration: Configuration) extends Logg result } + lazy val cookieSecure: Boolean = { + val parsed = SecurityService.tryCookieSecure(configuration) match { + case Success(cookieSecure) => cookieSecure + case Failure(throwable) => + Logger.error(s"Error parsing ${conf.AUTH_COOKIE_SECURE_KEY}: $throwable") + System.exit(1) + throw throwable + } + Logger.info(s"${conf.AUTH_COOKIE_SECURE_KEY}=$parsed") + parsed + } + private lazy val accounts: Set[Account] = { val accounts = SecurityService.tryAccounts(configuration) match { case Success(userObjects) => userObjects.toSet @@ -103,4 +115,8 @@ object SecurityService { }.getOrElse(conf.AUTH_MODE_CONF_ACCOUNTS_DEFAULT) } + private[services] def tryCookieSecure(configuration: Configuration): Try[Boolean] = Try { + configuration.getBoolean(conf.AUTH_COOKIE_SECURE_KEY).getOrElse(conf.AUTH_COOKIE_SECURE_DEFAULT) + } + } diff --git a/test/de/frosner/broccoli/services/SecurityServiceSpec.scala b/test/de/frosner/broccoli/services/SecurityServiceSpec.scala index b238ce89a..671080377 100644 --- a/test/de/frosner/broccoli/services/SecurityServiceSpec.scala +++ b/test/de/frosner/broccoli/services/SecurityServiceSpec.scala @@ -182,4 +182,29 @@ class SecurityServiceSpec extends Specification { } + "Parsing cookie secure from the configuration" should { + + "work if the field is a boolean" in { + val config = ConfigFactory.empty().withValue( + conf.AUTH_COOKIE_SECURE_KEY, + ConfigValueFactory.fromAnyRef(false) + ) + SecurityService.tryCookieSecure(Configuration(config)) === Success(false) + } + + "fail if the field is not a boolean" in { + val config = ConfigFactory.empty().withValue( + conf.AUTH_COOKIE_SECURE_KEY, + ConfigValueFactory.fromAnyRef("bla") + ) + SecurityService.tryCookieSecure(Configuration(config)).failed.get should beAnInstanceOf[Exception] + } + + "take the default if the field is not defined" in { + val config = ConfigFactory.empty() + SecurityService.tryCookieSecure(Configuration(config)) === Success(conf.AUTH_COOKIE_SECURE_DEFAULT) + } + + } + }