Blame view

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

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

d2ddf82ef   Marko Tikvić   started on new rbac
21
22
  const RoleAdmin string = "ADMINISTRATOR"
  const RoleManager string = "RUKOVODILAC"
33d137a67   Marko Tikvić   Functional role c...
23
24
  const RoleReporter string = "REPORTER"
  const RoleOperator string = "OPERATER"
d2ddf82ef   Marko Tikvić   started on new rbac
25
26
  const RoleAdminID uint32 = 1
  const RoleManagerID uint32 = 2
6b16c88f2   Marko Tikvić   Working version w...
27
28
  const RoleReporterID uint32 = 3
  const RoleOperatorID uint32 = 4
33d137a67   Marko Tikvić   Functional role c...
29

d2ddf82ef   Marko Tikvić   started on new rbac
30
31
32
33
  type Role struct {
  	name string
  	id   uint32
  }
6ec91280b   Marko Tikvić   working on docume...
34
  // TokenClaims are JWT token claims.
90fd36e9b   Marko Tikvić   resolved some dep...
35
36
  type TokenClaims struct {
  	Username string `json:"username"`
7d3deb50d   Marko Tikvić   modified list_con...
37
  	Role     string `json:"role"`
6b16c88f2   Marko Tikvić   Working version w...
38
  	RoleID   uint32 `json:"roleID"`
90fd36e9b   Marko Tikvić   resolved some dep...
39
40
  	jwt.StandardClaims
  }
1d0f61553   Marko Tikvić   can't fetch clob
41
  // CredentialsStruct is an instace of username/password values.
90fd36e9b   Marko Tikvić   resolved some dep...
42
43
44
45
  type CredentialsStruct struct {
  	Username string `json:"username"`
  	Password string `json:"password"`
  }
d2ddf82ef   Marko Tikvić   started on new rbac
46
47
48
49
  var admin Role = Role{RoleAdmin, RoleAdminID}
  var manager Role = Role{RoleManager, RoleManagerID}
  var reporter Role = Role{RoleReporter, RoleReporterID}
  var operator Role = Role{RoleOperator, RoleOperatorID}
6ec91280b   Marko Tikvić   working on docume...
50
  // generateSalt returns a string of random characters of 'saltSize' length.
4b4ea384f   Marko Tikvić   hmm
51
  func generateSalt() (salt string, err error) {
90fd36e9b   Marko Tikvić   resolved some dep...
52
  	rawsalt := make([]byte, saltSize)
33fd58161   markotikvic   minor changes, sh...
53

4b4ea384f   Marko Tikvić   hmm
54
  	_, err = rand.Read(rawsalt)
90fd36e9b   Marko Tikvić   resolved some dep...
55
56
57
  	if err != nil {
  		return "", err
  	}
33fd58161   markotikvic   minor changes, sh...
58

90fd36e9b   Marko Tikvić   resolved some dep...
59
60
61
  	salt = hex.EncodeToString(rawsalt)
  	return salt, nil
  }
e1fbb41f9   Marko Tikvić   added comments
62
63
  // HashString hashes input string with SHA256 algorithm.
  // If the presalt parameter is not provided HashString will generate new salt string.
6ec91280b   Marko Tikvić   working on docume...
64
  // Returns hash and salt string or an error if it fails.
33fd58161   markotikvic   minor changes, sh...
65
  func HashString(str string, presalt string) (hash, salt string, err error) {
90fd36e9b   Marko Tikvić   resolved some dep...
66
67
  	// chech if message is presalted
  	if presalt == "" {
33fd58161   markotikvic   minor changes, sh...
68
  		salt, err = generateSalt()
90fd36e9b   Marko Tikvić   resolved some dep...
69
70
71
72
73
74
75
76
  		if err != nil {
  			return "", "", err
  		}
  	} else {
  		salt = presalt
  	}
  
  	// convert strings to raw byte slices
33fd58161   markotikvic   minor changes, sh...
77
  	rawstr := []byte(str)
90fd36e9b   Marko Tikvić   resolved some dep...
78
79
80
81
  	rawsalt, err := hex.DecodeString(salt)
  	if err != nil {
  		return "", "", err
  	}
33fd58161   markotikvic   minor changes, sh...
82

d2ddf82ef   Marko Tikvić   started on new rbac
83
  	rawdata := make([]byte, len(rawstr)+len(rawsalt))
33fd58161   markotikvic   minor changes, sh...
84
  	rawdata = append(rawdata, rawstr...)
90fd36e9b   Marko Tikvić   resolved some dep...
85
86
87
88
89
90
  	rawdata = append(rawdata, rawsalt...)
  
  	// hash message + salt
  	hasher := sha256.New()
  	hasher.Write(rawdata)
  	rawhash := hasher.Sum(nil)
33fd58161   markotikvic   minor changes, sh...
91

90fd36e9b   Marko Tikvić   resolved some dep...
92
93
94
  	hash = hex.EncodeToString(rawhash)
  	return hash, salt, nil
  }
6ec91280b   Marko Tikvić   working on docume...
95
96
  // CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims.
  // It returns an error if it fails.
6b16c88f2   Marko Tikvić   Working version w...
97
  func CreateAPIToken(username, role string, roleID uint32) (string, error) {
7d3deb50d   Marko Tikvić   modified list_con...
98
  	var apiToken string
90fd36e9b   Marko Tikvić   resolved some dep...
99
100
101
  	var err error
  
  	if err != nil {
6f4b8a711   Marko Tikvić   token response ch...
102
  		return "", err
90fd36e9b   Marko Tikvić   resolved some dep...
103
104
105
106
107
  	}
  
  	claims := TokenClaims{
  		username,
  		role,
6b16c88f2   Marko Tikvić   Working version w...
108
  		roleID,
90fd36e9b   Marko Tikvić   resolved some dep...
109
110
111
112
113
114
115
  		jwt.StandardClaims{
  			ExpiresAt: (time.Now().Add(OneWeek)).Unix(),
  			Issuer:    appName,
  		},
  	}
  
  	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
6f4b8a711   Marko Tikvić   token response ch...
116
  	apiToken, err = jwtToken.SignedString([]byte(secret))
90fd36e9b   Marko Tikvić   resolved some dep...
117
  	if err != nil {
6f4b8a711   Marko Tikvić   token response ch...
118
  		return "", err
90fd36e9b   Marko Tikvić   resolved some dep...
119
120
121
  	}
  	return apiToken, nil
  }
6ec91280b   Marko Tikvić   working on docume...
122
123
  // RefreshAPIToken prolongs JWT token's expiration date for one week.
  // It returns new JWT token or an error if it fails.
6f4b8a711   Marko Tikvić   token response ch...
124
  func RefreshAPIToken(tokenString string) (string, error) {
7d3deb50d   Marko Tikvić   modified list_con...
125
  	var newToken string
90fd36e9b   Marko Tikvić   resolved some dep...
126
  	tokenString = strings.TrimPrefix(tokenString, "Bearer ")
e1fbb41f9   Marko Tikvić   added comments
127
  	token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc)
90fd36e9b   Marko Tikvić   resolved some dep...
128
  	if err != nil {
6f4b8a711   Marko Tikvić   token response ch...
129
  		return "", err
90fd36e9b   Marko Tikvić   resolved some dep...
130
131
132
133
134
  	}
  
  	// type assertion
  	claims, ok := token.Claims.(*TokenClaims)
  	if !ok || !token.Valid {
6f4b8a711   Marko Tikvić   token response ch...
135
  		return "", errors.New("token is not valid")
90fd36e9b   Marko Tikvić   resolved some dep...
136
137
138
139
  	}
  
  	claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix()
  	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
6f4b8a711   Marko Tikvić   token response ch...
140
  	newToken, err = jwtToken.SignedString([]byte(secret))
90fd36e9b   Marko Tikvić   resolved some dep...
141
  	if err != nil {
6f4b8a711   Marko Tikvić   token response ch...
142
  		return "", err
90fd36e9b   Marko Tikvić   resolved some dep...
143
144
145
146
  	}
  
  	return newToken, nil
  }
e1fbb41f9   Marko Tikvić   added comments
147
  // ParseAPIToken parses JWT token claims.
6ec91280b   Marko Tikvić   working on docume...
148
  // It returns a pointer to TokenClaims struct or an error if it fails.
b291ac8c4   Marko Tikvić   clened up
149
  func ParseAPIToken(tokenString string) (*TokenClaims, error) {
e1fbb41f9   Marko Tikvić   added comments
150
  	if ok := strings.HasPrefix(tokenString, "Bearer "); ok {
90fd36e9b   Marko Tikvić   resolved some dep...
151
152
153
154
  		tokenString = strings.TrimPrefix(tokenString, "Bearer ")
  	} else {
  		return &TokenClaims{}, errors.New("Authorization header is incomplete")
  	}
e1fbb41f9   Marko Tikvić   added comments
155
  	token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc)
90fd36e9b   Marko Tikvić   resolved some dep...
156
157
158
159
160
161
162
163
164
165
166
  	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
  }
33d137a67   Marko Tikvić   Functional role c...
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
  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
  }
6ec91280b   Marko Tikvić   working on docume...
187
  // secretFunc returns byte slice of API secret keyword.
e1fbb41f9   Marko Tikvić   added comments
188
189
  func secretFunc(token *jwt.Token) (interface{}, error) {
  	return []byte(secret), nil
90fd36e9b   Marko Tikvić   resolved some dep...
190
  }
33d137a67   Marko Tikvić   Functional role c...
191

1769d6d42   Marko Tikvić   rbac
192
  // RbacCheck returns true if role that made HTTP request is authorized to
d2ddf82ef   Marko Tikvić   started on new rbac
193
194
195
196
197
198
199
200
201
202
203
204
205
  // 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 {
33d137a67   Marko Tikvić   Functional role c...
206
207
  		return false
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
208
209
210
  
  	for _, r := range authRoles {
  		if claims.Role == r || r == "*" {
33d137a67   Marko Tikvić   Functional role c...
211
212
213
  			return true
  		}
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
214

33d137a67   Marko Tikvić   Functional role c...
215
216
  	return false
  }
d2ddf82ef   Marko Tikvić   started on new rbac
217
218
219
  
  // Rbac sets common headers and performs RBAC.
  // If RBAC  passes it calls the handlerFunc.
1769d6d42   Marko Tikvić   rbac
220
  func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc {
d2ddf82ef   Marko Tikvić   started on new rbac
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
  	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)
  	}
  }