From 0470100ebe1309d0f96d6c9d2a58472b2b826ce2 Mon Sep 17 00:00:00 2001 From: Jack <39212456+jack1902@users.noreply.github.com> Date: Tue, 14 Jul 2020 22:05:13 +0100 Subject: [PATCH] Feature/totp default admin (#111) * Add TotpKey to config, reset via settings page * Adding TOTP support for default Admin Signin * Add totp support, and regen of TOTP Keys for Admin * Fixing 'img-alt' bug picked up by SonarCloud --- cmd/subspace/config.go | 32 +++++++++++++++++++ cmd/subspace/handlers.go | 64 +++++++++++++++++++++++++++++++++++++ cmd/subspace/main.go | 11 +++++++ cmd/subspace/web.go | 3 ++ go.mod | 1 + go.sum | 5 +++ web/templates/settings.html | 47 ++++++++++++++++++++++++++- web/templates/signin.html | 12 ++++++- 8 files changed, 173 insertions(+), 2 deletions(-) diff --git a/cmd/subspace/config.go b/cmd/subspace/config.go index da0b1d15..00324de4 100644 --- a/cmd/subspace/config.go +++ b/cmd/subspace/config.go @@ -17,6 +17,8 @@ import ( "sort" "sync" "time" + + "github.com/pquerna/otp/totp" ) var ( @@ -61,6 +63,7 @@ type Info struct { Email string `json:"email"` Password []byte `json:"password"` Secret string `json:"secret"` + TotpKey string `json:"totp_key"` Configured bool `json:"configure"` Domain string `json:"domain"` HashKey string `json:"hash_key"` @@ -422,3 +425,32 @@ func (c *Config) save() error { } return Overwrite(c.filename, b, 0644) } + +func (c *Config) ResetTotp() error { + c.Lock() + defer c.Unlock() + + c.Info.TotpKey = "" + + if err := c.save(); err != nil { + return err + } + + return c.GenerateTOTP() +} + +func (c *Config) GenerateTOTP() error { + key, err := totp.Generate( + totp.GenerateOpts{ + Issuer: httpHost, + AccountName: config.Info.Email, + }, + ) + if err != nil { + return err + } + + tempTotpKey = key + + return nil +} diff --git a/cmd/subspace/handlers.go b/cmd/subspace/handlers.go index f89e2827..7122f083 100644 --- a/cmd/subspace/handlers.go +++ b/cmd/subspace/handlers.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "fmt" + "image/png" "io/ioutil" "net/http" "os" @@ -9,6 +11,7 @@ import ( "strings" "github.com/julienschmidt/httprouter" + "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" qrcode "github.com/skip2/go-qrcode" @@ -229,6 +232,7 @@ func signinHandler(w *Web) { email := strings.ToLower(strings.TrimSpace(w.r.FormValue("email"))) password := w.r.FormValue("password") + passcode := w.r.FormValue("totp") if email != config.FindInfo().Email { w.Redirect("/signin?error=invalid") @@ -239,6 +243,13 @@ func signinHandler(w *Web) { w.Redirect("/signin?error=invalid") return } + + if config.FindInfo().TotpKey != "" && !totp.Validate(passcode, config.FindInfo().TotpKey) { + // Totp has been configured and the provided code doesn't match + w.Redirect("/signin?error=invalid") + return + } + if err := w.SigninSession(true, ""); err != nil { Error(w.w, err) return @@ -247,6 +258,36 @@ func signinHandler(w *Web) { w.Redirect("/") } +func totpQRHandler(w *Web) { + if !w.Admin { + Error(w.w, fmt.Errorf("failed to view config: permission denied")) + return + } + + if config.Info.TotpKey != "" { + // TOTP is already configured, don't allow the current one to be leaked + w.Redirect("/") + return + } + + var buf bytes.Buffer + img, err := tempTotpKey.Image(200, 200) + if err != nil { + Error(w.w, err) + return + } + + png.Encode(&buf, img) + + w.w.Header().Set("Content-Type", "image/png") + w.w.Header().Set("Content-Length", fmt.Sprintf("%d", len(buf.Bytes()))) + if _, err := w.w.Write(buf.Bytes()); err != nil { + Error(w.w, err) + return + } + +} + func userEditHandler(w *Web) { userID := w.ps.ByName("user") if userID == "" { @@ -538,6 +579,9 @@ func settingsHandler(w *Web) { currentPassword := w.r.FormValue("current_password") newPassword := w.r.FormValue("new_password") + resetTotp := w.r.FormValue("reset_totp") + totpCode := w.r.FormValue("totp_code") + config.UpdateInfo(func(i *Info) error { i.SAML.IDPMetadata = samlMetadata i.Email = email @@ -577,6 +621,26 @@ func settingsHandler(w *Web) { }) } + if resetTotp == "true" { + err := config.ResetTotp() + if err != nil { + w.Redirect("/settings?error=totp") + return + } + + w.Redirect("/settings?success=totp") + return + } + + if config.Info.TotpKey == "" && totpCode != "" { + if !totp.Validate(totpCode, tempTotpKey.Secret()) { + w.Redirect("/settings?error=totp") + return + } + config.Info.TotpKey = tempTotpKey.Secret() + config.save() + } + w.Redirect("/settings?success=settings") } diff --git a/cmd/subspace/main.go b/cmd/subspace/main.go index aef13fb6..1956ac9d 100644 --- a/cmd/subspace/main.go +++ b/cmd/subspace/main.go @@ -18,6 +18,7 @@ import ( "time" "github.com/julienschmidt/httprouter" + "github.com/pquerna/otp" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" @@ -79,6 +80,9 @@ var ( // theme semanticTheme string + + // Totp + tempTotpKey *otp.Key ) func init() { @@ -144,6 +148,12 @@ func main() { logger.Fatal(err) } + // TOTP + err = config.GenerateTOTP() + if err != nil { + logger.Fatal(err) + } + // Secure token securetoken = securecookie.New([]byte(config.FindInfo().HashKey), []byte(config.FindInfo().BlockKey)) @@ -170,6 +180,7 @@ func main() { r.GET("/saml/acs", Log(samlHandler)) r.POST("/saml/acs", Log(samlHandler)) + r.GET("/totp/image", Log(WebHandler(totpQRHandler, "totp/image"))) r.GET("/signin", Log(WebHandler(signinHandler, "signin"))) r.GET("/signout", Log(WebHandler(signoutHandler, "signout"))) r.POST("/signin", Log(WebHandler(signinHandler, "signin"))) diff --git a/cmd/subspace/web.go b/cmd/subspace/web.go index c25818b5..b825e9a2 100644 --- a/cmd/subspace/web.go +++ b/cmd/subspace/web.go @@ -13,6 +13,7 @@ import ( "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" + "github.com/pquerna/otp" "golang.org/x/net/publicsuffix" @@ -58,6 +59,7 @@ type Web struct { TargetProfiles []Profile SemanticTheme string + TempTotpKey *otp.Key } func init() { @@ -159,6 +161,7 @@ func WebHandler(h func(*Web), section string) httprouter.Handle { Info: config.FindInfo(), SAML: samlSP, SemanticTheme: semanticTheme, + TempTotpKey: tempTotpKey, } if section == "signin" || section == "forgot" || section == "configure" { diff --git a/go.mod b/go.mod index 026e6409..135fad6f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/jteeuwen/go-bindata v3.0.8-0.20180305030458-6025e8de665b+incompatible github.com/julienschmidt/httprouter v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pquerna/otp v1.2.0 // indirect github.com/sirupsen/logrus v1.6.0 github.com/skip2/go-qrcode v0.0.0-20200519171959-a3b48390827e golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 diff --git a/go.sum b/go.sum index 9529a476..a46dc61a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beevik/etree v1.0.1 h1:lWzdj5v/Pj1X360EV7bUudox5SRipy4qZLjY0rhb0ck= github.com/beevik/etree v1.0.1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/crewjam/saml v0.3.0 h1:nsICCm1susKcMzqhZ+XwOvYUG55Omu1dHlyyknhgh1M= github.com/crewjam/saml v0.3.0/go.mod h1:pzACCdpqjQKTvpPZs5P3FzFNQ+RSOJX5StwHwh7ZUgw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -29,6 +31,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= +github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 h1:J4AOUcOh/t1XbQcJfkEqhzgvMJ2tDxdCVvmHxW5QXao= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= @@ -38,6 +42,7 @@ github.com/skip2/go-qrcode v0.0.0-20200519171959-a3b48390827e/go.mod h1:XV66xRDq github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= diff --git a/web/templates/settings.html b/web/templates/settings.html index 0e027f14..be750201 100644 --- a/web/templates/settings.html +++ b/web/templates/settings.html @@ -13,6 +13,8 @@ Device removed successfully {{else if eq $success "configured"}} Admin account is setup. Configure SAML for SSO (optional). + {{else if eq $success "totp"}} + TOTP reset for default user, please reconfigure for improved security. {{end}} @@ -26,6 +28,8 @@
{{if eq $error "invalid"}} Invalid. Please try again. + {{else if eq $error "totp"}} + Error Resetting totp settings. {{else}} Error. Please try again. {{end}} @@ -74,7 +78,7 @@ -
Admin Account
+
Admin Account: Reset Password
Email Address
@@ -98,6 +102,47 @@
+ + + {{if and $.Admin $.Info.TotpKey}} + +
Admin Account: Reset TOTP
+ + +
+ +
+
+ Cancel + +
+
+
+ {{else}} +
Admin Account: Setup MFA
+ +
Scan the below with your Authenticator App of choice (Google Authenticator, Authy etc...) and then put the code into the input box below
+ +
+
Secret: {{$.TempTotpKey.Secret}}
+ TOTP qr-code could not be displayed +
+ +
+
TOTP Code
+ +
+ +
+ +
+
+ Cancel + +
+
+
+ {{end}} diff --git a/web/templates/signin.html b/web/templates/signin.html index da7da8b9..f06bfc4f 100644 --- a/web/templates/signin.html +++ b/web/templates/signin.html @@ -43,9 +43,19 @@
- +
+ + {{ if $.Info.TotpKey}} +
+
+ + +
+
+ {{end}} +