fix: enforce demo no-mfa login and expose proxy uuid in session

This commit is contained in:
Haitao Pan 2026-02-04 14:59:19 +08:00
parent 4bbfdef8cd
commit f1f185b6a8
2 changed files with 45 additions and 15 deletions

View File

@ -968,6 +968,20 @@ func (h *handler) login(c *gin.Context) {
return return
} }
// Demo/read-only account explicitly disables MFA to keep the roaming
// experience simple while write operations remain blocked by policy.
if h.isReadOnlyAccount(user) {
if user.MFAEnabled || strings.TrimSpace(user.MFATOTPSecret) != "" || !user.MFASecretIssuedAt.IsZero() || !user.MFAConfirmedAt.IsZero() {
user.MFATOTPSecret = ""
user.MFAEnabled = false
user.MFASecretIssuedAt = time.Time{}
user.MFAConfirmedAt = time.Time{}
if err := h.store.UpdateUser(c.Request.Context(), user); err != nil {
slog.Warn("failed to reset mfa state for read-only account", "err", err, "userID", user.ID)
}
}
}
if user.MFAEnabled { if user.MFAEnabled {
if totpCode == "" { if totpCode == "" {
respondError(c, http.StatusBadRequest, "mfa_code_required", "totp code is required") respondError(c, http.StatusBadRequest, "mfa_code_required", "totp code is required")
@ -1021,10 +1035,12 @@ func (h *handler) login(c *gin.Context) {
"user": sanitizeUser(user, nil), "user": sanitizeUser(user, nil),
} }
if challengeToken, err := h.createMFAChallenge(user.ID); err != nil { if !h.isReadOnlyAccount(user) {
slog.Error("failed to create mfa challenge during login", "err", err, "userID", user.ID) if challengeToken, err := h.createMFAChallenge(user.ID); err != nil {
} else { slog.Error("failed to create mfa challenge during login", "err", err, "userID", user.ID)
response["mfaToken"] = challengeToken } else {
response["mfaToken"] = challengeToken
}
} }
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
@ -2203,6 +2219,10 @@ func (h *handler) cancelSubscription(c *gin.Context) {
func sanitizeUser(user *store.User, challenge *mfaChallenge) gin.H { func sanitizeUser(user *store.User, challenge *mfaChallenge) gin.H {
identifier := strings.TrimSpace(user.ID) identifier := strings.TrimSpace(user.ID)
proxyUUID := strings.TrimSpace(user.ProxyUUID)
if proxyUUID == "" {
proxyUUID = identifier
}
groups := user.Groups groups := user.Groups
if len(groups) == 0 { if len(groups) == 0 {
groups = []string{} groups = []string{}
@ -2220,17 +2240,19 @@ func sanitizeUser(user *store.User, challenge *mfaChallenge) gin.H {
permissions = cloned permissions = cloned
} }
return gin.H{ return gin.H{
"id": identifier, "id": identifier,
"uuid": identifier, "uuid": identifier,
"name": user.Name, "name": user.Name,
"username": user.Name, "username": user.Name,
"email": user.Email, "email": user.Email,
"emailVerified": user.EmailVerified, "emailVerified": user.EmailVerified,
"mfaEnabled": user.MFAEnabled, "mfaEnabled": user.MFAEnabled,
"mfa": buildMFAState(user, challenge), "mfa": buildMFAState(user, challenge),
"role": user.Role, "role": user.Role,
"groups": groups, "groups": groups,
"permissions": permissions, "permissions": permissions,
"proxyUuid": proxyUUID,
"proxyUuidExpiresAt": user.ProxyUUIDExpiresAt,
} }
} }

View File

@ -135,6 +135,10 @@ func ensureDemoUser(ctx context.Context, st store.Store, logger *slog.Logger) er
Email: demoEmail, Email: demoEmail,
EmailVerified: true, EmailVerified: true,
PasswordHash: string(hashed), PasswordHash: string(hashed),
MFATOTPSecret: "",
MFAEnabled: false,
MFASecretIssuedAt: time.Time{},
MFAConfirmedAt: time.Time{},
Level: store.LevelUser, Level: store.LevelUser,
Role: store.RoleReadOnly, Role: store.RoleReadOnly,
Groups: []string{demoGroup}, Groups: []string{demoGroup},
@ -156,6 +160,10 @@ func ensureDemoUser(ctx context.Context, st store.Store, logger *slog.Logger) er
demoUser.Email = demoEmail demoUser.Email = demoEmail
demoUser.EmailVerified = true demoUser.EmailVerified = true
demoUser.PasswordHash = string(hashed) demoUser.PasswordHash = string(hashed)
demoUser.MFATOTPSecret = ""
demoUser.MFAEnabled = false
demoUser.MFASecretIssuedAt = time.Time{}
demoUser.MFAConfirmedAt = time.Time{}
demoUser.Level = store.LevelUser demoUser.Level = store.LevelUser
demoUser.Role = store.RoleReadOnly demoUser.Role = store.RoleReadOnly
demoUser.Groups = []string{demoGroup} demoUser.Groups = []string{demoGroup}