Skip to content

Commit

Permalink
Merge pull request #32 from titouanfreville/issue-30-secureAPI
Browse files Browse the repository at this point in the history
Issue 30 secure api
  • Loading branch information
titouanfreville authored Mar 8, 2017
2 parents a7fc66e + cf9ea61 commit 22099ab
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 94 deletions.
110 changes: 89 additions & 21 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"crypto/rand"
"encoding/base32"
"flag"
"log"
"net/http"
"os"

jwt "github.com/dgrijalva/jwt-go"
"github.com/jinzhu/gorm"
Expand Down Expand Up @@ -66,12 +68,36 @@ func initAuth() {
tokenAuth = New("HS256", hmacSampleSecret, hmacSampleSecret)
}

// createToken create JWT auth token for current login user
func createToken(user models.User) (string, error) {
// createUserToken create JWT auth token for current login user
func createUserToken(user models.User, role models.Role) (string, error) {
claims := jwt.MapClaims{
"name": user.Username,
"email": user.Email,
"role": user.IDRole,
"name": user.Username,
"email": user.Email,
"role": role.RoleName,
"archive": role.CanArchive,
"invite": role.CanInvite,
"manage": role.CanManage,
"manageuser": role.CanManageUser,
"moderate": role.CanModerate,
"private": role.CanUsePrivate,
"type": "userauth",
}
unsignedToken := *jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := unsignedToken.SignedString(hmacSampleSecret)

if err != nil {
return "", err
}

return tokenString, nil
}

// createInviteToken create JWT auth token for current invitation
func createInviteToken(inviteMail string, organisationName string) (string, error) {
claims := jwt.MapClaims{
"email": inviteMail,
"organisation": organisationName,
"type": "invitation",
}
unsignedToken := *jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := unsignedToken.SignedString(hmacSampleSecret)
Expand Down Expand Up @@ -102,6 +128,7 @@ func initMiddleware(router *chi.Mux) {

// basicRoutes set basic routes for the API
func basicRoutes(router *chi.Mux) {
router.Use(tokenAuth.Verifier)
// swagger:route GET / Test hello
//
// Hello World
Expand Down Expand Up @@ -149,18 +176,56 @@ func basicRoutes(router *chi.Mux) {
// 422: wrongEntity
// 503: databaseError
router.Post("/login", loginMiddleware)
// swagger:route POST /user Users newPublicUser
//
// New user
//
// This will create an user for organisation if organisation is Public OR Email match parametetered emails
//
// Responses:
// 201: userObjectSuccess
// 422: wrongEntity
// 503: databaseError
// default: genericError
router.Post("/publicuser", newPublicUser)
router.Route("/publicuser", func(r chi.Router) {
// swagger:route POST /publicuser/new Users newPublicUser
//
// New user
//
// This will create an user for organisation if organisation is Public OR Email match parametetered emails
//
// Responses:
// 201: userObjectSuccess
// 422: wrongEntity
// 503: databaseError
// default: genericError
r.Post("/new", newPublicUser)
r.Route("/newfrominvite", func(r chi.Router) {
r.Use(tokenAuth.Verifier)
r.Use(allowUserCreationFromToken)
// swagger:route POST /publicuser/newfrominvite Users newPublicUser
//
// New user
//
// This will create an user for organisation if user was invited
//
// Responses:
// 201: userObjectSuccess
// 422: wrongEntity
// 503: databaseError
// default: genericError
r.Post("/", newUser)
})
})
}

func initDevGetter(router chi.Router) {
env := os.Getenv("POPCUBE_API_ENV")
if env == "prod" || env == "test" || env == "beta" || env == "alpha" || env == "production" {
return
}
log.Print("<><><><><><><> Using DEV routes <><><><><><><> \n")
router.Route("/devgetters", func(r chi.Router) {
r.Get("/avatar", getAllAvatar)
r.Get("/channel", getAllChannel)
r.Get("/emoji", getAllEmoji)
r.Get("/folder", getAllFolder)
r.Get("/member", getAllMember)
r.Get("/message", getAllMessage)
r.Get("/organisation", getAllOrganisation)
r.Get("/parameter", getAllParameter)
r.Get("/role", getAllRole)
r.Get("/user", getAllUser)
})
}

// loginMiddleware login funcion providing user && jwt auth token
Expand All @@ -183,15 +248,18 @@ func loginMiddleware(w http.ResponseWriter, r *http.Request) {
user, err := store.User().Login(data.Login, data.Password, db)
if err == nil {
var terr error
// role can't be empty if user exist => foreign key constraint
role := datastores.Store().Role().GetByID(user.IDRole, dbStore.db)
response.User = user
response.Token, terr = createToken(user)
response.Token, terr = createUserToken(user, role)
if terr == nil {
render.JSON(w, 200, response)
return
}
render.JSON(w, err.StatusCode, err)
return
render.JSON(w, 422, "Could not generate token")
}
render.JSON(w, err.StatusCode, err)
return
}
render.JSON(w, error503.StatusCode, error503)

Expand Down Expand Up @@ -245,7 +313,7 @@ func StartAPI(hostname string, port string, DbConnectionInfo *configs.DbConnecti
initParameterRoute(router)
initRoleRoute(router)
initUserRoute(router)

initDevGetter(router)
// Passing -routes to the program will generate docs for the above
// router definition. See the `routes.json` file in this folder for
// the output.
Expand Down
16 changes: 14 additions & 2 deletions api/api_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,21 @@ type generalOk struct {
// swagger:response loginOk
type loginOk struct {
// in:body
User models.User
User models.User `json:"user,omitempty"`
// in:body
Token string
Token string `json:"token,omitempty"`
}

// inviteOk when invite correctly proceed, return the invite token information and the JWT token.
//
// swagger:response loginOk
type inviteOk struct {
// in:body
Email string `json:"email,omitempty"`
// in:body
Organisation string `json:"organisation,omitempty"`
// in:body
Token string `json:"token,omitempty"`
}

// ---------------------------------------------------
Expand Down
65 changes: 61 additions & 4 deletions api/jwtauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

jwt "github.com/dgrijalva/jwt-go"
"github.com/titouanfreville/popcubeapi/datastores"
)

var (
Expand Down Expand Up @@ -202,10 +203,7 @@ func (ja *JwtAuth) IsExpired(t *jwt.Token) bool {
return false
}

// Authenticator is a default authentication middleware to enforce access following
// the Verifier middleware. The Authenticator sends a 401 Unauthorized response for
// all unverified tokens and passes the good ones through. It's just fine until you
// decide to write something similar and customize your client response.
// Authenticator validate that user has a valid user auth token before letting him access.
func Authenticator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand All @@ -223,6 +221,65 @@ func Authenticator(next http.Handler) http.Handler {
return
}

tokenType, ok := jwtToken.Claims.(jwt.MapClaims)["type"]

if !ok {
render.JSON(w, 401, "Token is not valid. Type is undifined")
return
}

if tokenType != "userauth" {
render.JSON(w, 401, "Token is not an user auth one")
return
}

// Token is authenticated, pass it through
next.ServeHTTP(w, r)
})
}

// allowUserCreationFromToken check the provided token is an invitation one
func allowUserCreationFromToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

if jwtErr, ok := ctx.Value(jwtErrorKey).(error); ok {
if jwtErr != nil {
render.JSON(w, 401, jwtErr)
return
}
}

jwtToken, ok := ctx.Value(jwtTokenKey).(*jwt.Token)
if !ok || jwtToken == nil || !jwtToken.Valid {
render.JSON(w, 401, "token is not valid or does not exist")
return
}

tokenType, ok := jwtToken.Claims.(jwt.MapClaims)["type"]

if !ok {
render.JSON(w, 401, "Token is not valid. Type is undifined")
return
}

if tokenType != "invitation" {
render.JSON(w, 401, "Token is not an invitation one")
return
}

tokenOrganisation, ok := jwtToken.Claims.(jwt.MapClaims)["organisation"].(string)

if !ok {
render.JSON(w, 401, "Token is not valid. Organisation is undifined")
return
}
apiOrganisation := datastores.Store().Organisation().Get(dbStore.db)

if tokenOrganisation != apiOrganisation.OrganisationName {
render.JSON(w, 401, "Token is not valid. Organisation does not match current organsisation")
return
}
// Token is authenticated, pass it through
next.ServeHTTP(w, r)
})
Expand Down
Loading

0 comments on commit 22099ab

Please sign in to comment.