linstrom/auth-new/password.go

149 lines
4.6 KiB
Go

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
}