add subtree renderer, fix static routing for index pages, don't log sessionId when authenticating, and add token auth

This commit is contained in:
Iris Lightshard 2022-07-31 18:08:05 -06:00
parent c8e5149237
commit 48dbb967f3
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
7 changed files with 152 additions and 7 deletions

View file

@ -33,7 +33,7 @@ Features may be added here at any time as things are in early stages right now:
* [x] top-level wrapper for attaching `UserStore` backends to cookie handler * [x] top-level wrapper for attaching `UserStore` backends to cookie handler
* [x] POC [indental](https://wiki.xxiivv.com/site/indental.html) `UserStore` implementation * [x] POC [indental](https://wiki.xxiivv.com/site/indental.html) `UserStore` implementation
* [ ] Bearer token-based authentication to supplement cookie-baesd auth * [x] both cookie- and token-based authentication (use one but not both together)
### etc ### etc
@ -43,6 +43,8 @@ Features may be added here at any time as things are in early stages right now:
- [x] `Bunt`: logout and redirect - [x] `Bunt`: logout and redirect
- [x] `Fortify`: setup CSRF protection (use on the form) - [x] `Fortify`: setup CSRF protection (use on the form)
- [x] `Defend`: enact CSRF protection (use on the endpoint) - [x] `Defend`: enact CSRF protection (use on the endpoint)
- [x] `Provision`: use BASIC authentication to provision an access token
- [x] `Validate`: valiate the bearer token against the `UserStore`
## license ## license

View file

@ -27,6 +27,9 @@ type UserStore interface {
GetLastTimeSeen(user string) (time.Time, error) GetLastTimeSeen(user string) (time.Time, error)
SetData(user string, key string, value interface{}) error SetData(user string, key string, value interface{}) error
GetData(user string, key string) (interface{}, error) GetData(user string, key string) (interface{}, error)
GrantToken(user, password, scope string, minutes int) (string, error)
ValidateToken(token string) (bool, error)
ValidateTokenWithScopes(token string, scopes map[string]string) (bool, error)
} }
func Login(user string, password string, userStore UserStore, w http.ResponseWriter, t int) error { func Login(user string, password string, userStore UserStore, w http.ResponseWriter, t int) error {

View file

@ -1,6 +1,7 @@
package indentalUserDB package indentalUserDB
import ( import (
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -50,6 +51,28 @@ func (self *IndentalUserDB) InitiateSession(user string, password string) (strin
return sessionId, nil return sessionId, nil
} }
func (self *IndentalUserDB) GrantToken(user, password, scope string, minutes int) (string, error) {
if _, exists := self.Users[user]; !exists {
return "", errors.New("User not in DB")
}
if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(password)) != nil {
return "", errors.New("Incorrect password")
}
s, err := self.GetData(user, "scope")
if err == nil && s == scope {
sessionId := cookie.GenToken(64)
self.Users[user].Session = sessionId
self.Users[user].LoginTime = time.Now()
self.Users[user].LastSeen = time.Now()
self.SetData(user, "token_expiry", time.Now().Add(time.Minute*time.Duration(minutes)).Format(timeFmt))
writeDB(self.Basis, self.Users)
return base64.StdEncoding.EncodeToString([]byte(user + "\n" + sessionId)), nil
}
return "", errors.New("Incorrect scope for this user")
}
func (self *IndentalUserDB) ValidateUser(user string, sessionId string) (bool, error) { func (self *IndentalUserDB) ValidateUser(user string, sessionId string) (bool, error) {
if _, exists := self.Users[user]; !exists { if _, exists := self.Users[user]; !exists {
return false, errors.New("User not in DB") return false, errors.New("User not in DB")
@ -64,6 +87,49 @@ func (self *IndentalUserDB) ValidateUser(user string, sessionId string) (bool, e
return validated, nil return validated, nil
} }
func (self *IndentalUserDB) ValidateToken(token string) (bool, error) {
data, err := base64.StdEncoding.DecodeString(token)
if err == nil {
parts := strings.Split(string(data), "\n")
if len(parts) == 2 {
expiry, err3 := self.GetData(parts[0], "token_expiry")
expiryTime, err4 := time.Parse(timeFmt, expiry.(string))
if err3 == nil && err4 == nil && time.Now().After(expiryTime) {
self.EndSession(parts[0])
return false, errors.New("token has expired")
} else {
return self.ValidateUser(parts[0], parts[1])
}
}
}
return false, errors.New("Token was not in a valid format: b64(USER\nSESSION)")
}
func (self *IndentalUserDB) ValidateTokenWithScopes(token string, scopes map[string]string) (bool, error) {
data, err := base64.StdEncoding.DecodeString(token)
if err == nil {
parts := strings.Split(string(data), "\n")
n := 0
for k, v := range scopes {
s, _ := self.GetData(parts[0], k)
if s.(string) == v {
n++
}
}
validated, err2 := self.ValidateToken(token)
if validated {
if n == len(scopes) {
return validated, nil
} else {
return false, errors.New("User does not have the proper scopes")
}
} else {
return validated, err2
}
}
return false, err
}
func (self *IndentalUserDB) EndSession(user string) error { func (self *IndentalUserDB) EndSession(user string) error {
if _, exists := self.Users[user]; !exists { if _, exists := self.Users[user]; !exists {
return errors.New("User not in DB") return errors.New("User not in DB")

View file

@ -6,8 +6,17 @@ import (
"net/http" "net/http"
"nilfm.cc/git/quartzgun/auth" "nilfm.cc/git/quartzgun/auth"
"nilfm.cc/git/quartzgun/cookie" "nilfm.cc/git/quartzgun/cookie"
"nilfm.cc/git/quartzgun/renderer"
"nilfm.cc/git/quartzgun/util"
"strings"
) )
type TokenPayload struct {
access_token string
token_type string
expires_in int
}
func Protected(next http.Handler, method string, userStore auth.UserStore, login string) http.Handler { func Protected(next http.Handler, method string, userStore auth.UserStore, login string) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) { handlerFunc := func(w http.ResponseWriter, req *http.Request) {
user, err := cookie.GetToken("user", req) user, err := cookie.GetToken("user", req)
@ -16,8 +25,7 @@ func Protected(next http.Handler, method string, userStore auth.UserStore, login
if err == nil { if err == nil {
login, err := userStore.ValidateUser(user, session) login, err := userStore.ValidateUser(user, session)
if err == nil && login { if err == nil && login {
fmt.Printf("authorized!\n") fmt.Printf("authorized user: %s\n", user)
fmt.Printf("user: %s, session: %s\n", user, session)
req.Method = method req.Method = method
next.ServeHTTP(w, req) next.ServeHTTP(w, req)
return return
@ -43,7 +51,6 @@ func Bunt(next string, userStore auth.UserStore, denied string) http.Handler {
if err == nil { if err == nil {
req.Method = http.MethodGet req.Method = http.MethodGet
http.Redirect(w, req, next, http.StatusSeeOther) http.Redirect(w, req, next, http.StatusSeeOther)
return
} }
} }
req.Method = http.MethodGet req.Method = http.MethodGet
@ -75,6 +82,49 @@ func Authorize(next string, userStore auth.UserStore, denied string) http.Handle
return http.HandlerFunc(handlerFunc) return http.HandlerFunc(handlerFunc)
} }
func Provision(userStore auth.UserStore, minutes int) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
user, password, ok := req.BasicAuth()
scope := req.FormValue("scope")
if ok && scope != "" {
token, err := userStore.GrantToken(user, password, scope, minutes)
if err == nil {
token := TokenPayload{
access_token: token,
token_type: "bearer",
expires_in: minutes,
}
util.AddContextValue(req, "token", token)
renderer.JSON("token").ServeHTTP(w, req)
return
}
}
w.Header().Add("WWW-Authenticate", "Basic")
w.WriteHeader(http.StatusUnauthorized)
return
}
return http.HandlerFunc(handlerFunc)
}
func Validate(next http.Handler, userStore auth.UserStore, scopes map[string]string) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
authHeader := req.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
authToken := strings.Split(authHeader, "Bearer ")[1]
validated, err := userStore.ValidateTokenWithScopes(authToken, scopes)
if validated && err == nil {
next.ServeHTTP(w, req)
return
}
}
w.Header().Add("WWW-Authenticate", "Basic")
w.WriteHeader(http.StatusUnauthorized)
}
return http.HandlerFunc(handlerFunc)
}
func Fortify(next http.Handler) http.Handler { func Fortify(next http.Handler) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) { handlerFunc := func(w http.ResponseWriter, req *http.Request) {
token, err := cookie.GetToken("csrfToken", req) token, err := cookie.GetToken("csrfToken", req)

View file

@ -21,6 +21,10 @@ func Template(t ...string) http.Handler {
return http.HandlerFunc(handlerFunc) return http.HandlerFunc(handlerFunc)
} }
func Subtree(path string) http.Handler {
return http.FileServer(http.Dir(path))
}
func JSON(key string) http.Handler { func JSON(key string) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) { handlerFunc := func(w http.ResponseWriter, req *http.Request) {
apiData := req.Context().Value(key) apiData := req.Context().Value(key)

View file

@ -85,11 +85,21 @@ func (self *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
/* If the file exists, try to serve it. */ /* If the file exists, try to serve it. */
info, err := os.Stat(p) info, err := os.Stat(p)
if err == nil && !info.IsDir() { if err == nil {
http.ServeFile(w, req, p) if !info.IsDir() {
http.ServeFile(w, req, p)
} else {
indexFile := path.Join(p, "index.html")
info2, err2 := os.Stat(indexFile)
if err2 == nil && !info2.IsDir() {
http.ServeFile(w, req, indexFile)
} else {
self.ErrorPage(w, req, 403, "Access forbidden")
}
}
/* Handle the common errors */ /* Handle the common errors */
} else if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrExist) { } else if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrExist) {
self.ErrorPage(w, req, 404, "The requested file does not exist") self.ErrorPage(w, req, 404, "The page you requested does not exist!")
} else if errors.Is(err, os.ErrPermission) || info.IsDir() { } else if errors.Is(err, os.ErrPermission) || info.IsDir() {
self.ErrorPage(w, req, 403, "Access forbidden") self.ErrorPage(w, req, 403, "Access forbidden")
/* If it's some weird error, serve a 500. */ /* If it's some weird error, serve a 500. */

10
util/util.go Normal file
View file

@ -0,0 +1,10 @@
package util
import (
"context"
"net/http"
)
func AddContextValue(req *http.Request, key string, value interface{}) {
*req = *req.WithContext(context.WithValue(req.Context(), key, value))
}