Blame view

auth_utility.go 5.55 KB
ea858b8a7   Marko Tikvić   refactoring
1
  package webutility
90fd36e9b   Marko Tikvić   resolved some dep...
2
3
  
  import (
90fd36e9b   Marko Tikvić   resolved some dep...
4
  	"crypto/rand"
d2ddf82ef   Marko Tikvić   started on new rbac
5
  	"crypto/sha256"
90fd36e9b   Marko Tikvić   resolved some dep...
6
  	"encoding/hex"
d2ddf82ef   Marko Tikvić   started on new rbac
7
  	"errors"
33d137a67   Marko Tikvić   Functional role c...
8
  	"net/http"
d2ddf82ef   Marko Tikvić   started on new rbac
9
10
  	"strings"
  	"time"
33d137a67   Marko Tikvić   Functional role c...
11

90fd36e9b   Marko Tikvić   resolved some dep...
12
  	"github.com/dgrijalva/jwt-go"
90fd36e9b   Marko Tikvić   resolved some dep...
13
  )
d2ddf82ef   Marko Tikvić   started on new rbac
14
15
  const OneDay = time.Hour * 24
  const OneWeek = OneDay * 7
90fd36e9b   Marko Tikvić   resolved some dep...
16
  const saltSize = 32
d2ddf82ef   Marko Tikvić   started on new rbac
17
18
  const appName = "korisnicki-centar"
  const secret = "korisnicki-centar-api"
90fd36e9b   Marko Tikvić   resolved some dep...
19

d2ddf82ef   Marko Tikvić   started on new rbac
20
  type Role struct {
077dae33c   Marko Tikvić   removed role cons...
21
22
  	Name string `json:"name"`
  	ID   uint32 `json:"id"`
d2ddf82ef   Marko Tikvić   started on new rbac
23
  }
6ec91280b   Marko Tikvić   working on docume...
24
  // TokenClaims are JWT token claims.
90fd36e9b   Marko Tikvić   resolved some dep...
25
  type TokenClaims struct {
bc3671b26   Marko Tikvić   refactoring token...
26
27
28
29
30
31
32
  	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
90fd36e9b   Marko Tikvić   resolved some dep...
33
  }
1d0f61553   Marko Tikvić   can't fetch clob
34
  // CredentialsStruct is an instace of username/password values.
90fd36e9b   Marko Tikvić   resolved some dep...
35
36
37
38
  type CredentialsStruct struct {
  	Username string `json:"username"`
  	Password string `json:"password"`
  }
bc3671b26   Marko Tikvić   refactoring token...
39
40
41
  // ValidateCredentials hashes pass and salt and returns comparison result with resultHash
  func ValidateCredentials(pass, salt, resultHash string) bool {
  	hash, _, err := CreateHash(pass, salt)
90fd36e9b   Marko Tikvić   resolved some dep...
42
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
43
  		return false
90fd36e9b   Marko Tikvić   resolved some dep...
44
  	}
bc3671b26   Marko Tikvić   refactoring token...
45
  	return hash == resultHash
90fd36e9b   Marko Tikvić   resolved some dep...
46
  }
bc3671b26   Marko Tikvić   refactoring token...
47
48
49
50
  // 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) {
90fd36e9b   Marko Tikvić   resolved some dep...
51
52
  	// chech if message is presalted
  	if presalt == "" {
bc3671b26   Marko Tikvić   refactoring token...
53
  		salt, err = randomSalt()
90fd36e9b   Marko Tikvić   resolved some dep...
54
55
56
57
58
59
60
61
  		if err != nil {
  			return "", "", err
  		}
  	} else {
  		salt = presalt
  	}
  
  	// convert strings to raw byte slices
33fd58161   markotikvic   minor changes, sh...
62
  	rawstr := []byte(str)
90fd36e9b   Marko Tikvić   resolved some dep...
63
64
65
66
  	rawsalt, err := hex.DecodeString(salt)
  	if err != nil {
  		return "", "", err
  	}
33fd58161   markotikvic   minor changes, sh...
67

d2ddf82ef   Marko Tikvić   started on new rbac
68
  	rawdata := make([]byte, len(rawstr)+len(rawsalt))
33fd58161   markotikvic   minor changes, sh...
69
  	rawdata = append(rawdata, rawstr...)
90fd36e9b   Marko Tikvić   resolved some dep...
70
71
72
73
74
75
  	rawdata = append(rawdata, rawsalt...)
  
  	// hash message + salt
  	hasher := sha256.New()
  	hasher.Write(rawdata)
  	rawhash := hasher.Sum(nil)
33fd58161   markotikvic   minor changes, sh...
76

90fd36e9b   Marko Tikvić   resolved some dep...
77
78
79
  	hash = hex.EncodeToString(rawhash)
  	return hash, salt, nil
  }
bc3671b26   Marko Tikvić   refactoring token...
80
  // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims.
6ec91280b   Marko Tikvić   working on docume...
81
  // It returns an error if it fails.
bc3671b26   Marko Tikvić   refactoring token...
82
83
84
  func CreateAuthToken(username string, role Role) (TokenClaims, error) {
  	t0 := (time.Now()).Unix()
  	t1 := (time.Now().Add(OneWeek)).Unix()
90fd36e9b   Marko Tikvić   resolved some dep...
85
  	claims := TokenClaims{
bc3671b26   Marko Tikvić   refactoring token...
86
87
88
89
90
  		TokenType: "Bearer",
  		Username:  username,
  		Role:      role.Name,
  		RoleID:    role.ID,
  		ExpiresIn: t1 - t0,
90fd36e9b   Marko Tikvić   resolved some dep...
91
  	}
bc3671b26   Marko Tikvić   refactoring token...
92
93
94
95
  	// initialize jwt.StandardClaims fields (anonymous struct)
  	claims.IssuedAt = t0
  	claims.ExpiresAt = t1
  	claims.Issuer = appName
90fd36e9b   Marko Tikvić   resolved some dep...
96
97
  
  	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
bc3671b26   Marko Tikvić   refactoring token...
98
  	token, err := jwtToken.SignedString([]byte(secret))
90fd36e9b   Marko Tikvić   resolved some dep...
99
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
100
  		return TokenClaims{}, err
90fd36e9b   Marko Tikvić   resolved some dep...
101
  	}
bc3671b26   Marko Tikvić   refactoring token...
102
103
  	claims.Token = token
  	return claims, nil
90fd36e9b   Marko Tikvić   resolved some dep...
104
  }
bc3671b26   Marko Tikvić   refactoring token...
105
  // RefreshAuthToken prolongs JWT token's expiration date for one week.
6ec91280b   Marko Tikvić   working on docume...
106
  // It returns new JWT token or an error if it fails.
bc3671b26   Marko Tikvić   refactoring token...
107
108
109
110
  func RefreshAuthToken(req *http.Request) (TokenClaims, error) {
  	authHead := req.Header.Get("Authorization")
  	tokenstr := strings.TrimPrefix(authHead, "Bearer ")
  	token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc)
90fd36e9b   Marko Tikvić   resolved some dep...
111
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
112
  		return TokenClaims{}, err
90fd36e9b   Marko Tikvić   resolved some dep...
113
114
115
116
117
  	}
  
  	// type assertion
  	claims, ok := token.Claims.(*TokenClaims)
  	if !ok || !token.Valid {
bc3671b26   Marko Tikvić   refactoring token...
118
  		return TokenClaims{}, errors.New("token is not valid")
90fd36e9b   Marko Tikvić   resolved some dep...
119
  	}
bc3671b26   Marko Tikvić   refactoring token...
120
121
  	// extend token expiration date
  	return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID})
90fd36e9b   Marko Tikvić   resolved some dep...
122
  }
bc3671b26   Marko Tikvić   refactoring token...
123
124
125
126
127
128
129
130
131
  // 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
90fd36e9b   Marko Tikvić   resolved some dep...
132
  	}
bc3671b26   Marko Tikvić   refactoring token...
133
134
  	// validate token and check expiration date
  	claims, err := GetTokenClaims(req)
90fd36e9b   Marko Tikvić   resolved some dep...
135
136
137
  	if err != nil {
  		return &TokenClaims{}, err
  	}
bc3671b26   Marko Tikvić   refactoring token...
138
139
140
141
  	// check if token has expired
  	if claims.ExpiresAt < (time.Now()).Unix() {
  		return &TokenClaims{}, errors.New("token has expired")
  	}
90fd36e9b   Marko Tikvić   resolved some dep...
142

bc3671b26   Marko Tikvić   refactoring token...
143
144
145
146
147
148
  	// 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
  		}
90fd36e9b   Marko Tikvić   resolved some dep...
149
  	}
bc3671b26   Marko Tikvić   refactoring token...
150
151
  
  	return &TokenClaims{}, errors.New("role is not authorized")
90fd36e9b   Marko Tikvić   resolved some dep...
152
  }
bc3671b26   Marko Tikvić   refactoring token...
153
154
155
156
157
158
159
160
  // 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 ")
33d137a67   Marko Tikvić   Functional role c...
161
  	} else {
bc3671b26   Marko Tikvić   refactoring token...
162
  		return &TokenClaims{}, errors.New("authorization header in incomplete")
33d137a67   Marko Tikvić   Functional role c...
163
  	}
bc3671b26   Marko Tikvić   refactoring token...
164
  	token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc)
33d137a67   Marko Tikvić   Functional role c...
165
166
167
168
169
  	if err != nil {
  		return &TokenClaims{}, err
  	}
  
  	// type assertion
bc3671b26   Marko Tikvić   refactoring token...
170
171
  	claims, ok := token.Claims.(*TokenClaims)
  	if !ok || !token.Valid {
33d137a67   Marko Tikvić   Functional role c...
172
173
  		return &TokenClaims{}, errors.New("token is not valid")
  	}
33d137a67   Marko Tikvić   Functional role c...
174

bc3671b26   Marko Tikvić   refactoring token...
175
  	return claims, nil
90fd36e9b   Marko Tikvić   resolved some dep...
176
  }
33d137a67   Marko Tikvić   Functional role c...
177

bc3671b26   Marko Tikvić   refactoring token...
178
179
180
  // randomSalt returns a string of random characters of 'saltSize' length.
  func randomSalt() (s string, err error) {
  	rawsalt := make([]byte, saltSize)
d2ddf82ef   Marko Tikvić   started on new rbac
181

bc3671b26   Marko Tikvić   refactoring token...
182
  	_, err = rand.Read(rawsalt)
d2ddf82ef   Marko Tikvić   started on new rbac
183
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
184
  		return "", err
33d137a67   Marko Tikvić   Functional role c...
185
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
186

bc3671b26   Marko Tikvić   refactoring token...
187
188
  	s = hex.EncodeToString(rawsalt)
  	return s, nil
33d137a67   Marko Tikvić   Functional role c...
189
  }
d2ddf82ef   Marko Tikvić   started on new rbac
190

bc3671b26   Marko Tikvić   refactoring token...
191
192
193
  // secretFunc returns byte slice of API secret keyword.
  func secretFunc(token *jwt.Token) (interface{}, error) {
  	return []byte(secret), nil
d2ddf82ef   Marko Tikvić   started on new rbac
194
  }