package auth import ( "database/sql" "fmt" "time" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" ) // WebAuthnUser implements the webauthn.User interface. type WebAuthnUser struct { ID int64 Username string Credentials []webauthn.Credential } func (u *WebAuthnUser) WebAuthnID() []byte { return []byte(fmt.Sprintf("%d", u.ID)) } func (u *WebAuthnUser) WebAuthnName() string { return u.Username } func (u *WebAuthnUser) WebAuthnDisplayName() string { return u.Username } func (u *WebAuthnUser) WebAuthnCredentials() []webauthn.Credential { return u.Credentials } // NewWebAuthn creates a configured WebAuthn instance. func NewWebAuthn(rpDisplayName, rpID string, rpOrigins []string) (*webauthn.WebAuthn, error) { return webauthn.New(&webauthn.Config{ RPDisplayName: rpDisplayName, RPID: rpID, RPOrigins: rpOrigins, AuthenticatorSelection: protocol.AuthenticatorSelection{ AuthenticatorAttachment: protocol.CrossPlatform, UserVerification: protocol.VerificationPreferred, }, }) } // LoadWebAuthnUser loads a user with their WebAuthn credentials. func LoadWebAuthnUser(database *sql.DB, userID int64) (*WebAuthnUser, error) { var username string err := database.QueryRow("SELECT username FROM users WHERE id = ?", userID).Scan(&username) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } rows, err := database.Query( "SELECT credential_id, public_key, sign_count FROM webauthn_credentials WHERE user_id = ?", userID, ) if err != nil { return nil, err } defer func() { _ = rows.Close() }() var creds []webauthn.Credential for rows.Next() { var cred webauthn.Credential var signCount uint32 if err := rows.Scan(&cred.ID, &cred.PublicKey, &signCount); err != nil { continue } cred.Authenticator.SignCount = signCount creds = append(creds, cred) } return &WebAuthnUser{ ID: userID, Username: username, Credentials: creds, }, nil } // StoreWebAuthnCredential saves a new credential for a user. func StoreWebAuthnCredential(database *sql.DB, userID int64, name string, cred *webauthn.Credential) error { _, err := database.Exec( "INSERT INTO webauthn_credentials (user_id, credential_id, public_key, name, sign_count, created_at) VALUES (?, ?, ?, ?, ?, ?)", userID, cred.ID, cred.PublicKey, name, cred.Authenticator.SignCount, time.Now().UnixMilli(), ) return err } // UpdateWebAuthnSignCount updates the signature counter after authentication. func UpdateWebAuthnSignCount(database *sql.DB, credentialID []byte, signCount uint32) error { _, err := database.Exec( "UPDATE webauthn_credentials SET sign_count = ? WHERE credential_id = ?", signCount, credentialID, ) return err } // ListWebAuthnCredentials returns all credentials for a user. func ListWebAuthnCredentials(database *sql.DB, userID int64) ([]WebAuthnCredentialInfo, error) { rows, err := database.Query( "SELECT id, name, created_at FROM webauthn_credentials WHERE user_id = ? ORDER BY created_at", userID, ) if err != nil { return nil, err } defer func() { _ = rows.Close() }() var creds []WebAuthnCredentialInfo for rows.Next() { var c WebAuthnCredentialInfo var createdAt int64 if err := rows.Scan(&c.ID, &c.Name, &createdAt); err != nil { continue } c.CreatedAt = time.UnixMilli(createdAt) creds = append(creds, c) } return creds, nil } type WebAuthnCredentialInfo struct { ID int64 Name string CreatedAt time.Time } // DeleteWebAuthnCredential removes a credential. func DeleteWebAuthnCredential(database *sql.DB, credID int64, userID int64) error { _, err := database.Exec( "DELETE FROM webauthn_credentials WHERE id = ? AND user_id = ?", credID, userID, ) return err } // FindUserByCredentialID looks up which user owns a credential. func FindUserByCredentialID(database *sql.DB, credentialID []byte) (int64, error) { var userID int64 err := database.QueryRow( "SELECT user_id FROM webauthn_credentials WHERE credential_id = ?", credentialID, ).Scan(&userID) if err != nil { return 0, fmt.Errorf("credential not found: %w", err) } return userID, nil }