Commit d29773cc4616afb3fe833cf21bb191451be2d101
1 parent
052f8a3a63
Exists in
master
and in
1 other branch
ProcessRBAC
Showing
1 changed file
with
32 additions
and
1 deletions
Show diff stats
auth_utility.go
1 | package webutility | 1 | package webutility |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "crypto/rand" | 4 | "crypto/rand" |
5 | "crypto/sha256" | 5 | "crypto/sha256" |
6 | "encoding/hex" | 6 | "encoding/hex" |
7 | "errors" | 7 | "errors" |
8 | "net/http" | 8 | "net/http" |
9 | "strings" | 9 | "strings" |
10 | "time" | 10 | "time" |
11 | 11 | ||
12 | "github.com/dgrijalva/jwt-go" | 12 | "github.com/dgrijalva/jwt-go" |
13 | ) | 13 | ) |
14 | 14 | ||
15 | const OneDay = time.Hour * 24 | 15 | const OneDay = time.Hour * 24 |
16 | const OneWeek = OneDay * 7 | 16 | const OneWeek = OneDay * 7 |
17 | const saltSize = 32 | 17 | const saltSize = 32 |
18 | const appName = "korisnicki-centar" | 18 | const appName = "korisnicki-centar" |
19 | const secret = "korisnicki-centar-api" | 19 | const secret = "korisnicki-centar-api" |
20 | 20 | ||
21 | type Role struct { | 21 | type Role struct { |
22 | Name string `json:"name"` | 22 | Name string `json:"name"` |
23 | ID uint32 `json:"id"` | 23 | ID uint32 `json:"id"` |
24 | } | 24 | } |
25 | 25 | ||
26 | // TokenClaims are JWT token claims. | 26 | // TokenClaims are JWT token claims. |
27 | type TokenClaims struct { | 27 | type TokenClaims struct { |
28 | Token string `json:"access_token"` | 28 | Token string `json:"access_token"` |
29 | TokenType string `json:"token_type"` | 29 | TokenType string `json:"token_type"` |
30 | Username string `json:"username"` | 30 | Username string `json:"username"` |
31 | Role string `json:"role"` | 31 | Role string `json:"role"` |
32 | RoleID uint32 `json:"role_id"` | 32 | RoleID uint32 `json:"role_id"` |
33 | ExpiresIn int64 `json:"expires_in"` | 33 | ExpiresIn int64 `json:"expires_in"` |
34 | 34 | ||
35 | // extending a struct | 35 | // extending a struct |
36 | jwt.StandardClaims | 36 | jwt.StandardClaims |
37 | } | 37 | } |
38 | 38 | ||
39 | // CredentialsStruct is an instace of username/password values. | 39 | // CredentialsStruct is an instace of username/password values. |
40 | type CredentialsStruct struct { | 40 | type CredentialsStruct struct { |
41 | Username string `json:"username"` | 41 | Username string `json:"username"` |
42 | Password string `json:"password"` | 42 | Password string `json:"password"` |
43 | RoleID uint32 `json:"roleID"` | 43 | RoleID uint32 `json:"roleID"` |
44 | } | 44 | } |
45 | 45 | ||
46 | // ValidateCredentials hashes pass and salt and returns comparison result with resultHash | 46 | // ValidateCredentials hashes pass and salt and returns comparison result with resultHash |
47 | func ValidateCredentials(pass, salt, resultHash string) bool { | 47 | func ValidateCredentials(pass, salt, resultHash string) bool { |
48 | hash, _, err := CreateHash(pass, salt) | 48 | hash, _, err := CreateHash(pass, salt) |
49 | if err != nil { | 49 | if err != nil { |
50 | return false | 50 | return false |
51 | } | 51 | } |
52 | return hash == resultHash | 52 | return hash == resultHash |
53 | } | 53 | } |
54 | 54 | ||
55 | // CreateHash hashes str using SHA256. | 55 | // CreateHash hashes str using SHA256. |
56 | // If the presalt parameter is not provided CreateHash will generate new salt string. | 56 | // If the presalt parameter is not provided CreateHash will generate new salt string. |
57 | // Returns hash and salt strings or an error if it fails. | 57 | // Returns hash and salt strings or an error if it fails. |
58 | func CreateHash(str, presalt string) (hash, salt string, err error) { | 58 | func CreateHash(str, presalt string) (hash, salt string, err error) { |
59 | // chech if message is presalted | 59 | // chech if message is presalted |
60 | if presalt == "" { | 60 | if presalt == "" { |
61 | salt, err = randomSalt() | 61 | salt, err = randomSalt() |
62 | if err != nil { | 62 | if err != nil { |
63 | return "", "", err | 63 | return "", "", err |
64 | } | 64 | } |
65 | } else { | 65 | } else { |
66 | salt = presalt | 66 | salt = presalt |
67 | } | 67 | } |
68 | 68 | ||
69 | // convert strings to raw byte slices | 69 | // convert strings to raw byte slices |
70 | rawstr := []byte(str) | 70 | rawstr := []byte(str) |
71 | rawsalt, err := hex.DecodeString(salt) | 71 | rawsalt, err := hex.DecodeString(salt) |
72 | if err != nil { | 72 | if err != nil { |
73 | return "", "", err | 73 | return "", "", err |
74 | } | 74 | } |
75 | 75 | ||
76 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) | 76 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) |
77 | rawdata = append(rawdata, rawstr...) | 77 | rawdata = append(rawdata, rawstr...) |
78 | rawdata = append(rawdata, rawsalt...) | 78 | rawdata = append(rawdata, rawsalt...) |
79 | 79 | ||
80 | // hash message + salt | 80 | // hash message + salt |
81 | hasher := sha256.New() | 81 | hasher := sha256.New() |
82 | hasher.Write(rawdata) | 82 | hasher.Write(rawdata) |
83 | rawhash := hasher.Sum(nil) | 83 | rawhash := hasher.Sum(nil) |
84 | 84 | ||
85 | hash = hex.EncodeToString(rawhash) | 85 | hash = hex.EncodeToString(rawhash) |
86 | return hash, salt, nil | 86 | return hash, salt, nil |
87 | } | 87 | } |
88 | 88 | ||
89 | // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims. | 89 | // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims. |
90 | // It returns an error if it fails. | 90 | // It returns an error if it fails. |
91 | func CreateAuthToken(username string, role Role) (TokenClaims, error) { | 91 | func CreateAuthToken(username string, role Role) (TokenClaims, error) { |
92 | t0 := (time.Now()).Unix() | 92 | t0 := (time.Now()).Unix() |
93 | t1 := (time.Now().Add(OneWeek)).Unix() | 93 | t1 := (time.Now().Add(OneWeek)).Unix() |
94 | claims := TokenClaims{ | 94 | claims := TokenClaims{ |
95 | TokenType: "Bearer", | 95 | TokenType: "Bearer", |
96 | Username: username, | 96 | Username: username, |
97 | Role: role.Name, | 97 | Role: role.Name, |
98 | RoleID: role.ID, | 98 | RoleID: role.ID, |
99 | ExpiresIn: t1 - t0, | 99 | ExpiresIn: t1 - t0, |
100 | } | 100 | } |
101 | // initialize jwt.StandardClaims fields (anonymous struct) | 101 | // initialize jwt.StandardClaims fields (anonymous struct) |
102 | claims.IssuedAt = t0 | 102 | claims.IssuedAt = t0 |
103 | claims.ExpiresAt = t1 | 103 | claims.ExpiresAt = t1 |
104 | claims.Issuer = appName | 104 | claims.Issuer = appName |
105 | 105 | ||
106 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | 106 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) |
107 | token, err := jwtToken.SignedString([]byte(secret)) | 107 | token, err := jwtToken.SignedString([]byte(secret)) |
108 | if err != nil { | 108 | if err != nil { |
109 | return TokenClaims{}, err | 109 | return TokenClaims{}, err |
110 | } | 110 | } |
111 | claims.Token = token | 111 | claims.Token = token |
112 | return claims, nil | 112 | return claims, nil |
113 | } | 113 | } |
114 | 114 | ||
115 | // RefreshAuthToken prolongs JWT token's expiration date for one week. | 115 | // RefreshAuthToken prolongs JWT token's expiration date for one week. |
116 | // It returns new JWT token or an error if it fails. | 116 | // It returns new JWT token or an error if it fails. |
117 | func RefreshAuthToken(req *http.Request) (TokenClaims, error) { | 117 | func RefreshAuthToken(req *http.Request) (TokenClaims, error) { |
118 | authHead := req.Header.Get("Authorization") | 118 | authHead := req.Header.Get("Authorization") |
119 | tokenstr := strings.TrimPrefix(authHead, "Bearer ") | 119 | tokenstr := strings.TrimPrefix(authHead, "Bearer ") |
120 | token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc) | 120 | token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc) |
121 | if err != nil { | 121 | if err != nil { |
122 | if validation, ok := err.(*jwt.ValidationError); ok { | 122 | if validation, ok := err.(*jwt.ValidationError); ok { |
123 | // don't return error if token is expired | 123 | // don't return error if token is expired |
124 | // just extend it | 124 | // just extend it |
125 | if !(validation.Errors&jwt.ValidationErrorExpired != 0) { | 125 | if !(validation.Errors&jwt.ValidationErrorExpired != 0) { |
126 | return TokenClaims{}, err | 126 | return TokenClaims{}, err |
127 | } | 127 | } |
128 | } else { | 128 | } else { |
129 | return TokenClaims{}, err | 129 | return TokenClaims{}, err |
130 | } | 130 | } |
131 | } | 131 | } |
132 | 132 | ||
133 | // type assertion | 133 | // type assertion |
134 | claims, ok := token.Claims.(*TokenClaims) | 134 | claims, ok := token.Claims.(*TokenClaims) |
135 | if !ok { | 135 | if !ok { |
136 | return TokenClaims{}, errors.New("token is not valid") | 136 | return TokenClaims{}, errors.New("token is not valid") |
137 | } | 137 | } |
138 | 138 | ||
139 | // extend token expiration date | 139 | // extend token expiration date |
140 | return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID}) | 140 | return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID}) |
141 | } | 141 | } |
142 | 142 | ||
143 | // RbacCheck returns true if role that made HTTP request is authorized to | 143 | // RbacCheck returns true if user that made HTTP request is authorized to |
144 | // access the resource it is targeting. | 144 | // access the resource it is targeting. |
145 | // It exctracts user's role from the JWT token located in Authorization header of | 145 | // It exctracts user's role from the JWT token located in Authorization header of |
146 | // http.Request and then compares it with the list of supplied roles and returns | 146 | // http.Request and then compares it with the list of supplied roles and returns |
147 | // true if there's a match, if "*" is provided or if the authRoles is nil. | 147 | // true if there's a match, if "*" is provided or if the authRoles is nil. |
148 | // Otherwise it returns false. | 148 | // Otherwise it returns false. |
149 | func RbacCheck(req *http.Request, authRoles []string) bool { | 149 | func RbacCheck(req *http.Request, authRoles []string) bool { |
150 | if authRoles == nil { | 150 | if authRoles == nil { |
151 | return true | 151 | return true |
152 | } | 152 | } |
153 | 153 | ||
154 | // validate token and check expiration date | 154 | // validate token and check expiration date |
155 | claims, err := GetTokenClaims(req) | 155 | claims, err := GetTokenClaims(req) |
156 | if err != nil { | 156 | if err != nil { |
157 | return false | 157 | return false |
158 | } | 158 | } |
159 | // check if token has expired | 159 | // check if token has expired |
160 | if claims.ExpiresAt < (time.Now()).Unix() { | 160 | if claims.ExpiresAt < (time.Now()).Unix() { |
161 | return false | 161 | return false |
162 | } | 162 | } |
163 | 163 | ||
164 | // check if role extracted from token matches | 164 | // check if role extracted from token matches |
165 | // any of the provided (allowed) ones | 165 | // any of the provided (allowed) ones |
166 | for _, r := range authRoles { | 166 | for _, r := range authRoles { |
167 | if claims.Role == r || r == "*" { | 167 | if claims.Role == r || r == "*" { |
168 | return true | 168 | return true |
169 | } | 169 | } |
170 | } | 170 | } |
171 | 171 | ||
172 | return false | 172 | return false |
173 | } | 173 | } |
174 | 174 | ||
175 | // ProcessRBAC returns token claims and boolean value based on user's rights to access resource specified in req. | ||
176 | // It exctracts user's role from the JWT token located in Authorization header of | ||
177 | // http.Request and then compares it with the list of supplied roles and returns | ||
178 | // true if there's a match, if "*" is provided or if the authRoles is nil. | ||
179 | // Otherwise it returns false. | ||
180 | func ProcessRBAC(req *http.Request, authRoles []string) (*TokenClaims, bool) { | ||
181 | if authRoles == nil { | ||
182 | return nil, true | ||
183 | } | ||
184 | |||
185 | // validate token and check expiration date | ||
186 | claims, err := GetTokenClaims(req) | ||
187 | if err != nil { | ||
188 | return claims, false | ||
189 | } | ||
190 | // check if token has expired | ||
191 | if claims.ExpiresAt < (time.Now()).Unix() { | ||
192 | return claims, false | ||
193 | } | ||
194 | |||
195 | // check if role extracted from token matches | ||
196 | // any of the provided (allowed) ones | ||
197 | for _, r := range authRoles { | ||
198 | if claims.Role == r || r == "*" { | ||
199 | return claims, true | ||
200 | } | ||
201 | } | ||
202 | |||
203 | return claims, false | ||
204 | } | ||
205 | |||
175 | // GetTokenClaims extracts JWT claims from Authorization header of the request. | 206 | // GetTokenClaims extracts JWT claims from Authorization header of the request. |
176 | // Returns token claims or an error. | 207 | // Returns token claims or an error. |
177 | func GetTokenClaims(req *http.Request) (*TokenClaims, error) { | 208 | func GetTokenClaims(req *http.Request) (*TokenClaims, error) { |
178 | // check for and strip 'Bearer' prefix | 209 | // check for and strip 'Bearer' prefix |
179 | var tokstr string | 210 | var tokstr string |
180 | authHead := req.Header.Get("Authorization") | 211 | authHead := req.Header.Get("Authorization") |
181 | if ok := strings.HasPrefix(authHead, "Bearer "); ok { | 212 | if ok := strings.HasPrefix(authHead, "Bearer "); ok { |
182 | tokstr = strings.TrimPrefix(authHead, "Bearer ") | 213 | tokstr = strings.TrimPrefix(authHead, "Bearer ") |
183 | } else { | 214 | } else { |
184 | return &TokenClaims{}, errors.New("authorization header in incomplete") | 215 | return &TokenClaims{}, errors.New("authorization header in incomplete") |
185 | } | 216 | } |
186 | 217 | ||
187 | token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc) | 218 | token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc) |
188 | if err != nil { | 219 | if err != nil { |
189 | return &TokenClaims{}, err | 220 | return &TokenClaims{}, err |
190 | } | 221 | } |
191 | 222 | ||
192 | // type assertion | 223 | // type assertion |
193 | claims, ok := token.Claims.(*TokenClaims) | 224 | claims, ok := token.Claims.(*TokenClaims) |
194 | if !ok || !token.Valid { | 225 | if !ok || !token.Valid { |
195 | return &TokenClaims{}, errors.New("token is not valid") | 226 | return &TokenClaims{}, errors.New("token is not valid") |
196 | } | 227 | } |
197 | 228 | ||
198 | return claims, nil | 229 | return claims, nil |
199 | } | 230 | } |
200 | 231 | ||
201 | // randomSalt returns a string of random characters of 'saltSize' length. | 232 | // randomSalt returns a string of random characters of 'saltSize' length. |
202 | func randomSalt() (s string, err error) { | 233 | func randomSalt() (s string, err error) { |
203 | rawsalt := make([]byte, saltSize) | 234 | rawsalt := make([]byte, saltSize) |
204 | 235 | ||
205 | _, err = rand.Read(rawsalt) | 236 | _, err = rand.Read(rawsalt) |
206 | if err != nil { | 237 | if err != nil { |
207 | return "", err | 238 | return "", err |
208 | } | 239 | } |
209 | 240 | ||
210 | s = hex.EncodeToString(rawsalt) | 241 | s = hex.EncodeToString(rawsalt) |
211 | return s, nil | 242 | return s, nil |
212 | } | 243 | } |
213 | 244 | ||
214 | // secretFunc returns byte slice of API secret keyword. | 245 | // secretFunc returns byte slice of API secret keyword. |
215 | func secretFunc(token *jwt.Token) (interface{}, error) { | 246 | func secretFunc(token *jwt.Token) (interface{}, error) { |
216 | return []byte(secret), nil | 247 | return []byte(secret), nil |
217 | } | 248 | } |
218 | 249 |