-
Notifications
You must be signed in to change notification settings - Fork 16
/
token.go
432 lines (364 loc) · 13.7 KB
/
token.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
package auth
import (
"fmt"
"math/rand"
"strings"
"time"
log "github.com/Sirupsen/logrus"
jwt "github.com/dgrijalva/jwt-go"
"github.com/contiv/auth_proxy/common"
auth_errors "github.com/contiv/auth_proxy/common/errors"
"github.com/contiv/auth_proxy/common/types"
"github.com/contiv/auth_proxy/db"
"github.com/contiv/auth_proxy/state"
)
// This file contains all utility methods to create and handle JWT tokens
const (
// TokenValidityInHours represents the token validity; used to set token expiry
TokenValidityInHours = 10
// our randomly generated token signing key will be 128 characters before encryption
tokenSigningKeyLength = 128
// This claim is only added to the token, and is not part of authorization db
principalsClaimKey = "principals"
// UsernameClaimKey is only added to the token, and is not part of authorization db
UsernameClaimKey = "username"
)
func init() {
// ensures we don't generate predictable token signing keys
rand.Seed(time.Now().UnixNano())
}
// generateTokenSigningKey generates, encrypts, stores, and returns a new token signing key
func generateTokenSigningKey(stateDrv types.StateDriver) (string, error) {
characters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
length := len(characters)
key := make([]rune, tokenSigningKeyLength)
for i := range key {
key[i] = characters[rand.Intn(length)]
}
encryptedKey, err := common.Encrypt(string(key))
if err != nil {
return "", err
}
if err := stateDrv.Write(db.GetPath(db.RootTokenSigningKey), []byte(encryptedKey)); err != nil {
return "", err
}
return string(key), nil
}
// getTokenSigningKey decrypts and returns the existing token signing key or generates, encrypts,
// stores, and returns a brand new one.
func getTokenSigningKey() (string, error) {
stateDrv, err := state.GetStateDriver()
if err != nil {
return "", err
}
// check for an existing existing key
existingKey, err := stateDrv.Read(db.GetPath(db.RootTokenSigningKey))
switch err {
case auth_errors.ErrKeyNotFound:
// generate, encrypt, store, and return a new key
return generateTokenSigningKey(stateDrv)
case nil:
// decrypt and return existing key
decryptedKey, err := common.Decrypt(string(existingKey))
if err != nil {
return "", err
}
return decryptedKey, nil
default:
return "", err
}
}
// Token represents the JSON Web Token which carries the authorization details
type Token struct {
// TODO: this could probably be an embedded type since we're just adding more
// functionality (i.e., functions) on top of the existing type
tkn *jwt.Token
}
// NewToken creates a new authorization token, sets expiry and returns token pointer
// return values:
// *Token: reference to authorization token object
func NewToken() *Token {
authZ := &Token{}
authZ.tkn = jwt.New(jwt.SigningMethodHS256)
// provide any reserved claims here
authZ.AddClaim("exp", time.Now().Add(time.Hour*TokenValidityInHours).Unix()) // expiration time
authZ.AddClaim("iss", "auth_proxy") // issuer
return authZ
}
// NewTokenWithClaims is a utility method that creates a new token with the list of principals.
// params:
// principals: a list of security principals for a user.
// In the case of a local user, this list should contain only a single principal.
// For ldap users, this list potentially contains multiple principals, each belonging to a ldap group.
// return values:
// *Token: a token object encapsulating authorization claims
// error: nil if successful, else as returned by sub-routines.
func NewTokenWithClaims(principals []string) (*Token, error) {
authZ := NewToken()
// Add principals to token as a claim. Also update the highest role
// claim based on prinipals' authorizations.
if err := authZ.AddPrincipalsClaim(principals); err != nil {
return nil, err
}
for _, principal := range principals {
authZ.AddRoleClaim(principal)
}
return authZ, nil
}
// AddPrincipalsClaim adds a role claim of type
// key="principals" to the token.
//
// Value of this claim is used to find authorization claims of associated principals at runtime.
// If this list changes (e.g., if user's ldap group membership changes), user needs to re-authenticate
// to get updated access.
//
// params:
// principals: security principals associated with a user
// return values:
// error: nil if successful, else relevant error if claim is malformed.
func (authZ *Token) AddPrincipalsClaim(principals []string) error {
// Serialize principals slice as a single string
authZ.AddClaim(principalsClaimKey, strings.Join(principals, ";"))
return nil
}
// AddRoleClaim adds/updates a role claim of type key="role" value=<RoleType>
// e.g. value="admin", value="ops" to the token. This claim represents the
// highest capability role available to the user, hence an update is only
// performed if principal's role claim is higher than current value of role
// claim.
//
// This claim is currently only useful for UI to offer differentiation in terms
// of look and feel based on the type of operations a user can perform. RBAC
// implementation at API level doesn't look at the role claim in Token - rather
// it pulls the current state from state store based on principals. This makes
// authorization changes almost instantaneous, at an increased cost of round
// trip communication with state store.
//
// params:
// principal: a security principal associated with a user
// return values:
// error: nil if successful, else relevant error if claim is malformed.
func (authZ *Token) AddRoleClaim(principal string) error {
authz, err := db.ListAuthorizationsByClaimAndPrincipal(types.RoleClaimKey, principal)
if err != nil {
return err
}
l := len(authz)
switch {
// If no authorizations are found, this user has not been authorized to
// access any resources yet. Return success without adding the claim.
case l == 0:
return nil
default:
grantedRole, err := types.Role(authz[0].ClaimValue)
// Invalid claim in authorizations db, skip over
if err != nil {
return nil
}
// Check if token already has a role claim.
v, ok := authZ.tkn.Claims.(jwt.MapClaims)[types.RoleClaimKey]
// Role claim available, update only if grantedRole has more
// privileges than role already available
if ok {
availableRole, err := types.Role(v.(string))
if err == nil {
// Higher privilege role available, update
if availableRole > grantedRole {
authZ.AddClaim(types.RoleClaimKey, grantedRole.String())
}
} else {
msg := "malformed token, error:" + err.Error()
log.Error(msg)
return auth_errors.NewError(auth_errors.Internal, msg)
}
} else {
// No role claim available, add
// Add key="role" value=<string representation of role
// as obtained from stored authorization>
authZ.AddClaim(types.RoleClaimKey, grantedRole.String())
}
}
return nil
}
// AddClaim adds a claim to an existing authorization token object. A claim is
// a key value pair, where key is a string which encodes the object, such as a
// role, tenant, etc. Since Add is called on a map, it also serves to update the claim.
// Value is generic which may mean different things based on different objects.
// params:
// (Receiver): authorization token object to which more claims need to be added.
// key: claim key string which corresponds to a claim for specific object or a predicate.
// value: generic value associated with claim's key.
func (authZ *Token) AddClaim(key string, value interface{}) {
authZ.tkn.Claims.(jwt.MapClaims)[key] = value
}
// Stringify returns an encoded string representation of the authorization token.
// params:
// (Receiver): authorization token object that should be carrying appropriate claims.
// return values:
// string: string representation of the token, if successful, "" otherwise
// error: nil on success otherwise as returned by SignedString if underlying JWT object
// cannot be encoded and signed appropriately.
func (authZ *Token) Stringify() (string, error) {
// Retrieve signed string encoded representation of underlying JWT token object.
log.Debugf("Claims %#v", authZ.tkn.Claims.(jwt.MapClaims))
key, err := getTokenSigningKey()
if err != nil {
return "", err
}
tokenString, err := authZ.tkn.SignedString([]byte(key))
if err != nil {
log.Errorf("Failed to sign token %#v", err)
return "", err
}
return tokenString, nil
}
// GenerateClaimKey is a helper method that creates a string encoding of a
// claim for an object that our policies care about, e.g role, tenant. This key is
// usually generated when an authorization is added for an object.
// The value to store with this key is based on the object type itself, and is provided to
// the AddClaim method.
// params:
// object: a generic object for which a key needs to be encoded.
// return values:
// string: encoding of the claim for the object.
// error: nil if successful, errors.ErrUnsupportedType if claims for a
// particular object type is not supported.
func GenerateClaimKey(object interface{}) (string, error) {
switch object.(type) {
case types.RoleType:
return types.RoleClaimKey, nil
case types.Tenant:
tenantName := object.(types.Tenant)
return types.TenantClaimKey + string(tenantName), nil
default:
log.Errorf("Unsupported object %#v for authorization claim", object)
return "", auth_errors.ErrUnsupportedType
}
}
// ParseToken parses a string representation of a token into Token object.
// params:
// tokenStr: string encoding of a JWT object.
// return values:
// Token: an authorization token object.
// error: nil if successful, else relevant error if token is expired, couldn't be validated, or
// any other error that happened during token parsing.
func ParseToken(tokenStr string) (*Token, error) {
// parse and validate the token
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
key, err := getTokenSigningKey()
if err != nil {
return nil, err
}
return []byte(key), nil
})
switch err.(type) {
case nil: // no error
if !token.Valid { // expired
log.Warn("Invalid token")
return nil, fmt.Errorf("Invalid token: %#v", err)
}
return &Token{tkn: token}, nil
case *jwt.ValidationError: // something was wrong during the validation
log.Errorf("Error validating access token %#v", err)
return nil, fmt.Errorf("Error validating access token %#v", err)
default: // something else went wrong
log.Errorf("Error parsing access token %#v", err)
return nil, fmt.Errorf("Error parsing access token %#v", err)
}
}
// GetClaim returns the value of the given claim key
// params:
// claimKey: string representing the claim key
// return values:
// string: claim value string obtained from the token
func (authZ *Token) GetClaim(claimKey string) string {
v, found := authZ.tkn.Claims.(jwt.MapClaims)[claimKey]
if !found {
log.Warnf("Illegal token, no %q claim present", claimKey)
return ""
}
claimVal, ok := v.(string)
if !ok {
log.Errorf("Illegal token, no %q present", claimKey)
return ""
}
return claimVal
}
// IsSuperuser checks if the token belongs to a superuser (i.e. `admin` in our
// system). It queries the authorization database to obtain this information.
// params:
// (Receiver): authorization token object which carries all principals
// associated with the user.
// return values:
// true if the token belongs to superuser else false
func (authZ *Token) IsSuperuser() bool {
// Deserialize principals as a slice
principals := strings.Split(authZ.GetClaim(principalsClaimKey), ";")
for _, p := range principals {
// Get role claim for the principal
authz, err := db.ListAuthorizationsByClaimAndPrincipal(types.RoleClaimKey, p)
// If not found, ignore error and move on to next principal
if err != nil || len(authz) == 0 {
log.Debug("no admin claim found for principal ", p)
continue
}
// If not a valid role, ignore error and move on to next principal
r, err := types.Role(authz[0].ClaimValue)
if err != nil {
log.Error("invalid role claim found for principal ", p)
continue
}
// If any principal has admin role, user overall has admin privileges.
if r == types.Admin {
log.Debug("admin role claim found for principal ", p)
return true
}
}
// No principal has admin role claim
log.Debug("no principals with admin claim present")
return false
}
//
// CheckClaims checks for specific claims in an authorization token object.
// These claims are evaluated based on object type, such as for a tenant or
// for a role, and an associated policy.
//
// Parameters:
// (Receiver): authorization token object that should be carrying appropriate claims.
// objects: claim targets. These can be specific objects, such as tenants or networks
// or specific types, such as a role.
//
// Return values:
// error: nil if successful, else
// errors.ErrUnauthorized: if authorization claim for a particular object is not
// present, or if claims for a particular object type are not supported.
//
func (authZ *Token) CheckClaims(objects ...interface{}) error {
for i := 0; i < len(objects); i++ {
v := objects[i]
switch v.(type) {
case types.RoleType:
// check if role given in list of objects matches
// role in the token
role := v.(types.RoleType)
if err := authZ.checkRolePolicy(role); err != nil {
return err
}
case types.Tenant:
tenant := v.(types.Tenant)
i++
if err := authZ.checkTenantPolicy(tenant, objects[i]); err != nil {
return err
}
// TODO Add other policy checks as needed, e.g. wildcard policy
default:
log.Errorf("Unsupported type for authorization claim; got: %#v"+
", expecting: types.RoleType or types.Tenant", v)
return auth_errors.ErrUnauthorized
}
}
return nil
}