This commit is contained in:
Jan-Lukas Else 2021-02-28 08:57:15 +01:00
parent 8a20285029
commit 71777613af
8 changed files with 85 additions and 33 deletions

View File

@ -10,14 +10,26 @@ import (
"time" "time"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/pquerna/otp/totp"
) )
func checkCredentials(username, password string) bool { func checkCredentials(username, password, totpPasscode string) bool {
return username == appConfig.User.Nick && password == appConfig.User.Password return username == appConfig.User.Nick &&
password == appConfig.User.Password &&
(appConfig.User.TOTP == "" || totp.Validate(totpPasscode, appConfig.User.TOTP))
} }
func checkUsername(username string) bool { func checkUsernameTOTP(username string, totp bool) bool {
return username == appConfig.User.Nick 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 { func jwtKey() []byte {
@ -31,8 +43,8 @@ func authMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
// 1. Check BasicAuth // 1. Check BasicAuth (just for app passwords)
if username, password, ok := r.BasicAuth(); ok && checkCredentials(username, password) { if username, password, ok := r.BasicAuth(); ok && checkAppPasswords(username, password) {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -52,10 +64,11 @@ func authMiddleware(next http.Handler) http.Handler {
b = []byte(r.PostForm.Encode()) b = []byte(r.PostForm.Encode())
} }
render(w, r, templateLogin, &renderData{ render(w, r, templateLogin, &renderData{
Data: map[string]string{ Data: map[string]interface{}{
"loginmethod": r.Method, "loginmethod": r.Method,
"loginheaders": base64.StdEncoding.EncodeToString(h), "loginheaders": base64.StdEncoding.EncodeToString(h),
"loginbody": base64.StdEncoding.EncodeToString(b), "loginbody": base64.StdEncoding.EncodeToString(b),
"totp": appConfig.User.TOTP != "",
}, },
}) })
}) })
@ -68,7 +81,7 @@ func checkAuthToken(r *http.Request) bool {
return jwtKey(), nil return jwtKey(), nil
}); err == nil && tkn.Valid && }); err == nil && tkn.Valid &&
claims.TokenType == "login" && claims.TokenType == "login" &&
checkUsername(claims.Username) { checkUsernameTOTP(claims.Username, claims.TOTP) {
return true return true
} }
} }
@ -105,7 +118,12 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
if r.FormValue("loginaction") != "login" { if r.FormValue("loginaction") != "login" {
return false 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")) loginbody, _ := base64.StdEncoding.DecodeString(r.FormValue("loginbody"))
req, _ := http.NewRequest(r.FormValue("loginmethod"), r.RequestURI, bytes.NewReader(loginbody)) req, _ := http.NewRequest(r.FormValue("loginmethod"), r.RequestURI, bytes.NewReader(loginbody))
// Copy original headers // Copy original headers
@ -115,18 +133,14 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
for k, v := range headers { for k, v := range headers {
req.Header[k] = v req.Header[k] = v
} }
// Check credential // Cookie
if checkCredentials(r.FormValue("username"), r.FormValue("password")) { tokenCookie, err := createTokenCookie()
tokenCookie, err := createTokenCookie(r.FormValue("username")) if err != nil {
if err != nil { serveError(w, r, err.Error(), http.StatusInternalServerError)
serveError(w, r, err.Error(), http.StatusInternalServerError) return true
return true
}
// Add cookie to original request
req.AddCookie(tokenCookie)
// Send cookie
http.SetCookie(w, tokenCookie)
} }
req.AddCookie(tokenCookie)
http.SetCookie(w, tokenCookie)
// Serve original request // Serve original request
d.ServeHTTP(w, req) d.ServeHTTP(w, req)
return true return true
@ -136,14 +150,16 @@ type authClaims struct {
*jwt.StandardClaims *jwt.StandardClaims
TokenType string TokenType string
Username 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) expiration := time.Now().Add(7 * 24 * time.Hour)
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &authClaims{ tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &authClaims{
&jwt.StandardClaims{ExpiresAt: expiration.Unix()}, &jwt.StandardClaims{ExpiresAt: expiration.Unix()},
"login", "login",
username, appConfig.User.Nick,
appConfig.User.TOTP != "",
}).SignedString(jwtKey()) }).SignedString(jwtKey())
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -130,14 +130,20 @@ type comments struct {
} }
type configUser struct { type configUser struct {
Nick string `mapstructure:"nick"` Nick string `mapstructure:"nick"`
Name string `mapstructure:"name"` Name string `mapstructure:"name"`
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
Picture string `mapstructure:"picture"` TOTP string `mapstructure:"totp"`
Emoji string `mapstructure:"emoji"` AppPasswords []*appPassword `mapstructure:"appPasswords"`
Email string `mapstructure:"email"` Picture string `mapstructure:"picture"`
Link string `mapstructure:"link"` Email string `mapstructure:"email"`
Identities []string `mapstructure:"identities"` Link string `mapstructure:"link"`
Identities []string `mapstructure:"identities"`
}
type appPassword struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
} }
type configHooks struct { type configHooks struct {

1
go.mod
View File

@ -36,6 +36,7 @@ require (
github.com/miekg/dns v1.1.40 // indirect github.com/miekg/dns v1.1.40 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.8.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/smartystreets/assertions v1.2.0 // indirect
github.com/snabb/sitemap v1.0.0 github.com/snabb/sitemap v1.0.0
github.com/spf13/afero v1.5.1 // indirect github.com/spf13/afero v1.5.1 // indirect

4
go.sum
View File

@ -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/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 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/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 h1:1f7kxykaJkOVVpXJ8ZrC6RAO5F6+kKm9U7dBFbLNeug=
github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U= github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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.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_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=

20
main.go
View File

@ -7,6 +7,8 @@ import (
"os/signal" "os/signal"
"runtime/pprof" "runtime/pprof"
"syscall" "syscall"
"github.com/pquerna/otp/totp"
) )
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
@ -26,12 +28,30 @@ func main() {
} }
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
// Initialize config // Initialize config
log.Println("Initialize configuration...") log.Println("Initialize configuration...")
err := initConfig() err := initConfig()
if err != nil { if err != nil {
log.Fatal(err) 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 // Execute pre-start hooks
preStartHooks() preStartHooks()
// Initialize everything else // Initialize everything else

View File

@ -10,8 +10,11 @@
<input type="hidden" name="loginmethod" value="{{ .Data.loginmethod }}"> <input type="hidden" name="loginmethod" value="{{ .Data.loginmethod }}">
<input type="hidden" name="loginheaders" value="{{ .Data.loginheaders }}"> <input type="hidden" name="loginheaders" value="{{ .Data.loginheaders }}">
<input type="hidden" name="loginbody" value="{{ .Data.loginbody }}"> <input type="hidden" name="loginbody" value="{{ .Data.loginbody }}">
<input type="text" name="username" placeholder="{{ string .Blog.Lang "username" }}"> <input type="text" name="username" autocomplete="username" placeholder="{{ string .Blog.Lang "username" }}">
<input type="password" name="password" placeholder="{{ string .Blog.Lang "password" }}"> <input type="password" name="password" autocomplete="current-password" placeholder="{{ string .Blog.Lang "password" }}">
{{ if .Data.totp }}
<input type="text" name="token" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="{{ string .Blog.Lang "totp" }}">
{{ end }}
<input class="fw" type="submit" value="{{ string .Blog.Lang "login" }}"> <input class="fw" type="submit" value="{{ string .Blog.Lang "login" }}">
</form> </form>
{{ include "author" . }} {{ include "author" . }}

View File

@ -35,6 +35,7 @@ speak: "Read to me, please."
stopspeak: "Stop speaking!" stopspeak: "Stop speaking!"
submit: "Submit" submit: "Submit"
total: "Total" total: "Total"
totp: "TOTP"
translations: "Translations" translations: "Translations"
update: "Update" update: "Update"
updatedon: "Updated on" updatedon: "Updated on"

View File

@ -76,7 +76,8 @@ func (m *mention) verifyMention() error {
req.Header.Set(userAgent, appUserAgent) req.Header.Set(userAgent, appUserAgent)
if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) { if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) {
// Set authentication // Set authentication
req.SetBasicAuth(appConfig.User.Nick, appConfig.User.Password) c, _ := createTokenCookie()
req.AddCookie(c)
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {