aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDerek Stevens <nilix@nilfm.cc>2022-07-31 18:08:05 -0600
committerDerek Stevens <nilix@nilfm.cc>2022-07-31 18:08:05 -0600
commit48dbb967f38ea4af6692e38c1676057315e06b2b (patch)
tree8e2ebb73a4af8f5e8684e2af791306b4337f3155
parentc8e51492376bc02133da4dd644d67e81546b5591 (diff)
add subtree renderer, fix static routing for index pages, don't log sessionId when authenticating, and add token auth
-rw-r--r--README.md4
-rw-r--r--auth/auth.go3
-rw-r--r--indentalUserDB/indentalUserDB.go66
-rw-r--r--middleware/middleware.go56
-rw-r--r--renderer/renderer.go4
-rw-r--r--router/router.go16
-rw-r--r--util/util.go10
7 files changed, 152 insertions, 7 deletions
diff --git a/README.md b/README.md
index 9732837..5b20f30 100644
--- a/README.md
+++ b/README.md
@@ -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] 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
@@ -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] `Fortify`: setup CSRF protection (use on the form)
- [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
diff --git a/auth/auth.go b/auth/auth.go
index c7a21e1..6b79b04 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -27,6 +27,9 @@ type UserStore interface {
GetLastTimeSeen(user string) (time.Time, error)
SetData(user string, key string, value 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 {
diff --git a/indentalUserDB/indentalUserDB.go b/indentalUserDB/indentalUserDB.go
index cdf73e9..fda255d 100644
--- a/indentalUserDB/indentalUserDB.go
+++ b/indentalUserDB/indentalUserDB.go
@@ -1,6 +1,7 @@
package indentalUserDB
import (
+ "encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
@@ -50,6 +51,28 @@ func (self *IndentalUserDB) InitiateSession(user string, password string) (strin
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) {
if _, exists := self.Users[user]; !exists {
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
}
+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 {
if _, exists := self.Users[user]; !exists {
return errors.New("User not in DB")
diff --git a/middleware/middleware.go b/middleware/middleware.go
index d83f4b1..67b38a1 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -6,8 +6,17 @@ import (
"net/http"
"nilfm.cc/git/quartzgun/auth"
"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 {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
user, err := cookie.GetToken("user", req)
@@ -16,8 +25,7 @@ func Protected(next http.Handler, method string, userStore auth.UserStore, login
if err == nil {
login, err := userStore.ValidateUser(user, session)
if err == nil && login {
- fmt.Printf("authorized!\n")
- fmt.Printf("user: %s, session: %s\n", user, session)
+ fmt.Printf("authorized user: %s\n", user)
req.Method = method
next.ServeHTTP(w, req)
return
@@ -43,7 +51,6 @@ func Bunt(next string, userStore auth.UserStore, denied string) http.Handler {
if err == nil {
req.Method = http.MethodGet
http.Redirect(w, req, next, http.StatusSeeOther)
- return
}
}
req.Method = http.MethodGet
@@ -75,6 +82,49 @@ func Authorize(next string, userStore auth.UserStore, denied string) http.Handle
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 {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
token, err := cookie.GetToken("csrfToken", req)
diff --git a/renderer/renderer.go b/renderer/renderer.go
index 3d912eb..dd1e6d4 100644
--- a/renderer/renderer.go
+++ b/renderer/renderer.go
@@ -21,6 +21,10 @@ func Template(t ...string) http.Handler {
return http.HandlerFunc(handlerFunc)
}
+func Subtree(path string) http.Handler {
+ return http.FileServer(http.Dir(path))
+}
+
func JSON(key string) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
apiData := req.Context().Value(key)
diff --git a/router/router.go b/router/router.go
index f80b8f6..3278771 100644
--- a/router/router.go
+++ b/router/router.go
@@ -85,11 +85,21 @@ func (self *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
/* If the file exists, try to serve it. */
info, err := os.Stat(p)
- if err == nil && !info.IsDir() {
- http.ServeFile(w, req, p)
+ if err == nil {
+ 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 */
} 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() {
self.ErrorPage(w, req, 403, "Access forbidden")
/* If it's some weird error, serve a 500. */
diff --git a/util/util.go b/util/util.go
new file mode 100644
index 0000000..ba2f5c3
--- /dev/null
+++ b/util/util.go
@@ -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))
+}