fix: harden passkey deployment readiness

This commit is contained in:
Zhixuan Jiang
2026-07-26 10:14:55 -04:00
parent cc62979aa7
commit 4158e73b3f
8 changed files with 55 additions and 1 deletions
@@ -49,6 +49,8 @@ type passkeyRenameRequest struct {
Name string `json:"name" binding:"required"`
}
const passkeyFinishBodyMaxBytes = 64 * 1024
// BeginLogin starts a usernameless, discoverable-credential login ceremony.
func (h *PasskeyHandler) BeginLogin(c *gin.Context) {
assertion, token, err := h.passkeys.BeginLogin(c.Request.Context())
@@ -78,6 +80,8 @@ func (h *PasskeyHandler) FinishLogin(c *gin.Context) {
response.ErrorFrom(c, err)
return
}
middleware2.SetAuditActor(c, user.ID, user.Email)
c.Set("auth_method", service.AuditAuthMethodPasskey)
h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
respondWithTokenPair(c, h.authService, user)
}
@@ -175,6 +179,7 @@ func (h *PasskeyHandler) ensureBackendModeAllowsUser(ctx context.Context, user *
}
func bindPasskeyFinishRequest(c *gin.Context) (*passkeyFinishRequest, bool) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, passkeyFinishBodyMaxBytes)
var req passkeyFinishRequest
if err := c.ShouldBindJSON(&req); err != nil || len(req.Credential) == 0 {
response.BadRequest(c, "Invalid passkey response")
@@ -0,0 +1,27 @@
package handler
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func TestBindPasskeyFinishRequestRejectsOversizedBody(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
context.Request = httptest.NewRequest(
http.MethodPost,
"/api/v1/auth/passkey/login/finish",
strings.NewReader(`{"credential":"`+strings.Repeat("x", passkeyFinishBodyMaxBytes)+`"}`),
)
context.Request.Header.Set("Content-Type", "application/json")
_, ok := bindPasskeyFinishRequest(context)
require.False(t, ok)
require.Equal(t, http.StatusBadRequest, recorder.Code)
}
@@ -124,6 +124,7 @@ var auditSensitiveReads = map[string]string{
var auditActionOverrides = map[string]string{
"POST /api/v1/auth/login": service.AuditActionLogin,
"POST /api/v1/auth/login/2fa": service.AuditActionLogin2FA,
"POST /api/v1/auth/passkey/login/finish": service.AuditActionLogin,
"POST /api/v1/auth/register": service.AuditActionRegister,
"POST /api/v1/auth/refresh": service.AuditActionTokenRefresh,
"POST /api/v1/user/totp/step-up": service.AuditActionStepUpVerify,
@@ -146,6 +146,12 @@ func TestPromptAuditMutationAuditRoutesHaveStableActionsAndOmitBodies(t *testing
}
}
func TestPasskeyLoginAuditUsesCanonicalLoginActionAndOmitsCredentialBody(t *testing.T) {
route := "POST /api/v1/auth/passkey/login/finish"
require.Equal(t, service.AuditActionLogin, auditActionOverrides[route])
require.Contains(t, auditBodyOmittedRoutes, route)
}
// Ollama 会话保存的请求体整体就是浏览器 Cookie 明文,键级脱敏清单曾漏掉裸键
// "session",必须走整体不入库路径,防止会话凭证长期留存在 audit_logs。
func TestOllamaCloudUsageSessionRouteOmitsAuditBody(t *testing.T) {
+1
View File
@@ -19,6 +19,7 @@ const (
// AuditAuthMethodJWT / AuditAuthMethodAdminAPIKey 与 auth 中间件写入的 auth_method 对齐。
AuditAuthMethodJWT = "jwt"
AuditAuthMethodAdminAPIKey = "admin_api_key"
AuditAuthMethodPasskey = "passkey"
// auditRequestBodyMaxBytes 请求体脱敏后入库的最大长度(字节),超出截断。
auditRequestBodyMaxBytes = 16 * 1024
+1 -1
View File
@@ -371,7 +371,7 @@ func passkeySummary(record *PasskeyCredentialRecord) *PasskeyCredentialSummary {
Name: record.Name,
CreatedAt: record.CreatedAt,
LastUsedAt: record.LastUsedAt,
Backup: record.Credential.Flags.BackupEligible,
Backup: record.Credential.Flags.BackupState,
}
}
+13
View File
@@ -4,6 +4,7 @@ import (
"strings"
"testing"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/require"
)
@@ -14,3 +15,15 @@ func TestNormalizePasskeyName(t *testing.T) {
longName := strings.Repeat("密", maxPasskeyNameLength+10)
require.Len(t, []rune(normalizePasskeyName(longName)), maxPasskeyNameLength)
}
func TestPasskeySummaryReportsCurrentBackupState(t *testing.T) {
record := &PasskeyCredentialRecord{
Credential: webauthn.Credential{
Flags: webauthn.CredentialFlags{BackupEligible: true},
},
}
require.False(t, passkeySummary(record).Backup)
record.Credential.Flags.BackupState = true
require.True(t, passkeySummary(record).Backup)
}
@@ -284,6 +284,7 @@ type PublicSettings struct {
PasswordResetEnabled bool
InvitationCodeEnabled bool
TotpEnabled bool // TOTP 双因素认证
PasskeyEnabled bool
LoginAgreementEnabled bool
LoginAgreementMode string
LoginAgreementUpdatedAt string