125 lines
3.3 KiB
Go
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
|
|
}
|