~jan0sch/smederee
Showing details for patch 8e784521934e446e29af5e7b537dfec17fcf6357.
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala 2025-01-16 09:54:51.101214182 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala 2025-01-16 09:54:51.101214182 +0000 @@ -22,6 +22,7 @@ import cats.data._ import cats.effect._ import cats.syntax.all._ +import de.smederee.html.ExternalUrlConfiguration import de.smederee.hub.RequestHelpers.instances.given_RequestHelpers_Request import de.smederee.hub.SessionHelpers.instances.toAuthenticationCookie import de.smederee.hub._ @@ -59,6 +60,9 @@ * A clock to generate nounces for cryptograpic operations. * @param authenticationConfig * The configuration for the authentication module. + * @param external + * Settings affecting how the service will communicate several information to the "outside world" e.g. if it runs + * behind a reverse proxy. * @param repo * The database repository providing needed functionality. * @param signAndValidate @@ -69,6 +73,7 @@ final class AuthenticationRoutes[F[_]: Async]( clock: java.time.Clock, authenticationConfig: AuthenticationConfiguration, + external: ExternalUrlConfiguration, repo: AuthenticationRepository[F], signAndValidate: SignAndValidate ) extends Http4sDsl[F] { @@ -152,7 +157,7 @@ signAndValidate.signToken(session.id.toString)(clock.millis.toString) ) response <- SeeOther(Location(Uri(path = Uri.Path.Root))) - .map(_.addCookie(token.toAuthenticationCookie(None)(secure = true))) + .map(_.addCookie(token.toAuthenticationCookie(external.host.toString.some)(None)(secure = true))) } yield response } } yield response diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala 2025-01-16 09:54:51.101214182 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala 2025-01-16 09:54:51.101214182 +0000 @@ -214,6 +214,7 @@ authenticationRoutes = new AuthenticationRoutes[IO]( cryptoClock, hubConfiguration.service.authentication, + hubConfiguration.service.external, authenticationRepo, signAndValidate ) diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala 2025-01-16 09:54:51.101214182 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala 2025-01-16 09:54:51.101214182 +0000 @@ -51,6 +51,10 @@ * cookie will be using a strict same-site policy, be http-only and respect the given values for expiration and * the secure flag. * + * @param domain + * An optional domain that shall be set for the cookie. If it is not set then a "host only" cookie will be + * created which is in most cases what you want. Setting a specific domain can be necessary if you want to + * share cookies between a domain and possible existing sub domains. * @param expires * A duration in which the cookie shall expire. It is optional resulting in the cookie being a non-persistent * "session cookie" if not set. @@ -60,23 +64,20 @@ * @return * A response cookie ready to be used as an authentication cookie. */ - def toAuthenticationCookie(expires: Option[FiniteDuration])(secure: Boolean): ResponseCookie = { - val cookie = - ResponseCookie( - name = Constants.authenticationCookieName.toString, - content = signedToken.toString, - sameSite = Option(SameSite.Strict), - secure = secure, - httpOnly = true - ) - expires.fold(cookie)(duration => - cookie.copy(expires = - HttpDate - .fromZonedDateTime(ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(duration.toSeconds)) - .toOption - ) + def toAuthenticationCookie( + domain: Option[String] + )(expires: Option[FiniteDuration])(secure: Boolean): ResponseCookie = + ResponseCookie( + name = Constants.authenticationCookieName.toString, + content = signedToken.toString, + expires = expires.flatMap(duration => + HttpDate.fromZonedDateTime(ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(duration.toSeconds)).toOption + ), + domain = domain, + sameSite = Option(SameSite.Strict), + secure = secure, + httpOnly = true ) - } } } } diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala 2025-01-16 09:54:51.101214182 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala 2025-01-16 09:54:51.101214182 +0000 @@ -48,11 +48,14 @@ val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external val repo = new TestAuthenticationRepository[IO](List.empty, List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, externalConfig, repo, signAndValidate).routes + ) def request = Request[IO](method = Method.GET, uri = loginPath) @@ -73,11 +76,14 @@ test("POST /login must return 415 - Unsupported Media Type if the request is malformed") { val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external val repo = new TestAuthenticationRepository[IO](List.empty, List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, externalConfig, repo, signAndValidate).routes + ) val payload = "This is not the request body you are looking for.".getBytes(StandardCharsets.UTF_8) def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) @@ -106,11 +112,14 @@ val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external val repo = new TestAuthenticationRepository[IO](List.empty, List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, externalConfig, repo, signAndValidate).routes + ) val payload = UrlForm.empty def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) @@ -135,11 +144,20 @@ case Some(account) => val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external val repo = new TestAuthenticationRepository[IO](List(account), List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) val payload = UrlForm.empty .updateFormField(LoginForm.fieldName.toString, account.name.toString) @@ -177,13 +195,22 @@ case Some(account) => val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external val repo = new TestAuthenticationRepository[IO](List(account), List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) for (_ <- 0 to authenticationConfig.lockAfter.toInt) yield repo.incrementFailedAttempts(account.uid) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) val payload = UrlForm.empty .updateFormField(LoginForm.fieldName.toString, account.name.toString) @@ -219,12 +246,21 @@ case Some(account) => val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external // A locked account is not supposed to be returned by findAccount, so we leave the account list empty. val repo = new TestAuthenticationRepository[IO](List.empty, List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) val payload = UrlForm.empty .updateFormField(LoginForm.fieldName.toString, account.name.toString) @@ -258,11 +294,20 @@ case Some(account) => val clock = java.time.Clock.systemUTC val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external val repo = new TestAuthenticationRepository[IO](List(account), List.empty) val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) def service: HttpRoutes[IO] = - Router("/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, repo, signAndValidate).routes) + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) val payload = UrlForm.empty .updateFormField(LoginForm.fieldName.toString, account.name.toString) diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala 2025-01-16 09:54:51.101214182 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala 2025-01-16 09:54:51.101214182 +0000 @@ -50,10 +50,12 @@ signAndValidate: SignAndValidate, token: String ) => + val domain = Option("example.com") val signedToken = signAndValidate.signToken(token)(nonce) - val obtained = signedToken.toAuthenticationCookie(expires)(secure) + val obtained = signedToken.toAuthenticationCookie(domain)(expires)(secure) assertEquals(obtained.name, Constants.authenticationCookieName.toString) assertEquals(obtained.content, signedToken.toString) + assertEquals(obtained.domain, domain) assertEquals(obtained.sameSite, Option(SameSite.Strict)) assertEquals(obtained.secure, secure) assertEquals(obtained.httpOnly, true)