new auth
parent
304956da74
commit
46bd1bbdfc
194
server/auth.go
194
server/auth.go
|
|
@ -1,194 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"local/dndex/config"
|
|
||||||
"local/dndex/storage"
|
|
||||||
"local/dndex/storage/entity"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Auth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if !config.New().Auth {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := auth(g, w, r); err != nil {
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "error when authorizing: " + err.Error()})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func auth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if isPublic(g, r) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !hasAuth(r) {
|
|
||||||
return requestAuth(g, w, r)
|
|
||||||
}
|
|
||||||
return checkAuth(g, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPublic(g storage.RateLimitedGraph, r *http.Request) bool {
|
|
||||||
namespace, err := getAuthNamespace(r)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ones, err := g.List(r.Context(), namespace, UserKey)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(ones) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return ones[0].Title == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasAuth(r *http.Request) bool {
|
|
||||||
_, err := r.Cookie(AuthKey)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAuth(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
namespace, err := getAuthNamespace(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
token, _ := r.Cookie(AuthKey)
|
|
||||||
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.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
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]
|
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
token := entity.One{
|
|
||||||
ID: id,
|
|
||||||
Name: id,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAuthNamespace(r *http.Request) (string, error) {
|
|
||||||
namespace, err := getNamespace(r)
|
|
||||||
return strings.Join([]string{namespace, AuthKey}, "."), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNamespace(r *http.Request) (string, error) {
|
|
||||||
if strings.HasPrefix(r.URL.Path, config.New().FilePrefix) {
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, config.New().FilePrefix+"/")
|
|
||||||
if path == r.URL.Path {
|
|
||||||
return "", errors.New("no namespace on files")
|
|
||||||
}
|
|
||||||
path = strings.Split(path, "/")[0]
|
|
||||||
if path == "" {
|
|
||||||
return "", errors.New("empty namespace on files")
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
namespace := r.URL.Query().Get("namespace")
|
|
||||||
if len(namespace) == 0 {
|
|
||||||
return "", errors.New("no namespace found")
|
|
||||||
}
|
|
||||||
return namespace, nil
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthKey = "DnDex-Auth"
|
||||||
|
UserKey = "DnDex-User"
|
||||||
|
NewAuthKey = "New-" + AuthKey
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encrypt(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 decrypt(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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAES(t *testing.T) {
|
||||||
|
for _, plaintext := range []string{"", "payload!", "a really long payload here"} {
|
||||||
|
key := "password"
|
||||||
|
|
||||||
|
enc, err := encrypt(key, plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot enc:", err)
|
||||||
|
}
|
||||||
|
if enc == plaintext {
|
||||||
|
t.Fatal(enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := decrypt(key, enc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot dec:", err)
|
||||||
|
}
|
||||||
|
if dec != plaintext {
|
||||||
|
t.Fatalf("want decrypted %q, got %q", plaintext, dec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"local/dndex/storage"
|
||||||
|
"local/dndex/storage/entity"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Generate(g storage.RateLimitedGraph, r *http.Request, salt string) (string, error) {
|
||||||
|
namespaceRequested := readRequestedNamespace(r)
|
||||||
|
key, err := getKeyForNamespace(r.Context(), g, namespaceRequested)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
token, err := makeTokenForNamespace(r.Context(), g, namespaceRequested)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token.Encode(salt + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRequestedNamespace(r *http.Request) string {
|
||||||
|
return readRequested(r, UserKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRequested(r *http.Request, key string) string {
|
||||||
|
return r.FormValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyForNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace string) (string, error) {
|
||||||
|
namespaceOne, err := g.Get(ctx, namespace, UserKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return namespaceOne.Title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTokenForNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace string) (Token, error) {
|
||||||
|
token := Token{
|
||||||
|
Namespace: namespace,
|
||||||
|
Token: uuid.New().String(),
|
||||||
|
}
|
||||||
|
obf, err := token.Obfuscate()
|
||||||
|
if err != nil {
|
||||||
|
return Token{}, err
|
||||||
|
}
|
||||||
|
one := entity.One{
|
||||||
|
ID: token.Token,
|
||||||
|
Title: obf,
|
||||||
|
}
|
||||||
|
return token, g.Insert(ctx, namespace+"."+AuthKey, one)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"local/dndex/storage"
|
||||||
|
"local/dndex/storage/entity"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerate(t *testing.T) {
|
||||||
|
os.Args = os.Args[:1]
|
||||||
|
os.Setenv("AUTH", "true")
|
||||||
|
defer os.Unsetenv("AUTH")
|
||||||
|
|
||||||
|
fresh := func() (storage.RateLimitedGraph, *http.Request, string) {
|
||||||
|
g := storage.NewRateLimitedGraph()
|
||||||
|
key := uuid.New().String()
|
||||||
|
namespace := uuid.New().String()
|
||||||
|
one := entity.One{
|
||||||
|
ID: UserKey,
|
||||||
|
Title: key,
|
||||||
|
}
|
||||||
|
if err := g.Insert(context.TODO(), namespace, one); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(UserKey+`=`+namespace))
|
||||||
|
r.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||||
|
return g,
|
||||||
|
r,
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
g, r, key := fresh()
|
||||||
|
salt := uuid.New().String()
|
||||||
|
encoded, err := Generate(g, r, salt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var token Token
|
||||||
|
if err := token.Decode(salt+key, encoded); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"local/dndex/storage"
|
||||||
|
"local/dndex/storage/entity"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(g storage.RateLimitedGraph, r *http.Request) error {
|
||||||
|
namespaceRequested := readRequestedNamespace(r)
|
||||||
|
keyRequested := readRequestedKey(r)
|
||||||
|
_, err := getKeyForNamespace(r.Context(), g, namespaceRequested)
|
||||||
|
if err == nil {
|
||||||
|
return errors.New("namespace already exists")
|
||||||
|
}
|
||||||
|
return makeNamespace(r.Context(), g, namespaceRequested, keyRequested)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRequestedKey(r *http.Request) string {
|
||||||
|
return readRequested(r, AuthKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace, key string) error {
|
||||||
|
one := entity.One{
|
||||||
|
ID: UserKey,
|
||||||
|
Title: key,
|
||||||
|
}
|
||||||
|
return g.Insert(ctx, namespace, one)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"local/dndex/storage"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegister(t *testing.T) {
|
||||||
|
g := storage.NewRateLimitedGraph()
|
||||||
|
namespace := uuid.New().String()
|
||||||
|
key := uuid.New().String()
|
||||||
|
r := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/",
|
||||||
|
strings.NewReader(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s=%s&%s=%s",
|
||||||
|
UserKey,
|
||||||
|
namespace,
|
||||||
|
AuthKey,
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
r.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if v := readRequested(r, UserKey); v == "" {
|
||||||
|
t.Fatal(UserKey, v)
|
||||||
|
}
|
||||||
|
if v := readRequested(r, AuthKey); v == "" {
|
||||||
|
t.Fatal(AuthKey, v)
|
||||||
|
}
|
||||||
|
err := Register(g, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
one, err := g.Get(context.TODO(), namespace, UserKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if one.ID != UserKey {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if one.Title != key {
|
||||||
|
t.Fatal(one)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Namespace string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Token) Obfuscate() (string, error) {
|
||||||
|
b, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
obfuscated := base64.StdEncoding.EncodeToString(b)
|
||||||
|
return obfuscated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Token) Encode(key string) (string, error) {
|
||||||
|
obfuscated, err := t.Obfuscate()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
encoded, err := encrypt(key, obfuscated)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) Deobfuscate(obfuscated string) error {
|
||||||
|
marshalled, err := base64.StdEncoding.DecodeString(obfuscated)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(marshalled), t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) Decode(key, encoded string) error {
|
||||||
|
obfuscated, err := decrypt(key, encoded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.Deobfuscate(obfuscated)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTokenEncDec(t *testing.T) {
|
||||||
|
token := Token{
|
||||||
|
Namespace: "username",
|
||||||
|
Token: uuid.New().String(),
|
||||||
|
}
|
||||||
|
key := "a"
|
||||||
|
|
||||||
|
encoded, err := token.Encode(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ntoken Token
|
||||||
|
err = ntoken.Decode(key, encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fmt.Sprint(token) != fmt.Sprint(ntoken) {
|
||||||
|
t.Fatal(ntoken)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("token=%+v, ntoken=%+v, encoded=%s", token, ntoken, encoded)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"local/dndex/config"
|
||||||
|
"local/dndex/storage"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Verify(g storage.RateLimitedGraph, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
token, ok := getToken(r)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("auth not found")
|
||||||
|
}
|
||||||
|
if !config.New().Auth {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if isPublic(token, g, r) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return verifyToken(token, g, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getToken(r *http.Request) (Token, bool) {
|
||||||
|
cookie, err := r.Cookie(AuthKey)
|
||||||
|
if err != nil {
|
||||||
|
return Token{}, false
|
||||||
|
}
|
||||||
|
obfuscated := cookie.Value
|
||||||
|
var token Token
|
||||||
|
err = token.Deobfuscate(obfuscated)
|
||||||
|
return token, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPublic(token Token, g storage.RateLimitedGraph, r *http.Request) bool {
|
||||||
|
return isPublicNamespace(r.Context(), g, token.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPublicNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace string) bool {
|
||||||
|
maybePublicContainer, err := g.Get(ctx, namespace, UserKey)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return maybePublicContainer.Title == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyToken(token Token, g storage.RateLimitedGraph, r *http.Request) error {
|
||||||
|
serverTokenContainer, err := g.Get(r.Context(), token.Namespace+"."+AuthKey, token.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverToken Token
|
||||||
|
if err := serverToken.Deobfuscate(serverTokenContainer.Title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.Namespace != serverToken.Namespace {
|
||||||
|
return errors.New("token namespace does not match request's namespace")
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := time.Unix(0, serverTokenContainer.Modified)
|
||||||
|
if time.Since(modified) > config.New().AuthLifetime {
|
||||||
|
return errors.New("token is expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"local/dndex/storage"
|
||||||
|
"local/dndex/storage/entity"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerify(t *testing.T) {
|
||||||
|
os.Args = os.Args[:1]
|
||||||
|
os.Setenv("AUTH", "true")
|
||||||
|
defer os.Unsetenv("AUTH")
|
||||||
|
|
||||||
|
fresh := func() (storage.RateLimitedGraph, *httptest.ResponseRecorder, *http.Request, Token, string) {
|
||||||
|
g := storage.NewRateLimitedGraph()
|
||||||
|
token := Token{
|
||||||
|
Token: uuid.New().String(),
|
||||||
|
Namespace: uuid.New().String(),
|
||||||
|
}
|
||||||
|
obf, _ := token.Obfuscate()
|
||||||
|
one := entity.One{
|
||||||
|
ID: token.Token,
|
||||||
|
Title: obf,
|
||||||
|
}
|
||||||
|
if err := g.Insert(context.TODO(), token.Namespace+"."+AuthKey, one); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return g,
|
||||||
|
httptest.NewRecorder(),
|
||||||
|
httptest.NewRequest(http.MethodGet, "/", nil),
|
||||||
|
token,
|
||||||
|
obf
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no auth", func(t *testing.T) {
|
||||||
|
g, w, r, _, _ := fresh()
|
||||||
|
err := Verify(g, w, r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "auth not found") {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok auth", func(t *testing.T) {
|
||||||
|
g, w, r, _, obf := fresh()
|
||||||
|
r.AddCookie(&http.Cookie{
|
||||||
|
Name: AuthKey,
|
||||||
|
Value: obf,
|
||||||
|
})
|
||||||
|
err := Verify(g, w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no ns auth", func(t *testing.T) {
|
||||||
|
g, w, r, token, _ := fresh()
|
||||||
|
token.Namespace = ""
|
||||||
|
obf, err := token.Obfuscate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r.AddCookie(&http.Cookie{
|
||||||
|
Name: AuthKey,
|
||||||
|
Value: obf,
|
||||||
|
})
|
||||||
|
err = Verify(g, w, r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong ns auth", func(t *testing.T) {
|
||||||
|
g, w, r, token, _ := fresh()
|
||||||
|
token.Namespace = uuid.New().String()
|
||||||
|
obf, err := token.Obfuscate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r.AddCookie(&http.Cookie{
|
||||||
|
Name: AuthKey,
|
||||||
|
Value: obf,
|
||||||
|
})
|
||||||
|
err = Verify(g, w, r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("expired auth", func(t *testing.T) {
|
||||||
|
t.Logf("not impl")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad auth", func(t *testing.T) {
|
||||||
|
g, w, r, token, _ := fresh()
|
||||||
|
token.Token = uuid.New().String()
|
||||||
|
obf, err := token.Obfuscate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r.AddCookie(&http.Cookie{
|
||||||
|
Name: AuthKey,
|
||||||
|
Value: obf,
|
||||||
|
})
|
||||||
|
err = Verify(g, w, r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public not ok", func(t *testing.T) {
|
||||||
|
g, w, r, _, _ := fresh()
|
||||||
|
if err := g.Insert(context.TODO(), "public", entity.One{ID: UserKey}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err := Verify(g, w, r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public ok", func(t *testing.T) {
|
||||||
|
g, w, r, token, _ := fresh()
|
||||||
|
if err := g.Insert(context.TODO(), token.Namespace, entity.One{ID: UserKey}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
token.Token = "gibberish-but-public-ns-so-its-ok"
|
||||||
|
obf, _ := token.Obfuscate()
|
||||||
|
r.AddCookie(&http.Cookie{
|
||||||
|
Name: AuthKey,
|
||||||
|
Value: obf,
|
||||||
|
})
|
||||||
|
err := Verify(g, w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"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]
|
|
||||||
|
|
||||||
rest, clean := testREST(t)
|
|
||||||
defer clean()
|
|
||||||
|
|
||||||
handler := rest.router
|
|
||||||
g := rest.g
|
|
||||||
|
|
||||||
os.Setenv("AUTH", "true")
|
|
||||||
defer os.Setenv("AUTH", "false")
|
|
||||||
|
|
||||||
if err := g.Insert(context.TODO(), "col."+AuthKey, entity.One{ID: UserKey, 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{ID: uuid.New().String(), 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.ID))
|
|
||||||
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: who", 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: none provided: files", func(t *testing.T) {
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/__files__/col/myfile", 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{ID: uuid.New().String(), 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())
|
|
||||||
}
|
|
||||||
|
|
||||||
rawtoken := getCookie(NewAuthKey, w.Header())
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
r = httptest.NewRequest(http.MethodTrace, "/__files__/col/myfile", nil)
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
r.Header.Set("Cookie", fmt.Sprintf("%s=%s", AuthKey, token))
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
if w.Code != http.StatusNotFound {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCookie(key string, header http.Header) string {
|
|
||||||
cookies, _ := header["Set-Cookie"]
|
|
||||||
if len(cookies) == 0 {
|
|
||||||
cookies, _ = header["Cookie"]
|
|
||||||
}
|
|
||||||
for i := range cookies {
|
|
||||||
value := strings.Split(cookies[i], ";")[0]
|
|
||||||
k := value[:strings.Index(value, "=")]
|
|
||||||
v := value[strings.Index(value, "=")+1:]
|
|
||||||
if k == key {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,3 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
const (
|
var GitCommit string
|
||||||
AuthKey = "DnDex-Auth"
|
|
||||||
UserKey = "DnDex-User"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
NewAuthKey = "New-" + AuthKey
|
|
||||||
GitCommit string
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,13 @@ func (rest *REST) scope(r *http.Request) RESTScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rest *REST) files(w http.ResponseWriter, _ *http.Request) {
|
func (rest *REST) files(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "not impl", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rest *REST) users(w http.ResponseWriter, _ *http.Request) {
|
func (rest *REST) users(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "not impl", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rest *REST) entities(w http.ResponseWriter, _ *http.Request) {
|
func (rest *REST) entities(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "not impl", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -209,9 +209,9 @@ func randomOne() entity.One {
|
||||||
Title: "titl-" + uuid.New().String()[:5],
|
Title: "titl-" + uuid.New().String()[:5],
|
||||||
Text: "text-" + uuid.New().String()[:5],
|
Text: "text-" + uuid.New().String()[:5],
|
||||||
Modified: time.Now().UnixNano(),
|
Modified: time.Now().UnixNano(),
|
||||||
Connections: map[string]entity.One{},
|
Connections: map[string]entity.Connection{},
|
||||||
Attachments: map[string]string{
|
Attachments: map[string]entity.Attachment{
|
||||||
uuid.New().String()[:5]: uuid.New().String()[:5],
|
uuid.New().String()[:5]: entity.Attachment{Location: uuid.New().String()[:5]},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,13 +71,13 @@ func (g Graph) gatherOnes(ctx context.Context, ch <-chan bson.Raw) ([]entity.One
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g Graph) Insert(ctx context.Context, namespace string, one entity.One) error {
|
func (g Graph) Insert(ctx context.Context, namespace string, one entity.One) error {
|
||||||
if one.Name == "" || one.ID == "" {
|
if one.ID == "" {
|
||||||
return errors.New("cannot create document without both name and id")
|
return errors.New("cannot create document without id")
|
||||||
}
|
}
|
||||||
if ones, err := g.ListCaseInsensitive(ctx, namespace, one.Name); err != nil {
|
if ones, err := g.ListCaseInsensitive(ctx, namespace, one.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if len(ones) > 0 {
|
} else if len(ones) > 0 {
|
||||||
return fmt.Errorf("collision on primary key when case insensitive: cannot create %q because %+v exists", one.Name, ones)
|
return fmt.Errorf("collision on primary key when case insensitive: cannot create %q because %+v exists", one.ID, ones)
|
||||||
}
|
}
|
||||||
return g.driver.Insert(ctx, namespace, one)
|
return g.driver.Insert(ctx, namespace, one)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue