Commit 1769d6d42e5abc1a6a9f997088da1e8eb2a51fff
1 parent
d2ddf82ef1
Exists in
master
and in
1 other branch
rbac
Showing
1 changed file
with
2 additions
and
2 deletions
Show diff stats
auth_utility.go
1 | // TODO: Improve roles | 1 | // TODO: Improve roles |
2 | package webutility | 2 | package webutility |
3 | 3 | ||
4 | import ( | 4 | import ( |
5 | "crypto/rand" | 5 | "crypto/rand" |
6 | "crypto/sha256" | 6 | "crypto/sha256" |
7 | "encoding/hex" | 7 | "encoding/hex" |
8 | "errors" | 8 | "errors" |
9 | "net/http" | 9 | "net/http" |
10 | "strings" | 10 | "strings" |
11 | "time" | 11 | "time" |
12 | 12 | ||
13 | "github.com/dgrijalva/jwt-go" | 13 | "github.com/dgrijalva/jwt-go" |
14 | ) | 14 | ) |
15 | 15 | ||
16 | const OneDay = time.Hour * 24 | 16 | const OneDay = time.Hour * 24 |
17 | const OneWeek = OneDay * 7 | 17 | const OneWeek = OneDay * 7 |
18 | const saltSize = 32 | 18 | const saltSize = 32 |
19 | const appName = "korisnicki-centar" | 19 | const appName = "korisnicki-centar" |
20 | const secret = "korisnicki-centar-api" | 20 | const secret = "korisnicki-centar-api" |
21 | 21 | ||
22 | const RoleAdmin string = "ADMINISTRATOR" | 22 | const RoleAdmin string = "ADMINISTRATOR" |
23 | const RoleManager string = "RUKOVODILAC" | 23 | const RoleManager string = "RUKOVODILAC" |
24 | const RoleReporter string = "REPORTER" | 24 | const RoleReporter string = "REPORTER" |
25 | const RoleOperator string = "OPERATER" | 25 | const RoleOperator string = "OPERATER" |
26 | const RoleAdminID uint32 = 1 | 26 | const RoleAdminID uint32 = 1 |
27 | const RoleManagerID uint32 = 2 | 27 | const RoleManagerID uint32 = 2 |
28 | const RoleReporterID uint32 = 3 | 28 | const RoleReporterID uint32 = 3 |
29 | const RoleOperatorID uint32 = 4 | 29 | const RoleOperatorID uint32 = 4 |
30 | 30 | ||
31 | type Role struct { | 31 | type Role struct { |
32 | name string | 32 | name string |
33 | id uint32 | 33 | id uint32 |
34 | } | 34 | } |
35 | 35 | ||
36 | // TokenClaims are JWT token claims. | 36 | // TokenClaims are JWT token claims. |
37 | type TokenClaims struct { | 37 | type TokenClaims struct { |
38 | Username string `json:"username"` | 38 | Username string `json:"username"` |
39 | Role string `json:"role"` | 39 | Role string `json:"role"` |
40 | RoleID uint32 `json:"roleID"` | 40 | RoleID uint32 `json:"roleID"` |
41 | jwt.StandardClaims | 41 | jwt.StandardClaims |
42 | } | 42 | } |
43 | 43 | ||
44 | // CredentialsStruct is an instace of username/password values. | 44 | // CredentialsStruct is an instace of username/password values. |
45 | type CredentialsStruct struct { | 45 | type CredentialsStruct struct { |
46 | Username string `json:"username"` | 46 | Username string `json:"username"` |
47 | Password string `json:"password"` | 47 | Password string `json:"password"` |
48 | } | 48 | } |
49 | 49 | ||
50 | var admin Role = Role{RoleAdmin, RoleAdminID} | 50 | var admin Role = Role{RoleAdmin, RoleAdminID} |
51 | var manager Role = Role{RoleManager, RoleManagerID} | 51 | var manager Role = Role{RoleManager, RoleManagerID} |
52 | var reporter Role = Role{RoleReporter, RoleReporterID} | 52 | var reporter Role = Role{RoleReporter, RoleReporterID} |
53 | var operator Role = Role{RoleOperator, RoleOperatorID} | 53 | var operator Role = Role{RoleOperator, RoleOperatorID} |
54 | 54 | ||
55 | // generateSalt returns a string of random characters of 'saltSize' length. | 55 | // generateSalt returns a string of random characters of 'saltSize' length. |
56 | func generateSalt() (salt string, err error) { | 56 | func generateSalt() (salt string, err error) { |
57 | rawsalt := make([]byte, saltSize) | 57 | rawsalt := make([]byte, saltSize) |
58 | 58 | ||
59 | _, err = rand.Read(rawsalt) | 59 | _, err = rand.Read(rawsalt) |
60 | if err != nil { | 60 | if err != nil { |
61 | return "", err | 61 | return "", err |
62 | } | 62 | } |
63 | 63 | ||
64 | salt = hex.EncodeToString(rawsalt) | 64 | salt = hex.EncodeToString(rawsalt) |
65 | return salt, nil | 65 | return salt, nil |
66 | } | 66 | } |
67 | 67 | ||
68 | // HashString hashes input string with SHA256 algorithm. | 68 | // HashString hashes input string with SHA256 algorithm. |
69 | // If the presalt parameter is not provided HashString will generate new salt string. | 69 | // If the presalt parameter is not provided HashString will generate new salt string. |
70 | // Returns hash and salt string or an error if it fails. | 70 | // Returns hash and salt string or an error if it fails. |
71 | func HashString(str string, presalt string) (hash, salt string, err error) { | 71 | func HashString(str string, presalt string) (hash, salt string, err error) { |
72 | // chech if message is presalted | 72 | // chech if message is presalted |
73 | if presalt == "" { | 73 | if presalt == "" { |
74 | salt, err = generateSalt() | 74 | salt, err = generateSalt() |
75 | if err != nil { | 75 | if err != nil { |
76 | return "", "", err | 76 | return "", "", err |
77 | } | 77 | } |
78 | } else { | 78 | } else { |
79 | salt = presalt | 79 | salt = presalt |
80 | } | 80 | } |
81 | 81 | ||
82 | // convert strings to raw byte slices | 82 | // convert strings to raw byte slices |
83 | rawstr := []byte(str) | 83 | rawstr := []byte(str) |
84 | rawsalt, err := hex.DecodeString(salt) | 84 | rawsalt, err := hex.DecodeString(salt) |
85 | if err != nil { | 85 | if err != nil { |
86 | return "", "", err | 86 | return "", "", err |
87 | } | 87 | } |
88 | 88 | ||
89 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) | 89 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) |
90 | rawdata = append(rawdata, rawstr...) | 90 | rawdata = append(rawdata, rawstr...) |
91 | rawdata = append(rawdata, rawsalt...) | 91 | rawdata = append(rawdata, rawsalt...) |
92 | 92 | ||
93 | // hash message + salt | 93 | // hash message + salt |
94 | hasher := sha256.New() | 94 | hasher := sha256.New() |
95 | hasher.Write(rawdata) | 95 | hasher.Write(rawdata) |
96 | rawhash := hasher.Sum(nil) | 96 | rawhash := hasher.Sum(nil) |
97 | 97 | ||
98 | hash = hex.EncodeToString(rawhash) | 98 | hash = hex.EncodeToString(rawhash) |
99 | return hash, salt, nil | 99 | return hash, salt, nil |
100 | } | 100 | } |
101 | 101 | ||
102 | // CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims. | 102 | // CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims. |
103 | // It returns an error if it fails. | 103 | // It returns an error if it fails. |
104 | func CreateAPIToken(username, role string, roleID uint32) (string, error) { | 104 | func CreateAPIToken(username, role string, roleID uint32) (string, error) { |
105 | var apiToken string | 105 | var apiToken string |
106 | var err error | 106 | var err error |
107 | 107 | ||
108 | if err != nil { | 108 | if err != nil { |
109 | return "", err | 109 | return "", err |
110 | } | 110 | } |
111 | 111 | ||
112 | claims := TokenClaims{ | 112 | claims := TokenClaims{ |
113 | username, | 113 | username, |
114 | role, | 114 | role, |
115 | roleID, | 115 | roleID, |
116 | jwt.StandardClaims{ | 116 | jwt.StandardClaims{ |
117 | ExpiresAt: (time.Now().Add(OneWeek)).Unix(), | 117 | ExpiresAt: (time.Now().Add(OneWeek)).Unix(), |
118 | Issuer: appName, | 118 | Issuer: appName, |
119 | }, | 119 | }, |
120 | } | 120 | } |
121 | 121 | ||
122 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | 122 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) |
123 | apiToken, err = jwtToken.SignedString([]byte(secret)) | 123 | apiToken, err = jwtToken.SignedString([]byte(secret)) |
124 | if err != nil { | 124 | if err != nil { |
125 | return "", err | 125 | return "", err |
126 | } | 126 | } |
127 | return apiToken, nil | 127 | return apiToken, nil |
128 | } | 128 | } |
129 | 129 | ||
130 | // RefreshAPIToken prolongs JWT token's expiration date for one week. | 130 | // RefreshAPIToken prolongs JWT token's expiration date for one week. |
131 | // It returns new JWT token or an error if it fails. | 131 | // It returns new JWT token or an error if it fails. |
132 | func RefreshAPIToken(tokenString string) (string, error) { | 132 | func RefreshAPIToken(tokenString string) (string, error) { |
133 | var newToken string | 133 | var newToken string |
134 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") | 134 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") |
135 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) | 135 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) |
136 | if err != nil { | 136 | if err != nil { |
137 | return "", err | 137 | return "", err |
138 | } | 138 | } |
139 | 139 | ||
140 | // type assertion | 140 | // type assertion |
141 | claims, ok := token.Claims.(*TokenClaims) | 141 | claims, ok := token.Claims.(*TokenClaims) |
142 | if !ok || !token.Valid { | 142 | if !ok || !token.Valid { |
143 | return "", errors.New("token is not valid") | 143 | return "", errors.New("token is not valid") |
144 | } | 144 | } |
145 | 145 | ||
146 | claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() | 146 | claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() |
147 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | 147 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) |
148 | 148 | ||
149 | newToken, err = jwtToken.SignedString([]byte(secret)) | 149 | newToken, err = jwtToken.SignedString([]byte(secret)) |
150 | if err != nil { | 150 | if err != nil { |
151 | return "", err | 151 | return "", err |
152 | } | 152 | } |
153 | 153 | ||
154 | return newToken, nil | 154 | return newToken, nil |
155 | } | 155 | } |
156 | 156 | ||
157 | // ParseAPIToken parses JWT token claims. | 157 | // ParseAPIToken parses JWT token claims. |
158 | // It returns a pointer to TokenClaims struct or an error if it fails. | 158 | // It returns a pointer to TokenClaims struct or an error if it fails. |
159 | func ParseAPIToken(tokenString string) (*TokenClaims, error) { | 159 | func ParseAPIToken(tokenString string) (*TokenClaims, error) { |
160 | if ok := strings.HasPrefix(tokenString, "Bearer "); ok { | 160 | if ok := strings.HasPrefix(tokenString, "Bearer "); ok { |
161 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") | 161 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") |
162 | } else { | 162 | } else { |
163 | return &TokenClaims{}, errors.New("Authorization header is incomplete") | 163 | return &TokenClaims{}, errors.New("Authorization header is incomplete") |
164 | } | 164 | } |
165 | 165 | ||
166 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) | 166 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) |
167 | if err != nil { | 167 | if err != nil { |
168 | return &TokenClaims{}, err | 168 | return &TokenClaims{}, err |
169 | } | 169 | } |
170 | 170 | ||
171 | // type assertion | 171 | // type assertion |
172 | claims, ok := token.Claims.(*TokenClaims) | 172 | claims, ok := token.Claims.(*TokenClaims) |
173 | if !ok || !token.Valid { | 173 | if !ok || !token.Valid { |
174 | return &TokenClaims{}, errors.New("token is not valid") | 174 | return &TokenClaims{}, errors.New("token is not valid") |
175 | } | 175 | } |
176 | return claims, nil | 176 | return claims, nil |
177 | } | 177 | } |
178 | 178 | ||
179 | func GetTokenClaims(r *http.Request) (claims *TokenClaims, err error) { | 179 | func GetTokenClaims(r *http.Request) (claims *TokenClaims, err error) { |
180 | token := r.Header.Get("Authorization") | 180 | token := r.Header.Get("Authorization") |
181 | if ok := strings.HasPrefix(token, "Bearer "); ok { | 181 | if ok := strings.HasPrefix(token, "Bearer "); ok { |
182 | token = strings.TrimPrefix(token, "Bearer ") | 182 | token = strings.TrimPrefix(token, "Bearer ") |
183 | } else { | 183 | } else { |
184 | return &TokenClaims{}, errors.New("Authorization header is incomplete") | 184 | return &TokenClaims{}, errors.New("Authorization header is incomplete") |
185 | } | 185 | } |
186 | 186 | ||
187 | parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc) | 187 | parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc) |
188 | if err != nil { | 188 | if err != nil { |
189 | return &TokenClaims{}, err | 189 | return &TokenClaims{}, err |
190 | } | 190 | } |
191 | 191 | ||
192 | // type assertion | 192 | // type assertion |
193 | claims, ok := parsedToken.Claims.(*TokenClaims) | 193 | claims, ok := parsedToken.Claims.(*TokenClaims) |
194 | if !ok || !parsedToken.Valid { | 194 | if !ok || !parsedToken.Valid { |
195 | return &TokenClaims{}, errors.New("token is not valid") | 195 | return &TokenClaims{}, errors.New("token is not valid") |
196 | } | 196 | } |
197 | return claims, err | 197 | return claims, err |
198 | } | 198 | } |
199 | 199 | ||
200 | // secretFunc returns byte slice of API secret keyword. | 200 | // secretFunc returns byte slice of API secret keyword. |
201 | func secretFunc(token *jwt.Token) (interface{}, error) { | 201 | func secretFunc(token *jwt.Token) (interface{}, error) { |
202 | return []byte(secret), nil | 202 | return []byte(secret), nil |
203 | } | 203 | } |
204 | 204 | ||
205 | // rbacEnforce returns true if role that made HTTP request is authorized to | 205 | // RbacCheck returns true if role that made HTTP request is authorized to |
206 | // access the resource it is targeting. | 206 | // access the resource it is targeting. |
207 | // It exctracts user's role from the JWT token located in Authorization header of | 207 | // It exctracts user's role from the JWT token located in Authorization header of |
208 | // http.Request and then compares it with the list of supplied roles and returns | 208 | // http.Request and then compares it with the list of supplied roles and returns |
209 | // true if there's a match, if "*" is provided or if the authRoles is nil. | 209 | // true if there's a match, if "*" is provided or if the authRoles is nil. |
210 | // Otherwise it returns false. | 210 | // Otherwise it returns false. |
211 | func RbacCheck(req *http.Request, authRoles []string) bool { | 211 | func RbacCheck(req *http.Request, authRoles []string) bool { |
212 | if authRoles == nil { | 212 | if authRoles == nil { |
213 | return true | 213 | return true |
214 | } | 214 | } |
215 | 215 | ||
216 | token := req.Header.Get("Authorization") | 216 | token := req.Header.Get("Authorization") |
217 | claims, err := ParseAPIToken(token) | 217 | claims, err := ParseAPIToken(token) |
218 | if err != nil { | 218 | if err != nil { |
219 | return false | 219 | return false |
220 | } | 220 | } |
221 | 221 | ||
222 | for _, r := range authRoles { | 222 | for _, r := range authRoles { |
223 | if claims.Role == r || r == "*" { | 223 | if claims.Role == r || r == "*" { |
224 | return true | 224 | return true |
225 | } | 225 | } |
226 | } | 226 | } |
227 | 227 | ||
228 | return false | 228 | return false |
229 | } | 229 | } |
230 | 230 | ||
231 | // Rbac sets common headers and performs RBAC. | 231 | // Rbac sets common headers and performs RBAC. |
232 | // If RBAC passes it calls the handlerFunc. | 232 | // If RBAC passes it calls the handlerFunc. |
233 | func Rbac(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc { | 233 | func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc { |
234 | return func(w http.ResponseWriter, req *http.Request) { | 234 | return func(w http.ResponseWriter, req *http.Request) { |
235 | w.Header().Set("Access-Control-Allow-Origin", "*") | 235 | w.Header().Set("Access-Control-Allow-Origin", "*") |
236 | 236 | ||
237 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") | 237 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") |
238 | 238 | ||
239 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, | 239 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, |
240 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) | 240 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) |
241 | 241 | ||
242 | w.Header().Set("Content-Type", "application/json; charset=utf-8") | 242 | w.Header().Set("Content-Type", "application/json; charset=utf-8") |
243 | 243 | ||
244 | // TODO: Check for content type | 244 | // TODO: Check for content type |
245 | 245 | ||
246 | if req.Method == "OPTIONS" { | 246 | if req.Method == "OPTIONS" { |
247 | return | 247 | return |
248 | } | 248 | } |
249 | 249 | ||
250 | err := req.ParseForm() | 250 | err := req.ParseForm() |
251 | if err != nil { | 251 | if err != nil { |
252 | BadRequestResponse(w, req) | 252 | BadRequestResponse(w, req) |
253 | return | 253 | return |
254 | } | 254 | } |
255 | 255 | ||
256 | if !RbacCheck(req, authRoles) { | 256 | if !RbacCheck(req, authRoles) { |
257 | UnauthorizedResponse(w, req) | 257 | UnauthorizedResponse(w, req) |
258 | return | 258 | return |
259 | } | 259 | } |
260 | 260 | ||
261 | // execute HandlerFunc | 261 | // execute HandlerFunc |
262 | handlerFunc(w, req) | 262 | handlerFunc(w, req) |
263 | } | 263 | } |
264 | } | 264 | } |
265 | 265 |