Files
eng-pad-server/internal/auth/webauthn.go
Kyle Isom 169063cd00 Implement Phase 9: FIDO2/U2F WebAuthn support
- WebAuthnUser implementing webauthn.User interface
- NewWebAuthn factory with configurable RP settings
- Credential storage: store, load, list, delete, update sign count
- User lookup by credential ID for login flow
- go-webauthn/webauthn library integrated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:00:10 -07:00

152 lines
4.1 KiB
Go

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
}