Skip to content

Instantly share code, notes, and snippets.

@gocs
Created June 3, 2022 06:47
Show Gist options
  • Save gocs/8ee81749694224098a286bf6690523b1 to your computer and use it in GitHub Desktop.
Save gocs/8ee81749694224098a286bf6690523b1 to your computer and use it in GitHub Desktop.
simple auth web app
package auth
import (
"errors"
"html/template"
"log"
"net/http"
"time"
"github.com/go-chi/jwtauth/v5"
)
// AuthController is a pluggable controller with handlers only for authenticating users
// This has its own login/logout form template, so long as it contains these endpoints:
// "/logout"
// "/login"
// "/signup"
// "/auth"
// This requires jwt in the cookies
// you can implement your own UserVerify and SignUp methods
// you can provide your own redirection URLs
type AuthController struct {
username string
loginErr error
tokenAuth *jwtauth.JWTAuth
loginURL string
logoutURL string
SignUpURL string
}
func (ac *AuthController) Verifier() func(http.Handler) http.Handler {
return jwtauth.Verifier(ac.tokenAuth)
}
func New(secret []byte) (*AuthController, error) {
return &AuthController{
tokenAuth: jwtauth.New("HS256", secret, nil),
loginURL: "/login",
logoutURL: "/logout",
SignUpURL: "/signup",
}, nil
}
func (ac *AuthController) GetUsername() string {
return ac.username
}
type UserVerifier interface {
UserVerify(username string, password []byte) error
}
func (ac *AuthController) Login(uv UserVerifier, redirectURL string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// verify username and password
if r.FormValue("Login") == "" && r.FormValue("Username") == "" {
if ac.username == "" {
ac.loginErr = errors.New("user not logged in")
} else {
log.Println("Logged in:", ac.username)
}
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
username := r.FormValue("Username")
password := r.FormValue("Password")
if err := uv.UserVerify(username, []byte(password)); err != nil {
log.Println("user verify err:", err)
ac.loginErr = errors.New("either username or password is wrong")
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
// create a new JSON Web Token and redirect to dashboard
_, tokenString, err := ac.tokenAuth.Encode(map[string]any{"username": username})
if err != nil {
log.Println("cannot generate token:", err) // of course, this is too simple, your program should prevent login if token cannot be generated!!
return
}
// create the cookie for client(browser)
http.SetCookie(w, &http.Cookie{
Name: "jwt", // TokenFromCookie
Value: tokenString,
Expires: time.Now().Add(2 * time.Minute), // cookie expired after 1 hour,
})
ac.username = username
ac.loginErr = nil
log.Println("Logged in:", ac.username)
http.Redirect(w, r, redirectURL, http.StatusFound)
})
}
func (ac *AuthController) Logout(redirectURL string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "jwt", MaxAge: -1}) // TokenFromCookie
ac.loginErr = nil
ac.username = ""
http.Redirect(w, r, redirectURL, http.StatusFound)
})
}
func (ac *AuthController) Form(tmpl *template.Template) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tmplVars := map[string]any{}
if ac.loginErr != nil {
tmplVars["LoginError"] = true
}
tmplVars["Username"] = ac.username
tmplVars["LogoutURL"] = ac.logoutURL
tmplVars["LoginURL"] = ac.loginURL
tmplVars["SignUpURL"] = ac.SignUpURL
if err := tmpl.Execute(w, tmplVars); err != nil {
log.Println(err)
}
})
}
type SignUper interface {
SignUp(username string, password []byte) error
}
func (ac *AuthController) SignUp(su SignUper, redirectURL string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.FormValue("Login") == "" && r.FormValue("Username") == "" {
if ac.username == "" {
ac.loginErr = errors.New("user not logged in")
} else {
log.Println("Logged in:", ac.username)
}
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
username := r.FormValue("Username")
password := r.FormValue("Password")
if err := su.SignUp(username, []byte(password)); err != nil {
log.Println("user verify err:", err)
ac.loginErr = errors.New("either username or password is wrong")
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, redirectURL, http.StatusFound)
})
}
package main
import (
"log"
"logio/controller"
)
// used to encrypt/decrypt JWT tokens. Change it to yours.
var jwtTokenSecret = "wwwwwfwfwfwfqwfqwfqwfxx123123@@@#!@#!shit"
func main() {
c, err := controller.New("users.db", jwtTokenSecret)
if err != nil {
log.Fatalln("controller error:", err)
}
log.Println("Server starting, point your browser to localhost:8080/auth to start")
log.Fatal(c.Serve(":8080"))
}
package controller
import (
"fmt"
"html/template"
"logio/auth"
"logio/store"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
)
type Controller struct {
r *chi.Mux
s *store.Store
jwtTokenSecret []byte
}
func New(filename, jwtTokenSecret string) (*Controller, error) {
r := chi.NewRouter()
s, err := store.New(filename)
if err != nil {
return nil, err
}
return &Controller{
r: r,
s: s,
jwtTokenSecret: []byte(jwtTokenSecret),
}, nil
}
var authformTemplate = template.Must(template.New("").Parse(`
<html>
<body>
{{if .Username}}
<p><b>{{.Username}}</b>, welcome to your dashboard! <a href="{{.LogoutURL}}">Logout!</a></p>
{{else}}
<form method="POST" action="{{.LoginURL}}">
<label>Username:</label>
<input type="text" name="Username"><br>
<label>Password:</label>
<input type="password" name="Password">
{{if .LoginError}}
<span style="font-style:italic; color:red">
Either username or password is not in our record!
<a href="{{.SignUpURL}}">
Sign Up?
</a>
</span><br>
{{end}}
<input type="submit" name="Login" value="submit">
</form>
{{end}}
</body>
</html>`))
func (c *Controller) Serve(addr string) error {
ac, _ := auth.New(c.jwtTokenSecret)
c.r.Post("/signup", ac.SignUp(c.s, "/auth"))
c.r.Post("/login", ac.Login(c.s, "/auth"))
c.r.Get("/logout", ac.Logout("/auth"))
c.r.Get("/auth", ac.Form(authformTemplate))
c.r.Group(func(r chi.Router) {
r.Use(ac.Verifier())
r.Use(jwtauth.Authenticator)
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
_, claims, _ := jwtauth.FromContext(r.Context())
w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["username"])))
})
})
defer c.s.Close()
return http.ListenAndServe(addr, c.r)
}
module logio
go 1.18
require (
github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/jwtauth/v5 v5.0.2
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect
github.com/goccy/go-json v0.7.6 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.0 // indirect
github.com/lestrrat-go/iter v1.0.1 // indirect
github.com/lestrrat-go/jwx v1.2.6 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
)
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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.0.2 h1:CSKtr+b6Jnfy5T27sMaiBPxaVE/bjnjS3ramFQ0526w=
github.com/go-chi/jwtauth/v5 v5.0.2/go.mod h1:TeA7vmPe3uYThvHw8O8W13HOOpOd4MTgToxL41gZyjs=
github.com/goccy/go-json v0.7.6 h1:H0wq4jppBQ+9222sk5+hPLL25abZQiRuQ6YPnjO9c+A=
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.6 h1:XAgfuHaOB7fDZ/6WhVgl8K89af768dU+3Nx4DlTbLIk=
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package store
import (
"fmt"
bolt "go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID []byte
Username string
Password []byte
}
type Store struct {
db *bolt.DB
bucketName string
}
func (s *Store) Close() {
s.db.Close()
}
func New(filename string) (*Store, error) {
db, err := bolt.Open(filename, 0644, nil)
if err != nil {
return nil, err
}
tx, err := db.Begin(true)
if err != nil {
return nil, err
}
defer tx.Rollback()
if b := tx.Bucket([]byte(filename)); b == nil {
if _, err := tx.CreateBucket([]byte(filename)); err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &Store{
db: db,
bucketName: filename,
}, nil
}
func (s *Store) SignUp(username string, password []byte) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(s.bucketName))
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
return err
}
return b.Put([]byte(username), hashedPassword)
})
}
func (s *Store) UserVerify(username string, password []byte) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(s.bucketName))
p := b.Get([]byte(username))
return bcrypt.CompareHashAndPassword(p, password)
})
}
func (s *Store) GetAll() error {
return s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(s.bucketName))
b.ForEach(func(k, v []byte) error {
fmt.Printf("key=%s, value=%s\n", k, v)
return nil
})
return nil
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment