Blame view

auth_utility.go 6.04 KB
ea858b8a7   Marko Tikvić   refactoring
1
  package webutility
90fd36e9b   Marko Tikvić   resolved some dep...
2
3
  
  import (
90fd36e9b   Marko Tikvić   resolved some dep...
4
  	"crypto/rand"
d2ddf82ef   Marko Tikvić   started on new rbac
5
  	"crypto/sha256"
90fd36e9b   Marko Tikvić   resolved some dep...
6
  	"encoding/hex"
d2ddf82ef   Marko Tikvić   started on new rbac
7
  	"errors"
33d137a67   Marko Tikvić   Functional role c...
8
  	"net/http"
d2ddf82ef   Marko Tikvić   started on new rbac
9
10
  	"strings"
  	"time"
33d137a67   Marko Tikvić   Functional role c...
11

90fd36e9b   Marko Tikvić   resolved some dep...
12
  	"github.com/dgrijalva/jwt-go"
90fd36e9b   Marko Tikvić   resolved some dep...
13
  )
d2ddf82ef   Marko Tikvić   started on new rbac
14
15
  const OneDay = time.Hour * 24
  const OneWeek = OneDay * 7
90fd36e9b   Marko Tikvić   resolved some dep...
16
  const saltSize = 32
d2ddf82ef   Marko Tikvić   started on new rbac
17
18
  const appName = "korisnicki-centar"
  const secret = "korisnicki-centar-api"
90fd36e9b   Marko Tikvić   resolved some dep...
19

d2ddf82ef   Marko Tikvić   started on new rbac
20
  type Role struct {
077dae33c   Marko Tikvić   removed role cons...
21
22
  	Name string `json:"name"`
  	ID   uint32 `json:"id"`
d2ddf82ef   Marko Tikvić   started on new rbac
23
  }
6ec91280b   Marko Tikvić   working on docume...
24
  // TokenClaims are JWT token claims.
90fd36e9b   Marko Tikvić   resolved some dep...
25
  type TokenClaims struct {
bc3671b26   Marko Tikvić   refactoring token...
26
27
28
29
30
31
32
  	Token              string `json:"access_token"`
  	TokenType          string `json:"token_type"`
  	Username           string `json:"username"`
  	Role               string `json:"role"`
  	RoleID             uint32 `json:"role_id"`
  	ExpiresIn          int64  `json:"expires_in"`
  	jwt.StandardClaims        // extending a struct
90fd36e9b   Marko Tikvić   resolved some dep...
33
  }
1d0f61553   Marko Tikvić   can't fetch clob
34
  // CredentialsStruct is an instace of username/password values.
90fd36e9b   Marko Tikvić   resolved some dep...
35
36
37
38
  type CredentialsStruct struct {
  	Username string `json:"username"`
  	Password string `json:"password"`
  }
bc3671b26   Marko Tikvić   refactoring token...
39
40
41
  // ValidateCredentials hashes pass and salt and returns comparison result with resultHash
  func ValidateCredentials(pass, salt, resultHash string) bool {
  	hash, _, err := CreateHash(pass, salt)
90fd36e9b   Marko Tikvić   resolved some dep...
42
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
43
  		return false
90fd36e9b   Marko Tikvić   resolved some dep...
44
  	}
bc3671b26   Marko Tikvić   refactoring token...
45
  	return hash == resultHash
90fd36e9b   Marko Tikvić   resolved some dep...
46
  }
bc3671b26   Marko Tikvić   refactoring token...
47
48
49
50
  // CreateHash hashes str using SHA256.
  // If the presalt parameter is not provided CreateHash will generate new salt string.
  // Returns hash and salt strings or an error if it fails.
  func CreateHash(str, presalt string) (hash, salt string, err error) {
90fd36e9b   Marko Tikvić   resolved some dep...
51
52
  	// chech if message is presalted
  	if presalt == "" {
bc3671b26   Marko Tikvić   refactoring token...
53
  		salt, err = randomSalt()
90fd36e9b   Marko Tikvić   resolved some dep...
54
55
56
57
58
59
60
61
  		if err != nil {
  			return "", "", err
  		}
  	} else {
  		salt = presalt
  	}
  
  	// convert strings to raw byte slices
33fd58161   markotikvic   minor changes, sh...
62
  	rawstr := []byte(str)
90fd36e9b   Marko Tikvić   resolved some dep...
63
64
65
66
  	rawsalt, err := hex.DecodeString(salt)
  	if err != nil {
  		return "", "", err
  	}
33fd58161   markotikvic   minor changes, sh...
67

d2ddf82ef   Marko Tikvić   started on new rbac
68
  	rawdata := make([]byte, len(rawstr)+len(rawsalt))
33fd58161   markotikvic   minor changes, sh...
69
  	rawdata = append(rawdata, rawstr...)
90fd36e9b   Marko Tikvić   resolved some dep...
70
71
72
73
74
75
  	rawdata = append(rawdata, rawsalt...)
  
  	// hash message + salt
  	hasher := sha256.New()
  	hasher.Write(rawdata)
  	rawhash := hasher.Sum(nil)
33fd58161   markotikvic   minor changes, sh...
76

90fd36e9b   Marko Tikvić   resolved some dep...
77
78
79
  	hash = hex.EncodeToString(rawhash)
  	return hash, salt, nil
  }
bc3671b26   Marko Tikvić   refactoring token...
80
  // CreateAuthToken returns JWT token with encoded username, role, expiration date and issuer claims.
6ec91280b   Marko Tikvić   working on docume...
81
  // It returns an error if it fails.
bc3671b26   Marko Tikvić   refactoring token...
82
83
84
  func CreateAuthToken(username string, role Role) (TokenClaims, error) {
  	t0 := (time.Now()).Unix()
  	t1 := (time.Now().Add(OneWeek)).Unix()
90fd36e9b   Marko Tikvić   resolved some dep...
85
  	claims := TokenClaims{
bc3671b26   Marko Tikvić   refactoring token...
86
87
88
89
90
  		TokenType: "Bearer",
  		Username:  username,
  		Role:      role.Name,
  		RoleID:    role.ID,
  		ExpiresIn: t1 - t0,
90fd36e9b   Marko Tikvić   resolved some dep...
91
  	}
bc3671b26   Marko Tikvić   refactoring token...
92
93
94
95
  	// initialize jwt.StandardClaims fields (anonymous struct)
  	claims.IssuedAt = t0
  	claims.ExpiresAt = t1
  	claims.Issuer = appName
90fd36e9b   Marko Tikvić   resolved some dep...
96
97
  
  	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
bc3671b26   Marko Tikvić   refactoring token...
98
  	token, err := jwtToken.SignedString([]byte(secret))
90fd36e9b   Marko Tikvić   resolved some dep...
99
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
100
  		return TokenClaims{}, err
90fd36e9b   Marko Tikvić   resolved some dep...
101
  	}
bc3671b26   Marko Tikvić   refactoring token...
102
103
  	claims.Token = token
  	return claims, nil
90fd36e9b   Marko Tikvić   resolved some dep...
104
  }
bc3671b26   Marko Tikvić   refactoring token...
105
  // RefreshAuthToken prolongs JWT token's expiration date for one week.
6ec91280b   Marko Tikvić   working on docume...
106
  // It returns new JWT token or an error if it fails.
bc3671b26   Marko Tikvić   refactoring token...
107
108
109
110
  func RefreshAuthToken(req *http.Request) (TokenClaims, error) {
  	authHead := req.Header.Get("Authorization")
  	tokenstr := strings.TrimPrefix(authHead, "Bearer ")
  	token, err := jwt.ParseWithClaims(tokenstr, &TokenClaims{}, secretFunc)
90fd36e9b   Marko Tikvić   resolved some dep...
111
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
112
  		return TokenClaims{}, err
90fd36e9b   Marko Tikvić   resolved some dep...
113
114
115
116
117
  	}
  
  	// type assertion
  	claims, ok := token.Claims.(*TokenClaims)
  	if !ok || !token.Valid {
bc3671b26   Marko Tikvić   refactoring token...
118
  		return TokenClaims{}, errors.New("token is not valid")
90fd36e9b   Marko Tikvić   resolved some dep...
119
  	}
bc3671b26   Marko Tikvić   refactoring token...
120
121
  	// extend token expiration date
  	return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID})
90fd36e9b   Marko Tikvić   resolved some dep...
122
  }
bc3671b26   Marko Tikvić   refactoring token...
123
124
125
126
127
128
  // RbacCheck returns true if role that made HTTP request is authorized to
  // access the resource it is targeting.
  // It exctracts user's role from the JWT token located in Authorization header of
  // http.Request and then compares it with the list of supplied roles and returns
  // true if there's a match, if "*" is provided or if the authRoles is nil.
  // Otherwise it returns false.
2d79a4120   Marko Tikvić   Responses contain...
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
  func RbacCheck(req *http.Request, authRoles []string) bool {
  	if authRoles == nil {
  		return true
  	}
  
  	// validate token and check expiration date
  	claims, err := GetTokenClaims(req)
  	if err != nil {
  		return false
  	}
  	// check if token has expired
  	if claims.ExpiresAt < (time.Now()).Unix() {
  		return false
  	}
  
  	// check if role extracted from token matches
  	// any of the provided (allowed) ones
  	for _, r := range authRoles {
  		if claims.Role == r || r == "*" {
  			return true
  		}
  	}
  
  	return false
  }
  
  // TODO
  func AuthCheck(req *http.Request, authRoles []string) (*TokenClaims, error) {
bc3671b26   Marko Tikvić   refactoring token...
157
158
  	if authRoles == nil {
  		return &TokenClaims{}, nil
90fd36e9b   Marko Tikvić   resolved some dep...
159
  	}
bc3671b26   Marko Tikvić   refactoring token...
160
161
  	// validate token and check expiration date
  	claims, err := GetTokenClaims(req)
90fd36e9b   Marko Tikvić   resolved some dep...
162
163
164
  	if err != nil {
  		return &TokenClaims{}, err
  	}
bc3671b26   Marko Tikvić   refactoring token...
165
166
167
168
  	// check if token has expired
  	if claims.ExpiresAt < (time.Now()).Unix() {
  		return &TokenClaims{}, errors.New("token has expired")
  	}
90fd36e9b   Marko Tikvić   resolved some dep...
169

bc3671b26   Marko Tikvić   refactoring token...
170
171
172
173
174
175
  	// check if role extracted from token matches
  	// any of the provided (allowed) ones
  	for _, r := range authRoles {
  		if claims.Role == r || r == "*" {
  			return claims, nil
  		}
90fd36e9b   Marko Tikvić   resolved some dep...
176
  	}
bc3671b26   Marko Tikvić   refactoring token...
177

2d79a4120   Marko Tikvić   Responses contain...
178
  	return claims, errors.New("role is not authorized")
90fd36e9b   Marko Tikvić   resolved some dep...
179
  }
bc3671b26   Marko Tikvić   refactoring token...
180
181
182
183
184
185
186
  // GetTokenClaims extracts JWT claims from Authorization header of the request.
  // Returns token claims or an error.
  func GetTokenClaims(req *http.Request) (*TokenClaims, error) {
  	// check for and strip 'Bearer' prefix
  	var tokstr string
  	authHead := req.Header.Get("Authorization")
  	if ok := strings.HasPrefix(authHead, "Bearer "); ok {
2d79a4120   Marko Tikvić   Responses contain...
187
  		tokstr = strings.TrimPrefix(authHead, "Bearer ")
33d137a67   Marko Tikvić   Functional role c...
188
  	} else {
bc3671b26   Marko Tikvić   refactoring token...
189
  		return &TokenClaims{}, errors.New("authorization header in incomplete")
33d137a67   Marko Tikvić   Functional role c...
190
  	}
bc3671b26   Marko Tikvić   refactoring token...
191
  	token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc)
33d137a67   Marko Tikvić   Functional role c...
192
193
194
195
196
  	if err != nil {
  		return &TokenClaims{}, err
  	}
  
  	// type assertion
bc3671b26   Marko Tikvić   refactoring token...
197
198
  	claims, ok := token.Claims.(*TokenClaims)
  	if !ok || !token.Valid {
33d137a67   Marko Tikvić   Functional role c...
199
200
  		return &TokenClaims{}, errors.New("token is not valid")
  	}
33d137a67   Marko Tikvić   Functional role c...
201

bc3671b26   Marko Tikvić   refactoring token...
202
  	return claims, nil
90fd36e9b   Marko Tikvić   resolved some dep...
203
  }
33d137a67   Marko Tikvić   Functional role c...
204

bc3671b26   Marko Tikvić   refactoring token...
205
206
207
  // randomSalt returns a string of random characters of 'saltSize' length.
  func randomSalt() (s string, err error) {
  	rawsalt := make([]byte, saltSize)
d2ddf82ef   Marko Tikvić   started on new rbac
208

bc3671b26   Marko Tikvić   refactoring token...
209
  	_, err = rand.Read(rawsalt)
d2ddf82ef   Marko Tikvić   started on new rbac
210
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
211
  		return "", err
33d137a67   Marko Tikvić   Functional role c...
212
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
213

bc3671b26   Marko Tikvić   refactoring token...
214
215
  	s = hex.EncodeToString(rawsalt)
  	return s, nil
33d137a67   Marko Tikvić   Functional role c...
216
  }
d2ddf82ef   Marko Tikvić   started on new rbac
217

bc3671b26   Marko Tikvić   refactoring token...
218
219
220
  // secretFunc returns byte slice of API secret keyword.
  func secretFunc(token *jwt.Token) (interface{}, error) {
  	return []byte(secret), nil
d2ddf82ef   Marko Tikvić   started on new rbac
221
  }