Blame view

auth.go 5.43 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
  )
f84e7607d   Marko Tikvić   added dictionary;...
14
15
  var appName = "webutility"
  var secret = "webutility"
90fd36e9b   Marko Tikvić   resolved some dep...
16

d2ddf82ef   Marko Tikvić   started on new rbac
17
  type Role struct {
077dae33c   Marko Tikvić   removed role cons...
18
  	Name string `json:"name"`
a205e8f40   Marko Tikvić   changes
19
  	ID   int64  `json:"id"`
d2ddf82ef   Marko Tikvić   started on new rbac
20
  }
6ec91280b   Marko Tikvić   working on docume...
21
  // TokenClaims are JWT token claims.
90fd36e9b   Marko Tikvić   resolved some dep...
22
  type TokenClaims struct {
a205e8f40   Marko Tikvić   changes
23
24
25
26
  	// extending a struct
  	jwt.StandardClaims
  
  	// custom claims
6620591d8   Marko Tikvić   moved DeliverPayl...
27
28
29
30
  	Token     string `json:"access_token"`
  	TokenType string `json:"token_type"`
  	Username  string `json:"username"`
  	Role      string `json:"role"`
a205e8f40   Marko Tikvić   changes
31
  	RoleID    int64  `json:"role_id"`
6620591d8   Marko Tikvić   moved DeliverPayl...
32
  	ExpiresIn int64  `json:"expires_in"`
90fd36e9b   Marko Tikvić   resolved some dep...
33
  }
f84e7607d   Marko Tikvić   added dictionary;...
34
35
36
37
  func InitJWT(appName, secret string) {
  	appName = appName
  	secret = secret
  }
bc3671b26   Marko Tikvić   refactoring token...
38
  // ValidateCredentials hashes pass and salt and returns comparison result with resultHash
f84e7607d   Marko Tikvić   added dictionary;...
39
  func ValidateCredentials(pass, salt, resultHash string) (bool, error) {
bc3671b26   Marko Tikvić   refactoring token...
40
  	hash, _, err := CreateHash(pass, salt)
90fd36e9b   Marko Tikvić   resolved some dep...
41
  	if err != nil {
f84e7607d   Marko Tikvić   added dictionary;...
42
  		return false, err
90fd36e9b   Marko Tikvić   resolved some dep...
43
  	}
f84e7607d   Marko Tikvić   added dictionary;...
44
45
  	res := hash == resultHash
  	return res, nil
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
  func CreateAuthToken(username string, role Role) (TokenClaims, error) {
  	t0 := (time.Now()).Unix()
3fffcb954   Marko Tikvić   removed old http API
84
  	t1 := (time.Now().Add(time.Hour * 24 * 7)).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
  }
3fffcb954   Marko Tikvić   removed old http API
105
106
  // RefreshAuthToken returns new JWT token with same claims contained in tok but with prolonged expiration date.
  // It returns an error if it fails.
c7aadbb39   Marko Tikvić   minor changes
107
108
  func RefreshAuthToken(tok string) (TokenClaims, error) {
  	token, err := jwt.ParseWithClaims(tok, &TokenClaims{}, secretFunc)
90fd36e9b   Marko Tikvić   resolved some dep...
109
  	if err != nil {
052f8a3a6   Marko Tikvić   token validation ...
110
111
112
113
114
115
116
117
118
  		if validation, ok := err.(*jwt.ValidationError); ok {
  			// don't return error if token is expired
  			// just extend it
  			if !(validation.Errors&jwt.ValidationErrorExpired != 0) {
  				return TokenClaims{}, err
  			}
  		} else {
  			return TokenClaims{}, err
  		}
90fd36e9b   Marko Tikvić   resolved some dep...
119
120
121
122
  	}
  
  	// type assertion
  	claims, ok := token.Claims.(*TokenClaims)
052f8a3a6   Marko Tikvić   token validation ...
123
  	if !ok {
bc3671b26   Marko Tikvić   refactoring token...
124
  		return TokenClaims{}, errors.New("token is not valid")
90fd36e9b   Marko Tikvić   resolved some dep...
125
  	}
bc3671b26   Marko Tikvić   refactoring token...
126
127
  	// extend token expiration date
  	return CreateAuthToken(claims.Username, Role{claims.Role, claims.RoleID})
90fd36e9b   Marko Tikvić   resolved some dep...
128
  }
3fffcb954   Marko Tikvić   removed old http API
129
130
131
132
133
134
135
  // AuthCheck returns JWT claims and boolean result of a check if req contains any role from roles.
  // It checks if role extracted from reqest's Authorization header (JWT claims) matches any of
  // provided comma-separated roles in roles. If roles is empty string check is skipped,
  // otherwise role is extracted from token claims and compared against roles.
  // If roles is "*" the check is automatically validated.
  func AuthCheck(req *http.Request, roles string) (*TokenClaims, bool) {
  	if roles == "" {
d29773cc4   Marko Tikvić   ProcessRBAC
136
137
138
139
140
141
142
143
144
145
146
147
  		return nil, true
  	}
  
  	// validate token and check expiration date
  	claims, err := GetTokenClaims(req)
  	if err != nil {
  		return claims, false
  	}
  	// check if token has expired
  	if claims.ExpiresAt < (time.Now()).Unix() {
  		return claims, false
  	}
3fffcb954   Marko Tikvić   removed old http API
148
149
150
151
152
153
154
155
  	if roles == "*" {
  		return claims, true
  	}
  
  	parts := strings.Split(roles, ",")
  	for i, _ := range parts {
  		r := strings.Trim(parts[i], " ")
  		if claims.Role == r {
d29773cc4   Marko Tikvić   ProcessRBAC
156
157
158
159
160
161
  			return claims, true
  		}
  	}
  
  	return claims, false
  }
3fffcb954   Marko Tikvić   removed old http API
162
  // GetTokenClaims extracts JWT claims from Authorization header of req.
bc3671b26   Marko Tikvić   refactoring token...
163
164
165
166
167
168
  // 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...
169
  		tokstr = strings.TrimPrefix(authHead, "Bearer ")
33d137a67   Marko Tikvić   Functional role c...
170
  	} else {
bc3671b26   Marko Tikvić   refactoring token...
171
  		return &TokenClaims{}, errors.New("authorization header in incomplete")
33d137a67   Marko Tikvić   Functional role c...
172
  	}
bc3671b26   Marko Tikvić   refactoring token...
173
  	token, err := jwt.ParseWithClaims(tokstr, &TokenClaims{}, secretFunc)
33d137a67   Marko Tikvić   Functional role c...
174
175
176
177
178
  	if err != nil {
  		return &TokenClaims{}, err
  	}
  
  	// type assertion
bc3671b26   Marko Tikvić   refactoring token...
179
180
  	claims, ok := token.Claims.(*TokenClaims)
  	if !ok || !token.Valid {
33d137a67   Marko Tikvić   Functional role c...
181
182
  		return &TokenClaims{}, errors.New("token is not valid")
  	}
33d137a67   Marko Tikvić   Functional role c...
183

bc3671b26   Marko Tikvić   refactoring token...
184
  	return claims, nil
90fd36e9b   Marko Tikvić   resolved some dep...
185
  }
33d137a67   Marko Tikvić   Functional role c...
186

3fffcb954   Marko Tikvić   removed old http API
187
188
  // randomSalt returns a string of 32 random characters.
  const saltSize = 32
bc3671b26   Marko Tikvić   refactoring token...
189
190
  func randomSalt() (s string, err error) {
  	rawsalt := make([]byte, saltSize)
d2ddf82ef   Marko Tikvić   started on new rbac
191

bc3671b26   Marko Tikvić   refactoring token...
192
  	_, err = rand.Read(rawsalt)
d2ddf82ef   Marko Tikvić   started on new rbac
193
  	if err != nil {
bc3671b26   Marko Tikvić   refactoring token...
194
  		return "", err
33d137a67   Marko Tikvić   Functional role c...
195
  	}
d2ddf82ef   Marko Tikvić   started on new rbac
196

bc3671b26   Marko Tikvić   refactoring token...
197
198
  	s = hex.EncodeToString(rawsalt)
  	return s, nil
33d137a67   Marko Tikvić   Functional role c...
199
  }
d2ddf82ef   Marko Tikvić   started on new rbac
200

bc3671b26   Marko Tikvić   refactoring token...
201
202
203
  // 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
204
  }