new auth
This commit is contained in:
7
server/auth/const.go
Normal file
7
server/auth/const.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package auth
|
||||
|
||||
const (
|
||||
AuthKey = "DnDex-Auth"
|
||||
UserKey = "DnDex-User"
|
||||
NewAuthKey = "New-" + AuthKey
|
||||
)
|
||||
60
server/auth/encrypt.go
Normal file
60
server/auth/encrypt.go
Normal file
@@ -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
|
||||
}
|
||||
27
server/auth/encrypt_test.go
Normal file
27
server/auth/encrypt_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
server/auth/generate.go
Normal file
55
server/auth/generate.go
Normal file
@@ -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)
|
||||
}
|
||||
51
server/auth/generate_test.go
Normal file
51
server/auth/generate_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
31
server/auth/register.go
Normal file
31
server/auth/register.go
Normal file
@@ -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)
|
||||
}
|
||||
56
server/auth/register_test.go
Normal file
56
server/auth/register_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
51
server/auth/token.go
Normal file
51
server/auth/token.go
Normal file
@@ -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)
|
||||
}
|
||||
33
server/auth/token_test.go
Normal file
33
server/auth/token_test.go
Normal file
@@ -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)
|
||||
}
|
||||
70
server/auth/verify.go
Normal file
70
server/auth/verify.go
Normal file
@@ -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
|
||||
}
|
||||
147
server/auth/verify_test.go
Normal file
147
server/auth/verify_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user