diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index b240d289b6..549b380f79 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -434,7 +434,7 @@ func main() { publicAPI.POST("/users/two-factor/passkeys/begin", userHandler.BeginPasskeyAuthenticationCeremony) publicAPI.POST("/users/two-factor/passkeys/finish", userHandler.FinishPasskeyAuthenticationCeremony) privateAPI.GET("/users/two-factor/recovery-status", userHandler.GetTwoFactorRecoveryStatus) - privateAPI.POST("/users/two-factor/passkeys/set-skip-challenge", userHandler.ConfigurePassKeySkipChallenge) + privateAPI.POST("/users/two-factor/passkeys/configure-reset", userHandler.ConfigurePassKeySkipChallenge) privateAPI.GET("/users/two-factor/status", userHandler.GetTwoFactorStatus) privateAPI.POST("/users/two-factor/setup", userHandler.SetupTwoFactor) privateAPI.POST("/users/two-factor/enable", userHandler.EnableTwoFactor) diff --git a/server/ente/passkey.go b/server/ente/passkey.go index 096d0d631c..b8ff85933e 100644 --- a/server/ente/passkey.go +++ b/server/ente/passkey.go @@ -13,8 +13,8 @@ type Passkey struct { var MaxPasskeys = 10 -type ConfigurePassKeyRecoveryRequest struct { - SkipSecret string `json:"resetSecret" binding:"required"` +type SetPassKeyRecoveryRequest struct { + Secret uuid.UUID `json:"secret" binding:"required"` // The UserSecretCipher has SkipSecret encrypted with the user's recoveryKey // If the user sends the correct UserSecretCipher, we can be sure that the user has the recoveryKey, // and we can allow the user to recover their MFA. @@ -25,11 +25,6 @@ type ConfigurePassKeyRecoveryRequest struct { type TwoFactorRecoveryStatus struct { // AllowAdminReset is a boolean that determines if the admin can reset the user's MFA. // If true, in the event that the user loses their MFA device, the admin can reset the user's MFA. - AllowAdminReset bool `json:"allowAdminReset" binding:"required"` - IsPassKeySkipEnabled bool `json:"isPassKeyResetEnabled" binding:"required"` -} - -type SkipPassKeyRequest struct { - SessionID string `json:"sessionID" binding:"required"` - SkipSecret string `json:"resetSecret" binding:"required"` + AllowAdminReset bool `json:"allowAdminReset" binding:"required"` + IsPassKeyRecoveryEnabled bool `json:"isPassKeyRecoveryEnabled" binding:"required"` } diff --git a/server/migrations/80_two_factor_recovery.up.sql b/server/migrations/80_two_factor_recovery.up.sql index c3eacdc372..93b2b3676d 100644 --- a/server/migrations/80_two_factor_recovery.up.sql +++ b/server/migrations/80_two_factor_recovery.up.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS two_factor_recovery ( - user_id bigint NOT NULL, + user_id bigint NOT NULL PRIMARY KEY, -- if false, the support team team will not be able to reset the MFA for the user enable_admin_mfa_reset boolean NOT NULL DEFAULT true, server_passkey_secret_data bytea, diff --git a/server/pkg/api/user.go b/server/pkg/api/user.go index 6795acf29a..9609ec813c 100644 --- a/server/pkg/api/user.go +++ b/server/pkg/api/user.go @@ -256,7 +256,7 @@ func (h *UserHandler) GetTwoFactorRecoveryStatus(c *gin.Context) { // ConfigurePassKeySkipChallenge configures the passkey skip challenge for a user. In case the user does not // have access to passkey, the user can bypass the passkey by providing the recovery key func (h *UserHandler) ConfigurePassKeySkipChallenge(c *gin.Context) { - var request ente.ConfigurePassKeyRecoveryRequest + var request ente.SetPassKeyRecoveryRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return @@ -402,10 +402,10 @@ func (h *UserHandler) DisableTwoFactor(c *gin.Context) { // recoveryKeyEncryptedTwoFactorSecret for the user to decrypt it and make twoFactor removal api call func (h *UserHandler) RecoverTwoFactor(c *gin.Context) { sessionID := c.Query("sessionID") - twoFactorType := c.Query("type") + twoFactorType := c.Query("twoFactorType") var response *ente.TwoFactorRecoveryResponse var err error - if twoFactorType == "passkey" { + if twoFactorType == "passKey" { response, err = h.UserController.GetPasskeyRecoveryResponse(c, sessionID) } else { response, err = h.UserController.RecoverTwoFactor(sessionID) @@ -427,10 +427,10 @@ func (h *UserHandler) RemoveTwoFactor(c *gin.Context) { } var response *ente.TwoFactorAuthorizationResponse var err error - if request.TwoFactorType == "passkey" { - response, err = h.UserController.SkipPassKey(c, &request) + if request.TwoFactorType == "passKey" { + response, err = h.UserController.SkipPasskeyVerification(c, &request) } else { - response, err = h.UserController.RemoveTwoFactor(c, request.SessionID, request.Secret) + response, err = h.UserController.RemoveTOTPTwoFactor(c, request.SessionID, request.Secret) } if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) diff --git a/server/pkg/controller/user/passkey.go b/server/pkg/controller/user/passkey.go index fe42d5a57e..a11c392c94 100644 --- a/server/pkg/controller/user/passkey.go +++ b/server/pkg/controller/user/passkey.go @@ -13,9 +13,9 @@ func (c *UserController) GetTwoFactorRecoveryStatus(ctx *gin.Context) (*ente.Two return c.TwoFactorRecoveryRepo.GetStatus(userID) } -func (c *UserController) ConfigurePassKeySkip(ctx *gin.Context, req *ente.ConfigurePassKeyRecoveryRequest) error { +func (c *UserController) ConfigurePassKeySkip(ctx *gin.Context, req *ente.SetPassKeyRecoveryRequest) error { userID := auth.GetUserID(ctx.Request.Header) - return c.TwoFactorRecoveryRepo.ConfigurePassKeySkipChallenge(ctx, userID, req) + return c.TwoFactorRecoveryRepo.SetPassKeyRecovery(ctx, userID, req) } func (c *UserController) GetPasskeyRecoveryResponse(ctx *gin.Context, passKeySessionID string) (*ente.TwoFactorRecoveryResponse, error) { @@ -27,7 +27,7 @@ func (c *UserController) GetPasskeyRecoveryResponse(ctx *gin.Context, passKeySes if err != nil { return nil, err } - if !recoveryStatus.IsPassKeySkipEnabled { + if !recoveryStatus.IsPassKeyRecoveryEnabled { return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured") } @@ -41,7 +41,7 @@ func (c *UserController) GetPasskeyRecoveryResponse(ctx *gin.Context, passKeySes return result, nil } -func (c *UserController) SkipPassKey(context *gin.Context, req *ente.TwoFactorRemovalRequest) (*ente.TwoFactorAuthorizationResponse, error) { +func (c *UserController) SkipPasskeyVerification(context *gin.Context, req *ente.TwoFactorRemovalRequest) (*ente.TwoFactorAuthorizationResponse, error) { userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(req.SessionID) if err != nil { return nil, stacktrace.Propagate(err, "") diff --git a/server/pkg/controller/user/twofactor.go b/server/pkg/controller/user/twofactor.go index 92a3a7ae5d..d788b66fd5 100644 --- a/server/pkg/controller/user/twofactor.go +++ b/server/pkg/controller/user/twofactor.go @@ -143,9 +143,9 @@ func (c *UserController) RecoverTwoFactor(sessionID string) (*ente.TwoFactorReco return &response, nil } -// RemoveTwoFactor handles two factor deactivation request if user lost his device +// RemoveTOTPTwoFactor handles two factor deactivation request if user lost his device // by authenticating him using his twoFactorsessionToken and twoFactor secret -func (c *UserController) RemoveTwoFactor(context *gin.Context, sessionID string, secret string) (*ente.TwoFactorAuthorizationResponse, error) { +func (c *UserController) RemoveTOTPTwoFactor(context *gin.Context, sessionID string, secret string) (*ente.TwoFactorAuthorizationResponse, error) { userID, err := c.TwoFactorRepo.GetUserIDWithTwoFactorSession(sessionID) if err != nil { return nil, stacktrace.Propagate(err, "") diff --git a/server/pkg/repo/two_factor_recovery/repository.go b/server/pkg/repo/two_factor_recovery/repository.go index fcbf586b47..327ec51fe8 100644 --- a/server/pkg/repo/two_factor_recovery/repository.go +++ b/server/pkg/repo/two_factor_recovery/repository.go @@ -25,24 +25,25 @@ func (r *Repository) GetStatus(userID int64) (*ente.TwoFactorRecoveryStatus, err if err == sql.ErrNoRows { // by default, admin return &ente.TwoFactorRecoveryStatus{ - AllowAdminReset: true, - IsPassKeySkipEnabled: false, + AllowAdminReset: true, + IsPassKeyRecoveryEnabled: false, }, nil } return nil, err } - return &ente.TwoFactorRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPassKeySkipEnabled: resetKey.Valid}, nil + return &ente.TwoFactorRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPassKeyRecoveryEnabled: resetKey.Valid}, nil } -func (r *Repository) ConfigurePassKeySkipChallenge(ctx context.Context, userID int64, req *ente.ConfigurePassKeyRecoveryRequest) error { - serveEncPassKey, encRrr := crypto.Encrypt(req.SkipSecret, r.SecretEncryptionKey) +func (r *Repository) SetPassKeyRecovery(ctx context.Context, userID int64, req *ente.SetPassKeyRecoveryRequest) error { + serveEncPassKey, encRrr := crypto.Encrypt(req.Secret.String(), r.SecretEncryptionKey) if encRrr != nil { return stacktrace.Propagate(encRrr, "failed to encrypt passkey secret") } _, err := r.Db.ExecContext(ctx, `INSERT INTO two_factor_recovery - (user_id, server_passkey_secret_data, server_passkey_secret_nonce, user_passkey_secret_data, user_passkey_secret_nonce)) - VALUES ($1, $2,$3,$4,$5) ON CONFLICT (user_id) - DO UPDATE SET server_passkey_secret_data = $2, server_passkey_secret_nonce = $3, user_passkey_secret_data=$4,user_passkey_secret_nonce=$5`, + (user_id, server_passkey_secret_data, server_passkey_secret_nonce, user_passkey_secret_data, user_passkey_secret_nonce) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id) + DO UPDATE SET server_passkey_secret_data = $2, server_passkey_secret_nonce = $3, user_passkey_secret_data = $4, user_passkey_secret_nonce = $5 + WHERE two_factor_recovery.user_passkey_secret_data IS NULL AND two_factor_recovery.server_passkey_secret_data IS NULL`, userID, serveEncPassKey.Cipher, serveEncPassKey.Nonce, req.UserSecretCipher, req.UserSecretNonce) return err }