Auth implemented ish

master
Bel LaPointe 2020-07-24 14:45:03 -06:00
parent 11e7d13cca
commit d3ac4f5c22
5 changed files with 366 additions and 59 deletions

View File

@ -4,6 +4,7 @@ import (
"io/ioutil" "io/ioutil"
"local/args" "local/args"
"os" "os"
"time"
) )
type Config struct { type Config struct {
@ -14,6 +15,7 @@ type Config struct {
FilePrefix string FilePrefix string
FileRoot string FileRoot string
Auth bool Auth bool
AuthLifetime time.Duration
} }
func New() Config { func New() Config {
@ -32,6 +34,7 @@ func New() Config {
as.Append(args.STRING, "database", "database name to use", "db") as.Append(args.STRING, "database", "database name to use", "db")
as.Append(args.STRING, "drivertype", "database driver to use", "boltdb") as.Append(args.STRING, "drivertype", "database driver to use", "boltdb")
as.Append(args.BOOL, "auth", "check for authorized access", false) as.Append(args.BOOL, "auth", "check for authorized access", false)
as.Append(args.DURATION, "authlifetime", "duration auth is valid for", time.Hour)
if err := as.Parse(); err != nil { if err := as.Parse(); err != nil {
panic(err) panic(err)
@ -45,5 +48,6 @@ func New() Config {
Database: as.GetString("database"), Database: as.GetString("database"),
DriverType: as.GetString("drivertype"), DriverType: as.GetString("drivertype"),
Auth: as.GetBool("auth"), Auth: as.GetBool("auth"),
AuthLifetime: as.GetDuration("authlifetime"),
} }
} }

View File

@ -1,15 +1,27 @@
package view package view
import ( import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"local/dndex/config" "local/dndex/config"
"local/dndex/storage" "local/dndex/storage"
"local/dndex/storage/entity"
"net/http" "net/http"
"strings"
"time"
"github.com/google/uuid"
) )
const ( const (
AuthKey = "DnDex-Auth" AuthKey = "DnDex-Auth"
NewAuthKey = "New-" + AuthKey
UserKey = "DnDex-User"
) )
func Auth(g storage.Graph, w http.ResponseWriter, r *http.Request) error { func Auth(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
@ -25,27 +37,119 @@ func Auth(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
func auth(g storage.Graph, w http.ResponseWriter, r *http.Request) error { func auth(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
if !hasAuth(r) { if !hasAuth(r) {
if err := requestAuth(g, w, r); err != nil { return requestAuth(g, w, r)
return err
} }
return errors.New("auth requested") return checkAuth(g, w, r)
}
return checkAuth(g, r)
} }
func hasAuth(r *http.Request) bool { func hasAuth(r *http.Request) bool {
_, ok := r.Cookie(AuthKey) _, err := r.Cookie(AuthKey)
return ok == nil return err == nil
} }
func checkAuth(g storage.Graph, r *http.Request) error { func checkAuth(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
panic(nil) namespace, err := getAuthNamespace(r)
/* if err != nil {
return err
}
token, _ := r.Cookie(AuthKey) token, _ := r.Cookie(AuthKey)
return errors.New("not impl") results, err := g.List(r.Context(), namespace, token.Value)
*/ if err != nil {
return err
}
if len(results) != 1 {
return requestAuth(g, w, r)
}
modified := time.Unix(0, results[0].Modified)
if time.Since(modified) > config.New().AuthLifetime {
return requestAuth(g, w, r)
}
return nil
} }
func requestAuth(g storage.Graph, w http.ResponseWriter, r *http.Request) error { func requestAuth(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl") namespace, err := getAuthNamespace(r)
if err != nil {
http.Error(w, `{"error": "namespace required"}`, http.StatusBadRequest)
return err
}
ones, err := g.List(r.Context(), namespace, UserKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
if len(ones) != 1 {
http.NotFound(w, r)
return errors.New("namespace not established")
}
userKey := ones[0]
token := entity.One{
Name: uuid.New().String(),
Title: namespace,
}
if err := g.Insert(r.Context(), namespace, token); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
encodedToken, err := aesEnc(userKey.Title, token.Name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
http.SetCookie(w, &http.Cookie{Name: NewAuthKey, Value: encodedToken})
http.Redirect(w, r, r.URL.String(), http.StatusSeeOther)
return errors.New("auth requested")
}
func aesEnc(key, payload string) (string, error) {
if len(key) == 0 {
return "", errors.New("key required")
}
key = strings.Repeat(key, 32)[:32]
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
b := gcm.Seal(nonce, nonce, []byte(payload), nil)
return base64.StdEncoding.EncodeToString(b), nil
}
func aesDec(key, payload string) (string, error) {
if len(key) == 0 {
return "", errors.New("key required")
}
key = strings.Repeat(key, 32)[:32]
ciphertext, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return "", err
}
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
if len(ciphertext) < gcm.NonceSize() {
return "", errors.New("short ciphertext")
}
b, err := gcm.Open(nil, ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():], nil)
return string(b), err
} }

169
view/auth_test.go Normal file
View File

@ -0,0 +1,169 @@
package view
import (
"context"
"fmt"
"io/ioutil"
"local/dndex/storage"
"local/dndex/storage/entity"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/google/uuid"
)
func TestAuth(t *testing.T) {
os.Args = os.Args[:1]
f, err := ioutil.TempFile(os.TempDir(), "pattern*")
if err != nil {
t.Fatal(err)
}
f.Close()
defer os.Remove(f.Name())
os.Setenv("DBURI", f.Name())
os.Setenv("AUTH", "true")
defer os.Setenv("AUTH", "false")
g := storage.NewGraph()
handler := jsonHandler(g)
if err := g.Insert(context.TODO(), "col."+AuthKey, entity.One{Name: UserKey, Title: "password"}); err != nil {
t.Fatal(err)
}
t.Run("auth: no namespace", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/who", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: bad provided", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=not-a-real-token", AuthKey))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: expired provided", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1ms")
defer os.Setenv("AUTHLIFETIME", "1h")
one := entity.One{Name: uuid.New().String(), Title: "title"}
if err := g.Insert(context.TODO(), "col", one); err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, one.Name))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: none provided", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/who?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: provided", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1h")
one := entity.One{Name: uuid.New().String(), Title: "title"}
if err := g.Insert(context.TODO(), "col."+AuthKey, one); err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, one.Name))
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: request unknown namespace", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1h")
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=not-col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
t.Run("auth: request", func(t *testing.T) {
os.Setenv("AUTHLIFETIME", "1h")
r := httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
cookies := w.Header()["Set-Cookie"]
if len(cookies) == 0 {
t.Fatal(w.Header())
}
var rawtoken string
for i := range cookies {
value := strings.Split(cookies[i], ";")[0]
key := value[:strings.Index(value, "=")]
value = value[strings.Index(value, "=")+1:]
if key == NewAuthKey {
rawtoken = value
}
}
if rawtoken == "" {
t.Fatal(w.Header())
}
token, err := aesDec("password", rawtoken)
if err != nil {
t.Fatal(err)
}
r = httptest.NewRequest(http.MethodTrace, "/who?namespace=col", nil)
w = httptest.NewRecorder()
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, token))
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("%d: %s", w.Code, w.Body.Bytes())
}
})
}
func TestAES(t *testing.T) {
for _, plaintext := range []string{"", "payload!", "a really long payload here"} {
key := "password"
enc, err := aesEnc(key, plaintext)
if err != nil {
t.Fatal("cannot enc:", err)
}
if enc == plaintext {
t.Fatal(enc)
}
dec, err := aesDec(key, enc)
if err != nil {
t.Fatal("cannot dec:", err)
}
if dec != plaintext {
t.Fatalf("want decrypted %q, got %q", plaintext, dec)
}
}
}

View File

@ -2,6 +2,7 @@ package view
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"local/dndex/config" "local/dndex/config"
"local/dndex/storage" "local/dndex/storage"
@ -24,6 +25,7 @@ func jsonHandler(g storage.Graph) http.Handler {
routes := []struct { routes := []struct {
path string path string
foo func(g storage.Graph, w http.ResponseWriter, r *http.Request) error foo func(g storage.Graph, w http.ResponseWriter, r *http.Request) error
noauth bool
}{ }{
{ {
path: "/who", path: "/who",
@ -32,12 +34,20 @@ func jsonHandler(g storage.Graph) http.Handler {
{ {
path: config.New().FilePrefix + "/", path: config.New().FilePrefix + "/",
foo: files, foo: files,
noauth: true,
}, },
} }
for _, route := range routes { for _, route := range routes {
foo := route.foo foo := route.foo
auth := !route.noauth
mux.HandleFunc(route.path, func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc(route.path, func(w http.ResponseWriter, r *http.Request) {
if auth {
if err := Auth(g, w, r); err != nil {
log.Println(err)
return
}
}
if err := foo(g, w, r); err != nil { if err := foo(g, w, r); err != nil {
status := http.StatusInternalServerError status := http.StatusInternalServerError
if strings.Contains(err.Error(), "collision") { if strings.Contains(err.Error(), "collision") {
@ -50,10 +60,6 @@ func jsonHandler(g storage.Graph) http.Handler {
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := Auth(g, w, r); err != nil {
log.Println(err)
return
}
if gziphttp.Can(r) { if gziphttp.Can(r) {
gz := gziphttp.New(w) gz := gziphttp.New(w)
defer gz.Close() defer gz.Close()
@ -62,3 +68,19 @@ func jsonHandler(g storage.Graph) http.Handler {
mux.ServeHTTP(w, r) mux.ServeHTTP(w, r)
}) })
} }
func getAuthNamespace(r *http.Request) (string, error) {
namespace := r.URL.Query().Get("namespace")
if len(namespace) == 0 {
return "", errors.New("no namespace found")
}
return strings.Join([]string{namespace, AuthKey}, "."), nil
}
func getNamespace(r *http.Request) (string, error) {
namespace := r.URL.Query().Get("namespace")
if len(namespace) == 0 {
return "", errors.New("no namespace found")
}
return namespace, nil
}

View File

@ -15,8 +15,8 @@ import (
) )
func who(g storage.Graph, w http.ResponseWriter, r *http.Request) error { func who(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
namespace := r.URL.Query().Get("namespace") namespace, err := getNamespace(r)
if len(namespace) == 0 { if err != nil {
http.NotFound(w, r) http.NotFound(w, r)
return nil return nil
} }
@ -41,10 +41,10 @@ func who(g storage.Graph, w http.ResponseWriter, r *http.Request) error {
} }
func whoGet(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error { func whoGet(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error {
id := r.URL.Query().Get("id") id, err := getID(r)
if id == "" { if err != nil {
http.Error(w, `{"error":"no ?id provided"}`, http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return nil return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
} }
_, light := r.URL.Query()["light"] _, light := r.URL.Query()["light"]
@ -78,10 +78,10 @@ func whoGet(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Re
} }
func whoPut(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error { func whoPut(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error {
id := r.URL.Query().Get("id") id, err := getID(r)
if id == "" { if err != nil {
http.Error(w, `{"error":"no ?id provided"}`, http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return nil return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
} }
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
@ -121,10 +121,10 @@ func whoPut(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Re
} }
func whoPost(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error { func whoPost(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error {
id := r.URL.Query().Get("id") id, err := getID(r)
if id == "" { if err != nil {
http.Error(w, `{"error":"no ?id provided"}`, http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return nil return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
} }
one := entity.One{} one := entity.One{}
@ -140,10 +140,10 @@ func whoPost(namespace string, g storage.Graph, w http.ResponseWriter, r *http.R
} }
func whoDelete(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error { func whoDelete(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error {
id := r.URL.Query().Get("id") id, err := getID(r)
if id == "" { if err != nil {
http.Error(w, `{"error":"no ?id provided"}`, http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return nil return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
} }
if err := g.Delete(r.Context(), namespace, entity.One{Name: id}); err != nil { if err := g.Delete(r.Context(), namespace, entity.One{Name: id}); err != nil {
@ -154,10 +154,10 @@ func whoDelete(namespace string, g storage.Graph, w http.ResponseWriter, r *http
} }
func whoPatch(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error { func whoPatch(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error {
id := r.URL.Query().Get("id") id, err := getID(r)
if id == "" { if err != nil {
http.Error(w, `{"error":"no ?id provided"}`, http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return nil return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
} }
one := entity.One{} one := entity.One{}
@ -194,3 +194,11 @@ func whoTrace(namespace string, g storage.Graph, w http.ResponseWriter, r *http.
enc.SetIndent("", " ") enc.SetIndent("", " ")
return enc.Encode(names) return enc.Encode(names)
} }
func getID(r *http.Request) (string, error) {
id := r.URL.Query().Get("id")
if id == "" {
return "", errors.New("no id provided")
}
return id, nil
}