Require a otp to disable 2fa

This commit is contained in:
eikek 2021-09-22 23:06:59 +02:00
parent bbfc5b56d8
commit e31107eb49
7 changed files with 84 additions and 41 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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 =

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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" ]

View File

@ -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!"