Skip to content

Commit

Permalink
Feature/totp default admin (#111)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jack1902 committed Jul 14, 2020
1 parent f0bde97 commit 0470100
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 2 deletions.
32 changes: 32 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 @@ -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
}
64 changes: 64 additions & 0 deletions cmd/subspace/handlers.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package main

import (
"bytes"
"fmt"
"image/png"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"

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

qrcode "github.com/skip2/go-qrcode"
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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 == "" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
}

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/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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
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

0 comments on commit 0470100

Please sign in to comment.