users: add authentication/key support to model

This commit is contained in:
Chris Sexton 2020-03-15 08:29:17 -04:00
parent b3eda2c3cf
commit ce02dca041
5 changed files with 164 additions and 10 deletions

View File

@ -1,9 +1,17 @@
package auth package auth
import ( import (
"code.chrissexton.org/cws/cabinet/db" "crypto/rand"
"encoding/hex"
"errors"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"code.chrissexton.org/cws/cabinet/config"
"code.chrissexton.org/cws/cabinet/db"
) )
type User struct { type User struct {
@ -12,21 +20,44 @@ type User struct {
ID int64 ID int64
Name string Name string
Hash []byte Hash []byte
AuthKey string `db:"auth_key"`
Invalidate time.Time
} }
func PrepareTable(tx *sqlx.Tx) error { func PrepareTable(tx *sqlx.Tx) error {
q := `create table if not exists users ( q := `create table if not exists users (
id integer primary key, id integer primary key,
name text unique, name text unique not null,
hash text hash text not null,
auth_key text,
invalidate datetime
)` )`
_, err := tx.Exec(q) _, err := tx.Exec(q)
return err return err
} }
func makeKey() (string, error) {
keySize := config.GetInt("key.size", 10)
buf := make([]byte, keySize)
_, err := rand.Read(buf)
if err != nil {
return "", err
}
key := hex.EncodeToString(buf)
log.Debug().Msgf("Encoded secret key %s as %s", string(buf), key)
return key, nil
}
func New(db *db.Database, name, password string) (*User, error) { func New(db *db.Database, name, password string) (*User, error) {
q := `insert into users (null, ?, ?)` q := `insert into users values (null, ?, ?, ?, ?)`
res, err := db.Exec(q, name, password)
key, err := makeKey()
if err != nil {
return nil, err
}
invalidate := time.Now().Add(time.Duration(config.GetInt("invalidate.hours", 7*24)) * time.Hour)
res, err := db.Exec(q, name, password, key, invalidate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -37,6 +68,8 @@ func New(db *db.Database, name, password string) (*User, error) {
u := &User{ u := &User{
ID: id, ID: id,
Name: name, Name: name,
AuthKey: key,
Invalidate: invalidate,
} }
u.Set(password) u.Set(password)
return u, nil return u, nil
@ -67,3 +100,19 @@ func (u *User) Validate(password string) bool {
} }
return true return true
} }
func GetByKey(db *db.Database, key string) (*User, error) {
q := `select * from users where auth_key = ?`
u := &User{}
invalid := errors.New("invalid key")
if err := db.Get(u, q, key); err != nil {
if err.Error() == "sql: no rows in result set" {
return nil, invalid
}
return nil, err
}
if u.Invalidate.Before(time.Now()) {
return nil, invalid
}
return u, nil
}

78
auth/auth_test.go Normal file
View File

@ -0,0 +1,78 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
"code.chrissexton.org/cws/cabinet/db"
)
func TestMakeKey(t *testing.T) {
k, err := makeKey()
assert.Nil(t, err)
assert.NotEmpty(t, k)
}
func TestGetByKey(t *testing.T) {
d, err := db.New(":memory:")
assert.Nil(t, err)
tx, err := d.Beginx()
assert.Nil(t, err)
err = PrepareTable(tx)
assert.Nil(t, err)
err = tx.Commit()
assert.Nil(t, err)
u, err := New(d, "test", "abc")
assert.Nil(t, err)
u2, err := GetByKey(d, u.AuthKey)
assert.Nil(t, err)
assert.Equal(t, u.ID, u2.ID)
}
func TestGetByKeyFailure(t *testing.T) {
d, err := db.New(":memory:")
assert.Nil(t, err)
tx, err := d.Beginx()
assert.Nil(t, err)
err = PrepareTable(tx)
assert.Nil(t, err)
err = tx.Commit()
assert.Nil(t, err)
_, err = New(d, "test", "abc")
assert.Nil(t, err)
u2, err := GetByKey(d, "foobar")
assert.Error(t, err)
assert.Nil(t, u2)
}
func TestGet(t *testing.T) {
d, err := db.New(":memory:")
assert.Nil(t, err)
tx, err := d.Beginx()
assert.Nil(t, err)
err = PrepareTable(tx)
assert.Nil(t, err)
err = tx.Commit()
assert.Nil(t, err)
u, err := New(d, "test", "abc")
assert.Nil(t, err)
u2, err := Get(d, "test")
assert.Nil(t, err)
assert.Equal(t, u.ID, u2.ID)
}
func TestUser_Validate(t *testing.T) {
d, err := db.New(":memory:")
assert.Nil(t, err)
tx, err := d.Beginx()
assert.Nil(t, err)
err = PrepareTable(tx)
assert.Nil(t, err)
err = tx.Commit()
assert.Nil(t, err)
u, err := New(d, "test", "abc")
assert.Nil(t, err)
actual := u.Validate("abc")
assert.True(t, actual)
}

24
config/config.go Normal file
View File

@ -0,0 +1,24 @@
package config
import (
"os"
"strconv"
"strings"
)
func GetInt(key string, fallback int) int {
v := Get(key, strconv.Itoa(fallback))
if out, err := strconv.Atoi(v); err == nil {
return out
}
return fallback
}
func Get(key, fallback string) string {
key = strings.ToUpper(key)
key = strings.ReplaceAll(key, ".", "_")
if v, found := os.LookupEnv(key); found {
return v
}
return fallback
}

View File

@ -2,6 +2,8 @@ package db
import "github.com/jmoiron/sqlx" import "github.com/jmoiron/sqlx"
import _ "github.com/mattn/go-sqlite3"
type Database struct { type Database struct {
*sqlx.DB *sqlx.DB
} }

1
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/rs/zerolog v1.16.0 github.com/rs/zerolog v1.16.0
github.com/speps/go-hashids v2.0.0+incompatible github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/graceful v1.2.15 github.com/stretchr/graceful v1.2.15
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6 golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
google.golang.org/appengine v1.6.5 // indirect google.golang.org/appengine v1.6.5 // indirect