mirror of https://github.com/jlelse/GoBlog
TOTP
This commit is contained in:
parent
8a20285029
commit
71777613af
|
@ -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
|
||||||
|
|
22
config.go
22
config.go
|
@ -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
1
go.mod
|
@ -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
4
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/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
20
main.go
|
@ -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
|
||||||
|
|
|
@ -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" . }}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue