Roughing in JWT-based authentication.
Todo: Actually utilize this stuff somewhere.
This commit is contained in:
parent
8dc07e3cc8
commit
be9e6481d0
8 changed files with 181 additions and 9 deletions
|
@ -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
|
||||
|
||||
|
|
146
api/auth.go
Normal file
146
api/auth.go
Normal file
|
@ -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"})
|
||||
}
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
2
keys/.gitignore
vendored
Normal file
2
keys/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -52,4 +52,7 @@ const (
|
|||
Measurements = "measurements:list"
|
||||
UpdateMeasurement = "measurements:update"
|
||||
DeleteMeasurement = "measurements:delete"
|
||||
|
||||
GetToken = "token:get"
|
||||
Restricted = "restricted:get"
|
||||
)
|
||||
|
|
Reference in a new issue