Fix passkey authentication
Also prep for better router layout
This commit is contained in:
parent
e2260e4a0f
commit
b9eb4234f4
11 changed files with 289 additions and 21 deletions
|
@ -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
|
||||
|
|
133
storage/user.go
133
storage/user.go
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue