diff --git a/.travis.yml b/.travis.yml index 0314f51..880f911 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,7 @@ addons: go: 1.3 before_script: - - psql -c 'create database bactdbtest;' -U postgres + - psql -c 'CREATE DATABASE bactdbtest;' -U postgres + - openssl genrsa -out keys/app.rsa 2048 + - openssl rsa -in keys/app.rsa -pubout > keys/app.rsa.pub + diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 0000000..4ce6f0e --- /dev/null +++ b/api/auth.go @@ -0,0 +1,146 @@ +package api + +import ( + "errors" + "io/ioutil" + "log" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" +) + +const ( + privKeyPath = "keys/app.rsa" // openssl genrsa -out app.rsa keysize + pubKeyPath = "keys/app.rsa.pub" // openssl rsa -in app.rsa -pubout > app.rsa.pub + tokenName = "AccessToken" +) + +var ( + verifyKey, signKey []byte + errWhileSigningToken = errors.New("error while signing token") + errPleaseLogIn = errors.New("please log in") + errWhileParsingCookie = errors.New("error while parsing cookie") + errTokenExpired = errors.New("token expired") + errGenericError = errors.New("generic error") +) + +func init() { + var err error + + signKey, err = ioutil.ReadFile(privKeyPath) + + if err != nil { + // Before exploding, check up one level... + signKey, err = ioutil.ReadFile("../" + privKeyPath) + if err != nil { + log.Fatalf("Error reading private key: ", err) + return + } + } + + verifyKey, err = ioutil.ReadFile(pubKeyPath) + if err != nil { + // Before exploding, check up one level... + verifyKey, err = ioutil.ReadFile("../" + pubKeyPath) + if err != nil { + log.Fatalf("Error reading public key: ", err) + return + } + } +} + +func serveToken(w http.ResponseWriter, r *http.Request) error { + t := jwt.New(jwt.GetSigningMethod("RS256")) + + // Set our claims + t.Claims["AccessToken"] = "level1" + t.Claims["CustomUserInfo"] = struct { + Name string + Kind string + }{"mrdillon", "human"} + + // Set the expire time + // See http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4 + t.Claims["exp"] = time.Now().Add(time.Minute * 1).Unix() + tokenString, err := t.SignedString(signKey) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return errWhileSigningToken + } + + http.SetCookie(w, &http.Cookie{ + Name: tokenName, + Value: tokenString, + Path: "/", + RawExpires: "0", + }) + + return writeJSON(w, Message{"success"}) +} + +type authHandler func(http.ResponseWriter, *http.Request) error + +// Only accessible with a valid token +func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Even though writeJSON sets the content type, we need to set it here because + // calls to WriteHeader write out the entire header. + w.Header().Set("content-type", "application/json; charset=utf-8") + tokenCookie, err := r.Cookie(tokenName) + switch { + case err == http.ErrNoCookie: + w.WriteHeader(http.StatusUnauthorized) + writeJSON(w, Error{errPleaseLogIn}) + return + case err != nil: + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, Error{errWhileParsingCookie}) + return + } + + if tokenCookie.Value == "" { + w.WriteHeader(http.StatusUnauthorized) + writeJSON(w, Error{errPleaseLogIn}) + return + } + + // Validate the token + token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (interface{}, error) { + return verifyKey, nil + }) + + // Branch out into the possible error from signing + switch err.(type) { + case nil: // No error + if !token.Valid { // But may still be invalid + w.WriteHeader(http.StatusUnauthorized) + writeJSON(w, Error{errPleaseLogIn}) + return + } + case *jwt.ValidationError: // Something was wrong during the validation + vErr := err.(*jwt.ValidationError) + switch vErr.Errors { + case jwt.ValidationErrorExpired: + w.WriteHeader(http.StatusUnauthorized) + writeJSON(w, Error{errTokenExpired}) + return + default: + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, Error{errGenericError}) + return + } + default: // Something else went wrong + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, Error{errGenericError}) + return + } + hErr := h(w, r) + if hErr != nil { + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, Error{hErr}) + } +} + +func restrictedHandler(w http.ResponseWriter, r *http.Request) error { + return writeJSON(w, Message{"great success"}) +} diff --git a/api/handler.go b/api/handler.go index 86e74be..bd80ca4 100644 --- a/api/handler.go +++ b/api/handler.go @@ -1,8 +1,6 @@ package api import ( - "fmt" - "log" "net/http" "github.com/gorilla/mux" @@ -71,6 +69,9 @@ func Handler() *mux.Router { m.Get(router.UpdateMeasurement).Handler(handler(serveUpdateMeasurement)) m.Get(router.DeleteMeasurement).Handler(handler(serveDeleteMeasurement)) + m.Get(router.GetToken).Handler(handler(serveToken)) + m.Get(router.Restricted).Handler(authHandler(restrictedHandler)) + return m } @@ -80,7 +81,6 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := h(w, r) if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "error: %s", err) - log.Println(err) + writeJSON(w, Error{err}) } } diff --git a/api/helpers.go b/api/helpers.go index 85ab78c..46c8b2f 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "log" "net/http" ) @@ -17,3 +18,20 @@ func writeJSON(w http.ResponseWriter, v interface{}) error { _, err = w.Write(data) return err } + +// Message is for returning simple message payloads to the user +type Message struct { + Message string `json:"message"` +} + +// Error is for returning simple error payloads to the user, as well as logging +type Error struct { + Error error +} + +func (e Error) MarshalJSON() ([]byte, error) { + log.Println(e.Error) + return json.Marshal(struct { + Error string `json:"error"` + }{e.Error.Error()}) +} diff --git a/datastore/db.go b/datastore/db.go index 71d613e..5d065b7 100644 --- a/datastore/db.go +++ b/datastore/db.go @@ -43,10 +43,6 @@ func Create(path string) { if err != nil { log.Fatal("Error initializing migrations: ", err) } - - pwd, err := os.Getwd() - log.Print("current path: ", pwd) - err = migrator.Migrate() if err != nil { log.Fatal("Error applying migrations: ", err) diff --git a/keys/.gitignore b/keys/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/keys/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/router/api.go b/router/api.go index 211baa1..cbf96cb 100644 --- a/router/api.go +++ b/router/api.go @@ -66,5 +66,9 @@ func API() *mux.Router { m.Path("/measurements/{Id:.+}").Methods("PUT").Name(UpdateMeasurement) m.Path("/measurements/{Id:.+}").Methods("DELETE").Name(DeleteMeasurement) + // Authentication + m.Path("/token/").Methods("GET").Name(GetToken) + m.Path("/restricted/").Methods("GET").Name(Restricted) + return m } diff --git a/router/routes.go b/router/routes.go index e2c9076..f93fc86 100644 --- a/router/routes.go +++ b/router/routes.go @@ -52,4 +52,7 @@ const ( Measurements = "measurements:list" UpdateMeasurement = "measurements:update" DeleteMeasurement = "measurements:delete" + + GetToken = "token:get" + Restricted = "restricted:get" )