- 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>
152 lines
4.1 KiB
Go
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
|
|
}
|