Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/totp default admin #126

Merged
merged 4 commits into from
Jun 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions cmd/subspace/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"sort"
"sync"
"time"

"github.com/pquerna/otp/totp"
)

var (
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -99,6 +102,7 @@ func NewConfig(filename string) (*Config, error) {
// Create new config with defaults
if os.IsNotExist(err) {
c.Info = &Info{
Email: "null",
HashKey: RandomString(32),
BlockKey: RandomString(32),
}
Expand Down Expand Up @@ -422,3 +426,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: c.Info.Email,
},
)
if err != nil {
return err
}

tempTotpKey = key

return nil
}
64 changes: 64 additions & 0 deletions cmd/subspace/handlers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"bytes"
"fmt"
"image/png"
"io/ioutil"
"net/http"
"os"
Expand All @@ -10,6 +12,7 @@ import (

"github.com/crewjam/saml/samlsp"
"github.com/julienschmidt/httprouter"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"

qrcode "github.com/skip2/go-qrcode"
Expand Down Expand Up @@ -240,6 +243,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")
Expand All @@ -250,6 +254,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
Expand All @@ -258,6 +269,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 == "" {
Expand Down Expand Up @@ -569,6 +610,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
Expand Down Expand Up @@ -608,6 +652,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")
}

Expand Down
11 changes: 11 additions & 0 deletions cmd/subspace/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"github.com/julienschmidt/httprouter"
"github.com/pquerna/otp"

"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
Expand Down Expand Up @@ -79,6 +80,9 @@ var (

// theme
semanticTheme string

// Totp
tempTotpKey *otp.Key
)

func init() {
Expand Down Expand Up @@ -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))

Expand All @@ -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")))
Expand Down
3 changes: 3 additions & 0 deletions cmd/subspace/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/pquerna/otp"

"golang.org/x/net/publicsuffix"

Expand Down Expand Up @@ -58,6 +59,7 @@ type Web struct {
TargetProfiles []Profile

SemanticTheme string
TempTotpKey *otp.Key
}

func init() {
Expand Down Expand Up @@ -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" {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da h1:WXnT88cFG2davqSFqvaFfzkSMC0lqh/8/rKZ+z7tYvI=
Expand Down Expand Up @@ -44,6 +46,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk=
Expand All @@ -55,6 +59,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/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
Expand Down
47 changes: 46 additions & 1 deletion web/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
</div>
<a class="close-link" href="/settings"><i class="close icon"></i></a>
Expand All @@ -26,6 +28,8 @@
<div class="header">
{{if eq $error "invalid"}}
Invalid. Please try again.
{{else if eq $error "totp"}}
Error Resetting totp settings.
{{else}}
Error. Please try again.
{{end}}
Expand Down Expand Up @@ -74,7 +78,7 @@

<div class="ui hidden section divider"></div>

<div class="ui {{$.SemanticTheme}} dividing header">Admin Account</div>
<div class="ui {{$.SemanticTheme}} dividing header">Admin Account: Reset Password</div>
<div class="ui hidden divider"></div>
<div class="field">
<div class="ui small header">Email Address</div>
Expand All @@ -98,6 +102,47 @@
</div>
</div>
</div>

<div class="ui hidden section divider"></div>
{{if and $.Admin $.Info.TotpKey}}

<div class="ui {{$.SemanticTheme}} dividing header">Admin Account: Reset TOTP</div>
<div class="ui hidden divider"></div>
<input type="hidden" name="reset_totp" value="true"/>
<div class="equal width fields">
<div class="field mobile hidden">&nbsp;</div>
<div class="field">
<div class="two ui buttons">
<a href="/" class="ui huge button">Cancel</a>
<button type="submit" class="ui huge red button">Remove Totp</button>
</div>
</div>
</div>
{{else}}
<div class="ui {{$.SemanticTheme}} dividing header">Admin Account: Setup MFA</div>
<div class="ui hidden divider"></div>
<div class="ui text container">Scan the below with your Authenticator App of choice (Google Authenticator, Authy etc...) and then put the code into the input box below</div>
<div class="ui hidden divider"></div>
<div class="ui centered segment">
<div class="ui bottom attached label">Secret: {{$.TempTotpKey.Secret}}</div>
<img class="ui centered image" src="/totp/image" alt="TOTP qr-code could not be displayed">
</div>
<div class="ui hidden divider"></div>
<div class="field">
<div class="ui small header">TOTP Code</div>
<input name="totp_code" type="text" placeholder="totp key" value="">
</div>
<div class="ui hidden divider"></div>
<div class="equal width fields">
<div class="field mobile hidden">&nbsp;</div>
<div class="field">
<div class="two ui buttons">
<a href="/" class="ui huge button">Cancel</a>
<button type="submit" class="ui huge {{$.SemanticTheme}} button">Save</button>
</div>
</div>
</div>
{{end}}
</form>
</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion web/templates/signin.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,19 @@
<div class="field">
<div class="ui left icon input">
<i class="key icon"></i>
<input name="password" type="password" placeholder="Password" autofocus>
<input name="password" type="password" placeholder="Password" autofocus required>
</div>
</div>

{{ if $.Info.TotpKey}}
<div class="field">
<div class="ui left icon input">
<i class="clock icon"></i>
<input name="totp" type="text" placeholder="One Time Password" autofocus required>
</div>
</div>
{{end}}

<div class="center-aligned field">
<button type="submit" class="ui huge fluid {{$.SemanticTheme}} button">Sign in</button>
<div class="ui hidden divider"></div>
Expand Down