Blame view

auth_utility.go 6.14 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
  type Role struct {
077dae33c   Marko Tikvić   removed role cons...
22
23
  	Name string `json:"name"`
  	ID   uint32 `json:"id"`
d2ddf82ef   Marko Tikvić   started on new rbac
24
  }
6ec91280b   Marko Tikvić   working on docume...
25
  // TokenClaims are JWT token claims.
90fd36e9b   Marko Tikvić   resolved some dep...
26
27
  type TokenClaims struct {
  	Username string `json:"username"`
7d3deb50d   Marko Tikvić   modified list_con...
28
  	Role     string `json:"role"`
6b16c88f2   Marko Tikvić   Working version w...
29
  	RoleID   uint32 `json:"roleID"`
90fd36e9b   Marko Tikvić   resolved some dep...
30
31
  	jwt.StandardClaims
  }
1d0f61553   Marko Tikvić   can't fetch clob
32
  // CredentialsStruct is an instace of username/password values.
90fd36e9b   Marko Tikvić   resolved some dep...
33
34
35
36
  type CredentialsStruct struct {
  	Username string `json:"username"`
  	Password string `json:"password"`
  }
6ec91280b   Marko Tikvić   working on docume...
37
  // generateSalt returns a string of random characters of 'saltSize' length.
4b4ea384f   Marko Tikvić   hmm
38
  func generateSalt() (salt string, err error) {
90fd36e9b   Marko Tikvić   resolved some dep...
39
  	rawsalt := make([]byte, saltSize)
33fd58161   markotikvic   minor changes, sh...
40

4b4ea384f   Marko Tikvić   hmm
41
  	_, err = rand.Read(rawsalt)
90fd36e9b   Marko Tikvić   resolved some dep...
42
43
44
  	if err != nil {
  		return "", err
  	}
33fd58161   markotikvic   minor changes, sh...
45

90fd36e9b   Marko Tikvić   resolved some dep...
46
47
48
  	salt = hex.EncodeToString(rawsalt)
  	return salt, nil
  }
077dae33c   Marko Tikvić   removed role cons...
49
  // HashString hashes input string using SHA256.
e1fbb41f9   Marko Tikvić   added comments
50
  // If the presalt parameter is not provided HashString will generate new salt string.
6ec91280b   Marko Tikvić   working on docume...
51
  // Returns hash and salt string or an error if it fails.
077dae33c   Marko Tikvić   removed role cons...
52
  func HashString(str, presalt string) (hash, salt string, err error) {
90fd36e9b   Marko Tikvić   resolved some dep...
53
54
  	// chech if message is presalted
  	if presalt == "" {
33fd58161   markotikvic   minor changes, sh...
55
  		salt, err = generateSalt()
90fd36e9b   Marko Tikvić   resolved some dep...
56
57
58
59
60
61
62
63
  		if err != nil {
  			return "", "", err
  		}
  	} else {
  		salt = presalt
  	}
  
  	// convert strings to raw byte slices
33fd58161   markotikvic   minor changes, sh...
64
  	rawstr := []byte(str)
90fd36e9b   Marko Tikvić   resolved some dep...
65
66
67
68
  	rawsalt, err := hex.DecodeString(salt)
  	if err != nil {
  		return "", "", err
  	}
33fd58161   markotikvic   minor changes, sh...
69

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

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

1769d6d42   Marko Tikvić   rbac
179
  // RbacCheck returns true if role that made HTTP request is authorized to
d2ddf82ef   Marko Tikvić   started on new rbac
180
181
182
183
184
185
186
187
188
189
190
191
192
  // 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...
193
194
  		return false
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
195
196
197
  
  	for _, r := range authRoles {
  		if claims.Role == r || r == "*" {
33d137a67   Marko Tikvić   Functional role c...
198
199
200
  			return true
  		}
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
201

33d137a67   Marko Tikvić   Functional role c...
202
203
  	return false
  }
d2ddf82ef   Marko Tikvić   started on new rbac
204
205
206
  
  // Rbac sets common headers and performs RBAC.
  // If RBAC  passes it calls the handlerFunc.
1769d6d42   Marko Tikvić   rbac
207
  func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc {
d2ddf82ef   Marko Tikvić   started on new rbac
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
  	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)
  	}
  }