mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-05 22:55:58 +00:00
Require a otp to disable 2fa
This commit is contained in:
parent
bbfc5b56d8
commit
e31107eb49
@ -18,13 +18,23 @@ import org.log4s.getLogger
|
|||||||
|
|
||||||
trait OTotp[F[_]] {
|
trait OTotp[F[_]] {
|
||||||
|
|
||||||
|
/** Return whether TOTP is enabled for this account or not. */
|
||||||
def state(accountId: AccountId): F[OtpState]
|
def state(accountId: AccountId): F[OtpState]
|
||||||
|
|
||||||
|
/** Initializes TOTP by generating a secret and storing it in the database. TOTP is
|
||||||
|
* still disabled, it must be confirmed in order to be active.
|
||||||
|
*/
|
||||||
def initialize(accountId: AccountId): F[InitResult]
|
def initialize(accountId: AccountId): F[InitResult]
|
||||||
|
|
||||||
|
/** Confirms and finishes initialization. TOTP is active after this for the given
|
||||||
|
* account.
|
||||||
|
*/
|
||||||
def confirmInit(accountId: AccountId, otp: OnetimePassword): F[ConfirmResult]
|
def confirmInit(accountId: AccountId, otp: OnetimePassword): F[ConfirmResult]
|
||||||
|
|
||||||
def disable(accountId: AccountId): F[UpdateResult]
|
/** Disables TOTP and removes the shared secret. If a otp is specified, it must be
|
||||||
|
* valid.
|
||||||
|
*/
|
||||||
|
def disable(accountId: AccountId, otp: Option[OnetimePassword]): F[UpdateResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
object OTotp {
|
object OTotp {
|
||||||
@ -133,8 +143,31 @@ object OTotp {
|
|||||||
}
|
}
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
def disable(accountId: AccountId): F[UpdateResult] =
|
def disable(accountId: AccountId, otp: Option[OnetimePassword]): F[UpdateResult] =
|
||||||
UpdateResult.fromUpdate(store.transact(RTotp.setEnabled(accountId, false)))
|
otp match {
|
||||||
|
case Some(pw) =>
|
||||||
|
for {
|
||||||
|
_ <- log.info(s"Validating TOTP, because it is requested to disable it.")
|
||||||
|
key <- store.transact(RTotp.findEnabledByLogin(accountId, true))
|
||||||
|
now <- Timestamp.current[F]
|
||||||
|
res <- key match {
|
||||||
|
case None =>
|
||||||
|
UpdateResult.failure(new Exception("TOTP not enabled.")).pure[F]
|
||||||
|
case Some(r) =>
|
||||||
|
val check = totp.checkPassword(r.secret, pw, now.value)
|
||||||
|
if (check)
|
||||||
|
UpdateResult.fromUpdate(
|
||||||
|
store.transact(RTotp.setEnabled(accountId, false))
|
||||||
|
)
|
||||||
|
else
|
||||||
|
log.info(s"TOTP code was invalid. Not disabling it.") *> UpdateResult
|
||||||
|
.failure(new Exception("Code invalid!"))
|
||||||
|
.pure[F]
|
||||||
|
}
|
||||||
|
} yield res
|
||||||
|
case None =>
|
||||||
|
UpdateResult.fromUpdate(store.transact(RTotp.setEnabled(accountId, false)))
|
||||||
|
}
|
||||||
|
|
||||||
def state(accountId: AccountId): F[OtpState] =
|
def state(accountId: AccountId): F[OtpState] =
|
||||||
for {
|
for {
|
||||||
|
@ -1442,11 +1442,18 @@ paths:
|
|||||||
summary: Disables two factor authentication.
|
summary: Disables two factor authentication.
|
||||||
description: |
|
description: |
|
||||||
Disables two factor authentication for the current user. If
|
Disables two factor authentication for the current user. If
|
||||||
the user has no two factor authentication enabled, this
|
the user has no two factor authentication enabled, an error is
|
||||||
returns success, too.
|
returned.
|
||||||
|
|
||||||
|
It requires to specify a valid otp.
|
||||||
|
|
||||||
After this completes successfully, two factor auth can be
|
After this completes successfully, two factor auth can be
|
||||||
enabled again by initializing it anew.
|
enabled again by initializing it anew.
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/OtpConfirm"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
|
@ -695,7 +695,7 @@ trait Conversions {
|
|||||||
case UpdateResult.Success => BasicResult(true, successMsg)
|
case UpdateResult.Success => BasicResult(true, successMsg)
|
||||||
case UpdateResult.NotFound => BasicResult(false, "Not found")
|
case UpdateResult.NotFound => BasicResult(false, "Not found")
|
||||||
case UpdateResult.Failure(ex) =>
|
case UpdateResult.Failure(ex) =>
|
||||||
BasicResult(false, s"Internal error: ${ex.getMessage}")
|
BasicResult(false, s"Error: ${ex.getMessage}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(ur: OUpload.UploadResult): BasicResult =
|
def basicResult(ur: OUpload.UploadResult): BasicResult =
|
||||||
|
@ -68,9 +68,13 @@ object TotpRoutes {
|
|||||||
}
|
}
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case POST -> Root / "disable" =>
|
case req @ POST -> Root / "disable" =>
|
||||||
for {
|
for {
|
||||||
result <- backend.totp.disable(user.account)
|
data <- req.as[OtpConfirm]
|
||||||
|
result <- backend.totp.disable(
|
||||||
|
user.account,
|
||||||
|
OnetimePassword(data.otp.pass).some
|
||||||
|
)
|
||||||
resp <- Ok(Conversions.basicResult(result, "TOTP setup disabled."))
|
resp <- Ok(Conversions.basicResult(result, "TOTP setup disabled."))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
@ -83,7 +87,7 @@ object TotpRoutes {
|
|||||||
HttpRoutes.of { case req @ POST -> Root / "resetOTP" =>
|
HttpRoutes.of { case req @ POST -> Root / "resetOTP" =>
|
||||||
for {
|
for {
|
||||||
data <- req.as[ResetPassword]
|
data <- req.as[ResetPassword]
|
||||||
result <- backend.totp.disable(data.account)
|
result <- backend.totp.disable(data.account, None)
|
||||||
resp <- Ok(Conversions.basicResult(result, "TOTP setup disabled."))
|
resp <- Ok(Conversions.basicResult(result, "TOTP setup disabled."))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
|
@ -2195,12 +2195,12 @@ confirmOtp flags confirm receive =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
disableOtp : Flags -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
disableOtp : Flags -> OtpConfirm -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||||
disableOtp flags receive =
|
disableOtp flags otp receive =
|
||||||
Http2.authPost
|
Http2.authPost
|
||||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/user/otp/disable"
|
{ url = flags.config.baseUrl ++ "/api/v1/sec/user/otp/disable"
|
||||||
, account = getAccount flags
|
, account = getAccount flags
|
||||||
, body = Http.emptyBody
|
, body = Http.jsonBody (Api.Model.OtpConfirm.encode otp)
|
||||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +58,8 @@ initDisabledModel =
|
|||||||
type alias EnabledModel =
|
type alias EnabledModel =
|
||||||
{ created : Int
|
{ created : Int
|
||||||
, loading : Bool
|
, loading : Bool
|
||||||
, confirmText : String
|
, confirmCode : String
|
||||||
, confirmTextWrong : Bool
|
, serverErrorMsg : String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -67,8 +67,8 @@ initEnabledModel : Int -> EnabledModel
|
|||||||
initEnabledModel created =
|
initEnabledModel created =
|
||||||
{ created = created
|
{ created = created
|
||||||
, loading = False
|
, loading = False
|
||||||
, confirmText = ""
|
, confirmCode = ""
|
||||||
, confirmTextWrong = False
|
, serverErrorMsg = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ type Msg
|
|||||||
| SecretMsg Comp.PasswordInput.Msg
|
| SecretMsg Comp.PasswordInput.Msg
|
||||||
| Confirm
|
| Confirm
|
||||||
| ConfirmResp (Result Http.Error BasicResult)
|
| ConfirmResp (Result Http.Error BasicResult)
|
||||||
| SetDisableConfirmText String
|
| SetDisableConfirmCode String
|
||||||
| Disable
|
| Disable
|
||||||
| DisableResp (Result Http.Error BasicResult)
|
| DisableResp (Result Http.Error BasicResult)
|
||||||
|
|
||||||
@ -178,10 +178,10 @@ update flags msg model =
|
|||||||
ConfirmResp (Err err) ->
|
ConfirmResp (Err err) ->
|
||||||
( ConfirmError err, Cmd.none )
|
( ConfirmError err, Cmd.none )
|
||||||
|
|
||||||
SetDisableConfirmText str ->
|
SetDisableConfirmCode str ->
|
||||||
case model of
|
case model of
|
||||||
StateEnabled m ->
|
StateEnabled m ->
|
||||||
( StateEnabled { m | confirmText = str }, Cmd.none )
|
( StateEnabled { m | confirmCode = str }, Cmd.none )
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
@ -189,13 +189,9 @@ update flags msg model =
|
|||||||
Disable ->
|
Disable ->
|
||||||
case model of
|
case model of
|
||||||
StateEnabled m ->
|
StateEnabled m ->
|
||||||
if String.toLower m.confirmText == "ok" then
|
( StateEnabled { m | loading = True }
|
||||||
( StateEnabled { m | confirmTextWrong = False, loading = True }
|
, Api.disableOtp flags (OtpConfirm m.confirmCode) DisableResp
|
||||||
, Api.disableOtp flags DisableResp
|
)
|
||||||
)
|
|
||||||
|
|
||||||
else
|
|
||||||
( StateEnabled { m | confirmTextWrong = True }, Cmd.none )
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
@ -205,7 +201,12 @@ update flags msg model =
|
|||||||
init flags
|
init flags
|
||||||
|
|
||||||
else
|
else
|
||||||
( model, Cmd.none )
|
case model of
|
||||||
|
StateEnabled m ->
|
||||||
|
( StateEnabled { m | serverErrorMsg = result.message, loading = False }, Cmd.none )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
DisableResp (Err err) ->
|
DisableResp (Err err) ->
|
||||||
( DisableError err, Cmd.none )
|
( DisableError err, Cmd.none )
|
||||||
@ -253,14 +254,15 @@ viewEnabled texts model =
|
|||||||
, p []
|
, p []
|
||||||
[ text texts.revert2FAText
|
[ text texts.revert2FAText
|
||||||
]
|
]
|
||||||
, div [ class "flex flex-col items-center mt-6" ]
|
, div [ class "flex flex-col mt-6" ]
|
||||||
[ div [ class "flex flex-row max-w-md" ]
|
[ div [ class "flex flex-row max-w-md" ]
|
||||||
[ input
|
[ input
|
||||||
[ type_ "text"
|
[ type_ "text"
|
||||||
, value model.confirmText
|
, value model.confirmCode
|
||||||
, onInput SetDisableConfirmText
|
, onInput SetDisableConfirmCode
|
||||||
, class S.textInput
|
, class S.textInput
|
||||||
, class "rounded-r-none"
|
, class "rounded-r-none pl-2 pr-10 py-2 rounded-lg max-w-xs text-center font-mono"
|
||||||
|
, placeholder "123456"
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
, B.genericButton
|
, B.genericButton
|
||||||
@ -281,9 +283,9 @@ viewEnabled texts model =
|
|||||||
, div
|
, div
|
||||||
[ class S.errorMessage
|
[ class S.errorMessage
|
||||||
, class "my-2"
|
, class "my-2"
|
||||||
, classList [ ( "hidden", not model.confirmTextWrong ) ]
|
, classList [ ( "hidden", model.serverErrorMsg == "" ) ]
|
||||||
]
|
]
|
||||||
[ text texts.disableConfirmErrorMsg
|
[ text texts.codeInvalid
|
||||||
]
|
]
|
||||||
, Markdown.toHtml [ class "mt-2" ] texts.disableConfirmBoxInfo
|
, Markdown.toHtml [ class "mt-2" ] texts.disableConfirmBoxInfo
|
||||||
]
|
]
|
||||||
@ -367,7 +369,7 @@ viewDisabled texts model =
|
|||||||
, class S.errorMessage
|
, class S.errorMessage
|
||||||
, class "mt-2"
|
, class "mt-2"
|
||||||
]
|
]
|
||||||
[ text texts.setupCodeInvalid ]
|
[ text texts.codeInvalid ]
|
||||||
, div [ class "mt-6" ]
|
, div [ class "mt-6" ]
|
||||||
[ p [] [ text texts.ifNotQRCode ]
|
[ p [] [ text texts.ifNotQRCode ]
|
||||||
, div [ class "max-w-md mx-auto mt-4" ]
|
, div [ class "max-w-md mx-auto mt-4" ]
|
||||||
|
@ -29,14 +29,13 @@ type alias Texts =
|
|||||||
, twoFaActiveSince : String
|
, twoFaActiveSince : String
|
||||||
, revert2FAText : String
|
, revert2FAText : String
|
||||||
, disableButton : String
|
, disableButton : String
|
||||||
, disableConfirmErrorMsg : String
|
|
||||||
, disableConfirmBoxInfo : String
|
, disableConfirmBoxInfo : String
|
||||||
, setupTwoFactorAuth : String
|
, setupTwoFactorAuth : String
|
||||||
, setupTwoFactorAuthInfo : String
|
, setupTwoFactorAuthInfo : String
|
||||||
, activateButton : String
|
, activateButton : String
|
||||||
, setupConfirmLabel : String
|
, setupConfirmLabel : String
|
||||||
, scanQRCode : String
|
, scanQRCode : String
|
||||||
, setupCodeInvalid : String
|
, codeInvalid : String
|
||||||
, ifNotQRCode : String
|
, ifNotQRCode : String
|
||||||
, reloadToTryAgain : String
|
, reloadToTryAgain : String
|
||||||
, twoFactorNowActive : String
|
, twoFactorNowActive : String
|
||||||
@ -57,14 +56,13 @@ gb =
|
|||||||
, twoFaActiveSince = "Two Factor Authentication is active since "
|
, twoFaActiveSince = "Two Factor Authentication is active since "
|
||||||
, revert2FAText = "If you really want to revert back to password-only authentication, you can do this here. You can run the setup any time to enable the second factor again."
|
, revert2FAText = "If you really want to revert back to password-only authentication, you can do this here. You can run the setup any time to enable the second factor again."
|
||||||
, disableButton = "Disable 2FA"
|
, disableButton = "Disable 2FA"
|
||||||
, disableConfirmErrorMsg = "Please type OK if you really want to disable this!"
|
, disableConfirmBoxInfo = "Enter a TOTP code and click the button to disable 2FA."
|
||||||
, disableConfirmBoxInfo = "Type `OK` into the text box and click the button to disable 2FA."
|
|
||||||
, setupTwoFactorAuth = "Setup Two Factor Authentication"
|
, setupTwoFactorAuth = "Setup Two Factor Authentication"
|
||||||
, setupTwoFactorAuthInfo = "You can setup a second factor for authentication using a one-time password. When clicking the button a secret is generated that you can load into an app on your mobile device. The app then provides a 6 digit code that you need to pass in the field in order to confirm and finalize the setup."
|
, setupTwoFactorAuthInfo = "You can setup a second factor for authentication using a one-time password. When clicking the button a secret is generated that you can load into an app on your mobile device. The app then provides a 6 digit code that you need to pass in the field in order to confirm and finalize the setup."
|
||||||
, activateButton = "Activate two-factor authentication"
|
, activateButton = "Activate two-factor authentication"
|
||||||
, setupConfirmLabel = "Confirm"
|
, setupConfirmLabel = "Confirm"
|
||||||
, scanQRCode = "Scan this QR code with your device and enter the 6 digit code:"
|
, scanQRCode = "Scan this QR code with your device and enter the 6 digit code:"
|
||||||
, setupCodeInvalid = "The confirmation code was invalid!"
|
, codeInvalid = "The code was invalid!"
|
||||||
, ifNotQRCode = "If you cannot use the qr code, enter this secret:"
|
, ifNotQRCode = "If you cannot use the qr code, enter this secret:"
|
||||||
, reloadToTryAgain = "If you want to try again, reload the page."
|
, reloadToTryAgain = "If you want to try again, reload the page."
|
||||||
, twoFactorNowActive = "Two Factor Authentication is now active!"
|
, twoFactorNowActive = "Two Factor Authentication is now active!"
|
||||||
@ -85,14 +83,13 @@ de =
|
|||||||
, twoFaActiveSince = "Die Zwei-Faktor-Authentifizierung ist aktiv seit "
|
, twoFaActiveSince = "Die Zwei-Faktor-Authentifizierung ist aktiv seit "
|
||||||
, revert2FAText = "Die Zwei-Faktor-Authentifizierung kann hier wieder deaktiviert werden. Danach kann die Einrichtung wieder von neuem gestartet werden, um 2FA wieder zu aktivieren."
|
, revert2FAText = "Die Zwei-Faktor-Authentifizierung kann hier wieder deaktiviert werden. Danach kann die Einrichtung wieder von neuem gestartet werden, um 2FA wieder zu aktivieren."
|
||||||
, disableButton = "Deaktiviere 2FA"
|
, disableButton = "Deaktiviere 2FA"
|
||||||
, disableConfirmErrorMsg = "Bitte tippe OK ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren."
|
|
||||||
, disableConfirmBoxInfo = "Tippe `OK` in das Feld und klicke, um die Zwei-Faktor-Authentifizierung zu deaktivieren."
|
, disableConfirmBoxInfo = "Tippe `OK` in das Feld und klicke, um die Zwei-Faktor-Authentifizierung zu deaktivieren."
|
||||||
, setupTwoFactorAuth = "Zwei-Faktor-Authentifizierung einrichten"
|
, setupTwoFactorAuth = "Zwei-Faktor-Authentifizierung einrichten"
|
||||||
, setupTwoFactorAuthInfo = "Ein zweiter Faktor zur Authentifizierung mittels eines Einmalkennworts kann eingerichtet werden. Beim Klicken des Button wird ein Schlüssel generiert, der an eine Authentifizierungs-App eines mobilen Gerätes übetragen werden kann. Danach präsentiert die App ein 6-stelliges Kennwort, welches zur Bestätigung und zum Abschluss angegeben werden muss."
|
, setupTwoFactorAuthInfo = "Ein zweiter Faktor zur Authentifizierung mittels eines Einmalkennworts kann eingerichtet werden. Beim Klicken des Button wird ein Schlüssel generiert, der an eine Authentifizierungs-App eines mobilen Gerätes übetragen werden kann. Danach präsentiert die App ein 6-stelliges Kennwort, welches zur Bestätigung und zum Abschluss angegeben werden muss."
|
||||||
, activateButton = "Zwei-Faktor-Authentifizierung aktivieren"
|
, activateButton = "Zwei-Faktor-Authentifizierung aktivieren"
|
||||||
, setupConfirmLabel = "Bestätigung"
|
, setupConfirmLabel = "Bestätigung"
|
||||||
, scanQRCode = "Scanne den QR Code mit der Authentifizierungs-App und gebe den 6-stelligen Code ein:"
|
, scanQRCode = "Scanne den QR Code mit der Authentifizierungs-App und gebe den 6-stelligen Code ein:"
|
||||||
, setupCodeInvalid = "Der Code war ungültig!"
|
, codeInvalid = "Der Code war ungültig!"
|
||||||
, ifNotQRCode = "Wenn der QR-Code nicht möglich ist, kann der Schlüssel manuell eingegeben werden:"
|
, ifNotQRCode = "Wenn der QR-Code nicht möglich ist, kann der Schlüssel manuell eingegeben werden:"
|
||||||
, reloadToTryAgain = "Um es noch einmal zu versuchen, bitte die Seite neu laden."
|
, reloadToTryAgain = "Um es noch einmal zu versuchen, bitte die Seite neu laden."
|
||||||
, twoFactorNowActive = "Die Zwei-Faktor-Authentifizierung ist nun aktiv!"
|
, twoFactorNowActive = "Die Zwei-Faktor-Authentifizierung ist nun aktiv!"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user