From 71777613af30c85ee035cbd1771a8638429318c5 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Sun, 28 Feb 2021 08:57:15 +0100 Subject: [PATCH] TOTP --- authentication.go | 60 +++++++++++++++++++++------------- config.go | 22 ++++++++----- go.mod | 1 + go.sum | 4 +++ main.go | 20 ++++++++++++ templates/login.gohtml | 7 ++-- templates/strings/default.yaml | 1 + webmentionVerification.go | 3 +- 8 files changed, 85 insertions(+), 33 deletions(-) diff --git a/authentication.go b/authentication.go index eff192d..424856f 100644 --- a/authentication.go +++ b/authentication.go @@ -10,14 +10,26 @@ import ( "time" "github.com/dgrijalva/jwt-go" + "github.com/pquerna/otp/totp" ) -func checkCredentials(username, password string) bool { - return username == appConfig.User.Nick && password == appConfig.User.Password +func checkCredentials(username, password, totpPasscode string) bool { + return username == appConfig.User.Nick && + password == appConfig.User.Password && + (appConfig.User.TOTP == "" || totp.Validate(totpPasscode, appConfig.User.TOTP)) } -func checkUsername(username string) bool { - return username == appConfig.User.Nick +func checkUsernameTOTP(username string, totp bool) bool { + return username == appConfig.User.Nick && totp == (appConfig.User.TOTP != "") +} + +func checkAppPasswords(username, password string) bool { + for _, apw := range appConfig.User.AppPasswords { + if apw.Username == username && apw.Password == password { + return true + } + } + return false } func jwtKey() []byte { @@ -31,8 +43,8 @@ func authMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) return } - // 1. Check BasicAuth - if username, password, ok := r.BasicAuth(); ok && checkCredentials(username, password) { + // 1. Check BasicAuth (just for app passwords) + if username, password, ok := r.BasicAuth(); ok && checkAppPasswords(username, password) { next.ServeHTTP(w, r) return } @@ -52,10 +64,11 @@ func authMiddleware(next http.Handler) http.Handler { b = []byte(r.PostForm.Encode()) } render(w, r, templateLogin, &renderData{ - Data: map[string]string{ + Data: map[string]interface{}{ "loginmethod": r.Method, "loginheaders": base64.StdEncoding.EncodeToString(h), "loginbody": base64.StdEncoding.EncodeToString(b), + "totp": appConfig.User.TOTP != "", }, }) }) @@ -68,7 +81,7 @@ func checkAuthToken(r *http.Request) bool { return jwtKey(), nil }); err == nil && tkn.Valid && claims.TokenType == "login" && - checkUsername(claims.Username) { + checkUsernameTOTP(claims.Username, claims.TOTP) { return true } } @@ -105,7 +118,12 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool { if r.FormValue("loginaction") != "login" { return false } - // Do original request + // Check credential + if !checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) { + serveError(w, r, "Incorrect credentials", http.StatusUnauthorized) + return true + } + // Prepare original request loginbody, _ := base64.StdEncoding.DecodeString(r.FormValue("loginbody")) req, _ := http.NewRequest(r.FormValue("loginmethod"), r.RequestURI, bytes.NewReader(loginbody)) // Copy original headers @@ -115,18 +133,14 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool { for k, v := range headers { req.Header[k] = v } - // Check credential - if checkCredentials(r.FormValue("username"), r.FormValue("password")) { - tokenCookie, err := createTokenCookie(r.FormValue("username")) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return true - } - // Add cookie to original request - req.AddCookie(tokenCookie) - // Send cookie - http.SetCookie(w, tokenCookie) + // Cookie + tokenCookie, err := createTokenCookie() + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return true } + req.AddCookie(tokenCookie) + http.SetCookie(w, tokenCookie) // Serve original request d.ServeHTTP(w, req) return true @@ -136,14 +150,16 @@ type authClaims struct { *jwt.StandardClaims TokenType string Username string + TOTP bool } -func createTokenCookie(username string) (*http.Cookie, error) { +func createTokenCookie() (*http.Cookie, error) { expiration := time.Now().Add(7 * 24 * time.Hour) tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &authClaims{ &jwt.StandardClaims{ExpiresAt: expiration.Unix()}, "login", - username, + appConfig.User.Nick, + appConfig.User.TOTP != "", }).SignedString(jwtKey()) if err != nil { return nil, err diff --git a/config.go b/config.go index 2b25da7..8e9be39 100644 --- a/config.go +++ b/config.go @@ -130,14 +130,20 @@ type comments struct { } type configUser struct { - Nick string `mapstructure:"nick"` - Name string `mapstructure:"name"` - Password string `mapstructure:"password"` - Picture string `mapstructure:"picture"` - Emoji string `mapstructure:"emoji"` - Email string `mapstructure:"email"` - Link string `mapstructure:"link"` - Identities []string `mapstructure:"identities"` + Nick string `mapstructure:"nick"` + Name string `mapstructure:"name"` + Password string `mapstructure:"password"` + TOTP string `mapstructure:"totp"` + AppPasswords []*appPassword `mapstructure:"appPasswords"` + Picture string `mapstructure:"picture"` + Email string `mapstructure:"email"` + Link string `mapstructure:"link"` + Identities []string `mapstructure:"identities"` +} + +type appPassword struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` } type configHooks struct { diff --git a/go.mod b/go.mod index 76e2dac..1a18043 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/miekg/dns v1.1.40 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pelletier/go-toml v1.8.1 // indirect + github.com/pquerna/otp v1.3.0 github.com/smartystreets/assertions v1.2.0 // indirect github.com/snabb/sitemap v1.0.0 github.com/spf13/afero v1.5.1 // indirect diff --git a/go.sum b/go.sum index c94b9f3..fe81e08 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +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/caddyserver/certmagic v0.12.0 h1:1f7kxykaJkOVVpXJ8ZrC6RAO5F6+kKm9U7dBFbLNeug= github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -231,6 +233,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= +github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= diff --git a/main.go b/main.go index f954760..91ee25a 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "os/signal" "runtime/pprof" "syscall" + + "github.com/pquerna/otp/totp" ) var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") @@ -26,12 +28,30 @@ func main() { } defer pprof.StopCPUProfile() } + // Initialize config log.Println("Initialize configuration...") err := initConfig() if err != nil { log.Fatal(err) } + + // Small tools + if len(os.Args) >= 2 { + if os.Args[1] == "totp-secret" { + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: appConfig.Server.PublicAddress, + AccountName: appConfig.User.Nick, + }) + if err != nil { + log.Fatal(err.Error()) + return + } + log.Println("TOTP-Secret:", key.Secret()) + return + } + } + // Execute pre-start hooks preStartHooks() // Initialize everything else diff --git a/templates/login.gohtml b/templates/login.gohtml index 056cf01..5831bef 100644 --- a/templates/login.gohtml +++ b/templates/login.gohtml @@ -10,8 +10,11 @@ - - + + + {{ if .Data.totp }} + + {{ end }} {{ include "author" . }} diff --git a/templates/strings/default.yaml b/templates/strings/default.yaml index e6d8af1..40c1d4c 100644 --- a/templates/strings/default.yaml +++ b/templates/strings/default.yaml @@ -35,6 +35,7 @@ speak: "Read to me, please." stopspeak: "Stop speaking!" submit: "Submit" total: "Total" +totp: "TOTP" translations: "Translations" update: "Update" updatedon: "Updated on" diff --git a/webmentionVerification.go b/webmentionVerification.go index e905885..37fd8a0 100644 --- a/webmentionVerification.go +++ b/webmentionVerification.go @@ -76,7 +76,8 @@ func (m *mention) verifyMention() error { req.Header.Set(userAgent, appUserAgent) if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) { // Set authentication - req.SetBasicAuth(appConfig.User.Nick, appConfig.User.Password) + c, _ := createTokenCookie() + req.AddCookie(c) } resp, err := http.DefaultClient.Do(req) if err != nil {