From bc3671b260f97ed4c81baeb2e2fbedb29f47f693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Tikvi=C4=87?= Date: Thu, 19 Oct 2017 13:09:35 +0200 Subject: [PATCH] refactoring token creation, validation and refreshing; refactoring rbac to return user data --- auth_utility.go | 222 +++++++++++++++++++++++--------------------------------- http_utility.go | 4 +- 2 files changed, 90 insertions(+), 136 deletions(-) diff --git a/auth_utility.go b/auth_utility.go index d931fff..91f39ac 100644 --- a/auth_utility.go +++ b/auth_utility.go @@ -1,4 +1,3 @@ -// TODO: Improve roles package webutility import ( @@ -26,10 +25,13 @@ type Role struct { // TokenClaims are JWT token claims. type TokenClaims struct { - Username string `json:"username"` - Role string `json:"role"` - RoleID uint32 `json:"roleID"` - jwt.StandardClaims + Token string `json:"access_token"` + TokenType string `json:"token_type"` + Username string `json:"username"` + Role string `json:"role"` + RoleID uint32 `json:"role_id"` + ExpiresIn int64 `json:"expires_in"` + jwt.StandardClaims // extending a struct } // CredentialsStruct is an instace of username/password values. @@ -38,26 +40,22 @@ type CredentialsStruct struct { Password string `json:"password"` } -// generateSalt returns a string of random characters of 'saltSize' length. -func generateSalt() (salt string, err error) { - rawsalt := make([]byte, saltSize) - - _, err = rand.Read(rawsalt) +// ValidateCredentials hashes pass and salt and returns comparison result with resultHash +func ValidateCredentials(pass, salt, resultHash string) bool { + hash, _, err := CreateHash(pass, salt) if err != nil { - return "", err + return false } - - salt = hex.EncodeToString(rawsalt) - return salt, nil + return hash == resultHash } -// HashString hashes input string using SHA256. -// If the presalt parameter is not provided HashString will generate new salt string. -// Returns hash and salt string or an error if it fails. -func HashString(str, presalt string) (hash, salt string, err error) { +// CreateHash hashes str using SHA256. +// If the presalt parameter is not provided CreateHash will generate new salt string. +// Returns hash and salt strings or an error if it fails. +func CreateHash(str, presalt string) (hash, salt string, err error) { // chech if message is presalted if presalt == "" { - salt, err = generateSalt() + salt, err = randomSalt() if err != nil { return "", "", err } @@ -85,166 +83,124 @@ func HashString(str, presalt string) (hash, salt string, err error) { return hash, salt, nil } -// CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims. +// CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims. // It returns an error if it fails. -func CreateAPIToken(username string, role Role) (string, error) { - var apiToken string - var err error - - if err != nil { - return "", err - } - +func CreateAuthToken(username string, role Role) (TokenClaims, error) { + t0 := (time.Now()).Unix() + t1 := (time.Now().Add(OneWeek)).Unix() claims := TokenClaims{ - username, - role.Name, - role.ID, - jwt.StandardClaims{ - ExpiresAt: (time.Now().Add(OneWeek)).Unix(), - Issuer: appName, - }, + TokenType: "Bearer", + Username: username, + Role: role.Name, + RoleID: role.ID, + ExpiresIn: t1 - t0, } + // initialize jwt.StandardClaims fields (anonymous struct) + claims.IssuedAt = t0 + claims.ExpiresAt = t1 + claims.Issuer = appName jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - apiToken, err = jwtToken.SignedString([]byte(secret)) + token, err := jwtToken.SignedString([]byte(secret)) if err != nil { - return "", err + return TokenClaims{}, err } - return apiToken, nil + claims.Token = token + return claims, nil } -// RefreshAPIToken prolongs JWT token's expiration date for one week. +// RefreshAuthToken prolongs JWT token's expiration date for one week. // It returns new JWT token or an error if it fails. -func RefreshAPIToken(tokenString string) (string, error) { - var newToken string - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) +func RefreshAuthToken(req *http.Request) (TokenClaims, error) { + authHead := req.Header.Get("Authorization") + tokenstr := strings.TrimPrefix(authHead, "Bearer ") + token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc) if err != nil { - return "", err + return TokenClaims{}, err } // type assertion claims, ok := token.Claims.(*TokenClaims) if !ok || !token.Valid { - return "", errors.New("token is not valid") - } - - claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() - jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - newToken, err = jwtToken.SignedString([]byte(secret)) - if err != nil { - return "", err + return TokenClaims{}, errors.New("token is not valid") } - return newToken, nil + // extend token expiration date + return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID}) } -// ParseAPIToken parses JWT token claims. -// It returns a pointer to TokenClaims struct or an error if it fails. -func ParseAPIToken(tokenString string) (*TokenClaims, error) { - if ok := strings.HasPrefix(tokenString, "Bearer "); ok { - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - } else { - return &TokenClaims{}, errors.New("Authorization header is incomplete") +// RbacCheck returns true if role that made HTTP request is authorized to +// access the resource it is targeting. +// It exctracts user's role from the JWT token located in Authorization header of +// http.Request and then compares it with the list of supplied roles and returns +// true if there's a match, if "*" is provided or if the authRoles is nil. +// Otherwise it returns false. +func RbacCheck(req *http.Request, authRoles []string) (*TokenClaims, error) { + if authRoles == nil { + return &TokenClaims{}, nil } - token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) + // validate token and check expiration date + claims, err := GetTokenClaims(req) if err != nil { return &TokenClaims{}, err } + // check if token has expired + if claims.ExpiresAt < (time.Now()).Unix() { + return &TokenClaims{}, errors.New("token has expired") + } - // type assertion - claims, ok := token.Claims.(*TokenClaims) - if !ok || !token.Valid { - return &TokenClaims{}, errors.New("token is not valid") + // check if role extracted from token matches + // any of the provided (allowed) ones + for _, r := range authRoles { + if claims.Role == r || r == "*" { + return claims, nil + } } - return claims, nil + + return &TokenClaims{}, errors.New("role is not authorized") } -func GetTokenClaims(r *http.Request) (claims *TokenClaims, err error) { - token := r.Header.Get("Authorization") - if ok := strings.HasPrefix(token, "Bearer "); ok { - token = strings.TrimPrefix(token, "Bearer ") +// GetTokenClaims extracts JWT claims from Authorization header of the request. +// Returns token claims or an error. +func GetTokenClaims(req *http.Request) (*TokenClaims, error) { + // check for and strip 'Bearer' prefix + var tokstr string + authHead := req.Header.Get("Authorization") + if ok := strings.HasPrefix(authHead, "Bearer "); ok { + tokstr = strings.TrimPrefix(tokstr, "Bearer ") } else { - return &TokenClaims{}, errors.New("Authorization header is incomplete") + return &TokenClaims{}, errors.New("authorization header in incomplete") } - parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc) + token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc) if err != nil { return &TokenClaims{}, err } // type assertion - claims, ok := parsedToken.Claims.(*TokenClaims) - if !ok || !parsedToken.Valid { + claims, ok := token.Claims.(*TokenClaims) + if !ok || !token.Valid { return &TokenClaims{}, errors.New("token is not valid") } - return claims, err -} -// secretFunc returns byte slice of API secret keyword. -func secretFunc(token *jwt.Token) (interface{}, error) { - return []byte(secret), nil + return claims, nil } -// RbacCheck returns true if role that made HTTP request is authorized to -// access the resource it is targeting. -// It exctracts user's role from the JWT token located in Authorization header of -// http.Request and then compares it with the list of supplied roles and returns -// true if there's a match, if "*" is provided or if the authRoles is nil. -// Otherwise it returns false. -func RbacCheck(req *http.Request, authRoles []string) bool { - if authRoles == nil { - return true - } +// randomSalt returns a string of random characters of 'saltSize' length. +func randomSalt() (s string, err error) { + rawsalt := make([]byte, saltSize) - token := req.Header.Get("Authorization") - claims, err := ParseAPIToken(token) + _, err = rand.Read(rawsalt) if err != nil { - return false - } - - for _, r := range authRoles { - if claims.Role == r || r == "*" { - return true - } + return "", err } - return false + s = hex.EncodeToString(rawsalt) + return s, nil } -// Rbac sets common headers and performs RBAC. -// If RBAC passes it calls the handlerFunc. -func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") - - w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, - Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - - // TODO: Check for content type - - if req.Method == "OPTIONS" { - return - } - - err := req.ParseForm() - if err != nil { - BadRequestResponse(w, req) - return - } - - if !RbacCheck(req, authRoles) { - UnauthorizedResponse(w, req) - return - } - - // execute HandlerFunc - handlerFunc(w, req) - } +// secretFunc returns byte slice of API secret keyword. +func secretFunc(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil } diff --git a/http_utility.go b/http_utility.go index c6df81a..ed8138a 100644 --- a/http_utility.go +++ b/http_utility.go @@ -49,6 +49,7 @@ func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) { // UnauthorizedError writes HTTP error 401 to w. func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) { + w.Header().Set("WWW-Authenticate", "Bearer") ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{ {"en", templateHttpErr401_EN}, {"rs", templateHttpErr401_RS}, @@ -70,11 +71,8 @@ func NotFoundHandler(w http.ResponseWriter, req *http.Request) { // SetDefaultHeaders set's default headers for an HTTP response. func SetDefaultHeaders(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) - w.Header().Set("Content-Type", "application/json; charset=utf-8") } -- 1.8.1.2