GoBlog/authentication.go

189 lines
5.5 KiB
Go
Raw Normal View History

2020-12-15 16:40:14 +00:00
package main
import (
2021-02-20 22:35:16 +00:00
"context"
2020-12-15 16:40:14 +00:00
"encoding/base64"
2021-01-21 16:59:47 +00:00
"encoding/json"
2020-12-15 16:40:14 +00:00
"io"
"net/http"
"strings"
2020-12-15 16:40:14 +00:00
2021-02-28 07:57:15 +00:00
"github.com/pquerna/otp/totp"
"go.goblog.app/app/pkgs/bodylimit"
2022-02-22 20:33:12 +00:00
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype"
2020-12-15 16:40:14 +00:00
)
const loggedInKey contextKey = "loggedIn"
// Check if credentials are correct
func (a *goBlog) checkCredentials(username, password, totpPasscode string) bool {
return username == a.cfg.User.Nick &&
password == a.cfg.User.Password &&
(a.cfg.User.TOTP == "" || totp.Validate(totpPasscode, a.cfg.User.TOTP))
2020-12-15 16:40:14 +00:00
}
// Check if app passwords are correct
func (a *goBlog) checkAppPasswords(username, password string) bool {
for _, apw := range a.cfg.User.AppPasswords {
2021-02-28 07:57:15 +00:00
if apw.Username == username && apw.Password == password {
return true
}
}
return false
}
// Check if cookie is known and logged in
func (a *goBlog) checkLoginCookie(r *http.Request) bool {
ses, err := a.loginSessions.Get(r, "l")
if err == nil && ses != nil {
if login, ok := ses.Values["login"]; ok && login.(bool) {
return true
}
}
return false
2020-12-15 16:40:14 +00:00
}
// Middleware to force login
func (a *goBlog) authMiddleware(next http.Handler) http.Handler {
2020-12-15 16:40:14 +00:00
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if already logged in
if a.isLoggedIn(r) {
next.ServeHTTP(w, r)
return
}
2022-02-22 20:33:12 +00:00
// Encode original request
headerBuffer, bodyBuffer := bufferpool.Get(), bufferpool.Get()
defer bufferpool.Put(headerBuffer, bodyBuffer)
// Encode headers
headerEncoder := base64.NewEncoder(base64.StdEncoding, headerBuffer)
_ = json.NewEncoder(headerEncoder).Encode(r.Header)
_ = headerEncoder.Close()
// Encode body
bodyEncoder := base64.NewEncoder(base64.StdEncoding, bodyBuffer)
limit := 3 * bodylimit.MB
2022-02-22 20:33:12 +00:00
written, _ := io.Copy(bodyEncoder, io.LimitReader(r.Body, limit))
if written == 0 {
2020-12-15 16:40:14 +00:00
// Maybe it's a form
_ = r.ParseForm()
2022-02-22 20:33:12 +00:00
// Encode form
sw, _ := io.WriteString(bodyEncoder, r.Form.Encode())
written = int64(sw)
2022-02-22 20:33:12 +00:00
}
bodyEncoder.Close()
if written >= limit {
a.serveError(w, r, "Request body too large, first login", http.StatusRequestEntityTooLarge)
return
2020-12-15 16:40:14 +00:00
}
2022-02-22 20:33:12 +00:00
// Render login form
w.Header().Set(cacheControl, "no-store,max-age=0")
w.Header().Set("X-Robots-Tag", "noindex")
a.render(w, r, a.renderLogin, &renderData{
Data: &loginRenderData{
loginMethod: r.Method,
2022-02-22 20:33:12 +00:00
loginHeaders: headerBuffer.String(),
loginBody: bodyBuffer.String(),
totp: a.cfg.User.TOTP != "",
2020-12-15 16:40:14 +00:00
},
})
})
}
// Middleware to check if the request is a login request
func (a *goBlog) checkIsLogin(next http.Handler) http.Handler {
2020-12-15 16:40:14 +00:00
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if !a.checkLogin(rw, r) {
2020-12-15 16:40:14 +00:00
next.ServeHTTP(rw, r)
}
})
}
// Checks login and returns true if it already served an error
func (a *goBlog) checkLogin(w http.ResponseWriter, r *http.Request) bool {
2021-02-24 12:16:33 +00:00
if r.Method != http.MethodPost {
return false
}
if !strings.Contains(r.Header.Get(contentType), contenttype.WWWForm) {
2021-02-24 12:16:33 +00:00
return false
}
if r.FormValue("loginaction") != "login" {
return false
}
2021-02-28 07:57:15 +00:00
// Check credential
if !a.checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
a.serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
2021-02-28 07:57:15 +00:00
return true
}
// Prepare original request
2022-02-22 20:33:12 +00:00
bodyDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r.FormValue("loginbody")))
2022-11-27 14:06:43 +00:00
origReq, _ := http.NewRequestWithContext(r.Context(), r.FormValue("loginmethod"), r.URL.RequestURI(), bodyDecoder)
2022-02-22 20:33:12 +00:00
headerDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r.FormValue("loginheaders")))
_ = json.NewDecoder(headerDecoder).Decode(&origReq.Header)
2021-02-28 07:57:15 +00:00
// Cookie
ses, err := a.loginSessions.Get(r, "l")
2021-02-28 07:57:15 +00:00
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
2021-02-28 07:57:15 +00:00
return true
2020-12-15 16:40:14 +00:00
}
2021-05-14 16:24:02 +00:00
ses.Values["login"] = true
err = a.loginSessions.Save(r, w, ses)
2021-05-14 16:24:02 +00:00
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
2021-05-14 16:24:02 +00:00
return true
}
2021-02-24 12:16:33 +00:00
// Serve original request
2022-02-22 20:33:12 +00:00
setLoggedIn(origReq, true)
a.d.ServeHTTP(w, origReq)
2021-02-24 12:16:33 +00:00
return true
2020-12-15 16:40:14 +00:00
}
func (a *goBlog) isLoggedIn(r *http.Request) bool {
// Check if context key already set
2021-07-25 08:53:12 +00:00
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok {
return loggedIn
}
// Check app passwords
if username, password, ok := r.BasicAuth(); ok && a.checkAppPasswords(username, password) {
2021-07-25 08:53:12 +00:00
setLoggedIn(r, true)
return true
}
// Check session cookie
if a.checkLoginCookie(r) {
2021-07-25 08:53:12 +00:00
setLoggedIn(r, true)
return true
}
// Not logged in
return false
}
// Set request context value
2021-07-25 08:53:12 +00:00
func setLoggedIn(r *http.Request, loggedIn bool) {
// Overwrite the value of r (r is a pointer)
(*r) = *(r.WithContext(context.WithValue(r.Context(), loggedInKey, loggedIn)))
}
// HandlerFunc to redirect to home after login
// Need to set auth middleware!
func serveLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
}
// HandlerFunc to delete login session and cookie
func (a *goBlog) serveLogout(w http.ResponseWriter, r *http.Request) {
if ses, err := a.loginSessions.Get(r, "l"); err == nil && ses != nil {
_ = a.loginSessions.Delete(r, w, ses)
2021-05-14 16:24:02 +00:00
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (a *goBlog) getDefaultPostStates(r *http.Request) (status []postStatus, visibility []postVisibility) {
if a.isLoggedIn(r) {
status = []postStatus{statusPublished}
visibility = []postVisibility{visibilityPublic, visibilityUnlisted, visibilityPrivate}
} else {
status = []postStatus{statusPublished}
visibility = []postVisibility{visibilityPublic}
}
return
}