auth_utility.go 6.61 KB
// TODO: Improve roles
package webutility

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/dgrijalva/jwt-go"
)

const OneDay = time.Hour * 24
const OneWeek = OneDay * 7
const saltSize = 32
const appName = "korisnicki-centar"
const secret = "korisnicki-centar-api"

const RoleAdmin string = "ADMINISTRATOR"
const RoleManager string = "RUKOVODILAC"
const RoleReporter string = "REPORTER"
const RoleOperator string = "OPERATER"
const RoleAdminID uint32 = 1
const RoleManagerID uint32 = 2
const RoleReporterID uint32 = 3
const RoleOperatorID uint32 = 4

type Role struct {
	name string
	id   uint32
}

// TokenClaims are JWT token claims.
type TokenClaims struct {
	Username string `json:"username"`
	Role     string `json:"role"`
	RoleID   uint32 `json:"roleID"`
	jwt.StandardClaims
}

// CredentialsStruct is an instace of username/password values.
type CredentialsStruct struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

var admin Role = Role{RoleAdmin, RoleAdminID}
var manager Role = Role{RoleManager, RoleManagerID}
var reporter Role = Role{RoleReporter, RoleReporterID}
var operator Role = Role{RoleOperator, RoleOperatorID}

// generateSalt returns a string of random characters of 'saltSize' length.
func generateSalt() (salt string, err error) {
	rawsalt := make([]byte, saltSize)

	_, err = rand.Read(rawsalt)
	if err != nil {
		return "", err
	}

	salt = hex.EncodeToString(rawsalt)
	return salt, nil
}

// HashString hashes input string with SHA256 algorithm.
// 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 string, presalt string) (hash, salt string, err error) {
	// chech if message is presalted
	if presalt == "" {
		salt, err = generateSalt()
		if err != nil {
			return "", "", err
		}
	} else {
		salt = presalt
	}

	// convert strings to raw byte slices
	rawstr := []byte(str)
	rawsalt, err := hex.DecodeString(salt)
	if err != nil {
		return "", "", err
	}

	rawdata := make([]byte, len(rawstr)+len(rawsalt))
	rawdata = append(rawdata, rawstr...)
	rawdata = append(rawdata, rawsalt...)

	// hash message + salt
	hasher := sha256.New()
	hasher.Write(rawdata)
	rawhash := hasher.Sum(nil)

	hash = hex.EncodeToString(rawhash)
	return hash, salt, nil
}

// CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims.
// It returns an error if it fails.
func CreateAPIToken(username, role string, roleID uint32) (string, error) {
	var apiToken string
	var err error

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

	claims := TokenClaims{
		username,
		role,
		roleID,
		jwt.StandardClaims{
			ExpiresAt: (time.Now().Add(OneWeek)).Unix(),
			Issuer:    appName,
		},
	}

	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	apiToken, err = jwtToken.SignedString([]byte(secret))
	if err != nil {
		return "", err
	}
	return apiToken, nil
}

// RefreshAPIToken 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)
	if err != nil {
		return "", 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 newToken, nil
}

// 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")
	}

	token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc)
	if err != nil {
		return &TokenClaims{}, err
	}

	// type assertion
	claims, ok := token.Claims.(*TokenClaims)
	if !ok || !token.Valid {
		return &TokenClaims{}, errors.New("token is not valid")
	}
	return claims, nil
}

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 ")
	} else {
		return &TokenClaims{}, errors.New("Authorization header is incomplete")
	}

	parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc)
	if err != nil {
		return &TokenClaims{}, err
	}

	// type assertion
	claims, ok := parsedToken.Claims.(*TokenClaims)
	if !ok || !parsedToken.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
}

// rbacEnforce 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
	}

	token := req.Header.Get("Authorization")
	claims, err := ParseAPIToken(token)
	if err != nil {
		return false
	}

	for _, r := range authRoles {
		if claims.Role == r || r == "*" {
			return true
		}
	}

	return false
}

// Rbac sets common headers and performs RBAC.
// If RBAC  passes it calls the handlerFunc.
func Rbac(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)
	}
}