mirror of https://github.com/jlelse/GoBlog
TOTP
This commit is contained in:
parent
8a20285029
commit
71777613af
|
@ -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
|
||||
|
|
22
config.go
22
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 {
|
||||
|
|
1
go.mod
1
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
|
||||
|
|
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/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=
|
||||
|
|
20
main.go
20
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
|
||||
|
|
|
@ -10,8 +10,11 @@
|
|||
<input type="hidden" name="loginmethod" value="{{ .Data.loginmethod }}">
|
||||
<input type="hidden" name="loginheaders" value="{{ .Data.loginheaders }}">
|
||||
<input type="hidden" name="loginbody" value="{{ .Data.loginbody }}">
|
||||
<input type="text" name="username" placeholder="{{ string .Blog.Lang "username" }}">
|
||||
<input type="password" name="password" placeholder="{{ string .Blog.Lang "password" }}">
|
||||
<input type="text" name="username" autocomplete="username" placeholder="{{ string .Blog.Lang "username" }}">
|
||||
<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" }}">
|
||||
</form>
|
||||
{{ include "author" . }}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue