Commit bc3671b260f97ed4c81baeb2e2fbedb29f47f693
1 parent
077dae33c8
Exists in
master
and in
1 other branch
refactoring token creation, validation and refreshing; refactoring rbac to return user data
Showing
2 changed files
with
90 additions
and
136 deletions
Show diff stats
auth_utility.go
1 | // TODO: Improve roles | ||
2 | package webutility | 1 | package webutility |
3 | 2 | ||
4 | import ( | 3 | import ( |
5 | "crypto/rand" | 4 | "crypto/rand" |
6 | "crypto/sha256" | 5 | "crypto/sha256" |
7 | "encoding/hex" | 6 | "encoding/hex" |
8 | "errors" | 7 | "errors" |
9 | "net/http" | 8 | "net/http" |
10 | "strings" | 9 | "strings" |
11 | "time" | 10 | "time" |
12 | 11 | ||
13 | "github.com/dgrijalva/jwt-go" | 12 | "github.com/dgrijalva/jwt-go" |
14 | ) | 13 | ) |
15 | 14 | ||
16 | const OneDay = time.Hour * 24 | 15 | const OneDay = time.Hour * 24 |
17 | const OneWeek = OneDay * 7 | 16 | const OneWeek = OneDay * 7 |
18 | const saltSize = 32 | 17 | const saltSize = 32 |
19 | const appName = "korisnicki-centar" | 18 | const appName = "korisnicki-centar" |
20 | const secret = "korisnicki-centar-api" | 19 | const secret = "korisnicki-centar-api" |
21 | 20 | ||
22 | type Role struct { | 21 | type Role struct { |
23 | Name string `json:"name"` | 22 | Name string `json:"name"` |
24 | ID uint32 `json:"id"` | 23 | ID uint32 `json:"id"` |
25 | } | 24 | } |
26 | 25 | ||
27 | // TokenClaims are JWT token claims. | 26 | // TokenClaims are JWT token claims. |
28 | type TokenClaims struct { | 27 | type TokenClaims struct { |
29 | Username string `json:"username"` | 28 | Token string `json:"access_token"` |
30 | Role string `json:"role"` | 29 | TokenType string `json:"token_type"` |
31 | RoleID uint32 `json:"roleID"` | 30 | Username string `json:"username"` |
32 | jwt.StandardClaims | 31 | Role string `json:"role"` |
32 | RoleID uint32 `json:"role_id"` | ||
33 | ExpiresIn int64 `json:"expires_in"` | ||
34 | jwt.StandardClaims // extending a struct | ||
33 | } | 35 | } |
34 | 36 | ||
35 | // CredentialsStruct is an instace of username/password values. | 37 | // CredentialsStruct is an instace of username/password values. |
36 | type CredentialsStruct struct { | 38 | type CredentialsStruct struct { |
37 | Username string `json:"username"` | 39 | Username string `json:"username"` |
38 | Password string `json:"password"` | 40 | Password string `json:"password"` |
39 | } | 41 | } |
40 | 42 | ||
41 | // generateSalt returns a string of random characters of 'saltSize' length. | 43 | // ValidateCredentials hashes pass and salt and returns comparison result with resultHash |
42 | func generateSalt() (salt string, err error) { | 44 | func ValidateCredentials(pass, salt, resultHash string) bool { |
43 | rawsalt := make([]byte, saltSize) | 45 | hash, _, err := CreateHash(pass, salt) |
44 | |||
45 | _, err = rand.Read(rawsalt) | ||
46 | if err != nil { | 46 | if err != nil { |
47 | return "", err | 47 | return false |
48 | } | 48 | } |
49 | 49 | return hash == resultHash | |
50 | salt = hex.EncodeToString(rawsalt) | ||
51 | return salt, nil | ||
52 | } | 50 | } |
53 | 51 | ||
54 | // HashString hashes input string using SHA256. | 52 | // CreateHash hashes str using SHA256. |
55 | // If the presalt parameter is not provided HashString will generate new salt string. | 53 | // If the presalt parameter is not provided CreateHash will generate new salt string. |
56 | // Returns hash and salt string or an error if it fails. | 54 | // Returns hash and salt strings or an error if it fails. |
57 | func HashString(str, presalt string) (hash, salt string, err error) { | 55 | func CreateHash(str, presalt string) (hash, salt string, err error) { |
58 | // chech if message is presalted | 56 | // chech if message is presalted |
59 | if presalt == "" { | 57 | if presalt == "" { |
60 | salt, err = generateSalt() | 58 | salt, err = randomSalt() |
61 | if err != nil { | 59 | if err != nil { |
62 | return "", "", err | 60 | return "", "", err |
63 | } | 61 | } |
64 | } else { | 62 | } else { |
65 | salt = presalt | 63 | salt = presalt |
66 | } | 64 | } |
67 | 65 | ||
68 | // convert strings to raw byte slices | 66 | // convert strings to raw byte slices |
69 | rawstr := []byte(str) | 67 | rawstr := []byte(str) |
70 | rawsalt, err := hex.DecodeString(salt) | 68 | rawsalt, err := hex.DecodeString(salt) |
71 | if err != nil { | 69 | if err != nil { |
72 | return "", "", err | 70 | return "", "", err |
73 | } | 71 | } |
74 | 72 | ||
75 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) | 73 | rawdata := make([]byte, len(rawstr)+len(rawsalt)) |
76 | rawdata = append(rawdata, rawstr...) | 74 | rawdata = append(rawdata, rawstr...) |
77 | rawdata = append(rawdata, rawsalt...) | 75 | rawdata = append(rawdata, rawsalt...) |
78 | 76 | ||
79 | // hash message + salt | 77 | // hash message + salt |
80 | hasher := sha256.New() | 78 | hasher := sha256.New() |
81 | hasher.Write(rawdata) | 79 | hasher.Write(rawdata) |
82 | rawhash := hasher.Sum(nil) | 80 | rawhash := hasher.Sum(nil) |
83 | 81 | ||
84 | hash = hex.EncodeToString(rawhash) | 82 | hash = hex.EncodeToString(rawhash) |
85 | return hash, salt, nil | 83 | return hash, salt, nil |
86 | } | 84 | } |
87 | 85 | ||
88 | // CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims. | 86 | // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims. |
89 | // It returns an error if it fails. | 87 | // It returns an error if it fails. |
90 | func CreateAPIToken(username string, role Role) (string, error) { | 88 | func CreateAuthToken(username string, role Role) (TokenClaims, error) { |
91 | var apiToken string | 89 | t0 := (time.Now()).Unix() |
92 | var err error | 90 | t1 := (time.Now().Add(OneWeek)).Unix() |
93 | |||
94 | if err != nil { | ||
95 | return "", err | ||
96 | } | ||
97 | |||
98 | claims := TokenClaims{ | 91 | claims := TokenClaims{ |
99 | username, | 92 | TokenType: "Bearer", |
100 | role.Name, | 93 | Username: username, |
101 | role.ID, | 94 | Role: role.Name, |
102 | jwt.StandardClaims{ | 95 | RoleID: role.ID, |
103 | ExpiresAt: (time.Now().Add(OneWeek)).Unix(), | 96 | ExpiresIn: t1 - t0, |
104 | Issuer: appName, | ||
105 | }, | ||
106 | } | 97 | } |
98 | // initialize jwt.StandardClaims fields (anonymous struct) | ||
99 | claims.IssuedAt = t0 | ||
100 | claims.ExpiresAt = t1 | ||
101 | claims.Issuer = appName | ||
107 | 102 | ||
108 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | 103 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) |
109 | apiToken, err = jwtToken.SignedString([]byte(secret)) | 104 | token, err := jwtToken.SignedString([]byte(secret)) |
110 | if err != nil { | 105 | if err != nil { |
111 | return "", err | 106 | return TokenClaims{}, err |
112 | } | 107 | } |
113 | return apiToken, nil | 108 | claims.Token = token |
109 | return claims, nil | ||
114 | } | 110 | } |
115 | 111 | ||
116 | // RefreshAPIToken prolongs JWT token's expiration date for one week. | 112 | // RefreshAuthToken prolongs JWT token's expiration date for one week. |
117 | // It returns new JWT token or an error if it fails. | 113 | // It returns new JWT token or an error if it fails. |
118 | func RefreshAPIToken(tokenString string) (string, error) { | 114 | func RefreshAuthToken(req *http.Request) (TokenClaims, error) { |
119 | var newToken string | 115 | authHead := req.Header.Get("Authorization") |
120 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") | 116 | tokenstr := strings.TrimPrefix(authHead, "Bearer ") |
121 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) | 117 | token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc) |
122 | if err != nil { | 118 | if err != nil { |
123 | return "", err | 119 | return TokenClaims{}, err |
124 | } | 120 | } |
125 | 121 | ||
126 | // type assertion | 122 | // type assertion |
127 | claims, ok := token.Claims.(*TokenClaims) | 123 | claims, ok := token.Claims.(*TokenClaims) |
128 | if !ok || !token.Valid { | 124 | if !ok || !token.Valid { |
129 | return "", errors.New("token is not valid") | 125 | return TokenClaims{}, errors.New("token is not valid") |
130 | } | ||
131 | |||
132 | claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() | ||
133 | jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
134 | |||
135 | newToken, err = jwtToken.SignedString([]byte(secret)) | ||
136 | if err != nil { | ||
137 | return "", err | ||
138 | } | 126 | } |
139 | 127 | ||
140 | return newToken, nil | 128 | // extend token expiration date |
129 | return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID}) | ||
141 | } | 130 | } |
142 | 131 | ||
143 | // ParseAPIToken parses JWT token claims. | 132 | // RbacCheck returns true if role that made HTTP request is authorized to |
144 | // It returns a pointer to TokenClaims struct or an error if it fails. | 133 | // access the resource it is targeting. |
145 | func ParseAPIToken(tokenString string) (*TokenClaims, error) { | 134 | // It exctracts user's role from the JWT token located in Authorization header of |
146 | if ok := strings.HasPrefix(tokenString, "Bearer "); ok { | 135 | // http.Request and then compares it with the list of supplied roles and returns |
147 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") | 136 | // true if there's a match, if "*" is provided or if the authRoles is nil. |
148 | } else { | 137 | // Otherwise it returns false. |
149 | return &TokenClaims{}, errors.New("Authorization header is incomplete") | 138 | func RbacCheck(req *http.Request, authRoles []string) (*TokenClaims, error) { |
139 | if authRoles == nil { | ||
140 | return &TokenClaims{}, nil | ||
150 | } | 141 | } |
151 | 142 | ||
152 | token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) | 143 | // validate token and check expiration date |
144 | claims, err := GetTokenClaims(req) | ||
153 | if err != nil { | 145 | if err != nil { |
154 | return &TokenClaims{}, err | 146 | return &TokenClaims{}, err |
155 | } | 147 | } |
148 | // check if token has expired | ||
149 | if claims.ExpiresAt < (time.Now()).Unix() { | ||
150 | return &TokenClaims{}, errors.New("token has expired") | ||
151 | } | ||
156 | 152 | ||
157 | // type assertion | 153 | // check if role extracted from token matches |
158 | claims, ok := token.Claims.(*TokenClaims) | 154 | // any of the provided (allowed) ones |
159 | if !ok || !token.Valid { | 155 | for _, r := range authRoles { |
160 | return &TokenClaims{}, errors.New("token is not valid") | 156 | if claims.Role == r || r == "*" { |
157 | return claims, nil | ||
158 | } | ||
161 | } | 159 | } |
162 | return claims, nil | 160 | |
161 | return &TokenClaims{}, errors.New("role is not authorized") | ||
163 | } | 162 | } |
164 | 163 | ||
165 | func GetTokenClaims(r *http.Request) (claims *TokenClaims, err error) { | 164 | // GetTokenClaims extracts JWT claims from Authorization header of the request. |
166 | token := r.Header.Get("Authorization") | 165 | // Returns token claims or an error. |
167 | if ok := strings.HasPrefix(token, "Bearer "); ok { | 166 | func GetTokenClaims(req *http.Request) (*TokenClaims, error) { |
168 | token = strings.TrimPrefix(token, "Bearer ") | 167 | // check for and strip 'Bearer' prefix |
168 | var tokstr string | ||
169 | authHead := req.Header.Get("Authorization") | ||
170 | if ok := strings.HasPrefix(authHead, "Bearer "); ok { | ||
171 | tokstr = strings.TrimPrefix(tokstr, "Bearer ") | ||
169 | } else { | 172 | } else { |
170 | return &TokenClaims{}, errors.New("Authorization header is incomplete") | 173 | return &TokenClaims{}, errors.New("authorization header in incomplete") |
171 | } | 174 | } |
172 | 175 | ||
173 | parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc) | 176 | token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc) |
174 | if err != nil { | 177 | if err != nil { |
175 | return &TokenClaims{}, err | 178 | return &TokenClaims{}, err |
176 | } | 179 | } |
177 | 180 | ||
178 | // type assertion | 181 | // type assertion |
179 | claims, ok := parsedToken.Claims.(*TokenClaims) | 182 | claims, ok := token.Claims.(*TokenClaims) |
180 | if !ok || !parsedToken.Valid { | 183 | if !ok || !token.Valid { |
181 | return &TokenClaims{}, errors.New("token is not valid") | 184 | return &TokenClaims{}, errors.New("token is not valid") |
182 | } | 185 | } |
183 | return claims, err | ||
184 | } | ||
185 | 186 | ||
186 | // secretFunc returns byte slice of API secret keyword. | 187 | return claims, nil |
187 | func secretFunc(token *jwt.Token) (interface{}, error) { | ||
188 | return []byte(secret), nil | ||
189 | } | 188 | } |
190 | 189 | ||
191 | // RbacCheck returns true if role that made HTTP request is authorized to | 190 | // randomSalt returns a string of random characters of 'saltSize' length. |
192 | // access the resource it is targeting. | 191 | func randomSalt() (s string, err error) { |
193 | // It exctracts user's role from the JWT token located in Authorization header of | 192 | rawsalt := make([]byte, saltSize) |
194 | // http.Request and then compares it with the list of supplied roles and returns | ||
195 | // true if there's a match, if "*" is provided or if the authRoles is nil. | ||
196 | // Otherwise it returns false. | ||
197 | func RbacCheck(req *http.Request, authRoles []string) bool { | ||
198 | if authRoles == nil { | ||
199 | return true | ||
200 | } | ||
201 | 193 | ||
202 | token := req.Header.Get("Authorization") | 194 | _, err = rand.Read(rawsalt) |
203 | claims, err := ParseAPIToken(token) | ||
204 | if err != nil { | 195 | if err != nil { |
205 | return false | 196 | return "", err |
206 | } | ||
207 | |||
208 | for _, r := range authRoles { | ||
209 | if claims.Role == r || r == "*" { | ||
210 | return true | ||
211 | } | ||
212 | } | 197 | } |
213 | 198 | ||
214 | return false | 199 | s = hex.EncodeToString(rawsalt) |
200 | return s, nil | ||
215 | } | 201 | } |
216 | 202 | ||
217 | // Rbac sets common headers and performs RBAC. | 203 | // secretFunc returns byte slice of API secret keyword. |
218 | // If RBAC passes it calls the handlerFunc. | 204 | func secretFunc(token *jwt.Token) (interface{}, error) { |
219 | func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc { | 205 | return []byte(secret), nil |
220 | return func(w http.ResponseWriter, req *http.Request) { | ||
221 | w.Header().Set("Access-Control-Allow-Origin", "*") | ||
222 | |||
223 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") | ||
224 | |||
225 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, | ||
226 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) | ||
227 |
http_utility.go
1 | package webutility | 1 | package webutility |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "encoding/json" | 4 | "encoding/json" |
5 | "net/http" | 5 | "net/http" |
6 | ) | 6 | ) |
7 | 7 | ||
8 | const ( | 8 | const ( |
9 | templateHttpErr500_EN = "An internal server error has occurred." | 9 | templateHttpErr500_EN = "An internal server error has occurred." |
10 | templateHttpErr500_RS = "Došlo je do greške na serveru." | 10 | templateHttpErr500_RS = "Došlo je do greške na serveru." |
11 | templateHttpErr400_EN = "Bad request: invalid request body." | 11 | templateHttpErr400_EN = "Bad request: invalid request body." |
12 | templateHttpErr400_RS = "Neispravan zahtev." | 12 | templateHttpErr400_RS = "Neispravan zahtev." |
13 | templateHttpErr401_EN = "Unauthorized request." | 13 | templateHttpErr401_EN = "Unauthorized request." |
14 | templateHttpErr401_RS = "Neautorizovan zahtev." | 14 | templateHttpErr401_RS = "Neautorizovan zahtev." |
15 | ) | 15 | ) |
16 | 16 | ||
17 | type httpError struct { | 17 | type httpError struct { |
18 | Error []HttpErrorDesc `json:"error"` | 18 | Error []HttpErrorDesc `json:"error"` |
19 | Request string `json:"request"` | 19 | Request string `json:"request"` |
20 | } | 20 | } |
21 | 21 | ||
22 | type HttpErrorDesc struct { | 22 | type HttpErrorDesc struct { |
23 | Lang string `json:"lang"` | 23 | Lang string `json:"lang"` |
24 | Desc string `json:"description"` | 24 | Desc string `json:"description"` |
25 | } | 25 | } |
26 | 26 | ||
27 | // ErrorResponse writes HTTP error to w. | 27 | // ErrorResponse writes HTTP error to w. |
28 | func ErrorResponse(w http.ResponseWriter, r *http.Request, code int, desc []HttpErrorDesc) { | 28 | func ErrorResponse(w http.ResponseWriter, r *http.Request, code int, desc []HttpErrorDesc) { |
29 | err := httpError{desc, r.Method + " " + r.URL.Path} | 29 | err := httpError{desc, r.Method + " " + r.URL.Path} |
30 | w.WriteHeader(code) | 30 | w.WriteHeader(code) |
31 | json.NewEncoder(w).Encode(err) | 31 | json.NewEncoder(w).Encode(err) |
32 | } | 32 | } |
33 | 33 | ||
34 | // BadRequestResponse writes HTTP error 400 to w. | 34 | // BadRequestResponse writes HTTP error 400 to w. |
35 | func BadRequestResponse(w http.ResponseWriter, req *http.Request) { | 35 | func BadRequestResponse(w http.ResponseWriter, req *http.Request) { |
36 | ErrorResponse(w, req, http.StatusBadRequest, []HttpErrorDesc{ | 36 | ErrorResponse(w, req, http.StatusBadRequest, []HttpErrorDesc{ |
37 | {"en", templateHttpErr400_EN}, | 37 | {"en", templateHttpErr400_EN}, |
38 | {"rs", templateHttpErr400_RS}, | 38 | {"rs", templateHttpErr400_RS}, |
39 | }) | 39 | }) |
40 | } | 40 | } |
41 | 41 | ||
42 | // InternalSeverErrorResponse writes HTTP error 500 to w. | 42 | // InternalSeverErrorResponse writes HTTP error 500 to w. |
43 | func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) { | 43 | func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) { |
44 | ErrorResponse(w, req, http.StatusInternalServerError, []HttpErrorDesc{ | 44 | ErrorResponse(w, req, http.StatusInternalServerError, []HttpErrorDesc{ |
45 | {"en", templateHttpErr500_EN}, | 45 | {"en", templateHttpErr500_EN}, |
46 | {"rs", templateHttpErr500_RS}, | 46 | {"rs", templateHttpErr500_RS}, |
47 | }) | 47 | }) |
48 | } | 48 | } |
49 | 49 | ||
50 | // UnauthorizedError writes HTTP error 401 to w. | 50 | // UnauthorizedError writes HTTP error 401 to w. |
51 | func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) { | 51 | func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) { |
52 | w.Header().Set("WWW-Authenticate", "Bearer") | ||
52 | ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{ | 53 | ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{ |
53 | {"en", templateHttpErr401_EN}, | 54 | {"en", templateHttpErr401_EN}, |
54 | {"rs", templateHttpErr401_RS}, | 55 | {"rs", templateHttpErr401_RS}, |
55 | }) | 56 | }) |
56 | } | 57 | } |
57 | 58 | ||
58 | // NotFoundHandler writes HTTP error 404 to w. | 59 | // NotFoundHandler writes HTTP error 404 to w. |
59 | func NotFoundHandler(w http.ResponseWriter, req *http.Request) { | 60 | func NotFoundHandler(w http.ResponseWriter, req *http.Request) { |
60 | SetDefaultHeaders(w) | 61 | SetDefaultHeaders(w) |
61 | if req.Method == "OPTIONS" { | 62 | if req.Method == "OPTIONS" { |
62 | return | 63 | return |
63 | } | 64 | } |
64 | ErrorResponse(w, req, http.StatusNotFound, []HttpErrorDesc{ | 65 | ErrorResponse(w, req, http.StatusNotFound, []HttpErrorDesc{ |
65 | {"en", "Not found."}, | 66 | {"en", "Not found."}, |
66 | {"rs", "Traženi resurs ne postoji."}, | 67 | {"rs", "Traženi resurs ne postoji."}, |
67 | }) | 68 | }) |
68 | } | 69 | } |
69 | 70 | ||
70 | // SetDefaultHeaders set's default headers for an HTTP response. | 71 | // SetDefaultHeaders set's default headers for an HTTP response. |
71 | func SetDefaultHeaders(w http.ResponseWriter) { | 72 | func SetDefaultHeaders(w http.ResponseWriter) { |
72 | w.Header().Set("Access-Control-Allow-Origin", "*") | 73 | w.Header().Set("Access-Control-Allow-Origin", "*") |
73 | |||
74 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") | 74 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") |
75 | |||
76 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, | 75 | w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, |
77 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) | 76 | Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) |
78 | |||
79 | w.Header().Set("Content-Type", "application/json; charset=utf-8") | 77 | w.Header().Set("Content-Type", "application/json; charset=utf-8") |
80 | } | 78 | } |