diff --git a/controllers/token.go b/controllers/token.go new file mode 100644 index 0000000..4b15b08 --- /dev/null +++ b/controllers/token.go @@ -0,0 +1,113 @@ +package controllers + +import ( + "net/http" + + "github.com/uploadexpress/app/services/config" + + "github.com/gin-gonic/gin" + "github.com/uploadexpress/app/helpers" + "github.com/uploadexpress/app/helpers/params" + "github.com/uploadexpress/app/models" + "github.com/uploadexpress/app/store" +) + +// TokenController holds all controller functions related to the Token entity +type TokenController struct{} + +// NewTokenController instantiates of the controller +func NewTokenController() TokenController { + return TokenController{} +} + +// CreateToken to create a new Token +func (tc TokenController) CreateToken(c *gin.Context) { + token := &models.Token{} + + if err := c.BindJSON(token); err != nil { + c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_input", "Failed to bind the body data", err)) + return + } + + token.Ip = c.ClientIP() + if err := store.CreateToken(c, token); err != nil { + c.Error(err) + c.Abort() + return + } + + secret := config.GetString(c, "jwt_secret") + accessToken, err := helpers.GenerateApiToken(secret, store.Current(c).Id, token.Id) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, helpers.ErrorWithCode("token_generation_failed", "Could not generate the access token", err)) + return + } + + // Only sent once + token.Token = accessToken + + c.JSON(http.StatusCreated, token) +} + +// GetToken from id (in context) +func (tc TokenController) GetToken(c *gin.Context) { + token, err := store.FindTokenById(c, c.Param("id")) + + if err != nil { + c.AbortWithError(http.StatusNotFound, helpers.ErrorWithCode("Token_not_found", "The token does not exist", err)) + return + } + + c.JSON(http.StatusOK, token) +} + +// GetAllTokens to get all Tokens +func (tc TokenController) GetAllTokens(c *gin.Context) { + tokens, err := store.GetAllTokens(c) + if err != nil { + c.Error(err) + c.Abort() + return + } + + c.JSON(http.StatusOK, tokens) +} + +//UpdateToken updates the Token entity +func (tc TokenController) UpdateToken(c *gin.Context) { + newToken := models.Token{} + + err := c.BindJSON(&newToken) + if err != nil { + c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_input", "Failed to bind the body data", err)) + return + } + + _, err = store.FindTokenById(c, c.Param("id")) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, helpers.ErrorWithCode("Token_not_found", "Failed to find Token id", err)) + return + } + + err = store.UpdateToken(c, c.Param("id"), params.M{"$set": newToken}) + if err != nil { + c.Error(err) + c.Abort() + return + } + + c.JSON(http.StatusOK, nil) +} + +// DeleteToken to delete an existing Token +func (Tc TokenController) DeleteToken(c *gin.Context) { + err := store.DeleteToken(c, c.Param("id")) + + if err != nil { + c.Error(err) + c.Abort() + return + } + + c.JSON(http.StatusOK, nil) +} diff --git a/helpers/jwt.go b/helpers/jwt.go index 45c9f32..a642b90 100644 --- a/helpers/jwt.go +++ b/helpers/jwt.go @@ -24,11 +24,27 @@ func GenerateAccessToken(secret string, subject string) (*string, error) { return &accessString, nil } -func ValidateJwtToken(token string, secret string, audience string) (jwt.MapClaims, error) { +func GenerateApiToken(secret string, subject, creator string) (*string, error) { + access := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iss": creator, + "sub": subject, + "aud": "api", + "iat": time.Now().Unix(), + }) + + accessString, err := access.SignedString([]byte(secret)) + if err != nil { + return nil, err + } + + return &accessString, nil +} + +func GetParsedToken(token, secret string) (*jwt.Token, error) { rawToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { // validate the alg if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(secret), nil @@ -37,6 +53,29 @@ func ValidateJwtToken(token string, secret string, audience string) (jwt.MapClai return nil, err } + return rawToken, nil +} + +func GetTokenAudience(token, secret string) (string, error) { + rawToken, err := GetParsedToken(token, secret) + if err != nil { + return "", err + } + + claims, ok := rawToken.Claims.(jwt.MapClaims) + if !ok { + return "", errors.New("could not parse the claims") + } + + return claims["aud"].(string), nil +} + +func ValidateJwtToken(token string, secret string, audience string) (jwt.MapClaims, error) { + rawToken, err := GetParsedToken(token, secret) + if err != nil { + return nil, err + } + //validate signature if !rawToken.Valid { return nil, errors.New("token in invalid (wrong signature)") @@ -47,17 +86,19 @@ func ValidateJwtToken(token string, secret string, audience string) (jwt.MapClai return nil, errors.New("could not parse the claims") } - //validate exp - tokenExp := claims["exp"].(float64) - if tokenExp < float64(time.Now().Unix()) { - return nil, errors.New("token in invalid (expired)") - } - //validate aud tokenAud := claims["aud"].(string) if tokenAud != audience { return nil, errors.New("token in invalid (wrong audience)") } + //validate exp + if tokenAud != "api" { // api tokens don't expire + tokenExp := claims["exp"].(float64) + if tokenExp < float64(time.Now().Unix()) { + return nil, errors.New("token in invalid (expired)") + } + } + return claims, nil } diff --git a/middlewares/auth.go b/middlewares/auth.go index 81c0c0d..6eebc08 100644 --- a/middlewares/auth.go +++ b/middlewares/auth.go @@ -21,16 +21,57 @@ func AuthMiddleware() gin.HandlerFunc { return } + token := authHeaderParts[1] secret := config.GetString(c, "jwt_secret") - claims, err := helpers.ValidateJwtToken(authHeaderParts[1], secret, "access") + tokenAudience, err := helpers.GetTokenAudience(token, secret) if err != nil { c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_token", "the given token is invalid", err)) return } - user, _ := store.FindUserById(c, claims["sub"].(string)) - c.Set(store.CurrentKey, user) + if tokenAudience == "api" { + if validateApiToken(token, secret, c) { + c.Next() + } + return + } + + if validateAccessToken(token, secret, c) { + c.Next() + } + } +} + +func validateAccessToken(token, secret string, c *gin.Context) bool { + claims, err := helpers.ValidateJwtToken(token, secret, "access") + if err != nil { + c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_token", "the given token is invalid", err)) + return false + } - c.Next() + user, err := store.FindUserById(c, claims["sub"].(string)) + if err != nil { + c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("unknown_user", "the given user is invalid", err)) + return false } + + c.Set(store.CurrentKey, user) + + return true +} + +func validateApiToken(token, secret string, c *gin.Context) bool { + claims, err := helpers.ValidateJwtToken(token, secret, "api") + if err != nil { + c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("invalid_token", "the given token is invalid", err)) + return false + } + + _, err = store.FindTokenById(c, claims["iss"].(string)) + if err != nil { + c.AbortWithError(http.StatusBadRequest, helpers.ErrorWithCode("unknown_token", "the token is invalid or expired", err)) + return false + } + + return true } diff --git a/models/token.go b/models/token.go new file mode 100644 index 0000000..7cc59fb --- /dev/null +++ b/models/token.go @@ -0,0 +1,23 @@ +package models + +import ( + "time" + + "gopkg.in/mgo.v2/bson" +) + +type Token struct { + Id string `json:"id" bson:"_id"` + Name string `json:"name" bson:"name"` + Ip string `json:"ip" bson:"ip"` + CreationDate int64 `json:"creation_date" bson:"creation_date"` + Token *string `json:"token,omitempty" bson:"-"` +} + +func (token *Token) BeforeCreate() error { + token.Id = bson.NewObjectId().Hex() + token.CreationDate = time.Now().Unix() + return nil +} + +const TokensCollection = "tokens" diff --git a/server/router.go b/server/router.go index 2da7fbc..ffdd4b4 100644 --- a/server/router.go +++ b/server/router.go @@ -61,11 +61,11 @@ func (a *API) SetupRouter() { uploader.POST("/", uploaderController.Create) uploader.PUT("/:upload_id/complete", uploaderController.CompleteUpload) uploader.GET("/:upload_id/file/:file_id/upload_url", uploaderController.CreatePreSignedRequest) + uploader.POST("/:upload_id/mail", uploaderController.SendMail) uploader.Use(authMiddleware) uploader.GET("/", uploaderController.Index) uploader.DELETE("/:upload_id/", uploaderController.DeleteUpload) uploader.POST("/:upload_id/background", uploaderController.AttachBackground) - uploader.POST("/:upload_id/mail", uploaderController.SendMail) } downloader := v1.Group("/downloader") @@ -81,7 +81,7 @@ func (a *API) SetupRouter() { { settingsController := controllers.NewSettingsController() settings.GET("/", settingsController.Index) - uploader.Use(authMiddleware) + settings.Use(authMiddleware) settings.PUT("/", settingsController.Edit) settings.POST("/logo/", settingsController.CreateLogo) settings.POST("/background/", settingsController.CreateBackground) @@ -89,6 +89,16 @@ func (a *API) SetupRouter() { settings.DELETE("/logo/", settingsController.DeleteLogo) } + + tokens := v1.Group("/tokens") + { + tokensController := controllers.NewTokenController() + tokens.Use(authMiddleware) + tokens.POST("/", tokensController.CreateToken) + tokens.GET("/", tokensController.GetAllTokens) + tokens.PUT("/:id", tokensController.UpdateToken) + tokens.DELETE("/:id", tokensController.DeleteToken) + } } router.PUT("/upload/:upload_name", uploaderController.CreateDirectUpload) diff --git a/store/mongodb/token.go b/store/mongodb/token.go new file mode 100644 index 0000000..4a62f49 --- /dev/null +++ b/store/mongodb/token.go @@ -0,0 +1,87 @@ +package mongodb + +import ( + "net/http" + + "github.com/globalsign/mgo/bson" + "github.com/uploadexpress/app/helpers" + "github.com/uploadexpress/app/helpers/params" + "github.com/uploadexpress/app/models" +) + +// CreateToken checks if token already exists, and if not, creates it +func (db *mongo) CreateToken(token *models.Token) error { + session := db.Session.Copy() + defer session.Close() + tokens := db.C(models.TokensCollection).With(session) + + token.Id = bson.NewObjectId().Hex() + err := token.BeforeCreate() + if err != nil { + return err + } + + err = tokens.Insert(token) + if err != nil { + return helpers.NewError(http.StatusInternalServerError, "token_creation_failed", "Failed to insert the token in the database", err) + } + + return nil +} + +// FindTokenById allows to retrieve a token by its id +func (db *mongo) FindTokenById(id string) (*models.Token, error) { + session := db.Session.Copy() + defer session.Close() + tokens := db.C(models.TokensCollection).With(session) + + token := &models.Token{} + err := tokens.FindId(id).One(token) + if err != nil { + return nil, helpers.NewError(http.StatusNotFound, "token_not_found", "Token not found", err) + } + + return token, err +} + +// GetAllToken allows to get all tokens +func (db *mongo) GetAllTokens() ([]*models.Token, error) { + session := db.Session.Copy() + defer session.Close() + + tokens := db.C(models.TokensCollection).With(session) + + list := []*models.Token{} + if err := tokens.Find(params.M{}).All(&list); err != nil { + return nil, helpers.NewError(http.StatusNotFound, "tokens_not_found", "Token not found", err) + } + + return list, nil +} + +// UpdateToken allows to update one or more token characteristics +func (db *mongo) UpdateToken(tokenId string, params params.M) error { + session := db.Session.Copy() + defer session.Close() + tokens := db.C(models.TokensCollection).With(session) + + if err := tokens.UpdateId(tokenId, params); err != nil { + return helpers.NewError(http.StatusInternalServerError, "token_update_failed", "Failed to update the token", err) + } + + return nil +} + +// DeleteToken allows to delete a token by its id +func (db *mongo) DeleteToken(tokenId string) error { + session := db.Session.Copy() + defer session.Close() + tokens := db.C(models.TokensCollection).With(session) + + err := tokens.Remove(bson.M{"_id": tokenId}) + if err != nil { + return helpers.NewError(http.StatusInternalServerError, "token_delete_failed", "Failed to delete the token", err) + } + + return nil +} diff --git a/store/store.go b/store/store.go index 3cb58c6..c1c6c09 100644 --- a/store/store.go +++ b/store/store.go @@ -30,4 +30,10 @@ type Store interface { EditSetting(models.Setting) ([]models.Setting, error) PutBackground(models.Image) error RemoveBackground(string) error + + CreateToken(*models.Token) error + FindTokenById(string) (*models.Token, error) + GetAllTokens() ([]*models.Token, error) + UpdateToken(string, params.M) error + DeleteToken(string) error } diff --git a/store/token.go b/store/token.go new file mode 100644 index 0000000..922d59f --- /dev/null +++ b/store/token.go @@ -0,0 +1,33 @@ +package store + +import ( + "context" + + "github.com/uploadexpress/app/helpers/params" + "github.com/uploadexpress/app/models" +) + +//CreateToken checks if token already exists, and if not, creates it +func CreateToken(c context.Context, record *models.Token) error { + return FromContext(c).CreateToken(record) +} + +// FindTokenById allows to retrieve a token by its id +func FindTokenById(c context.Context, id string) (*models.Token, error) { + return FromContext(c).FindTokenById(id) +} + +// GetAllToken allows to get all tokens +func GetAllTokens(c context.Context) ([]*models.Token, error) { + return FromContext(c).GetAllTokens() +} + +// UpdateToken allows to update one or more token characteristics +func UpdateToken(c context.Context, tokenId string, params params.M) error { + return FromContext(c).UpdateToken(tokenId, params) +} + +// DeleteToken allows to delete a token by its id +func DeleteToken(c context.Context, tokenId string) error { + return FromContext(c).DeleteToken(tokenId) +}