linstrom/auth-new/totp.go
2025-03-31 23:23:25 +02:00

125 lines
3.3 KiB
Go

package auth
import (
"time"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"gorm.io/gorm/clause"
"git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
)
const totpUnverifiedSuffix = "-NOT_VERIFIED"
func (a *Authenticator) PerformTotpLogin(
username string,
totpToken string,
) (LoginNextState, string, error) {
if ok, err := a.canUsernameLogin(username); !ok {
return 0, "", other.Error("auth", "user may not login", err)
}
acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
if err != nil {
return LoginNextFailure, "", other.Error("auth", "failed to find account", err)
}
encryptedSecrets := sliceutils.Map(
sliceutils.Filter(acc.AuthMethods, func(t models.UserAuthMethod) bool {
return t.AuthMethod == models.AuthMethodGAuth
}),
func(t models.UserAuthMethod) string {
return string(t.Token)
},
)
secrets := []string{}
for _, key := range encryptedSecrets {
decrypted, err := Decrypt([]byte(config.GlobalConfig.Storage.EncryptionKey), []byte(key))
if err != nil {
return 0, "", other.Error(
"auth",
"failed to decrypt secret",
&CombinedError{ErrDecryptionFailure, err},
)
}
secrets = append(secrets, string(decrypted))
}
found := false
for _, secret := range secrets {
if totp.Validate(totpToken, secret) {
found = true
break
}
}
if !found {
return LoginNextFailure, "", other.Error(
"auth",
"no fitting credential found",
ErrInvalidCombination,
)
}
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
}
func (a *Authenticator) StartTotpRegistration(
username string,
tokenName string,
) (*otp.Key, error) {
if ok, err := a.canUsernameLogin(username); !ok {
return nil, other.Error("auth", "user may not login", err)
}
acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
if err != nil {
return nil, other.Error("auth", "failed to find account", err)
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: config.GlobalConfig.General.GetFullDomain(),
AccountName: username,
SecretSize: 160,
})
if err != nil {
return nil, err
}
secret := key.Secret()
encryptedSecret, err := Encrypt(
[]byte(config.GlobalConfig.Storage.EncryptionKey),
[]byte(secret),
)
if err != nil {
return nil, other.Error("auth", "failed to encrypt secret", err)
}
authToken := models.UserAuthMethod{
UserId: acc.ID,
User: *acc,
Token: encryptedSecret,
AuthMethod: models.AuthMethodGAuth,
Name: tokenName + totpUnverifiedSuffix,
}
err = dbgen.UserAuthMethod.Create(&authToken)
if err != nil {
return nil, err
}
return key, nil
}