// Package auth is responsible for everything authentication // // Be that checking login data and handing out an access token on sucess, // checking if a given access token can do the requested action // or adding or updating the authentication information of an account. // And I probably forgot something package auth import ( "time" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/go-webauthn/webauthn/webauthn" "git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" ) type Authenticator struct { webauthn *webauthn.WebAuthn } type LoginNextState uint8 const ( LoginNextFailure LoginNextState = 0 // Login failed (default state) LoginNextSucess LoginNextState = 1 << iota // Login suceeded LoginUnknown // Unknown login method type, should result in failure LoginNext2FaTotp // Login requires a totp token next as 2fa response LoginNext2FaPasskey // Login requires a passkey token next as 2fa response LoginNext2FaMail // Login requires an email token next as 2fa response LoginStartPassword // Login starts with a password LoginStartPasskey // Login starts with a passkey ) func calcAccessExpirationTimestamp() time.Time { // For now, the default expiration is one month after creation // though "never" might also be a good option return time.Now().Add(time.Hour * 24 * 30) } func ConvertNewStorageAuthMethodsToLoginState( methods []models.AuthenticationMethodType, isStart bool, ) LoginNextState { translatedMethods := sliceutils.Map(methods, oneStorageAuthToLoginState) // Filter out only the valid methods for the current request valids := sliceutils.Filter(translatedMethods, func(t LoginNextState) bool { if isStart { return t == LoginStartPasskey || t == LoginStartPassword } else { return t == LoginNext2FaTotp || t == LoginNext2FaPasskey || t == LoginNext2FaMail } }) // And then compact them down into one bit flag return sliceutils.Compact( valids, func(acc, next LoginNextState) LoginNextState { return acc | next }, ) } func oneStorageAuthToLoginState(in models.AuthenticationMethodType) LoginNextState { switch in { case models.AuthMethodGAuth: return LoginNext2FaTotp case models.AuthMethodMail: return LoginNext2FaMail case models.AuthMethodPasskey: return LoginStartPasskey case models.AuthMethodPasskey2fa: return LoginNext2FaPasskey case models.AuthMethodPassword: return LoginStartPassword default: return LoginUnknown } } // Check whether a given username can log in. // It only provides a yes/no answer and an error in case some check failed // due to unforseen circumstances. Though the error will always be ErrCantLogin // in case the known information (excluding database or other failures) prevents // a login // // TODO: Decide whether to include the reason for disallowed login func (a *Authenticator) canUsernameLogin(username string) (bool, error) { acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First() if err != nil { return false, err } if !acc.FinishedRegistration { return false, ErrCantLogin } finalRole := storage.CollapseRolesIntoOne( sliceutils.Map(acc.Roles, func(t models.UserToRole) models.Role { return t.Role })...) if finalRole.CanLogin != nil && !*finalRole.CanLogin { return false, ErrCantLogin } return true, nil }