add subtree renderer, fix static routing for index pages, don't log sessionId when authenticating, and add token auth
This commit is contained in:
parent
c8e5149237
commit
48dbb967f3
7 changed files with 152 additions and 7 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. */
|
||||
|
|
10
util/util.go
Normal file
10
util/util.go
Normal 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))
|
||||
}
|
Loading…
Reference in a new issue