diff --git a/tools/srv/assets/templates/base.html.twig b/tools/srv/assets/templates/base.gohtml similarity index 76% rename from tools/srv/assets/templates/base.html.twig rename to tools/srv/assets/templates/base.gohtml index 9740375..e811482 100644 --- a/tools/srv/assets/templates/base.html.twig +++ b/tools/srv/assets/templates/base.gohtml @@ -1,3 +1,4 @@ +{{ define "base" }} @@ -5,11 +6,11 @@ 2fa demo - + -
{% block body %}{% endblock %}
+
+ {{ template "body" . }} +
+{{ end }} diff --git a/tools/srv/assets/templates/blocks/flash.gohtml b/tools/srv/assets/templates/blocks/flash.gohtml new file mode 100644 index 0000000..df7cefd --- /dev/null +++ b/tools/srv/assets/templates/blocks/flash.gohtml @@ -0,0 +1,7 @@ +{{ define "flash" }} + {{ if .flash}} +
+ {{ .flash.Message }} +
+ {{ end }} +{{ end }} diff --git a/tools/srv/assets/templates/blocks/flash.html.twig b/tools/srv/assets/templates/blocks/flash.html.twig deleted file mode 100644 index ec6548a..0000000 --- a/tools/srv/assets/templates/blocks/flash.html.twig +++ /dev/null @@ -1 +0,0 @@ -{% if flash.Message %}
{{ flash.Message }}
{% endif %} diff --git a/tools/srv/assets/templates/pages/2fa-create.html.twig b/tools/srv/assets/templates/pages/2fa-create.gohtml similarity index 50% rename from tools/srv/assets/templates/pages/2fa-create.html.twig rename to tools/srv/assets/templates/pages/2fa-create.gohtml index eeae583..767cae8 100644 --- a/tools/srv/assets/templates/pages/2fa-create.html.twig +++ b/tools/srv/assets/templates/pages/2fa-create.gohtml @@ -1,16 +1,13 @@ -{% extends "base.html.twig" %} - -{% block body %} +{{ define "body" }}

Create OTP


-{% include "blocks/flash.html.twig" %} +{{ template "flash" . }}
-
qr
+
qr
- -{% endblock %} +{{ end }} diff --git a/tools/srv/assets/templates/pages/2fa.html.twig b/tools/srv/assets/templates/pages/2fa.gohtml similarity index 65% rename from tools/srv/assets/templates/pages/2fa.html.twig rename to tools/srv/assets/templates/pages/2fa.gohtml index 79b5dda..f6ab007 100644 --- a/tools/srv/assets/templates/pages/2fa.html.twig +++ b/tools/srv/assets/templates/pages/2fa.gohtml @@ -1,14 +1,12 @@ -{% extends "base.html.twig" %} - -{% block body %} +{{ define "body" }}

Token


-{% include "blocks/flash.html.twig" %} +{{ template "flash" . }}
-{% endblock %} +{{ end }} diff --git a/tools/srv/assets/templates/pages/login.html.twig b/tools/srv/assets/templates/pages/login.gohtml similarity index 76% rename from tools/srv/assets/templates/pages/login.html.twig rename to tools/srv/assets/templates/pages/login.gohtml index a00feb5..a360c8c 100644 --- a/tools/srv/assets/templates/pages/login.html.twig +++ b/tools/srv/assets/templates/pages/login.gohtml @@ -1,10 +1,8 @@ -{% extends "base.html.twig" %} - -{% block body %} +{{ define "body" }}

Login


-{% include "blocks/flash.html.twig" %} +{{ template "flash" . }}
@@ -16,4 +14,4 @@
Register new account
-{% endblock %} +{{ end }} diff --git a/tools/srv/assets/templates/pages/register.html.twig b/tools/srv/assets/templates/pages/register.gohtml similarity index 76% rename from tools/srv/assets/templates/pages/register.html.twig rename to tools/srv/assets/templates/pages/register.gohtml index a05b895..7a4a115 100644 --- a/tools/srv/assets/templates/pages/register.html.twig +++ b/tools/srv/assets/templates/pages/register.gohtml @@ -1,10 +1,8 @@ -{% extends "base.html.twig" %} - -{% block body %} +{{ define "body" }}

Register account


-{% include "blocks/flash.html.twig" %} +{{ template "flash" . }}
@@ -16,4 +14,4 @@
Cancel
-{% endblock %} +{{ end }} diff --git a/tools/srv/assets/templates/pages/user.gohtml b/tools/srv/assets/templates/pages/user.gohtml new file mode 100644 index 0000000..f8167e0 --- /dev/null +++ b/tools/srv/assets/templates/pages/user.gohtml @@ -0,0 +1,6 @@ +{{ define "body" }} +

User area

+ {{ template "flash" . }} + Hello {{ .user.Username }}! + Logout +{{ end }} diff --git a/tools/srv/assets/templates/pages/user.html.twig b/tools/srv/assets/templates/pages/user.html.twig deleted file mode 100644 index 83e8a38..0000000 --- a/tools/srv/assets/templates/pages/user.html.twig +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html.twig" %} - -{% block body %} -

User area

- {% include "blocks/flash.html.twig" %} - Hello {{ user.Username }}! - Logout -{% endblock %} diff --git a/tools/srv/go.mod b/tools/srv/go.mod index 64f9cfe..8688979 100644 --- a/tools/srv/go.mod +++ b/tools/srv/go.mod @@ -6,11 +6,7 @@ require ( github.com/alexedwards/scs/v2 v2.9.0 github.com/go-chi/chi/v5 v5.3.1 github.com/pquerna/otp v1.5.0 - github.com/tyler-sommer/stick v1.0.10 golang.org/x/crypto v0.54.0 ) -require ( - github.com/boombuler/barcode v1.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect -) +require github.com/boombuler/barcode v1.1.0 // indirect diff --git a/tools/srv/go.sum b/tools/srv/go.sum index 5eccdc2..0152f13 100644 --- a/tools/srv/go.sum +++ b/tools/srv/go.sum @@ -1,44 +1,18 @@ -github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= -github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= 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/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= -github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= 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.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tyler-sommer/stick v1.0.6 h1:LLdJ8oGotXCsAuVx2BRZZT6s3bYXHe0ImQ+azF4HtJg= -github.com/tyler-sommer/stick v1.0.6/go.mod h1:rjBy3zi6GwoxExa6OSRPPPaLqUEKNsBxTeWckhIX1us= -github.com/tyler-sommer/stick v1.0.10 h1:VlXTvaxXnOtfkoTxqy+FWRM0OdN7sGX8Nfh8t2I1GSI= -github.com/tyler-sommer/stick v1.0.10/go.mod h1:CYdsPuvxGEU32yi4ZTDul+55R8oS8/aHuWSppEq01B8= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= diff --git a/tools/srv/main.go b/tools/srv/main.go index f4a65e5..b3431db 100644 --- a/tools/srv/main.go +++ b/tools/srv/main.go @@ -2,9 +2,12 @@ package main import ( "bytes" + "embed" + "encoding/base32" "encoding/base64" "encoding/gob" "fmt" + "html/template" "image/png" "log" "net/http" @@ -17,7 +20,6 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" - "github.com/tyler-sommer/stick" "golang.org/x/crypto/bcrypt" ) @@ -39,7 +41,7 @@ type ( flash struct{ Message, Type string } - m map[string]stick.Value + m map[string]any db struct { *sync.Mutex @@ -48,11 +50,31 @@ type ( ) var ( - env = stick.New(stick.NewFilesystemLoader("./assets/templates")) + //go:embed assets/templates assets/css + assets embed.FS + pages = map[string]*template.Template{} store = &db{new(sync.Mutex), make(map[string]*user)} session = scs.New() ) +func init() { + for _, p := range []string{"login", "register", "2fa", "2fa-create", "user"} { + pages[p] = template.Must(template.ParseFS( + assets, + "assets/templates/base.gohtml", + "assets/templates/blocks/flash.gohtml", + "assets/templates/pages/"+p+".gohtml", + )) + } +} + +func render(w http.ResponseWriter, page string, data m) { + if err := pages[page].ExecuteTemplate(w, "base", data); err != nil { + log.Println("render:", err) + http.Error(w, "internal error", 500) + } +} + /// app func main() { @@ -90,8 +112,7 @@ func mux() chi.Router { r.Get("/logout", getLogout) }) - fs := http.FileServer(http.Dir("./assets/css")) - mux.Handle("/assets/*", http.StripPrefix("/assets", fs)) + mux.Handle("/assets/*", http.FileServerFS(assets)) return mux } @@ -112,11 +133,10 @@ func getLogin(w http.ResponseWriter, r *http.Request) { } flash, _ := session.Pop(r.Context(), "flash").(*flash) - env.Execute("pages/login.html.twig", w, m{"flash": flash}) + render(w, "login", m{"flash": flash}) } func postLogin(w http.ResponseWriter, r *http.Request) { - u, err := store.Find(r.PostFormValue("username")) if err != nil { log.Println(err) @@ -143,7 +163,7 @@ func postLogin(w http.ResponseWriter, r *http.Request) { func getRegister(w http.ResponseWriter, r *http.Request) { flash, _ := session.Pop(r.Context(), "flash").(*flash) - env.Execute("pages/register.html.twig", w, m{"flash": flash}) + render(w, "register", m{"flash": flash}) } func postRegister(w http.ResponseWriter, r *http.Request) { @@ -189,15 +209,27 @@ func createOTP(w http.ResponseWriter, r *http.Request) { return } + var secret []byte + if u.Secret != "" { + var err error + if secret, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(u.Secret); err != nil { + log.Println("otp:", err) + session.Put(r.Context(), "flash", &flash{err.Error(), "danger"}) + http.Redirect(w, r, "/otp/new", http.StatusFound) + return + } + } + opts := totp.GenerateOpts{ Issuer: issuer, Algorithm: otpAlg, AccountName: u.Username, SecretSize: otpSecretSize, Digits: otp.DigitsSix, + Secret: secret, } - otp, err := totp.Generate(opts) + key, err := totp.Generate(opts) if err != nil { log.Println("otp:", err) session.Put(r.Context(), "flash", &flash{err.Error(), "danger"}) @@ -205,7 +237,8 @@ func createOTP(w http.ResponseWriter, r *http.Request) { return } - u.Secret = otp.Secret() + u.Secret = key.Secret() + if err := store.Update(u); err != nil { log.Println(err) session.Put(r.Context(), "flash", &flash{err.Error(), "danger"}) @@ -214,7 +247,7 @@ func createOTP(w http.ResponseWriter, r *http.Request) { } buf := new(bytes.Buffer) - qr, err := otp.Image(240, 240) + qr, err := key.Image(240, 240) if err != nil { session.Put(r.Context(), "flash", &flash{err.Error(), "danger"}) http.Redirect(w, r, "/", http.StatusFound) @@ -228,8 +261,10 @@ func createOTP(w http.ResponseWriter, r *http.Request) { } flash, _ := session.Pop(r.Context(), "flash").(*flash) - env.Execute("pages/2fa-create.html.twig", w, m{ - "qr": base64.RawStdEncoding.EncodeToString(buf.Bytes()), + imgBytes := base64.RawStdEncoding.EncodeToString(buf.Bytes()) + + render(w, "2fa-create", m{ + "qr": template.URL("data:image/png;base64," + imgBytes), "flash": flash, }) } @@ -241,7 +276,8 @@ func getOTP(w http.ResponseWriter, r *http.Request) { } flash, _ := session.Pop(r.Context(), "flash").(*flash) - env.Execute("pages/2fa.html.twig", w, m{"flash": flash}) + + render(w, "2fa", m{"flash": flash}) } func postOTP(w http.ResponseWriter, r *http.Request) { @@ -255,7 +291,13 @@ func postOTP(w http.ResponseWriter, r *http.Request) { return } - opts := totp.ValidateOpts{Algorithm: otpAlg, Digits: otpLen} + opts := totp.ValidateOpts{ + Algorithm: otpAlg, + Digits: otpLen, + Period: 30, + Skew: 1, + } + if valid, err := totp.ValidateCustom(token, user.Secret, time.Now().UTC(), opts); !valid || err != nil { if err != nil { log.Println("otp: validate:", err) @@ -274,13 +316,14 @@ func postOTP(w http.ResponseWriter, r *http.Request) { } session.Put(r.Context(), "otp-validated", true) + session.Put(r.Context(), "user", user) http.Redirect(w, r, "/user", http.StatusFound) } func getUser(w http.ResponseWriter, r *http.Request) { u, _ := session.Get(r.Context(), "user").(*user) - env.Execute("pages/user.html.twig", w, m{"user": u}) + render(w, "user", m{"user": u}) } func getLogout(w http.ResponseWriter, r *http.Request) {