Add APIs to allow user to skip passkey based two-fa
This commit is contained in:
parent
09a7d557d2
commit
42e4364fda
|
@ -433,9 +433,9 @@ func main() {
|
||||||
publicAPI.POST("/users/two-factor/passkeys/begin", userHandler.BeginPasskeyAuthenticationCeremony)
|
publicAPI.POST("/users/two-factor/passkeys/begin", userHandler.BeginPasskeyAuthenticationCeremony)
|
||||||
publicAPI.POST("/users/two-factor/passkeys/finish", userHandler.FinishPasskeyAuthenticationCeremony)
|
publicAPI.POST("/users/two-factor/passkeys/finish", userHandler.FinishPasskeyAuthenticationCeremony)
|
||||||
privateAPI.GET("/users/two-factor/account-recovery-status", userHandler.GetAccountRecoveryStatus)
|
privateAPI.GET("/users/two-factor/account-recovery-status", userHandler.GetAccountRecoveryStatus)
|
||||||
privateAPI.POST("/users/two-factor/passkeys/set-reset-challenge", userHandler.ConfigurePassKeyRecovery)
|
privateAPI.POST("/users/two-factor/passkeys/set-skip-challenge", userHandler.ConfigurePassKeySkipChallenge)
|
||||||
publicAPI.GET("/users/two-factor/passkeys/reset-challenge", userHandler.GetPasskeyResetChallenge)
|
publicAPI.GET("/users/two-factor/passkeys/skip-challenge", userHandler.GetPasskeySkipChallenge)
|
||||||
publicAPI.POST("/users/two-factor/passkeys/reset", userHandler.ResetPasskey)
|
publicAPI.POST("/users/two-factor/passkeys/skip", userHandler.SkipPassKey)
|
||||||
privateAPI.GET("/users/two-factor/status", userHandler.GetTwoFactorStatus)
|
privateAPI.GET("/users/two-factor/status", userHandler.GetTwoFactorStatus)
|
||||||
privateAPI.POST("/users/two-factor/setup", userHandler.SetupTwoFactor)
|
privateAPI.POST("/users/two-factor/setup", userHandler.SetupTwoFactor)
|
||||||
privateAPI.POST("/users/two-factor/enable", userHandler.EnableTwoFactor)
|
privateAPI.POST("/users/two-factor/enable", userHandler.EnableTwoFactor)
|
||||||
|
|
|
@ -13,25 +13,23 @@ type Passkey struct {
|
||||||
|
|
||||||
var MaxPasskeys = 10
|
var MaxPasskeys = 10
|
||||||
|
|
||||||
type EnablePassKeyRecovery struct {
|
type ConfigurePassKeySkipRequest struct {
|
||||||
UserID int64 `json:"userID" binding:"required"`
|
PassKeySkipSecret string `json:"passKeySkipSecret" binding:"required"`
|
||||||
ResetKey string `json:"resetKey" binding:"required"`
|
EncPassKeySkipSecret EncData `json:"encPassKeySkipSecret" binding:"required"`
|
||||||
EncResetKey EncData `json:"encResetKey" binding:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountRecoveryStatus struct {
|
type AccountRecoveryStatus struct {
|
||||||
// AllowAdminReset is a boolean that determines if the admin can reset the user's MFA.
|
// 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.
|
// 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"`
|
AllowAdminReset bool `json:"allowAdminReset" binding:"required"`
|
||||||
IsPassKeyResetEnabled bool `json:"isPassKeyResetEnabled" binding:"required"`
|
IsPassKeySkipEnabled bool `json:"isPassKeySkipEnabled" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasseKeyResetChallengeResponse struct {
|
type PasseKeySkipChallengeResponse struct {
|
||||||
EncResetKey string `json:"encResetKey" binding:"required"`
|
EncData
|
||||||
EncResetKeyNonce string `json:"encResetKeyNonce" binding:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetPassKey struct {
|
type SkipPassKeyRequest struct {
|
||||||
SessionID string `json:"sessionID" binding:"required"`
|
SessionID string `json:"sessionID" binding:"required"`
|
||||||
RestKey string `json:"resetKey" binding:"required"`
|
PassKeySkipSecret string `json:"passKeySkipSecret" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,16 +253,48 @@ func (h *UserHandler) GetAccountRecoveryStatus(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, res)
|
c.JSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) ConfigurePassKeyRecovery(c *gin.Context) {
|
// ConfigurePassKeySkipChallenge configures the passkey skip challenge for a user. In case the user does not
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented"})
|
// 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.ConfigurePassKeySkipRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := h.UserController.ConfigurePassKeySkip(c, &request)
|
||||||
|
if err != nil {
|
||||||
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) GetPasskeyResetChallenge(c *gin.Context) {
|
func (h *UserHandler) GetPasskeySkipChallenge(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented"})
|
passKeySessionID := c.Query("passKeySessionID")
|
||||||
|
if passKeySessionID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"message": "passKeySessionID is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.UserController.GetPasskeySkipChallenge(c, passKeySessionID)
|
||||||
|
if err != nil {
|
||||||
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) ResetPasskey(c *gin.Context) {
|
func (h *UserHandler) SkipPassKey(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented"})
|
var req ente.SkipPassKeyRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.UserController.SkipPassKey(c, &req)
|
||||||
|
if err != nil {
|
||||||
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupTwoFactor generates a two factor secret and sends it to user to setup his authenticator app with
|
// SetupTwoFactor generates a two factor secret and sends it to user to setup his authenticator app with
|
||||||
|
|
|
@ -3,6 +3,7 @@ package user
|
||||||
import (
|
import (
|
||||||
"github.com/ente-io/museum/ente"
|
"github.com/ente-io/museum/ente"
|
||||||
"github.com/ente-io/museum/pkg/utils/auth"
|
"github.com/ente-io/museum/pkg/utils/auth"
|
||||||
|
"github.com/ente-io/stacktrace"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,3 +12,50 @@ func (c *UserController) GetAccountRecoveryStatus(ctx *gin.Context) (*ente.Accou
|
||||||
userID := auth.GetUserID(ctx.Request.Header)
|
userID := auth.GetUserID(ctx.Request.Header)
|
||||||
return c.AccountRecoveryRepo.GetAccountRecoveryStatus(userID)
|
return c.AccountRecoveryRepo.GetAccountRecoveryStatus(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *UserController) ConfigurePassKeySkip(ctx *gin.Context, req *ente.ConfigurePassKeySkipRequest) error {
|
||||||
|
userID := auth.GetUserID(ctx.Request.Header)
|
||||||
|
return c.AccountRecoveryRepo.ConfigurePassKeyRecovery(ctx, userID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserController) GetPasskeySkipChallenge(ctx *gin.Context, passKeySessionID string) (*ente.EncData, error) {
|
||||||
|
userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(passKeySessionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recoveryStatus, err := c.AccountRecoveryRepo.GetAccountRecoveryStatus(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !recoveryStatus.IsPassKeySkipEnabled {
|
||||||
|
return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.AccountRecoveryRepo.GetPasskeyResetChallenge(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserController) SkipPassKey(context *gin.Context, req *ente.SkipPassKeyRequest) (*ente.TwoFactorAuthorizationResponse, error) {
|
||||||
|
userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(req.SessionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
exists, err := c.AccountRecoveryRepo.VerifyRecoveryKeyForPassKey(userID, req.PassKeySkipSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||||
|
}
|
||||||
|
response, err := c.GetKeyAttributeAndToken(context, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package accountrecovery
|
package accountrecovery
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/ente-io/museum/ente"
|
"github.com/ente-io/museum/ente"
|
||||||
|
"github.com/ente-io/stacktrace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
|
@ -20,10 +22,37 @@ func (r *Repository) GetAccountRecoveryStatus(userID int64) (*ente.AccountRecove
|
||||||
// by default, admin
|
// by default, admin
|
||||||
return &ente.AccountRecoveryStatus{
|
return &ente.AccountRecoveryStatus{
|
||||||
AllowAdminReset: true,
|
AllowAdminReset: true,
|
||||||
IsPassKeyResetEnabled: false,
|
IsPassKeySkipEnabled: false,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ente.AccountRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPassKeyResetEnabled: resetKey.Valid}, nil
|
return &ente.AccountRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPassKeySkipEnabled: resetKey.Valid}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ConfigurePassKeyRecovery(ctx context.Context, userID int64, req *ente.ConfigurePassKeySkipRequest) error {
|
||||||
|
_, err := r.Db.ExecContext(ctx, `INSERT INTO account_recovery (user_id, pass_key_reset_key, pass_key_reset_enc_data)
|
||||||
|
VALUES ($1, $2,$3) ON CONFLICT (user_id)
|
||||||
|
DO UPDATE SET pass_key_reset_key = $2, pass_key_reset_enc_data = $3`, userID, req.PassKeySkipKey, req.EncPassKeySkipSecret)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetPasskeyResetChallenge(ctx context.Context, userID int64) (*ente.EncData, error) {
|
||||||
|
var encData *ente.EncData
|
||||||
|
err := r.Db.QueryRowContext(ctx, "SELECT pass_key_reset_enc_data FROM account_recovery WHERE user_id= $1", userID).Scan(encData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return encData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyRecoveryKeyForPassKey checks if the passkey reset key is valid for a user
|
||||||
|
func (r *Repository) VerifyRecoveryKeyForPassKey(userID int64, passKeyResetKey string) (bool, error) {
|
||||||
|
var exists bool
|
||||||
|
row := r.Db.QueryRow(`SELECT EXISTS( SELECT 1 FROM account_recovery WHERE user_id = $1 AND pass_key_reset_key = $2)`, userID, passKeyResetKey)
|
||||||
|
err := row.Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
return false, stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue