Use remember-me cookie if present

This commit is contained in:
Eike Kettner
2020-12-04 00:51:56 +01:00
parent c10c1fad72
commit a0642905db
13 changed files with 217 additions and 59 deletions

View File

@ -57,7 +57,7 @@ docspell.server {
remember-me {
enabled = true
# How long the remember me cookie/token is valid.
valid = "3 month"
valid = "21 days"
}
}

View File

@ -64,7 +64,7 @@ object RestServer {
token: AuthToken
): HttpRoutes[F] =
Router(
"auth" -> LoginRoutes.session(restApp.backend.login, cfg),
"auth" -> LoginRoutes.session(restApp.backend.login, cfg, token),
"tag" -> TagRoutes(restApp.backend, token),
"equipment" -> EquipmentRoutes(restApp.backend, token),
"organization" -> OrganizationRoutes(restApp.backend, token),

View File

@ -23,6 +23,10 @@ case class CookieData(auth: AuthToken) {
secure = sec
)
}
def addCookie[F[_]](baseUrl: LenientUri)(resp: Response[F]): Response[F] =
resp.addCookie(asCookie(baseUrl))
}
object CookieData {
val cookieName = "docspell_auth"

View File

@ -0,0 +1,51 @@
package docspell.restserver.auth
import docspell.backend.auth._
import docspell.common._
import org.http4s._
case class RememberCookieData(token: RememberToken) {
def asString: String = token.asString
def asCookie(config: Login.RememberMe, baseUrl: LenientUri): ResponseCookie = {
val sec = baseUrl.scheme.exists(_.endsWith("s"))
val path = baseUrl.path / "api" / "v1"
ResponseCookie(
name = RememberCookieData.cookieName,
content = asString,
domain = None,
path = Some(path.asString),
httpOnly = true,
secure = sec,
maxAge = Some(config.valid.seconds)
)
}
def addCookie[F[_]](cfg: Login.RememberMe, baseUrl: LenientUri)(
resp: Response[F]
): Response[F] =
resp.addCookie(asCookie(cfg, baseUrl))
}
object RememberCookieData {
val cookieName = "docspell_remember"
def fromCookie[F[_]](req: Request[F]): Option[String] =
for {
header <- headers.Cookie.from(req.headers)
cookie <- header.values.toList.find(_.name == cookieName)
} yield cookie.content
def delete(baseUrl: LenientUri): ResponseCookie =
ResponseCookie(
cookieName,
"",
domain = None,
path = Some(baseUrl.path / "api" / "v1").map(_.asString),
httpOnly = true,
secure = baseUrl.scheme.exists(_.endsWith("s")),
maxAge = Some(-1)
)
}

View File

@ -15,11 +15,19 @@ import org.http4s.server._
object Authenticate {
def authenticateRequest[F[_]: Effect](
auth: String => F[Login.Result]
auth: (String, Option[String]) => F[Login.Result]
)(req: Request[F]): F[Login.Result] =
CookieData.authenticator(req) match {
case Right(str) => auth(str)
case Left(_) => Login.Result.invalidAuth.pure[F]
case Right(str) =>
val rememberMe = RememberCookieData.fromCookie(req)
auth(str, rememberMe)
case Left(_) =>
RememberCookieData.fromCookie(req) match {
case Some(rc) =>
auth("", rc.some)
case None =>
Login.Result.invalidAuth.pure[F]
}
}
def of[F[_]: Effect](S: Login[F], cfg: Login.Config)(
@ -28,7 +36,7 @@ object Authenticate {
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
import dsl._
val authUser = getUser[F](S.loginSession(cfg))
val authUser = getUser[F](S.loginSessionOrRememberMe(cfg))
val onFailure: AuthedRoutes[String, F] =
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
@ -45,7 +53,7 @@ object Authenticate {
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
import dsl._
val authUser = getUser[F](S.loginSession(cfg))
val authUser = getUser[F](S.loginSessionOrRememberMe(cfg))
val onFailure: AuthedRoutes[String, F] =
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
@ -57,7 +65,7 @@ object Authenticate {
}
private def getUser[F[_]: Effect](
auth: String => F[Login.Result]
auth: (String, Option[String]) => F[Login.Result]
): Kleisli[F, Request[F], Either[String, AuthToken]] =
Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
}

View File

@ -32,18 +32,24 @@ object LoginRoutes {
}
}
def session[F[_]: Effect](S: Login[F], cfg: Config): HttpRoutes[F] = {
def session[F[_]: Effect](S: Login[F], cfg: Config, token: AuthToken): HttpRoutes[F] = {
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of[F] {
case req @ POST -> Root / "session" =>
Authenticate
.authenticateRequest(S.loginSession(cfg.auth))(req)
AuthToken
.update(token, cfg.auth.serverSecret)
.map(newToken => Login.Result.ok(newToken, None))
.flatMap(res => makeResponse(dsl, cfg, req, res, ""))
case req @ POST -> Root / "logout" =>
Ok().map(_.addCookie(CookieData.deleteCookie(getBaseUrl(cfg, req))))
for {
_ <- RememberCookieData.fromCookie(req).traverse(S.removeRememberToken)
res <- Ok()
} yield res
.removeCookie(CookieData.deleteCookie(getBaseUrl(cfg, req)))
.removeCookie(RememberCookieData.delete(getBaseUrl(cfg, req)))
}
}
@ -59,9 +65,10 @@ object LoginRoutes {
): F[Response[F]] = {
import dsl._
res match {
case Login.Result.Ok(token) =>
case Login.Result.Ok(token, remember) =>
val cd = CookieData(token)
val rem = remember.map(RememberCookieData.apply)
for {
cd <- AuthToken.user(token.account, cfg.auth.serverSecret).map(CookieData.apply)
resp <- Ok(
AuthResult(
token.account.collective.id,
@ -71,7 +78,13 @@ object LoginRoutes {
Some(cd.asString),
cfg.auth.sessionValid.millis
)
).map(_.addCookie(cd.asCookie(getBaseUrl(cfg, req))))
).map(cd.addCookie(getBaseUrl(cfg, req)))
.map(resp =>
rem
.map(_.addCookie(cfg.auth.rememberMe, getBaseUrl(cfg, req))(resp))
.getOrElse(resp)
)
} yield resp
case _ =>
Ok(AuthResult("", account, false, "Login failed.", None, 0L))