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>
This commit is contained in:
151
internal/auth/webauthn.go
Normal file
151
internal/auth/webauthn.go
Normal file
@@ -0,0 +1,151 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user