package auth import ( "time" "git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" ) // Start a login process with a username (NOT account ID) and password // Returns the next state, a token corresponding to that state and error // Token will be empty on failure, error describes the reason for the // failure func (a *Authenticator) PerformPasswordLogin( username string, password string, ) (nextState LoginNextState, token string, err error) { if ok, err := a.canUsernameLogin(username); !ok { return LoginNextFailure, "", other.Error("auth", "user may not login", err) } acc, err := dbgen.User.GetByUsername(username) switch err { case nil: break case gorm.ErrRecordNotFound: return LoginNextFailure, "", ErrInvalidCombination } var method *models.UserAuthMethod for _, authMethod := range acc.AuthMethods { if oneStorageAuthToLoginState(authMethod.AuthMethod) == LoginStartPassword { method = &authMethod break } } if method == nil { return LoginNextFailure, "", ErrUnsupportedAuthMethod } if !comparePassword(password, method.Token) { return LoginNextFailure, "", ErrInvalidCombination } nextStates := ConvertNewStorageAuthMethodsToLoginState( sliceutils.Map( acc.AuthMethods, func(t models.UserAuthMethod) models.AuthenticationMethodType { return t.AuthMethod }, ), false, ) // Catch unknown login methods if nextStates&LoginUnknown == LoginUnknown { return LoginNextFailure, "", ErrUnknownAuthMethod } if nextStates == LoginNextFailure { // Login ok, no 2fa needed // Create a new token. Don't generate the token itself, let postgres handle that token := models.AccessToken{ User: *acc, UserId: acc.ID, ExpiresAt: time.Now().Add(time.Hour * 24 * 365), } err = dbgen.AccessToken. // technically, the chance of a conflict is so incredibly low that it should never ever occur // since both the username and a randomly generated uuid would need to be created the same. Twice // But, just in case, do this Clauses(clause.OnConflict{DoNothing: true}). Omit(dbgen.AccessToken.Token). Create(&token) if err != nil { return LoginNextFailure, "", other.Error( "auth", "failed to create new access token", err, ) } return LoginNextSucess, token.Token, nil } // TODO: Generate login process token loginToken := models.LoginProcessToken{ User: *acc, UserId: acc.ID, ExpiresAt: calcAccessExpirationTimestamp(), Token: uuid.NewString(), } err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{UpdateAll: true}). Create(&loginToken) if err != nil { return LoginNextFailure, "", other.Error( "auth", "failed to create login process token", err, ) } return nextStates, loginToken.Token, nil } // Register a password to an account // There is no check for whether that is allowed from the active roles. // If the given username already has a password set, it gets updated to the new one. // If there is no password set yet (i.e. during account registration or passkey only so far) // it creates the password link func (a *Authenticator) PerformPasswordRegister(username, password string) error { acc, err := dbgen.User.GetByUsername(username) if err != nil { return other.Error("auth", "failed to get user to add a password to", err) } passwordHash, err := hashPassword(password) if err != nil { return other.Error("auth", "failed to hash password", err) } passwordMethods := sliceutils.Filter( acc.AuthMethods, func(t models.UserAuthMethod) bool { return t.AuthMethod == models.AuthMethodPassword }, ) if len(passwordMethods) > 0 { // TODO: Decide whether PerformPasswordRegister can also update existing passwords // Imo yes, since an account can have at most one password set and having a separate method for updating // would increase complexity // For now, do perform an update dbPass := passwordMethods[0] _, err = dbgen.UserAuthMethod.Where(dbgen.UserAuthMethod.ID.Eq(dbPass.ID)). Update(dbgen.UserAuthMethod.Token, passwordHash) if err != nil { return other.Error("auth", "failed to update password", err) } } else { dbPass := models.UserAuthMethod{ Token: passwordHash, AuthMethod: models.AuthMethodPassword, User: *acc, UserId: acc.ID, } err = dbgen.UserAuthMethod.Omit(dbgen.UserAuthMethod.ID).Create(&dbPass) if err != nil { return other.Error("auth", "failed to insert password", err) } } return nil }