Fix passkey authentication

Also prep for better router layout
This commit is contained in:
Melody Becker 2024-10-15 16:16:18 +02:00
parent e2260e4a0f
commit b9eb4234f4
11 changed files with 289 additions and 21 deletions

View file

@ -9,15 +9,17 @@ import (
// various prefixes for accessing items in the cache (since it's a simple key-value store)
const (
cacheUserHandleToIdPrefix = "acc-name-to-id:"
cacheUserIdToAccPrefix = "acc-id-to-data:"
cacheNoteIdToNotePrefix = "note-id-to-data:"
cacheUserHandleToIdPrefix = "acc-name-to-id:"
cacheLocalUsernameToIdPrefix = "acc-local-name-to-id:"
cachePasskeyIdToAccIdPrefix = "acc-pkey-id-to-id:"
cacheUserIdToAccPrefix = "acc-id-to-data:"
cacheNoteIdToNotePrefix = "note-id-to-data:"
)
// An error describing the case where some value was just not found in the cache
var errCacheNotFound = errors.New("not found in cache")
// Find an account id in cache using a given user handle
// Find an account id in cache using a given user handle ("@bob@example.com" or "bob@example.com")
// accId contains the Id of the account if found
// err contains an error describing why an account's id couldn't be found
// The most common one should be errCacheNotFound
@ -38,6 +40,44 @@ func (s *Storage) cacheHandleToAccUid(handle string) (accId *string, err error)
return &target, nil
}
// Find a local account's id in cache using a given username ("bob")
// accId containst the Id of the account if found
// err contains an error describing why an account's id couldn't be found
// The most common one should be errCacheNotFound
func (s *Storage) cacheLocalUsernameToAccUid(username string) (accId *string, err error) {
// Where to put the data (in case it's found)
var target string
found, err := s.cache.Get(cacheLocalUsernameToIdPrefix+username, &target)
// If nothing was found, check error
if !found {
// Case error is set and NOT redis' error for nothing found: Return that error
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
} else {
// Else return errCacheNotFound
return nil, errCacheNotFound
}
}
return &target, nil
}
func (s *Storage) cachePkeyIdToAccId(pkeyId []byte) (accId *string, err error) {
// Where to put the data (in case it's found)
var target string
found, err := s.cache.Get(cachePasskeyIdToAccIdPrefix+string(pkeyId), &target)
// If nothing was found, check error
if !found {
// Case error is set and NOT redis' error for nothing found: Return that error
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
} else {
// Else return errCacheNotFound
return nil, errCacheNotFound
}
}
return &target, nil
}
// Find an account's data in cache using a given account id
// acc contains the full account as stored last time if found
// err contains an error describing why an account couldn't be found

View file

@ -1,6 +1,7 @@
package storage
import (
"crypto/ed25519"
"crypto/rand"
"errors"
"strings"
@ -20,10 +21,10 @@ import (
// If remote, this is used for caching the account
type Account struct {
ID string `gorm:"primarykey"` // ID is a uuid for this account
// Handle of the user (eg "max" if the full username is @max@example.com)
// Username of the user (eg "max" if the full username is @max@example.com)
// Assume unchangable (once set by a user) to be kind to other implementations
// Would be an easy avenue to fuck with them though
Handle string
Username string
CreatedAt time.Time // When this entry was created. Automatically set by gorm
// When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm
UpdatedAt time.Time
@ -44,7 +45,7 @@ type Account struct {
Background string // ID of a media file used as background image
Banner string // ID of a media file used as banner
Indexable bool // Whether this account can be found by crawlers
PublicKeyPem *string // The public key of the account
PublicKey []byte // The public key of the account
// Whether this account restricts following
// If true, the owner must approve of a follow request first
RestrictedFollow bool
@ -61,8 +62,8 @@ type Account struct {
// --- And internal account stuff ---
// Still public fields since they wouldn't be able to be stored in the db otherwise
PrivateKeyPem *string // The private key of the account. Nil if remote user
WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
PrivateKey []byte // The private key of the account. Nil if remote user
WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
// Whether the account got verified and is allowed to be active
// For local accounts being active means being allowed to login and perform interactions
// For remote users, if an account is not verified, any interactions it sends are discarded
@ -176,6 +177,114 @@ func (s *Storage) FindAccountById(id string) (*Account, error) {
return acc, nil
}
func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) {
log.Trace().Caller().Send()
log.Debug().Str("account-username", username).Msg("Looking for local account")
log.Debug().Str("account-username", username).Msg("Checking cache first")
// Try and find the account in cache first
cacheAccId, err := s.cacheLocalUsernameToAccUid(username)
if err == nil {
log.Info().Str("account-username", username).Msg("Hit account handle in cache")
// Then always load via id since unique key access should be faster than string matching
return s.FindAccountById(*cacheAccId)
} else {
if !errors.Is(err, errCacheNotFound) {
log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account")
return nil, err
}
}
// Failed to find in cache, go the slow route of hitting the db
log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db")
if err != nil {
log.Warn().
Err(err).
Str("account-username", username).
Msg("Failed to split up account username")
return nil, err
}
acc := Account{}
res := s.db.Where("username = ?", username).
Where("server = ?", config.GlobalConfig.General.GetFullDomain()).
First(&acc)
if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
log.Info().
Str("account-username", username).
Msg("Local account with username not found")
} else {
log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username")
}
return nil, res.Error
}
log.Info().Str("account-username", username).Msg("Found account, also inserting into cache")
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
log.Warn().
Err(err).
Str("account-username", username).
Msg("Found account but failed to insert into cache")
}
if err = s.cache.Set(cacheLocalUsernameToIdPrefix+username, acc.ID); err != nil {
log.Warn().
Err(err).
Str("account-username", username).
Msg("Failed to store local username to id in cache")
}
return &acc, nil
}
func (s *Storage) FindAccountByPasskeyId(pkeyId []byte) (*Account, error) {
log.Trace().Caller().Send()
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Looking for account")
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Checking cache first")
// Try and find the account in cache first
cacheAccId, err := s.cachePkeyIdToAccId(pkeyId)
if err == nil {
log.Info().Bytes("account-passkey-id", pkeyId).Msg("Hit passkey id in cache")
// Then always load via id since unique key access should be faster than string matching
return s.FindAccountById(*cacheAccId)
} else {
if err != errCacheNotFound {
log.Error().Err(err).Bytes("account-passkey-id", pkeyId).Msg("Problem while checking cache for account")
return nil, err
}
}
// Failed to find in cache, go the slow route of hitting the db
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Didn't hit account in cache, going to db")
acc := Account{}
res := s.db.Where("web_authn_id = ?", pkeyId).
First(&acc)
if res.Error != nil {
if res.Error == gorm.ErrRecordNotFound {
log.Info().
Bytes("account-passkey-id", pkeyId).
Msg("Local account with passkey id not found")
} else {
log.Error().Err(res.Error).Bytes("account-passkey-id", pkeyId).Msg("Failed to get local account with passkey id")
}
return nil, res.Error
}
log.Info().Bytes("account-passkey-id", pkeyId).Msg("Found account, also inserting into cache")
// if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
// log.Warn().
// Err(err).
// Bytes("account-passkey-id", pkeyId).
// Msg("Found account but failed to insert into cache")
// }
// if err = s.cache.Set(cachePasskeyIdToAccIdPrefix+string(pkeyId), acc.ID); err != nil {
// log.Warn().
// Err(err).
// Bytes("account-passkey-id", pkeyId).
// Msg("Failed to store local username to id in cache")
// }
return &acc, nil
}
// Update a given account in storage and cache
func (s *Storage) UpdateAccount(acc *Account) error {
// If the account is nil or doesn't have an id, error out
@ -236,11 +345,19 @@ func (s *Storage) NewLocalAccount(handle string) (*Account, error) {
log.Error().Err(err).Msg("Failed to create empty account for use")
return nil, err
}
acc.Handle = handle
acc.Username = handle
acc.Server = config.GlobalConfig.General.GetFullDomain()
acc.Remote = false
acc.DisplayName = handle
publicKey, privateKey, err := ed25519.GenerateKey(nil)
if err != nil {
log.Error().Err(err).Msg("Failed to generate key pair for new local account")
return nil, err
}
acc.PrivateKey = privateKey
acc.PublicKey = publicKey
log.Debug().
Str("account-handle", handle).
Str("account-id", acc.ID).
@ -267,7 +384,7 @@ func (a *Account) WebAuthnID() []byte {
func (u *Account) WebAuthnName() string {
log.Trace().Caller().Send()
return u.Handle
return u.Username
}
func (u *Account) WebAuthnDisplayName() string {
@ -302,7 +419,7 @@ func (s *Storage) GetOrCreateUser(userID string) passkey.User {
Str("account-handle", userID).
Msg("Looking for or creating account for passkey stuff")
acc := &Account{}
res := s.db.Where(Account{Handle: userID, Server: config.GlobalConfig.General.GetFullDomain()}).
res := s.db.Where(Account{Username: userID, Server: config.GlobalConfig.General.GetFullDomain()}).
First(acc)
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
log.Debug().Str("account-handle", userID)