Commit bc3671b260f97ed4c81baeb2e2fbedb29f47f693

Authored by Marko Tikvić
1 parent 077dae33c8
Exists in master and in 1 other branch v2

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
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
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 }