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] 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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
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