Skip to content
Snippets Groups Projects
Commit a9ae96b5 authored by meskio's avatar meskio :tent:
Browse files

Add authentication in the API side

parent b29d9eff
No related branches found
No related tags found
No related merge requests found
......@@ -7,7 +7,8 @@ import (
)
type api struct {
db *gorm.DB
db *gorm.DB
signKey []byte
}
func initDB(dbPath string) (*gorm.DB, error) {
......@@ -20,17 +21,18 @@ func initDB(dbPath string) (*gorm.DB, error) {
return db, err
}
func Init(dbPath string, r *mux.Router) error {
func Init(dbPath string, signKey string, r *mux.Router) error {
db, err := initDB(dbPath)
if err != nil {
return err
}
a := api{db}
r.HandleFunc("/member", a.ListMembers).Methods("GET")
r.HandleFunc("/member", a.AddMember).Methods("POST")
r.HandleFunc("/member/{num:[0-9]+}", a.GetMember).Methods("GET")
r.HandleFunc("/member/{num:[0-9]+}", a.UpdateMember).Methods("PUT")
r.HandleFunc("/member/{num:[0-9]+}", a.DeleteMember).Methods("DELETE")
a := api{db, []byte(signKey)}
r.HandleFunc("/signin", a.SignIn).Methods("POST")
r.HandleFunc("/member", a.auth(a.ListMembers)).Methods("GET")
r.HandleFunc("/member", a.auth(a.AddMember)).Methods("POST")
r.HandleFunc("/member/{num:[0-9]+}", a.auth(a.GetMember)).Methods("GET")
r.HandleFunc("/member/{num:[0-9]+}", a.auth(a.UpdateMember)).Methods("PUT")
r.HandleFunc("/member/{num:[0-9]+}", a.auth(a.DeleteMember)).Methods("DELETE")
return nil
}
......@@ -9,10 +9,16 @@ import (
"os"
"path"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
)
const (
signKey = "secret"
)
func TestInit(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
......@@ -29,6 +35,7 @@ type testAPI struct {
client *http.Client
server *httptest.Server
testPath string
token string
}
func newTestAPI(t *testing.T) *testAPI {
......@@ -39,13 +46,23 @@ func newTestAPI(t *testing.T) *testAPI {
dbPath := path.Join(testPath, "test.db")
r := mux.NewRouter()
err = Init(dbPath, r)
err = Init(dbPath, signKey, r)
if err != nil {
t.Fatal("Init error:", err)
}
server := httptest.NewServer(r)
return &testAPI{t, server.URL, &http.Client{}, server, testPath}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"num": 0,
"role": "admin",
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
tokenString, err := token.SignedString([]byte(signKey))
if err != nil {
t.Fatal("Can't generate token:", err)
}
return &testAPI{t, server.URL, &http.Client{}, server, testPath, tokenString}
}
func (ta *testAPI) do(method string, url string, body interface{}, respBody interface{}) *http.Response {
......@@ -62,6 +79,7 @@ func (ta *testAPI) do(method string, url string, body interface{}, respBody inte
if err != nil {
ta.t.Fatal("Can't build request", method, url, err)
}
req.Header.Add("x-authentication", ta.token)
resp, err := ta.client.Do(req)
if err != nil {
ta.t.Fatal("HTTP query failed", method, url, err)
......
package api
import (
"crypto/rand"
"crypto/subtle"
"encoding/json"
"log"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"golang.org/x/crypto/argon2"
)
type creds struct {
Name string `json:"name"`
Password string `json:"password"`
}
func (a *api) SignIn(w http.ResponseWriter, req *http.Request) {
var c creds
err := json.NewDecoder(req.Body).Decode(&c)
if err != nil {
log.Printf("Can't decode auth credentials: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var member Member
err = a.db.Where("name = ?", c.Name).First(&member).Error
if err != nil {
log.Printf("Can't locate user %s: %v", c.Name, err)
w.WriteHeader(http.StatusBadRequest)
return
}
hash := hashPass(c.Password, member.Salt)
if subtle.ConstantTimeCompare(hash, member.PassHash) == 0 {
log.Printf("Invalid pass for %s", c.Name)
w.WriteHeader(http.StatusBadRequest)
return
}
log.Printf("Logged in as %s", c.Name)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
token, err := a.newToken(member.Num, member.Role)
if err != nil {
log.Printf("Can't create a token: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(map[string]interface{}{
"token": token,
"member": member})
if err != nil {
log.Printf("Can't encode member: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func (a *api) auth(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
token := req.Header.Get("x-authentication")
if !a.validToken(token) {
w.WriteHeader(http.StatusUnauthorized)
return
}
fn(w, req)
}
}
func (a *api) newToken(num int, role string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"num": num,
"role": role,
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
return token.SignedString(a.signKey)
}
func (a *api) validToken(token string) bool {
t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return a.signKey, nil
})
if err != nil {
return false
}
if !t.Valid {
return false
}
claims, ok := t.Claims.(jwt.MapClaims)
if !ok {
return false
}
exp, ok := claims["exp"].(float64)
if !ok {
return false
}
// TODO: num, role
return time.Unix(int64(exp), 0).After(time.Now())
}
func newHashPass(password string) (hash []byte, salt []byte, err error) {
salt = make([]byte, 32)
_, err = rand.Read(salt)
if err != nil {
return
}
hash = hashPass(password, salt)
return
}
func hashPass(password string, salt []byte) []byte {
const (
time = 1
memory = 64 * 1024
threads = 2
keyLen = 32
)
return argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen)
}
package api
import (
"net/http"
"testing"
)
func TestSignIn(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
var member struct {
Member
Password string `json:"password"`
}
member.Num = 10
member.Name = "foo"
member.Password = "password"
resp := tapi.do("POST", "/member", member, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create member:", resp.Status)
}
tapi.token = ""
resp = tapi.do("GET", "/member", nil, nil)
if resp.StatusCode != http.StatusUnauthorized {
t.Error("Got members without auth")
}
var respMember struct {
Token string `json:"token"`
Member Member `json:"member"`
}
jsonAuth := creds{
Name: member.Name,
Password: member.Password,
}
resp = tapi.do("POST", "/signin", jsonAuth, &respMember)
if resp.StatusCode != http.StatusOK {
t.Fatal("Can't sign in:", resp.Status)
}
if respMember.Member.Name != member.Name {
t.Fatal("Unexpected member:", respMember)
}
tapi.token = respMember.Token
resp = tapi.do("GET", "/member", nil, nil)
if resp.StatusCode != http.StatusOK {
t.Fatal("Can't get members:", resp.Status)
}
}
......@@ -11,21 +11,39 @@ import (
type Member struct {
gorm.Model `json:"-"`
Num int `json:"num"`
Name string `json:"name"`
Num int `json:"num",gorm:"unique;index"`
Name string `json:"name",gorm:"unique;index"`
Email string `json:"email"`
Balance int `json:"balance"`
Role string `json:"role"`
PassHash []byte `json:"-"`
Salt []byte `json:"-"`
}
func (a *api) AddMember(w http.ResponseWriter, req *http.Request) {
var member Member
err := json.NewDecoder(req.Body).Decode(&member)
var memberReq struct {
Member
Password string `json:"password"`
}
err := json.NewDecoder(req.Body).Decode(&memberReq)
if err != nil {
log.Printf("Can't create member: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
member := Member{
Num: memberReq.Num,
Name: memberReq.Name,
Email: memberReq.Email,
Balance: memberReq.Balance,
Role: memberReq.Role,
}
member.PassHash, member.Salt, err = newHashPass(memberReq.Password)
if err != nil {
log.Printf("Can't hash new member: %v\n%v", err, member)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = a.db.Create(&member).Error
if err != nil {
log.Printf("Can't create member: %v\n%v", err, member)
......
......@@ -3,8 +3,10 @@ module 0xacab.org/meskio/cicer
go 1.14
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/olivere/env v1.1.0
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
gorm.io/driver/sqlite v1.1.2
gorm.io/gorm v1.20.1
)
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
......@@ -10,11 +13,13 @@ github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjld
github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/olivere/env v1.1.0 h1:owp/uwMwhru5668JjMDp8UTG3JGT27GTCk4ufYQfaTw=
github.com/olivere/env v1.1.0/go.mod h1:zaoXy53SjZfxqZBGiGrZCkuVLYPdwrc+vArPuUVhJdQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gorm.io/driver/sqlite v1.1.2 h1:6LsQVSO93WU4Xv2NTwIk2jE3bbKBLgMGmertBleuSTE=
......
......@@ -12,14 +12,15 @@ import (
func main() {
var (
dbPath = flag.String("db-path", env.String("./test.db", "DB_PATH"), "Path where the sqlite will be located")
addr = flag.String("addr", env.String(":8080", "HTTP_ADDR", "ADDR"), "Address where the http server will bind")
dbPath = flag.String("db-path", env.String("./test.db", "DB_PATH"), "Path where the sqlite will be located")
addr = flag.String("addr", env.String(":8080", "HTTP_ADDR", "ADDR"), "Address where the http server will bind")
signKey = flag.String("signkey", env.String("", "SIGNKEY"), "Sign key for authentication tokens. DO NOT LEAVE UNSET!!!")
)
flag.Parse()
r := mux.NewRouter()
apiRouter := r.PathPrefix("/api/").Subrouter()
err := api.Init(*dbPath, apiRouter)
err := api.Init(*dbPath, *signKey, apiRouter)
if err != nil {
log.Panicln("Can't open the database:", err)
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment