package auth import ( "bytes" "context" "encoding/base64" "fmt" "image/png" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/pkg/errors" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" ) // ErrUserNotFound is returned when a user is not found var ErrUserNotFound = errors.New("user not found") // Users returns all users in the database func (auth *Auth) Users(ctx context.Context) (users []*AuthUser, err error) { // query the user table table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable) if err != nil { return } // find all the users var dUsers []models.User err = table.Find(&dUsers).Error if err != nil { return nil, err } // and map them to high-level user objects users = make([]*AuthUser, len(dUsers)) for i, user := range dUsers { users[i] = &AuthUser{ User: user, auth: auth, } } return users, nil } // User returns a single user. // If the user does not exist, returns ErrUserNotFound. func (auth *Auth) User(ctx context.Context, name string) (user *AuthUser, err error) { // quick and dirty check for the empty username (which is not allowed) if name == "" { return nil, ErrUserNotFound } // return the user table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable) if err != nil { return } user = &AuthUser{} // find the user res := table.Where(&models.User{User: name}).Find(&user.User) err = res.Error if err != nil { return } // check if the user was not found if res.RowsAffected == 0 { return nil, ErrUserNotFound } user.auth = auth return } // CreateUser creates a new user and returns it. // The user is not associated to any WissKIs, and has no password set. func (auth *Auth) CreateUser(ctx context.Context, name string) (user *AuthUser, err error) { // return the user table, err := auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable) if err != nil { return } user = &AuthUser{ User: models.User{ User: name, }, } user.SetAdmin(false) user.SetEnabled(false) user.SetTOTPEnabled(false) // do the create statement err = table.Select("*").Create(&user.User).Error if err != nil { return nil, errors.Wrapf(err, "Create") } user.auth = auth return user, nil } // AuthUser represents an authorized user type AuthUser struct { auth *Auth models.User } func (au *AuthUser) String() string { if au == nil { return "User{nil}" } hasPassword := len(au.PasswordHash) > 0 return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.IsEnabled(), hasPassword, au.User.IsAdmin()) } var ( ErrTOTPEnabled = errors.New("TOTP is enabled") ErrTOTPDisabled = errors.New("TOTP is disabled") ErrTOTPFailed = errors.New("TOTP failed") ) func (au *AuthUser) TOTP() (*otp.Key, error) { if au.TOTPURL == "" { return nil, ErrTOTPDisabled } return otp.NewKeyFromURL(au.TOTPURL) } // CheckTOTP validates the given totp passcode against the saved secret. // If totp is not enabled, any passcode will pass the check. func (au *AuthUser) CheckTOTP(passcode string) error { secret, err := au.TOTP() if err != nil { return err } if au.IsTOTPEnabled() && !totp.Validate(passcode, secret.Secret()) { return ErrTOTPFailed } return nil } // NewTOTP generates a new TOTP secret, returning a totp key. func (au *AuthUser) NewTOTP(ctx context.Context) (*otp.Key, error) { if au.User.IsTOTPEnabled() { return nil, ErrTOTPEnabled } key, err := totp.Generate(totp.GenerateOpts{ Issuer: "WissKI Distillery", AccountName: au.User.User, }) if err != nil { return nil, err } au.User.TOTPURL = key.URL() return key, au.Save(ctx) } func TOTPLink(secret *otp.Key, width, height int) (string, error) { // make an image img, err := secret.Image(width, height) if err != nil { return "", err } // encode image as base64 var buffer bytes.Buffer if err := png.Encode(&buffer, img); err != nil { return "", err } // return the image url return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes()), nil } // EnableTOTP enables totp for the given user func (au *AuthUser) EnableTOTP(ctx context.Context, passcode string) error { secret, err := au.TOTP() if err != nil { return err } if !totp.Validate(passcode, secret.Secret()) { return ErrTOTPFailed } au.User.SetTOTPEnabled(true) return au.Save(ctx) } // DisableTOTP disables totp for the given user func (au *AuthUser) DisableTOTP(ctx context.Context) (err error) { au.User.SetTOTPEnabled(false) au.User.TOTPURL = "" return au.Save(ctx) } // SetPassword sets the password for this user and turns the user on func (au *AuthUser) SetPassword(ctx context.Context, password []byte) (err error) { au.User.PasswordHash, err = bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { return err } au.User.SetEnabled(true) return au.Save(ctx) } // UnsetPassword removes the password from this user, and disables them func (au *AuthUser) UnsetPassword(ctx context.Context) error { au.User.PasswordHash = nil au.User.SetEnabled(false) return au.Save(ctx) } var ErrNoUser = errors.New("user is nil") var ErrUserDisabled = errors.New("user is disabled") var ErrUserBlank = errors.New("user has no password set") // CheckPassword checks if this user can login with the provided password. // Returns nil on success, an error otherwise. func (au *AuthUser) CheckPassword(ctx context.Context, password []byte) error { if au == nil { return ErrNoUser } if !au.User.IsEnabled() { return ErrUserDisabled } if len(au.User.PasswordHash) == 0 { return ErrUserDisabled } return bcrypt.CompareHashAndPassword(au.User.PasswordHash, password) } func (au *AuthUser) CheckCredentials(ctx context.Context, password []byte, passcode string) error { if err := au.CheckPassword(ctx, password); err != nil { return err } if err := au.CheckTOTP(passcode); err != nil && err != ErrTOTPDisabled { return err } return nil } // MakeAdmin makes this user an admin, and saves the update in the database. // If the user is already an admin, does not return an error. func (au *AuthUser) MakeAdmin(ctx context.Context) error { au.User.SetAdmin(true) return au.Save(ctx) } // MakeRegular removes admin rights from this user. // If this user is not an dmin, does not return an error. func (au *AuthUser) MakeRegular(ctx context.Context) error { au.User.SetAdmin(false) return au.Save(ctx) } // Save saves the given user in the database func (au *AuthUser) Save(ctx context.Context) error { table, err := au.auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable) if err != nil { return err } return table.Select("*").Updates(&au.User).Error } // Delete deletes the user from the database func (au *AuthUser) Delete(ctx context.Context) error { table, err := au.auth.Dependencies.SQL.QueryTable(ctx, false, models.UserTable) if err != nil { return err } // run all the user delete hooks for _, c := range au.auth.Dependencies.UserDeleteHooks { if err := c.OnUserDelete(ctx, &au.User); err != nil { return err } } return table.Delete(&au.User).Error }