Compare commits

..

No commits in common. "ef9155860082e9d9f33040a2c70e7bc76dd0f902" and "e7e48bfd5165d137746915e8dc81c679d8ac8a84" have entirely different histories.

9 changed files with 19 additions and 78 deletions

View file

@ -30,7 +30,6 @@ const (
LoginStartPasskey // Login starts with a passkey LoginStartPasskey // Login starts with a passkey
) )
// Create a new authenticator
func New(webauthnConfig *webauthn.Config) (*Authenticator, error) { func New(webauthnConfig *webauthn.Config) (*Authenticator, error) {
webauthn, err := webauthn.New(webauthnConfig) webauthn, err := webauthn.New(webauthnConfig)
if err != nil { if err != nil {

View file

@ -1,31 +1,20 @@
package auth package auth
import ( import "errors"
"errors"
)
var ( var (
// The provided authentication method is not known to the server // The provided authentication method is not known to the server
ErrUnknownAuthMethod = errors.New("unknown authentication method") ErrUnknownAuthMethod = errors.New("unknown authentication method")
// The user hasn't setup the provided authentication method // The user hasn't setup the provided authentication method
ErrUnsupportedAuthMethod = errors.New("authentication method not supported for this user") ErrUnsupportedAuthMethod = errors.New("authentication method not supported for this user")
// The given combination of token and account is invalid
// Explicitly doesn't mention which part is valid to improve security
ErrInvalidCombination = errors.New("invalid account and token combination") ErrInvalidCombination = errors.New("invalid account and token combination")
// The current authentication attempt has expired and needs to be restarted
ErrProcessTimeout = errors.New("authentication process timed out") ErrProcessTimeout = errors.New("authentication process timed out")
// A user may not login, for whatever reason // A user may not login, for whatever reason
ErrCantLogin = errors.New("user can't login") ErrCantLogin = errors.New("user can't login")
// Failed to decrypt the relevant data
ErrDecryptionFailure = errors.New("failed to decrypt content") ErrDecryptionFailure = errors.New("failed to decrypt content")
// The given totp token was recently (90 seconds) used for that username
// For security reasons, this case will be caught and blocked
ErrTotpRecentlyUsed = errors.New("totp token was used too recently") ErrTotpRecentlyUsed = errors.New("totp token was used too recently")
) )
// Helper error type to combine two errors into one
// For when two different errors need to be passed together
// since fmt.Errorf doesn't really allow that as far as I know
type CombinedError struct { type CombinedError struct {
Err1, Err2 error Err1, Err2 error
} }
@ -37,7 +26,3 @@ func (c *CombinedError) Is(e error) bool {
func (c *CombinedError) Error() string { func (c *CombinedError) Error() string {
return c.Err1.Error() + " + " + c.Err2.Error() return c.Err1.Error() + " + " + c.Err2.Error()
} }
func (c *CombinedError) Unwrap() []error {
return []error{c.Err1, c.Err2}
}

View file

@ -10,8 +10,6 @@ import (
"git.mstar.dev/mstar/linstrom/storage-new/models" "git.mstar.dev/mstar/linstrom/storage-new/models"
) )
// A fake user struct for implementing the webauthn.User interface
// on top of the storage system
type fakeUser struct { type fakeUser struct {
actualUser *models.User actualUser *models.User
} }

View file

@ -10,7 +10,7 @@ import (
"git.mstar.dev/mstar/goutils/sliceutils" "git.mstar.dev/mstar/goutils/sliceutils"
"golang.org/x/crypto/argon2" "golang.org/x/crypto/argon2"
"git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models" "git.mstar.dev/mstar/linstrom/storage-new/models"
) )

View file

@ -3,7 +3,6 @@ package auth
// Some helpful comments from: https://waters.me/internet/google-authenticator-implementation-note-key-length-token-reuse/ // Some helpful comments from: https://waters.me/internet/google-authenticator-implementation-note-key-length-token-reuse/
import ( import (
"strings"
"time" "time"
"git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/other"
@ -22,7 +21,6 @@ const totpTokenNoLongerRecentlyUsed = time.Second * 90
func (a *Authenticator) PerformTotpLogin( func (a *Authenticator) PerformTotpLogin(
username string, username string,
sessionId uint64,
totpToken string, totpToken string,
) (LoginNextState, string, error) { ) (LoginNextState, string, error) {
// First check if that token has been seen recently for that user // First check if that token has been seen recently for that user
@ -33,26 +31,17 @@ func (a *Authenticator) PerformTotpLogin(
delete(a.recentlyUsedTotpTokens, totpToken+"+"+username) delete(a.recentlyUsedTotpTokens, totpToken+"+"+username)
} }
} }
// Then ensure user is allowed to log in
if ok, err := a.canUsernameLogin(username); !ok { if ok, err := a.canUsernameLogin(username); !ok {
return 0, "", other.Error("auth", "user may not login", err) return 0, "", other.Error("auth", "user may not login", err)
} }
// DO NOT fetch the account directly. Go via session id acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
// otherwise you could in theory log in with only the totp token
// which obviously is a huge risk with it being at most 1'000'000 unique values.
// And even with rate limiting (performed by one of the upper layers)
// this wouldn't take too long
loginSession, err := dbgen.LoginProcessToken.Where(dbgen.LoginProcessToken.ID.Eq(sessionId)).
First()
if err != nil { if err != nil {
return 0, "", other.Error("auth", "no login session with this id", err) return LoginNextFailure, "", other.Error("auth", "failed to find account", err)
} }
acc := loginSession.User
dbSecrets := sliceutils.Filter(acc.AuthMethods, func(t models.UserAuthMethod) bool {
return t.AuthMethod == models.AuthMethodGAuth
})
encryptedSecrets := sliceutils.Map( encryptedSecrets := sliceutils.Map(
dbSecrets, sliceutils.Filter(acc.AuthMethods, func(t models.UserAuthMethod) bool {
return t.AuthMethod == models.AuthMethodGAuth
}),
func(t models.UserAuthMethod) string { func(t models.UserAuthMethod) string {
return string(t.Token) return string(t.Token)
}, },
@ -70,11 +59,9 @@ func (a *Authenticator) PerformTotpLogin(
secrets = append(secrets, string(decrypted)) secrets = append(secrets, string(decrypted))
} }
found := false found := false
foundIndex := -1 for _, secret := range secrets {
for i, secret := range secrets {
if totp.Validate(totpToken, secret) { if totp.Validate(totpToken, secret) {
found = true found = true
foundIndex = i
break break
} }
} }
@ -85,20 +72,11 @@ func (a *Authenticator) PerformTotpLogin(
ErrInvalidCombination, ErrInvalidCombination,
) )
} }
// If not verified yet, mark as verified
if strings.HasSuffix(dbSecrets[foundIndex].Name, totpUnverifiedSuffix) {
dbgen.UserAuthMethod.
Where(dbgen.UserAuthMethod.ID.
Eq(dbSecrets[foundIndex].ID)).
Update(dbgen.UserAuthMethod.Name, strings.TrimSuffix(dbSecrets[foundIndex].Name, totpUnverifiedSuffix))
}
// store this token and username combination as recently used
a.recentlyUsedTotpTokens[totpToken+"+"+username] = time.Now() a.recentlyUsedTotpTokens[totpToken+"+"+username] = time.Now()
// Generate access token and return since totp would be the end station for 2fa
token := models.AccessToken{ token := models.AccessToken{
User: acc, User: *acc,
UserId: acc.ID, UserId: acc.ID,
ExpiresAt: time.Now().Add(time.Hour * 24 * 365), ExpiresAt: time.Now().Add(time.Hour * 24 * 365),
} }

2
go.mod
View file

@ -29,7 +29,6 @@ require (
gorm.io/gen v0.3.26 gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3 gorm.io/plugin/dbresolver v1.5.3
github.com/pquerna/otp v1.4.0
) )
require ( require (
@ -70,6 +69,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect

View file

@ -26,10 +26,7 @@ func newUserAuthMethod(db *gorm.DB, opts ...gen.DOOption) userAuthMethod {
tableName := _userAuthMethod.userAuthMethodDo.TableName() tableName := _userAuthMethod.userAuthMethodDo.TableName()
_userAuthMethod.ALL = field.NewAsterisk(tableName) _userAuthMethod.ALL = field.NewAsterisk(tableName)
_userAuthMethod.ID = field.NewUint(tableName, "id") _userAuthMethod.ID = field.NewUint64(tableName, "id")
_userAuthMethod.CreatedAt = field.NewTime(tableName, "created_at")
_userAuthMethod.UpdatedAt = field.NewTime(tableName, "updated_at")
_userAuthMethod.DeletedAt = field.NewField(tableName, "deleted_at")
_userAuthMethod.UserId = field.NewString(tableName, "user_id") _userAuthMethod.UserId = field.NewString(tableName, "user_id")
_userAuthMethod.AuthMethod = field.NewField(tableName, "auth_method") _userAuthMethod.AuthMethod = field.NewField(tableName, "auth_method")
_userAuthMethod.Token = field.NewBytes(tableName, "token") _userAuthMethod.Token = field.NewBytes(tableName, "token")
@ -184,10 +181,7 @@ type userAuthMethod struct {
userAuthMethodDo userAuthMethodDo
ALL field.Asterisk ALL field.Asterisk
ID field.Uint ID field.Uint64
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
UserId field.String UserId field.String
AuthMethod field.Field AuthMethod field.Field
Token field.Bytes Token field.Bytes
@ -209,10 +203,7 @@ func (u userAuthMethod) As(alias string) *userAuthMethod {
func (u *userAuthMethod) updateTableName(table string) *userAuthMethod { func (u *userAuthMethod) updateTableName(table string) *userAuthMethod {
u.ALL = field.NewAsterisk(table) u.ALL = field.NewAsterisk(table)
u.ID = field.NewUint(table, "id") u.ID = field.NewUint64(table, "id")
u.CreatedAt = field.NewTime(table, "created_at")
u.UpdatedAt = field.NewTime(table, "updated_at")
u.DeletedAt = field.NewField(table, "deleted_at")
u.UserId = field.NewString(table, "user_id") u.UserId = field.NewString(table, "user_id")
u.AuthMethod = field.NewField(table, "auth_method") u.AuthMethod = field.NewField(table, "auth_method")
u.Token = field.NewBytes(table, "token") u.Token = field.NewBytes(table, "token")
@ -233,11 +224,8 @@ func (u *userAuthMethod) GetFieldByName(fieldName string) (field.OrderExpr, bool
} }
func (u *userAuthMethod) fillFieldMap() { func (u *userAuthMethod) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 9) u.fieldMap = make(map[string]field.Expr, 6)
u.fieldMap["id"] = u.ID u.fieldMap["id"] = u.ID
u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt
u.fieldMap["deleted_at"] = u.DeletedAt
u.fieldMap["user_id"] = u.UserId u.fieldMap["user_id"] = u.UserId
u.fieldMap["auth_method"] = u.AuthMethod u.fieldMap["auth_method"] = u.AuthMethod
u.fieldMap["token"] = u.Token u.fieldMap["token"] = u.Token

View file

@ -1,5 +0,0 @@
package models
// TODO: Struct for mapping a note to a user for their personal feed
// Storing timeline info in redis could also be an idea, but I kinda like
// everything being in one place

View file

@ -1,7 +1,5 @@
package models package models
import "gorm.io/gorm"
// One authentication method linked to one account. // One authentication method linked to one account.
// Contains the method and whatever the token may be // Contains the method and whatever the token may be
// For a password, this would be a hash of that password, // For a password, this would be a hash of that password,
@ -10,7 +8,7 @@ import "gorm.io/gorm"
// //
// Password hashes may only exist at most once per user, the rest 0-m // Password hashes may only exist at most once per user, the rest 0-m
type UserAuthMethod struct { type UserAuthMethod struct {
gorm.Model ID uint64 `gorm:"primarykey"`
User User User User
UserId string UserId string
AuthMethod AuthenticationMethodType `gorm:"type:auth_method_type"` AuthMethod AuthenticationMethodType `gorm:"type:auth_method_type"`