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 1 package webutility
3 2  
4 3 import (
... ... @@ -26,10 +25,13 @@ type Role struct {
26 25  
27 26 // TokenClaims are JWT token claims.
28 27 type TokenClaims struct {
29   - Username string `json:"username"`
30   - Role string `json:"role"`
31   - RoleID uint32 `json:"roleID"`
32   - jwt.StandardClaims
  28 + Token string `json:"access_token"`
  29 + TokenType string `json:"token_type"`
  30 + Username string `json:"username"`
  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 37 // CredentialsStruct is an instace of username/password values.
... ... @@ -38,26 +40,22 @@ type CredentialsStruct struct {
38 40 Password string `json:"password"`
39 41 }
40 42  
41   -// generateSalt returns a string of random characters of 'saltSize' length.
42   -func generateSalt() (salt string, err error) {
43   - rawsalt := make([]byte, saltSize)
44   -
45   - _, err = rand.Read(rawsalt)
  43 +// ValidateCredentials hashes pass and salt and returns comparison result with resultHash
  44 +func ValidateCredentials(pass, salt, resultHash string) bool {
  45 + hash, _, err := CreateHash(pass, salt)
46 46 if err != nil {
47   - return "", err
  47 + return false
48 48 }
49   -
50   - salt = hex.EncodeToString(rawsalt)
51   - return salt, nil
  49 + return hash == resultHash
52 50 }
53 51  
54   -// HashString hashes input string using SHA256.
55   -// If the presalt parameter is not provided HashString will generate new salt string.
56   -// Returns hash and salt string or an error if it fails.
57   -func HashString(str, presalt string) (hash, salt string, err error) {
  52 +// CreateHash hashes str using SHA256.
  53 +// If the presalt parameter is not provided CreateHash will generate new salt string.
  54 +// Returns hash and salt strings or an error if it fails.
  55 +func CreateHash(str, presalt string) (hash, salt string, err error) {
58 56 // chech if message is presalted
59 57 if presalt == "" {
60   - salt, err = generateSalt()
  58 + salt, err = randomSalt()
61 59 if err != nil {
62 60 return "", "", err
63 61 }
... ... @@ -85,166 +83,124 @@ func HashString(str, presalt string) (hash, salt string, err error) {
85 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 87 // It returns an error if it fails.
90   -func CreateAPIToken(username string, role Role) (string, error) {
91   - var apiToken string
92   - var err error
93   -
94   - if err != nil {
95   - return "", err
96   - }
97   -
  88 +func CreateAuthToken(username string, role Role) (TokenClaims, error) {
  89 + t0 := (time.Now()).Unix()
  90 + t1 := (time.Now().Add(OneWeek)).Unix()
98 91 claims := TokenClaims{
99   - username,
100   - role.Name,
101   - role.ID,
102   - jwt.StandardClaims{
103   - ExpiresAt: (time.Now().Add(OneWeek)).Unix(),
104   - Issuer: appName,
105   - },
  92 + TokenType: "Bearer",
  93 + Username: username,
  94 + Role: role.Name,
  95 + RoleID: role.ID,
  96 + ExpiresIn: t1 - t0,
106 97 }
  98 + // initialize jwt.StandardClaims fields (anonymous struct)
  99 + claims.IssuedAt = t0
  100 + claims.ExpiresAt = t1
  101 + claims.Issuer = appName
107 102  
108 103 jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
109   - apiToken, err = jwtToken.SignedString([]byte(secret))
  104 + token, err := jwtToken.SignedString([]byte(secret))
110 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 113 // It returns new JWT token or an error if it fails.
118   -func RefreshAPIToken(tokenString string) (string, error) {
119   - var newToken string
120   - tokenString = strings.TrimPrefix(tokenString, "Bearer ")
121   - token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc)
  114 +func RefreshAuthToken(req *http.Request) (TokenClaims, error) {
  115 + authHead := req.Header.Get("Authorization")
  116 + tokenstr := strings.TrimPrefix(authHead, "Bearer ")
  117 + token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc)
122 118 if err != nil {
123   - return "", err
  119 + return TokenClaims{}, err
124 120 }
125 121  
126 122 // type assertion
127 123 claims, ok := token.Claims.(*TokenClaims)
128 124 if !ok || !token.Valid {
129   - return "", 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
  125 + return TokenClaims{}, errors.New("token is not valid")
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.
144   -// It returns a pointer to TokenClaims struct or an error if it fails.
145   -func ParseAPIToken(tokenString string) (*TokenClaims, error) {
146   - if ok := strings.HasPrefix(tokenString, "Bearer "); ok {
147   - tokenString = strings.TrimPrefix(tokenString, "Bearer ")
148   - } else {
149   - return &TokenClaims{}, errors.New("Authorization header is incomplete")
  132 +// RbacCheck returns true if role that made HTTP request is authorized to
  133 +// access the resource it is targeting.
  134 +// It exctracts user's role from the JWT token located in Authorization header of
  135 +// http.Request and then compares it with the list of supplied roles and returns
  136 +// true if there's a match, if "*" is provided or if the authRoles is nil.
  137 +// Otherwise it returns false.
  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 145 if err != nil {
154 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
158   - claims, ok := token.Claims.(*TokenClaims)
159   - if !ok || !token.Valid {
160   - return &TokenClaims{}, errors.New("token is not valid")
  153 + // check if role extracted from token matches
  154 + // any of the provided (allowed) ones
  155 + for _, r := range authRoles {
  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) {
166   - token := r.Header.Get("Authorization")
167   - if ok := strings.HasPrefix(token, "Bearer "); ok {
168   - token = strings.TrimPrefix(token, "Bearer ")
  164 +// GetTokenClaims extracts JWT claims from Authorization header of the request.
  165 +// Returns token claims or an error.
  166 +func GetTokenClaims(req *http.Request) (*TokenClaims, error) {
  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 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 177 if err != nil {
175 178 return &TokenClaims{}, err
176 179 }
177 180  
178 181 // type assertion
179   - claims, ok := parsedToken.Claims.(*TokenClaims)
180   - if !ok || !parsedToken.Valid {
  182 + claims, ok := token.Claims.(*TokenClaims)
  183 + if !ok || !token.Valid {
181 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   -func secretFunc(token *jwt.Token) (interface{}, error) {
188   - return []byte(secret), nil
  187 + return claims, nil
189 188 }
190 189  
191   -// RbacCheck returns true if role that made HTTP request is authorized to
192   -// access the resource it is targeting.
193   -// It exctracts user's role from the JWT token located in Authorization header of
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   - }
  190 +// randomSalt returns a string of random characters of 'saltSize' length.
  191 +func randomSalt() (s string, err error) {
  192 + rawsalt := make([]byte, saltSize)
201 193  
202   - token := req.Header.Get("Authorization")
203   - claims, err := ParseAPIToken(token)
  194 + _, err = rand.Read(rawsalt)
204 195 if err != nil {
205   - return false
206   - }
207   -
208   - for _, r := range authRoles {
209   - if claims.Role == r || r == "*" {
210   - return true
211   - }
  196 + return "", err
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.
218   -// If RBAC passes it calls the handlerFunc.
219   -func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc {
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   -
228   - w.Header().Set("Content-Type", "application/json; charset=utf-8")
229   -
230   - // TODO: Check for content type
231   -
232   - if req.Method == "OPTIONS" {
233   - return
234   - }
235   -
236   - err := req.ParseForm()
237   - if err != nil {
238   - BadRequestResponse(w, req)
239   - return
240   - }
241   -
242   - if !RbacCheck(req, authRoles) {
243   - UnauthorizedResponse(w, req)
244   - return
245   - }
246   -
247   - // execute HandlerFunc
248   - handlerFunc(w, req)
249   - }
  203 +// secretFunc returns byte slice of API secret keyword.
  204 +func secretFunc(token *jwt.Token) (interface{}, error) {
  205 + return []byte(secret), nil
250 206 }
... ...
... ... @@ -49,6 +49,7 @@ func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) {
49 49  
50 50 // UnauthorizedError writes HTTP error 401 to w.
51 51 func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) {
  52 + w.Header().Set("WWW-Authenticate", "Bearer")
52 53 ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{
53 54 {"en", templateHttpErr401_EN},
54 55 {"rs", templateHttpErr401_RS},
... ... @@ -70,11 +71,8 @@ func NotFoundHandler(w http.ResponseWriter, req *http.Request) {
70 71 // SetDefaultHeaders set's default headers for an HTTP response.
71 72 func SetDefaultHeaders(w http.ResponseWriter) {
72 73 w.Header().Set("Access-Control-Allow-Origin", "*")
73   -
74 74 w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
75   -
76 75 w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type,
77 76 Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`)
78   -
79 77 w.Header().Set("Content-Type", "application/json; charset=utf-8")
80 78 }
... ...