feat(security): add query mode handling for database operations
Tests / Integration Tests (push) Failing after 1s
Tests / Unit Tests (push) Failing after 24s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 59s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 58s
Build , Vet Test, and Lint / Build (push) Successful in 4m37s
Build , Vet Test, and Lint / Lint Code (push) Failing after 5m35s

* Introduced QueryMode to select between stored procedure and direct SQL execution.
* Implemented dbCapability to probe for stored procedure existence.
* Added table names configuration for direct SQL operations.
* Updated DatabaseTwoFactorProvider to support query mode and table names.
* Implemented direct SQL methods mirroring stored procedures for TOTP operations.
* Added tests for query mode logic and table names validation.
This commit is contained in:
Hein
2026-07-07 15:29:29 +02:00
parent 8a06aacfb2
commit eee83f9dc6
21 changed files with 3303 additions and 171 deletions
+261
View File
@@ -0,0 +1,261 @@
package security
import (
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"time"
)
// Direct-mode implementations mirroring the resolvespec_passkey_* stored
// procedures in database_schema.sql using plain SQL against TableNames.
// credential_id/public_key/aaguid are stored as base64 TEXT (not native
// bytea) and transports as JSON-encoded TEXT, so the same schema works on
// SQLite, MySQL, and Postgres.
type storeCredentialParams struct {
UserID int
CredentialID string // base64
PublicKey string // base64
AttestationType string
SignCount int
Transports []string
BackupEligible bool
BackupState bool
Name string
}
func (p *DatabasePasskeyProvider) storeCredentialDirect(ctx context.Context, params storeCredentialParams) (int64, error) {
transportsJSON, err := json.Marshal(params.Transports)
if err != nil {
return 0, fmt.Errorf("failed to marshal transports: %w", err)
}
var credentialID int64
err = p.runDBOpWithReconnect(func(db *sql.DB) error {
var exists int
checkQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT 1 FROM %s WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
if err := db.QueryRowContext(ctx, checkQuery, params.CredentialID).Scan(&exists); err == nil {
return fmt.Errorf("Credential already exists")
} else if !errors.Is(err, sql.ErrNoRows) {
return err
}
var userExists int
userCheckQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT 1 FROM %s WHERE id = ?`, p.tableNames.Users))
if err := db.QueryRowContext(ctx, userCheckQuery, params.UserID).Scan(&userExists); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("User not found")
}
return err
}
now := time.Now()
insertQuery := rewritePlaceholders(db, fmt.Sprintf(
`INSERT INTO %s (user_id, credential_id, public_key, attestation_type, aaguid, sign_count, transports, backup_eligible, backup_state, name, created_at, last_used_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, p.tableNames.UserPasskeyCredentials))
res, err := db.ExecContext(ctx, insertQuery, params.UserID, params.CredentialID, params.PublicKey, params.AttestationType,
"", params.SignCount, string(transportsJSON), params.BackupEligible, params.BackupState, params.Name, now, now)
if err != nil {
return err
}
credentialID, err = res.LastInsertId()
return err
})
if err != nil {
return 0, err
}
return credentialID, nil
}
func (p *DatabasePasskeyProvider) getCredentialDirect(ctx context.Context, credentialIDB64 string) (userID int, signCount uint32, err error) {
err = p.runDBOpWithReconnect(func(db *sql.DB) error {
query := rewritePlaceholders(db, fmt.Sprintf(`SELECT user_id, sign_count FROM %s WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
return db.QueryRowContext(ctx, query, credentialIDB64).Scan(&userID, &signCount)
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, 0, fmt.Errorf("Credential not found")
}
return 0, 0, fmt.Errorf("failed to get credential: %w", err)
}
return userID, signCount, nil
}
func (p *DatabasePasskeyProvider) updateCounterDirect(ctx context.Context, credentialIDB64 string, newCounter uint32) (cloneWarning bool, err error) {
err = p.runDBOpWithReconnect(func(db *sql.DB) error {
var oldCounter int
query := rewritePlaceholders(db, fmt.Sprintf(`SELECT sign_count FROM %s WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
if err := db.QueryRowContext(ctx, query, credentialIDB64).Scan(&oldCounter); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("Credential not found")
}
return err
}
if int(newCounter) <= oldCounter {
cloneWarning = true
updQuery := rewritePlaceholders(db, fmt.Sprintf(`UPDATE %s SET clone_warning = ? WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
_, err := db.ExecContext(ctx, updQuery, true, credentialIDB64)
return err
}
updQuery := rewritePlaceholders(db, fmt.Sprintf(`UPDATE %s SET sign_count = ?, last_used_at = ? WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
_, err := db.ExecContext(ctx, updQuery, newCounter, time.Now(), credentialIDB64)
return err
})
return cloneWarning, err
}
func (p *DatabasePasskeyProvider) getUserCredentialsDirect(ctx context.Context, userID int) ([]PasskeyCredential, error) {
var credentials []PasskeyCredential
err := p.runDBOpWithReconnect(func(db *sql.DB) error {
query := rewritePlaceholders(db, fmt.Sprintf(
`SELECT id, user_id, credential_id, public_key, attestation_type, aaguid, sign_count, clone_warning, transports, backup_eligible, backup_state, name, created_at, last_used_at
FROM %s WHERE user_id = ? ORDER BY created_at DESC`, p.tableNames.UserPasskeyCredentials))
rows, err := db.QueryContext(ctx, query, userID)
if err != nil {
return err
}
defer rows.Close()
credentials = make([]PasskeyCredential, 0)
for rows.Next() {
var id, uid int
var credIDB64, pubKeyB64, attestationType, aaguidB64, name string
var signCount uint32
var cloneWarning, backupEligible, backupState bool
var transportsJSON sql.NullString
var createdAt, lastUsedAt time.Time
if err := rows.Scan(&id, &uid, &credIDB64, &pubKeyB64, &attestationType, &aaguidB64, &signCount,
&cloneWarning, &transportsJSON, &backupEligible, &backupState, &name, &createdAt, &lastUsedAt); err != nil {
return err
}
credID, err := base64.StdEncoding.DecodeString(credIDB64)
if err != nil {
continue
}
pubKey, err := base64.StdEncoding.DecodeString(pubKeyB64)
if err != nil {
continue
}
aaguid, _ := base64.StdEncoding.DecodeString(aaguidB64)
var transports []string
if transportsJSON.Valid && transportsJSON.String != "" {
_ = json.Unmarshal([]byte(transportsJSON.String), &transports)
}
credentials = append(credentials, PasskeyCredential{
ID: fmt.Sprintf("%d", id),
UserID: uid,
CredentialID: credID,
PublicKey: pubKey,
AttestationType: attestationType,
AAGUID: aaguid,
SignCount: signCount,
CloneWarning: cloneWarning,
Transports: transports,
BackupEligible: backupEligible,
BackupState: backupState,
Name: name,
CreatedAt: createdAt,
LastUsedAt: lastUsedAt,
})
}
return rows.Err()
})
if err != nil {
return nil, fmt.Errorf("failed to get credentials: %w", err)
}
return credentials, nil
}
func (p *DatabasePasskeyProvider) deleteCredentialDirect(ctx context.Context, userID int, credentialIDB64 string) error {
return p.runDBOpWithReconnect(func(db *sql.DB) error {
query := rewritePlaceholders(db, fmt.Sprintf(`DELETE FROM %s WHERE user_id = ? AND credential_id = ?`, p.tableNames.UserPasskeyCredentials))
res, err := db.ExecContext(ctx, query, userID, credentialIDB64)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("Credential not found")
}
return nil
})
}
func (p *DatabasePasskeyProvider) updateNameDirect(ctx context.Context, userID int, credentialIDB64, name string) error {
return p.runDBOpWithReconnect(func(db *sql.DB) error {
query := rewritePlaceholders(db, fmt.Sprintf(`UPDATE %s SET name = ? WHERE user_id = ? AND credential_id = ?`, p.tableNames.UserPasskeyCredentials))
res, err := db.ExecContext(ctx, query, name, userID, credentialIDB64)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("Credential not found")
}
return nil
})
}
func (p *DatabasePasskeyProvider) getCredsByUsernameDirect(ctx context.Context, username string) (int, []struct {
ID string `json:"credential_id"`
Transports []string `json:"transports"`
}, error) {
type credT = struct {
ID string `json:"credential_id"`
Transports []string `json:"transports"`
}
var userID int
var creds []credT
err := p.runDBOpWithReconnect(func(db *sql.DB) error {
userQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT id FROM %s WHERE username = ? AND is_active = ?`, p.tableNames.Users))
if err := db.QueryRowContext(ctx, userQuery, username, true).Scan(&userID); err != nil {
return err
}
query := rewritePlaceholders(db, fmt.Sprintf(`SELECT credential_id, transports FROM %s WHERE user_id = ?`, p.tableNames.UserPasskeyCredentials))
rows, err := db.QueryContext(ctx, query, userID)
if err != nil {
return err
}
defer rows.Close()
creds = make([]credT, 0)
for rows.Next() {
var credID string
var transportsJSON sql.NullString
if err := rows.Scan(&credID, &transportsJSON); err != nil {
return err
}
var transports []string
if transportsJSON.Valid && transportsJSON.String != "" {
_ = json.Unmarshal([]byte(transportsJSON.String), &transports)
}
creds = append(creds, credT{ID: credID, Transports: transports})
}
return rows.Err()
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, nil, fmt.Errorf("User not found")
}
return 0, nil, fmt.Errorf("failed to get credentials: %w", err)
}
return userID, creds, nil
}