Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2fa demo</title>
<link rel="stylesheet" href="/assets/pico.min.css">
<link rel="stylesheet" href="/assets/css/pico.min.css">
<style>
:root {
--pico-spacing: 1rem;
--pico-form-element-spacing-vertical: 0.35rem;
--pico-form-element-spacing-vertical: 0.5rem;
--pico-form-element-spacing-horizontal: 1rem;
--pico-font-size: 1rem;
}
Expand All @@ -20,6 +21,9 @@
</style>
</head>
<body>
<main class="container"> {% block body %}{% endblock %} </main>
<main class="container">
{{ template "body" . }}
</main>
</body>
</html>
{{ end }}
7 changes: 7 additions & 0 deletions tools/srv/assets/templates/blocks/flash.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{ define "flash" }}
{{ if .flash}}
<div class="notification is-{{ .flash.Type }}">
{{ .flash.Message }}
</div>
{{ end }}
{{ end }}
1 change: 0 additions & 1 deletion tools/srv/assets/templates/blocks/flash.html.twig

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
{% extends "base.html.twig" %}

{% block body %}
{{ define "body" }}
<h3>Create OTP</h3>
<hr>

{% include "blocks/flash.html.twig" %}
{{ template "flash" . }}

<form method="post" action="/otp">
<div class="centered"> <img src="data:image/png;base64,{{ qr }}" alt="qr"> </div>
<div class="centered"> <img src="{{ .qr }}" alt="qr"> </div>
<label for="">Enter Token:</label>
<input type="number" name="token">
<button type="submit">Submit</button>
</form>

{% endblock %}
{{ end }}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
{% extends "base.html.twig" %}

{% block body %}
{{ define "body" }}
<h3 class="title">Token</h3>
<hr>

{% include "blocks/flash.html.twig" %}
{{ template "flash" . }}

<form method="post" action="/otp">
<label for="">Enter OTP:</label>
<input type="number" name="token">
<button type="submit">Submit</button>
</form>
{% endblock %}
{{ end }}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
{% extends "base.html.twig" %}

{% block body %}
{{ define "body" }}
<h3>Login</h3>
<hr>

{% include "blocks/flash.html.twig" %}
{{ template "flash" . }}

<form method="post" action="/login">
<label for="">Username</label>
Expand All @@ -16,4 +14,4 @@
<button type="submit">Login</button>
<div class="centered"><a href="/register">Register new account</a></div>
</form>
{% endblock %}
{{ end }}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
{% extends "base.html.twig" %}

{% block body %}
{{ define "body" }}
<h3>Register account</h3>
<hr>

{% include "blocks/flash.html.twig" %}
{{ template "flash" . }}

<form method="post" action="/register">
<label for="">Username</label>
Expand All @@ -16,4 +14,4 @@
<button type="submit">Register</button>
<div class="centered"><a href="/">Cancel</a></div>
</form>
{% endblock %}
{{ end }}
6 changes: 6 additions & 0 deletions tools/srv/assets/templates/pages/user.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{ define "body" }}
<h3>User area</h3>
{{ template "flash" . }}
Hello {{ .user.Username }}!
<a href="/user/logout">Logout</a>
{{ end }}
8 changes: 0 additions & 8 deletions tools/srv/assets/templates/pages/user.html.twig

This file was deleted.

6 changes: 1 addition & 5 deletions tools/srv/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 0 additions & 26 deletions tools/srv/go.sum
Original file line number Diff line number Diff line change
@@ -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=
75 changes: 59 additions & 16 deletions tools/srv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package main

import (
"bytes"
"embed"
"encoding/base32"
"encoding/base64"
"encoding/gob"
"fmt"
"html/template"
"image/png"
"log"
"net/http"
Expand All @@ -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"
)

Expand All @@ -39,7 +41,7 @@ type (

flash struct{ Message, Type string }

m map[string]stick.Value
m map[string]any

db struct {
*sync.Mutex
Expand All @@ -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() {
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -189,23 +209,36 @@ 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"})
http.Redirect(w, r, "/otp/new", http.StatusFound)
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"})
Expand All @@ -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)
Expand All @@ -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,
})
}
Expand All @@ -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) {
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
Loading