Initial impl for totp

This commit is contained in:
eikek
2021-08-30 16:15:13 +02:00
parent 2b46cc7970
commit 309a52393a
17 changed files with 568 additions and 20 deletions

View File

@ -76,6 +76,7 @@ object RestServer {
"organization" -> OrganizationRoutes(restApp.backend, token),
"person" -> PersonRoutes(restApp.backend, token),
"source" -> SourceRoutes(restApp.backend, token),
"user/otp" -> TotpRoutes(restApp.backend, cfg, token),
"user" -> UserRoutes(restApp.backend, token),
"collective" -> CollectiveRoutes(restApp.backend, token),
"queue" -> JobQueueRoutes(restApp.backend, token),
@ -109,6 +110,7 @@ object RestServer {
def adminRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
Router(
"fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend),
"user/otp" -> TotpRoutes.admin(restApp.backend),
"user" -> UserRoutes.admin(restApp.backend),
"info" -> InfoRoutes.admin(cfg),
"attachments" -> AttachmentRoutes.admin(restApp.backend)

View File

@ -82,7 +82,8 @@ object LoginRoutes {
true,
"Login successful",
Some(cd.asString),
cfg.auth.sessionValid.millis
cfg.auth.sessionValid.millis,
token.requireSecondFactor
)
).map(cd.addCookie(getBaseUrl(cfg, req)))
.map(resp =>
@ -93,7 +94,7 @@ object LoginRoutes {
} yield resp
case _ =>
Ok(AuthResult("", account, false, "Login failed.", None, 0L))
Ok(AuthResult("", account, false, "Login failed.", None, 0L, false))
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2020 Docspell Contributors
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package docspell.restserver.routes
import cats.effect._
import cats.implicits._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.backend.ops.OTotp
import docspell.restapi.model._
import docspell.restserver.Config
import docspell.restserver.conv.Conversions
import docspell.totp.OnetimePassword
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl
object TotpRoutes {
def apply[F[_]: Async](
backend: BackendApp[F],
cfg: Config,
user: AuthToken
): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of {
case GET -> Root / "state" =>
for {
result <- backend.totp.state(user.account)
resp <- Ok(
result.fold(en => OtpState(true, en.created.some), _ => OtpState(false, None))
)
} yield resp
case POST -> Root / "init" =>
for {
result <- backend.totp.initialize(user.account)
resp <- result match {
case OTotp.InitResult.AlreadyExists =>
UnprocessableEntity(BasicResult(false, "A totp setup already exists!"))
case OTotp.InitResult.NotFound =>
NotFound(BasicResult(false, "User not found"))
case OTotp.InitResult.Failed(ex) =>
InternalServerError(BasicResult(false, ex.getMessage))
case s @ OTotp.InitResult.Success(_, key) =>
val issuer = cfg.appName
val uri = s.authenticatorUrl(issuer)
Ok(OtpResult(uri, key.data.toBase32, "totp", issuer))
}
} yield resp
case req @ POST -> Root / "confirm" =>
for {
data <- req.as[OtpConfirm]
result <- backend.totp.confirmInit(user.account, OnetimePassword(data.otp.pass))
resp <- result match {
case OTotp.ConfirmResult.Success =>
Ok(BasicResult(true, "TOTP setup successful."))
case OTotp.ConfirmResult.Failed =>
Ok(BasicResult(false, "TOTP setup failed!"))
}
} yield resp
case POST -> Root / "disable" =>
for {
result <- backend.totp.disable(user.account)
resp <- Ok(Conversions.basicResult(result, "TOTP setup disabled."))
} yield resp
}
}
def admin[F[_]: Async](backend: BackendApp[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of { case req @ POST -> Root / "resetOTP" =>
for {
data <- req.as[ResetPassword]
result <- backend.totp.disable(data.account)
resp <- Ok(Conversions.basicResult(result, "TOTP setup disabled."))
} yield resp
}
}
}

View File

@ -39,6 +39,10 @@
<script type="application/javascript">
var storedAccount = localStorage.getItem('account');
var account = storedAccount ? JSON.parse(storedAccount) : null;
if (account && !account.hasOwnProperty("requireSecondFactor")) {
// this is required for transitioning; elm fails to parse the account
account["requireSecondFactor"] = false;
}
var elmFlags = {
"account": account,
"config": {{{flagsJson}}}