diff --git a/go.mod b/go.mod index c335912..541e46e 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/boombuler/barcode v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect @@ -64,6 +65,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pquerna/otp v1.4.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect diff --git a/go.sum b/go.sum index 92b9b92..bc1c3c8 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,9 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= +github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -271,6 +274,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= diff --git a/pkg/internal/http/api/factors_api.go b/pkg/internal/http/api/factors_api.go index ea26446..cc87fdd 100644 --- a/pkg/internal/http/api/factors_api.go +++ b/pkg/internal/http/api/factors_api.go @@ -8,7 +8,10 @@ import ( "git.solsynth.dev/hypernet/passport/pkg/internal/http/exts" "git.solsynth.dev/hypernet/passport/pkg/internal/services" "github.com/gofiber/fiber/v2" + "github.com/pquerna/otp/totp" "github.com/samber/lo" + "github.com/spf13/viper" + "gorm.io/datatypes" ) func getAvailableFactors(c *fiber.Ctx) error { @@ -100,10 +103,45 @@ func createFactor(c *fiber.Ctx) error { Account: user, AccountID: user.ID, } + + additionalOnceConfig := map[string]any{} + + switch data.Type { + case models.TimeOtpFactor: + cfg := totp.GenerateOpts{ + Issuer: viper.GetString("name"), + AccountName: user.Name, + Period: 30, + SecretSize: 20, + Digits: 6, + } + key, err := totp.Generate(cfg) + if err != nil { + return fmt.Errorf("unable to generate totp key: %v", err) + } + factor.Secret = key.Secret() + factor.Config = datatypes.NewJSONType(map[string]any{ + "issuer": cfg.Issuer, + "account_name": cfg.AccountName, + "period": cfg.Period, + "secret_size": cfg.SecretSize, + "digits": cfg.Digits, + }) + additionalOnceConfig["url"] = key.URL() + } + if err := database.C.Create(&factor).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } + if len(additionalOnceConfig) > 0 { + data := factor.Config.Data() + for k, v := range additionalOnceConfig { + data[k] = v + } + factor.Config = datatypes.NewJSONType(data) + } + return c.JSON(factor) } diff --git a/pkg/internal/services/factors.go b/pkg/internal/services/factors.go index 8afa6bc..55cf4f2 100644 --- a/pkg/internal/services/factors.go +++ b/pkg/internal/services/factors.go @@ -11,6 +11,7 @@ import ( "git.solsynth.dev/hypernet/pusher/pkg/pushkit" "github.com/google/uuid" "github.com/nats-io/nats.go" + "github.com/pquerna/otp/totp" "github.com/rs/zerolog/log" "github.com/samber/lo" "github.com/spf13/viper" @@ -70,6 +71,36 @@ func CountUserFactor(userId uint) int64 { func GetFactorCode(factor models.AuthFactor) (bool, error) { switch factor.Type { + case models.InAppNotifyFactor: + var user models.Account + if err := database.C.Where(&models.Account{ + BaseModel: models.BaseModel{ID: factor.AccountID}, + }).First(&user).Error; err != nil { + return true, err + } + + secret := uuid.NewString()[:6] + + identifier := fmt.Sprintf("%s%d", gap.FactorOtpPrefix, factor.ID) + _, err := gap.Jt.Publish(identifier, []byte(secret)) + if err != nil { + return true, fmt.Errorf("error during publish message: %v", err) + } else { + log.Info().Uint("factor", factor.ID).Str("secret", secret).Msg("Published one-time-password to JetStream...") + } + + err = PushNotification(models.Notification{ + Topic: "passport.security.otp", + Title: "Your login one-time-password", + Body: fmt.Sprintf("`%s` is your login verification code. It will expires in 30 minutes.", secret), + Account: user, + AccountID: user.ID, + }, true) + if err != nil { + log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to delivery one-time-password via notify...") + return true, nil + } + return true, nil case models.EmailPasswordFactor: var user models.Account if err := database.C.Where(&models.Account{ @@ -103,7 +134,6 @@ func GetFactorCode(factor models.AuthFactor) (bool, error) { return true, nil } return true, nil - default: return false, nil } @@ -117,6 +147,13 @@ func CheckFactor(factor models.AuthFactor, code string) error { nil, fmt.Errorf("invalid password"), ) + case models.TimeOtpFactor: + lo.Ternary( + totp.Validate(code, factor.Secret), + nil, + fmt.Errorf("invalid verification code"), + ) + case models.InAppNotifyFactor: case models.EmailPasswordFactor: identifier := fmt.Sprintf("%s%d", gap.FactorOtpPrefix, factor.ID) sub, err := gap.Jt.PullSubscribe(identifier, "otp_validator", nats.BindStream("OTPs"))