Add auth package

This commit is contained in:
Nicolas Duhamel 2016-02-20 19:37:48 +01:00
parent 34c965d8ee
commit bb9a5789d7
2 changed files with 259 additions and 0 deletions

98
auth/auth.go Normal file
View File

@ -0,0 +1,98 @@
package auth
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
"golang.org/x/crypto/bcrypt"
)
var (
// ErrInvalidPassword returned when password and hash don't match
ErrInvalidPassword = fmt.Errorf("Invalid password")
// ErrCorrupted returned when session have been corrupted
ErrCorrupted = fmt.Errorf("Corrupted session")
)
// Authorizer handle sesssion
type Authorizer struct {
cookiejar *sessions.CookieStore
cookieName string
peeper string
cost int
}
// New Authorizer peeper is like a salt but not stored in database,
// cost is the bcrypt cost for hashing the password
func New(peeper, cookieName, cookieKey string, cost int) *Authorizer {
return &Authorizer{
cookiejar: sessions.NewCookieStore([]byte(cookieKey)),
cookieName: cookieName,
peeper: peeper,
cost: cost,
}
}
// GenHash generates a new hash from a password
func (a *Authorizer) GenHash(password string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(password+a.peeper), a.cost)
if err != nil {
return "", err
}
return string(b), nil
}
// Login cheks password and updates cookie info
func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, hash, password string) error {
cookie, err := a.cookiejar.Get(req, a.cookieName)
if err != nil {
return err
}
err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+a.peeper))
if err != nil {
return ErrInvalidPassword
}
cookie.Values["username"] = username
err = cookie.Save(req, rw)
if err != nil {
return err
}
return nil
}
// Logout remove cookie info
func (a *Authorizer) Logout(rw http.ResponseWriter, req *http.Request) error {
cookie, err := a.cookiejar.Get(req, a.cookieName)
if err != nil {
return err
}
cookie.Values["username"] = ""
cookie.Options.MaxAge = -1 // kill the cookie
err = cookie.Save(req, rw)
if err != nil {
return err
}
return nil
}
// CurrentUser returns the logged in username from session
func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (string, error) {
cookie, err := a.cookiejar.Get(req, a.cookieName)
if err != nil {
return "", err
}
username := cookie.Values["username"]
if !cookie.IsNew && username != nil {
str, ok := username.(string)
if !ok {
return "", ErrCorrupted
}
return str, nil
}
return "", nil
}

161
auth/auth_test.go Normal file
View File

@ -0,0 +1,161 @@
package auth
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/kr/pretty"
)
const (
peeper = "polp"
key = "plop"
cookie = "auth"
cost = 10
username = "plop"
password = "ploppwd"
hash = "$2a$10$eVye8xbs6nj4TWnlTmifRuBsAU3F2hkxEcFz9WXdYjUuE6uKLVuzK"
)
func login(w http.ResponseWriter, r *http.Request) {
a := New(peeper, cookie, key, cost)
err := a.Login(w, r, username, hash, password)
if err != nil {
fmt.Fprintf(w, "%s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func logout(w http.ResponseWriter, r *http.Request) {
a := New(peeper, cookie, key, cost)
err := a.Logout(w, r)
if err != nil {
fmt.Fprintf(w, "%s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func check(w http.ResponseWriter, r *http.Request) {
a := New(peeper, cookie, key, cost)
u, err := a.CurrentUser(w, r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "%s", err)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s", u)
}
func handlers() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/login", login).Methods("GET")
r.HandleFunc("/logout", logout).Methods("GET")
r.HandleFunc("/check", check).Methods("GET")
return r
}
func TestAuth(t *testing.T) {
ts := httptest.NewServer(handlers())
defer ts.Close()
cookieJar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: cookieJar,
}
// Check no user logged in =
res, err := client.Get(ts.URL + "/check")
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Fatal(body)
}
if string(body) != "" {
t.Fatalf("No user logged in expected but found: %s", body)
}
// Login
res, err = client.Get(ts.URL + "/login")
if err != nil {
t.Fatal(err)
}
body, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Fatal(string(body))
}
// Checks we are logged in
res, err = client.Get(ts.URL + "/check")
if err != nil {
t.Fatal(err)
}
body, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
pretty.Println(res.StatusCode)
t.Fatal(body)
}
if string(body) != username {
t.Fatalf("We expect be logged in as %s but we got: %s", username, body)
}
// Logout
res, err = client.Get(ts.URL + "/logout")
if err != nil {
t.Fatal(err)
}
body, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Fatal(string(body))
}
// Check no username logged in anymore
res, err = client.Get(ts.URL + "/check")
if err != nil {
t.Fatal(err)
}
body, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Fatal(body)
}
if string(body) != "" {
t.Fatalf("No user logged in expected but found: %s", body)
}
}