Authenticate with external accounts using OIDC

After successful authentication at the provider, an account is
automatically created at docspell and the user is logged in.
This commit is contained in:
eikek
2021-09-05 21:39:09 +02:00
parent 7edb96a297
commit f8362329a9
13 changed files with 382 additions and 75 deletions

View File

@ -23,6 +23,8 @@ import scodec.bits.ByteVector
trait Login[F[_]] {
def loginExternal(config: Config)(accountId: AccountId): F[Result]
def loginSession(config: Config)(sessionKey: String): F[Result]
def loginUserPass(config: Config)(up: UserPass): F[Result]
@ -93,6 +95,16 @@ object Login {
private val logF = Logger.log4s(logger)
def loginExternal(config: Config)(accountId: AccountId): F[Result] =
for {
data <- store.transact(QLogin.findUser(accountId))
_ <- logF.trace(s"Account lookup: $data")
res <-
if (data.exists(checkNoPassword(_, Set(AccountSource.OpenId))))
doLogin(config, accountId, false)
else Result.invalidAuth.pure[F]
} yield res
def loginSession(config: Config)(sessionKey: String): F[Result] =
AuthToken.fromString(sessionKey) match {
case Right(at) =>
@ -110,24 +122,11 @@ object Login {
def loginUserPass(config: Config)(up: UserPass): F[Result] =
AccountId.parse(up.user) match {
case Right(acc) =>
val okResult =
for {
require2FA <- store.transact(RTotp.isEnabled(acc))
_ <-
if (require2FA) ().pure[F]
else store.transact(RUser.updateLogin(acc))
token <- AuthToken.user(acc, require2FA, config.serverSecret)
rem <- OptionT
.whenF(!require2FA && up.rememberMe && config.rememberMe.enabled)(
insertRememberToken(store, acc, config)
)
.value
} yield Result.ok(token, rem)
for {
data <- store.transact(QLogin.findUser(acc))
_ <- Sync[F].delay(logger.trace(s"Account lookup: $data"))
res <-
if (data.exists(check(up.pass))) okResult
if (data.exists(check(up.pass))) doLogin(config, acc, up.rememberMe)
else Result.invalidAuth.pure[F]
} yield res
case Left(_) =>
@ -247,6 +246,24 @@ object Login {
0.pure[F]
}
private def doLogin(
config: Config,
acc: AccountId,
rememberMe: Boolean
): F[Result] =
for {
require2FA <- store.transact(RTotp.isEnabled(acc))
_ <-
if (require2FA) ().pure[F]
else store.transact(RUser.updateLogin(acc))
token <- AuthToken.user(acc, require2FA, config.serverSecret)
rem <- OptionT
.whenF(!require2FA && rememberMe && config.rememberMe.enabled)(
insertRememberToken(store, acc, config)
)
.value
} yield Result.ok(token, rem)
private def insertRememberToken(
store: Store[F],
acc: AccountId,

View File

@ -9,6 +9,7 @@ package docspell.backend.ops
import cats.effect.{Async, Resource}
import cats.implicits._
import fs2.Stream
import docspell.backend.JobFactory
import docspell.backend.PasswordCrypt
import docspell.backend.ops.OCollective._
@ -19,6 +20,7 @@ import docspell.store.queue.JobQueue
import docspell.store.records._
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
import docspell.store.{AddResult, Store}
import com.github.eikek.calev._
trait OCollective[F[_]] {

View File

@ -1,3 +1,9 @@
/*
* Copyright 2020 Docspell Contributors
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package docspell.backend.signup
import docspell.common._
@ -11,3 +17,9 @@ final case class ExternalAccount(
def toAccountId: AccountId =
AccountId(collName, login)
}
object ExternalAccount {
def apply(accountId: AccountId): ExternalAccount =
ExternalAccount(accountId.collective, accountId.user, AccountSource.OpenId)
}

View File

@ -8,11 +8,13 @@ package docspell.backend.signup
import cats.effect.{Async, Resource}
import cats.implicits._
import docspell.backend.PasswordCrypt
import docspell.common._
import docspell.common.syntax.all._
import docspell.store.records.{RCollective, RInvitation, RUser}
import docspell.store.{AddResult, Store}
import doobie.free.connection.ConnectionIO
import org.log4s.getLogger
@ -83,23 +85,29 @@ object OSignup {
SignupResult.signupClosed.pure[F]
case _ =>
if (data.source == AccountSource.Local)
SignupResult.failure(new Exception("Account source must not be LOCAL!")).pure[F]
else for {
recs <- makeRecords(data.collName, data.login, Password(""), data.source)
cres <- store.add(RCollective.insert(recs._1), RCollective.existsById(data.collName))
ures <- store.add(RUser.insert(recs._2), RUser.exists(data.login))
res = cres match {
case AddResult.Failure(ex) =>
SignupResult.failure(ex)
case _ =>
ures match {
case AddResult.Failure(ex) =>
SignupResult.failure(ex)
case _ =>
SignupResult.success
}
}
} yield res
SignupResult
.failure(new Exception("Account source must not be LOCAL!"))
.pure[F]
else
for {
recs <- makeRecords(data.collName, data.login, Password(""), data.source)
cres <- store.add(
RCollective.insert(recs._1),
RCollective.existsById(data.collName)
)
ures <- store.add(RUser.insert(recs._2), RUser.exists(data.login))
res = cres match {
case AddResult.Failure(ex) =>
SignupResult.failure(ex)
case _ =>
ures match {
case AddResult.Failure(ex) =>
SignupResult.failure(ex)
case _ =>
SignupResult.success
}
}
} yield res
}
private def retryInvite(res: SignupResult): Boolean =

View File

@ -1,3 +1,9 @@
/*
* Copyright 2020 Docspell Contributors
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package docspell.backend.signup
import docspell.common._