From bfe8a9bccccb86233bf627f4627c724a70e4a188 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 31 Jan 2020 08:29:22 +0530 Subject: [PATCH] jwt: Simplify JWT parsing (#8802) JWT parsing is simplified by using a custom claim data structure such as MapClaims{}, also writes a custom Unmarshaller for faster unmarshalling. - Avoid as much reflections as possible - Provide the right types for functions as much as possible - Avoid strings.Join, strings.Split to reduce allocations, rely on indexes directly. --- cmd/auth-handler.go | 60 ++--- cmd/handler-utils.go | 4 +- cmd/jwt.go | 187 ++++---------- cmd/jwt/parser.go | 371 ++++++++++++++++++++++++++++ cmd/jwt/parser_test.go | 214 ++++++++++++++++ cmd/jwt_test.go | 69 +++++- cmd/lock-rest-server-common_test.go | 2 +- cmd/rest/client.go | 6 +- cmd/storage-rest-server.go | 27 +- cmd/web-handlers.go | 96 +++---- cmd/web-handlers_test.go | 13 +- 11 files changed, 798 insertions(+), 251 deletions(-) create mode 100644 cmd/jwt/parser.go create mode 100644 cmd/jwt/parser_test.go diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index 6ba74d8a6..db9f2d126 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -28,8 +28,8 @@ import ( "net/http" "strings" - jwtgo "github.com/dgrijalva/jwt-go" xhttp "github.com/minio/minio/cmd/http" + xjwt "github.com/minio/minio/cmd/jwt" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/bucket/policy" @@ -179,12 +179,14 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} { // Fetch claims in the security token returned by the client. func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) { - claims := make(map[string]interface{}) + claims := xjwt.NewMapClaims() + token := getSessionToken(r) if token == "" { - return claims, nil + return claims.Map(), nil } - stsTokenCallback := func(jwtToken *jwtgo.Token) (interface{}, error) { + + stsTokenCallback := func(claims *xjwt.MapClaims) ([]byte, error) { // JWT token for x-amz-security-token is signed with admin // secret key, temporary credentials become invalid if // server admin credentials change. This is done to ensure @@ -195,66 +197,42 @@ func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) { // on the client side and is treated like an opaque value. return []byte(globalActiveCred.SecretKey), nil } - p := &jwtgo.Parser{ - ValidMethods: []string{ - jwtgo.SigningMethodHS256.Alg(), - jwtgo.SigningMethodHS512.Alg(), - }, - } - jtoken, err := p.ParseWithClaims(token, jwtgo.MapClaims(claims), stsTokenCallback) - if err != nil { - return nil, err - } - if !jtoken.Valid { + + if err := xjwt.ParseWithClaims(token, claims, stsTokenCallback); err != nil { return nil, errAuthentication } - v, ok := claims["accessKey"] - if !ok { - return nil, errInvalidAccessKeyID - } - if _, ok = v.(string); !ok { - return nil, errInvalidAccessKeyID - } if globalPolicyOPA == nil { - // If OPA is not set and if ldap claim key is set, - // allow the claim. - if _, ok := claims[ldapUser]; ok { - return claims, nil + // If OPA is not set and if ldap claim key is set, allow the claim. + if _, ok := claims.Lookup(ldapUser); ok { + return claims.Map(), nil } // If OPA is not set, session token should // have a policy and its mandatory, reject // requests without policy claim. - p, pok := claims[iamPolicyClaimName()] + _, pok := claims.Lookup(iamPolicyClaimName()) if !pok { return nil, errAuthentication } - if _, pok = p.(string); !pok { - return nil, errAuthentication - } - sp, spok := claims[iampolicy.SessionPolicyName] - // Sub policy is optional, if not set return success. - if !spok { - return claims, nil - } - // Sub policy is set but its not a string, reject such requests - spStr, spok := sp.(string) + + sp, spok := claims.Lookup(iampolicy.SessionPolicyName) if !spok { - return nil, errAuthentication + return claims.Map(), nil } // Looks like subpolicy is set and is a string, if set then its // base64 encoded, decode it. Decoding fails reject such requests. - spBytes, err := base64.StdEncoding.DecodeString(spStr) + spBytes, err := base64.StdEncoding.DecodeString(sp) if err != nil { // Base64 decoding fails, we should log to indicate // something is malforming the request sent by client. logger.LogIf(context.Background(), err, logger.Application) return nil, errAuthentication } - claims[iampolicy.SessionPolicyName] = string(spBytes) + claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes) } - return claims, nil + + return claims.Map(), nil } // Fetch claims in the security token returned by the client and validate the token. diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index c67a20a14..754f689c6 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -201,7 +201,9 @@ func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) { if owner { return globalActiveCred } - cred, _ = globalIAMSys.GetUser(claims.AccessKey()) + if claims != nil { + cred, _ = globalIAMSys.GetUser(claims.AccessKey) + } } return cred } diff --git a/cmd/jwt.go b/cmd/jwt.go index d3233aa2b..b888d2a18 100644 --- a/cmd/jwt.go +++ b/cmd/jwt.go @@ -1,5 +1,5 @@ /* - * MinIO Cloud Storage, (C) 2016, 2017 MinIO, Inc. + * MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ package cmd import ( "context" "errors" - "fmt" "net/http" "time" jwtgo "github.com/dgrijalva/jwt-go" jwtreq "github.com/dgrijalva/jwt-go/request" + xjwt "github.com/minio/minio/cmd/jwt" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" ) @@ -35,8 +35,8 @@ const ( // Default JWT token for web handlers is one day. defaultJWTExpiry = 24 * time.Hour - // Inter-node JWT token expiry is 100 years approx. - defaultInterNodeJWTExpiry = 100 * 365 * 24 * time.Hour + // Inter-node JWT token expiry is 15 minutes. + defaultInterNodeJWTExpiry = 15 * time.Minute // URL JWT token expiry is one minute (might be exposed). defaultURLJWTExpiry = time.Minute @@ -73,42 +73,23 @@ func authenticateJWTUsersWithCredentials(credentials auth.Credentials, expiresAt return "", errAuthentication } - claims := jwtgo.MapClaims{} - claims["exp"] = expiresAt.Unix() - claims["sub"] = credentials.AccessKey - claims["accessKey"] = credentials.AccessKey + claims := xjwt.NewMapClaims() + claims.SetExpiry(expiresAt) + claims.SetAccessKey(credentials.AccessKey) jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims) return jwt.SignedString([]byte(serverCred.SecretKey)) } -func authenticateJWTAdmin(accessKey, secretKey string, expiry time.Duration) (string, error) { - passedCredential, err := auth.CreateCredentials(accessKey, secretKey) - if err != nil { - return "", err - } - - serverCred := globalActiveCred - - if serverCred.AccessKey != passedCredential.AccessKey { - return "", errInvalidAccessKeyID - } - - if !serverCred.Equal(passedCredential) { - return "", errAuthentication - } - - claims := jwtgo.MapClaims{} - claims["exp"] = UTCNow().Add(expiry).Unix() - claims["sub"] = passedCredential.AccessKey - claims["accessKey"] = passedCredential.AccessKey +func authenticateNode(accessKey, secretKey, audience string) (string, error) { + claims := xjwt.NewStandardClaims() + claims.SetExpiry(UTCNow().Add(defaultInterNodeJWTExpiry)) + claims.SetAccessKey(accessKey) + claims.SetAudience(audience) + claims.SetIssuer(ReleaseTag) jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims) - return jwt.SignedString([]byte(serverCred.SecretKey)) -} - -func authenticateNode(accessKey, secretKey string) (string, error) { - return authenticateJWTAdmin(accessKey, secretKey, defaultInterNodeJWTExpiry) + return jwt.SignedString([]byte(secretKey)) } func authenticateWeb(accessKey, secretKey string) (string, error) { @@ -120,63 +101,29 @@ func authenticateURL(accessKey, secretKey string) (string, error) { } // Callback function used for parsing -func webTokenCallback(jwtToken *jwtgo.Token) (interface{}, error) { - if _, ok := jwtToken.Method.(*jwtgo.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"]) +func webTokenCallback(claims *xjwt.MapClaims) ([]byte, error) { + if claims.AccessKey == globalActiveCred.AccessKey { + return []byte(globalActiveCred.SecretKey), nil } - - if err := jwtToken.Claims.Valid(); err != nil { - return nil, errAuthentication + if globalIAMSys == nil { + return nil, errInvalidAccessKeyID } - - if claimsPtr, ok := jwtToken.Claims.(*jwtgo.MapClaims); ok { - claims := *claimsPtr - accessKey, ok := claims["accessKey"].(string) - if !ok { - accessKey, ok = claims["sub"].(string) - if !ok { - return nil, errInvalidAccessKeyID - } - } - if accessKey == globalActiveCred.AccessKey { - return []byte(globalActiveCred.SecretKey), nil - } - if globalIAMSys == nil { + ok, err := globalIAMSys.IsTempUser(claims.AccessKey) + if err != nil { + if err == errNoSuchUser { return nil, errInvalidAccessKeyID } - if _, ok = claims["aud"].(string); !ok { - cred, ok := globalIAMSys.GetUser(accessKey) - if !ok { - return nil, errInvalidAccessKeyID - } - return []byte(cred.SecretKey), nil - } - return []byte(globalActiveCred.SecretKey), nil + return nil, err } - - return nil, errAuthentication -} - -func parseJWTWithClaims(tokenString string, claims jwtgo.Claims) (*jwtgo.Token, error) { - p := &jwtgo.Parser{ - SkipClaimsValidation: true, - ValidMethods: []string{ - jwtgo.SigningMethodHS256.Alg(), - jwtgo.SigningMethodHS512.Alg(), - }, + if ok { + return []byte(globalActiveCred.SecretKey), nil } - jwtToken, err := p.ParseWithClaims(tokenString, claims, webTokenCallback) - if err != nil { - switch e := err.(type) { - case *jwtgo.ValidationError: - if e.Inner == nil { - return nil, errAuthentication - } - return nil, e.Inner - } - return nil, errAuthentication + cred, ok := globalIAMSys.GetUser(claims.AccessKey) + if !ok { + return nil, errInvalidAccessKeyID } - return jwtToken, nil + return []byte(cred.SecretKey), nil + } func isAuthTokenValid(token string) bool { @@ -184,78 +131,40 @@ func isAuthTokenValid(token string) bool { return err == nil } -func webTokenAuthenticate(token string) (mapClaims, bool, error) { - var claims = jwtgo.MapClaims{} +func webTokenAuthenticate(token string) (*xjwt.MapClaims, bool, error) { if token == "" { - return mapClaims{claims}, false, errNoAuthToken - } - jwtToken, err := parseJWTWithClaims(token, &claims) - if err != nil { - return mapClaims{claims}, false, err - } - if !jwtToken.Valid { - return mapClaims{claims}, false, errAuthentication - } - accessKey, ok := claims["accessKey"].(string) - if !ok { - accessKey, ok = claims["sub"].(string) - if !ok { - return mapClaims{claims}, false, errAuthentication - } + return nil, false, errNoAuthToken } - owner := accessKey == globalActiveCred.AccessKey - return mapClaims{claims}, owner, nil -} - -type mapClaims struct { - jwtgo.MapClaims -} - -func (m mapClaims) Map() map[string]interface{} { - return m.MapClaims -} - -func (m mapClaims) AccessKey() string { - claimSub, ok := m.MapClaims["accessKey"].(string) - if !ok { - claimSub, _ = m.MapClaims["sub"].(string) + claims := xjwt.NewMapClaims() + if err := xjwt.ParseWithClaims(token, claims, webTokenCallback); err != nil { + return claims, false, errAuthentication } - return claimSub + owner := claims.AccessKey == globalActiveCred.AccessKey + return claims, owner, nil } // Check if the request is authenticated. // Returns nil if the request is authenticated. errNoAuthToken if token missing. // Returns errAuthentication for all other errors. -func webRequestAuthenticate(req *http.Request) (mapClaims, bool, error) { - var claims = jwtgo.MapClaims{} - tokStr, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req) +func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, bool, error) { + token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req) if err != nil { if err == jwtreq.ErrNoTokenInRequest { - return mapClaims{claims}, false, errNoAuthToken + return nil, false, errNoAuthToken } - return mapClaims{claims}, false, err + return nil, false, err } - jwtToken, err := parseJWTWithClaims(tokStr, &claims) - if err != nil { - return mapClaims{claims}, false, err - } - if !jwtToken.Valid { - return mapClaims{claims}, false, errAuthentication - } - accessKey, ok := claims["accessKey"].(string) - if !ok { - accessKey, ok = claims["sub"].(string) - if !ok { - return mapClaims{claims}, false, errAuthentication - } + claims := xjwt.NewMapClaims() + if err := xjwt.ParseWithClaims(token, claims, webTokenCallback); err != nil { + return claims, false, errAuthentication } - owner := accessKey == globalActiveCred.AccessKey - return mapClaims{claims}, owner, nil + owner := claims.AccessKey == globalActiveCred.AccessKey + return claims, owner, nil } -func newAuthToken() string { +func newAuthToken(audience string) string { cred := globalActiveCred - token, err := authenticateNode(cred.AccessKey, cred.SecretKey) + token, err := authenticateNode(cred.AccessKey, cred.SecretKey, audience) logger.CriticalIf(context.Background(), err) return token } diff --git a/cmd/jwt/parser.go b/cmd/jwt/parser.go new file mode 100644 index 000000000..a62b9c781 --- /dev/null +++ b/cmd/jwt/parser.go @@ -0,0 +1,371 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +// This file is a re-implementation of the original code here with some +// additional allocation tweaks reproduced using GODEBUG=allocfreetrace=1 +// original file https://github.com/dgrijalva/jwt-go/blob/master/parser.go +// borrowed under MIT License https://github.com/dgrijalva/jwt-go/blob/master/LICENSE + +import ( + "crypto" + "crypto/hmac" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" + "time" + + jwtgo "github.com/dgrijalva/jwt-go" + jsoniter "github.com/json-iterator/go" +) + +// SigningMethodHMAC - Implements the HMAC-SHA family of signing methods signing methods +// Expects key type of []byte for both signing and validation +type SigningMethodHMAC struct { + Name string + Hash crypto.Hash +} + +// Specific instances for HS256, HS384, HS512 +var ( + SigningMethodHS256 *SigningMethodHMAC + SigningMethodHS384 *SigningMethodHMAC + SigningMethodHS512 *SigningMethodHMAC +) + +var ( + base64BufPool sync.Pool + hmacSigners []*SigningMethodHMAC +) + +func init() { + base64BufPool = sync.Pool{ + New: func() interface{} { + buf := make([]byte, 1024) + return &buf + }, + } + + hmacSigners = []*SigningMethodHMAC{ + {"HS256", crypto.SHA256}, + {"HS384", crypto.SHA384}, + {"HS512", crypto.SHA512}, + } +} + +// StandardClaims are basically standard claims with "accessKey" +type StandardClaims struct { + AccessKey string `json:"accessKey,omitempty"` + jwtgo.StandardClaims +} + +// MapClaims - implements custom unmarshaller +type MapClaims struct { + AccessKey string `json:"accessKey,omitempty"` + jwtgo.MapClaims +} + +// NewStandardClaims - initializes standard claims +func NewStandardClaims() *StandardClaims { + return &StandardClaims{} +} + +// SetIssuer sets issuer for these claims +func (c *StandardClaims) SetIssuer(issuer string) { + c.Issuer = issuer +} + +// SetAudience sets audience for these claims +func (c *StandardClaims) SetAudience(aud string) { + c.Audience = aud +} + +// SetExpiry sets expiry in unix epoch secs +func (c *StandardClaims) SetExpiry(t time.Time) { + c.ExpiresAt = t.Unix() +} + +// SetAccessKey sets access key as jwt subject and custom +// "accessKey" field. +func (c *StandardClaims) SetAccessKey(accessKey string) { + c.Subject = accessKey + c.AccessKey = accessKey +} + +// Valid - implements https://godoc.org/github.com/dgrijalva/jwt-go#Claims compatible +// claims interface, additionally validates "accessKey" fields. +func (c *StandardClaims) Valid() error { + if err := c.StandardClaims.Valid(); err != nil { + return err + } + + if c.AccessKey == "" && c.Subject == "" { + return jwtgo.NewValidationError("accessKey/sub missing", + jwtgo.ValidationErrorClaimsInvalid) + } + + return nil +} + +// NewMapClaims - Initializes a new map claims +func NewMapClaims() *MapClaims { + return &MapClaims{MapClaims: jwtgo.MapClaims{}} +} + +// Lookup returns the value and if the key is found. +func (c *MapClaims) Lookup(key string) (value string, ok bool) { + var vinterface interface{} + vinterface, ok = c.MapClaims[key] + if ok { + value, ok = vinterface.(string) + } + return +} + +// SetExpiry sets expiry in unix epoch secs +func (c *MapClaims) SetExpiry(t time.Time) { + c.MapClaims["exp"] = t.Unix() +} + +// SetAccessKey sets access key as jwt subject and custom +// "accessKey" field. +func (c *MapClaims) SetAccessKey(accessKey string) { + c.MapClaims["sub"] = accessKey + c.MapClaims["accessKey"] = accessKey +} + +// Valid - implements https://godoc.org/github.com/dgrijalva/jwt-go#Claims compatible +// claims interface, additionally validates "accessKey" fields. +func (c *MapClaims) Valid() error { + if err := c.MapClaims.Valid(); err != nil { + return err + } + + if c.AccessKey == "" { + return jwtgo.NewValidationError("accessKey/sub missing", + jwtgo.ValidationErrorClaimsInvalid) + } + + return nil +} + +// Map returns underlying low-level map claims. +func (c *MapClaims) Map() map[string]interface{} { + return c.MapClaims +} + +// MarshalJSON marshals the MapClaims struct +func (c *MapClaims) MarshalJSON() ([]byte, error) { + return json.Marshal(c.MapClaims) +} + +// ParseWithStandardClaims - parse the token string, valid methods. +func ParseWithStandardClaims(tokenStr string, claims *StandardClaims, key []byte) error { + // Key is not provided. + if key == nil { + // keyFunc was not provided, return error. + return jwtgo.NewValidationError("no key was provided.", jwtgo.ValidationErrorUnverifiable) + } + + bufp := base64BufPool.Get().(*[]byte) + defer base64BufPool.Put(bufp) + + signer, err := ParseUnverifiedStandardClaims(tokenStr, claims, *bufp) + if err != nil { + return err + } + + i := strings.LastIndex(tokenStr, ".") + if i < 0 { + return jwtgo.ErrSignatureInvalid + } + + n, err := base64Decode(tokenStr[i+1:], *bufp) + if err != nil { + return err + } + + hasher := hmac.New(signer.Hash.New, key) + hasher.Write([]byte(tokenStr[:i])) + if !hmac.Equal((*bufp)[:n], hasher.Sum(nil)) { + return jwtgo.ErrSignatureInvalid + } + + if claims.AccessKey == "" && claims.Subject == "" { + return jwtgo.NewValidationError("accessKey/sub missing", + jwtgo.ValidationErrorClaimsInvalid) + } + + // Signature is valid, lets validate the claims for + // other fields such as expiry etc. + return claims.Valid() +} + +// https://tools.ietf.org/html/rfc7519#page-11 +type jwtHeader struct { + Algorithm string `json:"alg"` + Type string `json:"typ"` +} + +// ParseUnverifiedStandardClaims - WARNING: Don't use this method unless you know what you're doing +// +// This method parses the token but doesn't validate the signature. It's only +// ever useful in cases where you know the signature is valid (because it has +// been checked previously in the stack) and you want to extract values from +// it. +func ParseUnverifiedStandardClaims(tokenString string, claims *StandardClaims, buf []byte) (*SigningMethodHMAC, error) { + if strings.Count(tokenString, ".") != 2 { + return nil, jwtgo.ErrSignatureInvalid + } + + i := strings.Index(tokenString, ".") + j := strings.LastIndex(tokenString, ".") + + n, err := base64Decode(tokenString[:i], buf) + if err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + var header = jwtHeader{} + var json = jsoniter.ConfigCompatibleWithStandardLibrary + if err = json.Unmarshal(buf[:n], &header); err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + n, err = base64Decode(tokenString[i+1:j], buf) + if err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + if err = json.Unmarshal(buf[:n], claims); err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + for _, signer := range hmacSigners { + if header.Algorithm == signer.Name { + return signer, nil + } + } + + return nil, jwtgo.NewValidationError(fmt.Sprintf("signing method (%s) is unavailable.", header.Algorithm), + jwtgo.ValidationErrorUnverifiable) +} + +// ParseWithClaims - parse the token string, valid methods. +func ParseWithClaims(tokenStr string, claims *MapClaims, fn func(*MapClaims) ([]byte, error)) error { + // Key lookup function has to be provided. + if fn == nil { + // keyFunc was not provided, return error. + return jwtgo.NewValidationError("no Keyfunc was provided.", jwtgo.ValidationErrorUnverifiable) + } + + bufp := base64BufPool.Get().(*[]byte) + defer base64BufPool.Put(bufp) + + signer, err := ParseUnverifiedMapClaims(tokenStr, claims, *bufp) + if err != nil { + return err + } + + i := strings.LastIndex(tokenStr, ".") + if i < 0 { + return jwtgo.ErrSignatureInvalid + } + + n, err := base64Decode(tokenStr[i+1:], *bufp) + if err != nil { + return err + } + + var ok bool + claims.AccessKey, ok = claims.Lookup("accessKey") + if !ok { + claims.AccessKey, ok = claims.Lookup("sub") + if !ok { + return jwtgo.NewValidationError("accessKey/sub missing", + jwtgo.ValidationErrorClaimsInvalid) + } + } + + // Lookup key from claims, claims may not be valid and may return + // invalid key which is okay as the signature verification will fail. + key, err := fn(claims) + if err != nil { + return err + } + + hasher := hmac.New(signer.Hash.New, key) + hasher.Write([]byte(tokenStr[:i])) + if !hmac.Equal((*bufp)[:n], hasher.Sum(nil)) { + return jwtgo.ErrSignatureInvalid + } + + // Signature is valid, lets validate the claims for + // other fields such as expiry etc. + return claims.Valid() +} + +// base64Decode returns the bytes represented by the base64 string s. +func base64Decode(s string, buf []byte) (int, error) { + return base64.RawURLEncoding.Decode(buf, []byte(s)) +} + +// ParseUnverifiedMapClaims - WARNING: Don't use this method unless you know what you're doing +// +// This method parses the token but doesn't validate the signature. It's only +// ever useful in cases where you know the signature is valid (because it has +// been checked previously in the stack) and you want to extract values from +// it. +func ParseUnverifiedMapClaims(tokenString string, claims *MapClaims, buf []byte) (*SigningMethodHMAC, error) { + if strings.Count(tokenString, ".") != 2 { + return nil, jwtgo.ErrSignatureInvalid + } + + i := strings.Index(tokenString, ".") + j := strings.LastIndex(tokenString, ".") + + n, err := base64Decode(tokenString[:i], buf) + if err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + var header = jwtHeader{} + var json = jsoniter.ConfigCompatibleWithStandardLibrary + if err = json.Unmarshal(buf[:n], &header); err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + n, err = base64Decode(tokenString[i+1:j], buf) + if err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + if err = json.Unmarshal(buf[:n], &claims.MapClaims); err != nil { + return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed} + } + + for _, signer := range hmacSigners { + if header.Algorithm == signer.Name { + return signer, nil + } + } + + return nil, jwtgo.NewValidationError(fmt.Sprintf("signing method (%s) is unavailable.", header.Algorithm), + jwtgo.ValidationErrorUnverifiable) +} diff --git a/cmd/jwt/parser_test.go b/cmd/jwt/parser_test.go new file mode 100644 index 000000000..8b689635c --- /dev/null +++ b/cmd/jwt/parser_test.go @@ -0,0 +1,214 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jwt + +// This file is a re-implementation of the original code here with some +// additional allocation tweaks reproduced using GODEBUG=allocfreetrace=1 +// original file https://github.com/dgrijalva/jwt-go/blob/master/parser.go +// borrowed under MIT License https://github.com/dgrijalva/jwt-go/blob/master/LICENSE + +import ( + "fmt" + "testing" + "time" + + "github.com/dgrijalva/jwt-go" + jwtgo "github.com/dgrijalva/jwt-go" +) + +var ( + defaultKeyFunc = func(claim *MapClaims) ([]byte, error) { return []byte("HelloSecret"), nil } + emptyKeyFunc = func(claim *MapClaims) ([]byte, error) { return nil, nil } + errorKeyFunc = func(claim *MapClaims) ([]byte, error) { return nil, fmt.Errorf("error loading key") } +) + +var jwtTestData = []struct { + name string + tokenString string + keyfunc func(*MapClaims) ([]byte, error) + claims jwt.Claims + valid bool + errors int32 +}{ + { + "basic", + "", + defaultKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + }, + }, + true, + 0, + }, + { + "basic expired", + "", // autogen + defaultKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + "exp": float64(time.Now().Unix() - 100), + }, + }, + false, + -1, + }, + { + "basic nbf", + "", // autogen + defaultKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + "nbf": float64(time.Now().Unix() + 100), + }, + }, + false, + -1, + }, + { + "expired and nbf", + "", // autogen + defaultKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + "nbf": float64(time.Now().Unix() + 100), + "exp": float64(time.Now().Unix() - 100), + }, + }, + false, + -1, + }, + { + "basic invalid", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", + defaultKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + }, + }, + false, + -1, + }, + { + "basic nokeyfunc", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", + nil, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + }, + }, + false, + -1, + }, + { + "basic nokey", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", + emptyKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + }, + }, + false, + -1, + }, + { + "basic errorkey", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", + errorKeyFunc, + &MapClaims{ + MapClaims: jwtgo.MapClaims{ + "foo": "bar", + }, + }, + false, + -1, + }, + { + "Standard Claims", + "", + defaultKeyFunc, + &StandardClaims{ + StandardClaims: jwtgo.StandardClaims{ + ExpiresAt: time.Now().Add(time.Second * 10).Unix(), + }, + }, + true, + 0, + }, +} + +func mapClaimsToken(claims *MapClaims) string { + claims.SetAccessKey("test") + j := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims) + tk, _ := j.SignedString([]byte("HelloSecret")) + return tk +} + +func standardClaimsToken(claims *StandardClaims) string { + claims.AccessKey = "test" + claims.Subject = "test" + j := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims) + tk, _ := j.SignedString([]byte("HelloSecret")) + return tk +} + +func TestParserParse(t *testing.T) { + // Iterate over test data set and run tests + for _, data := range jwtTestData { + data := data + t.Run(data.name, func(t *testing.T) { + // Parse the token + var err error + + // Figure out correct claims type + switch claims := data.claims.(type) { + case *MapClaims: + if data.tokenString == "" { + data.tokenString = mapClaimsToken(claims) + } + err = ParseWithClaims(data.tokenString, &MapClaims{}, data.keyfunc) + case *StandardClaims: + if data.tokenString == "" { + data.tokenString = standardClaimsToken(claims) + } + err = ParseWithStandardClaims(data.tokenString, &StandardClaims{}, []byte("HelloSecret")) + } + + if data.valid && err != nil { + t.Errorf("Error while verifying token: %T:%v", err, err) + } + + if !data.valid && err == nil { + t.Errorf("Invalid token passed validation") + } + + if data.errors != 0 { + _, ok := err.(*jwt.ValidationError) + if !ok { + t.Errorf("Expected *jwt.ValidationError, but got %#v instead", err) + } + } + }) + } +} diff --git a/cmd/jwt_test.go b/cmd/jwt_test.go index 5eec8f2b6..685d7afcb 100644 --- a/cmd/jwt_test.go +++ b/cmd/jwt_test.go @@ -21,6 +21,7 @@ import ( "os" "testing" + xjwt "github.com/minio/minio/cmd/jwt" "github.com/minio/minio/pkg/auth" ) @@ -62,9 +63,7 @@ func testAuthenticate(authType string, t *testing.T) { // Run tests. for _, testCase := range testCases { var err error - if authType == "node" { - _, err = authenticateNode(testCase.accessKey, testCase.secretKey) - } else if authType == "web" { + if authType == "web" { _, err = authenticateWeb(testCase.accessKey, testCase.secretKey) } else if authType == "url" { _, err = authenticateURL(testCase.accessKey, testCase.secretKey) @@ -83,10 +82,6 @@ func testAuthenticate(authType string, t *testing.T) { } } -func TestAuthenticateNode(t *testing.T) { - testAuthenticate("node", t) -} - func TestAuthenticateWeb(t *testing.T) { testAuthenticate("web", t) } @@ -150,6 +145,64 @@ func TestWebRequestAuthenticate(t *testing.T) { } } +func BenchmarkParseJWTStandardClaims(b *testing.B) { + obj, fsDir, err := prepareFS() + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(fsDir) + if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil { + b.Fatal(err) + } + + creds := globalActiveCred + token, err := authenticateNode(creds.AccessKey, creds.SecretKey, "") + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err = xjwt.ParseWithStandardClaims(token, xjwt.NewStandardClaims(), []byte(creds.SecretKey)) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkParseJWTMapClaims(b *testing.B) { + obj, fsDir, err := prepareFS() + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(fsDir) + if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil { + b.Fatal(err) + } + + creds := globalActiveCred + token, err := authenticateNode(creds.AccessKey, creds.SecretKey, "") + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err = xjwt.ParseWithClaims(token, xjwt.NewMapClaims(), func(*xjwt.MapClaims) ([]byte, error) { + return []byte(creds.SecretKey), nil + }) + if err != nil { + b.Fatal(err) + } + } + }) +} + func BenchmarkAuthenticateNode(b *testing.B) { obj, fsDir, err := prepareFS() if err != nil { @@ -164,7 +217,7 @@ func BenchmarkAuthenticateNode(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - authenticateNode(creds.AccessKey, creds.SecretKey) + authenticateNode(creds.AccessKey, creds.SecretKey, "") } } diff --git a/cmd/lock-rest-server-common_test.go b/cmd/lock-rest-server-common_test.go index c031d2ddc..150027b2a 100644 --- a/cmd/lock-rest-server-common_test.go +++ b/cmd/lock-rest-server-common_test.go @@ -40,7 +40,7 @@ func createLockTestServer(t *testing.T) (string, *lockRESTServer, string) { }, } creds := globalActiveCred - token, err := authenticateNode(creds.AccessKey, creds.SecretKey) + token, err := authenticateNode(creds.AccessKey, creds.SecretKey, "") if err != nil { t.Fatal(err) } diff --git a/cmd/rest/client.go b/cmd/rest/client.go index 9a0fd51d9..43e9b132a 100644 --- a/cmd/rest/client.go +++ b/cmd/rest/client.go @@ -47,7 +47,7 @@ type Client struct { httpClient *http.Client httpIdleConnsCloser func() url *url.URL - newAuthToken func() string + newAuthToken func(audience string) string } // URL query separator constants @@ -62,7 +62,7 @@ func (c *Client) CallWithContext(ctx context.Context, method string, values url. return nil, &NetworkError{err} } req = req.WithContext(ctx) - req.Header.Set("Authorization", "Bearer "+c.newAuthToken()) + req.Header.Set("Authorization", "Bearer "+c.newAuthToken(req.URL.Query().Encode())) req.Header.Set("X-Minio-Time", time.Now().UTC().Format(time.RFC3339)) if length > 0 { req.ContentLength = length @@ -102,7 +102,7 @@ func (c *Client) Close() { } // NewClient - returns new REST client. -func NewClient(url *url.URL, newCustomTransport func() *http.Transport, newAuthToken func() string) (*Client, error) { +func NewClient(url *url.URL, newCustomTransport func() *http.Transport, newAuthToken func(aud string) string) (*Client, error) { // Transport is exactly same as Go default in https://golang.org/pkg/net/http/#RoundTripper // except custom DialContext and TLSClientConfig. tr := newCustomTransport() diff --git a/cmd/storage-rest-server.go b/cmd/storage-rest-server.go index 325bc32e7..0abe16b6f 100644 --- a/cmd/storage-rest-server.go +++ b/cmd/storage-rest-server.go @@ -30,9 +30,11 @@ import ( "strings" "time" + jwtreq "github.com/dgrijalva/jwt-go/request" "github.com/gorilla/mux" "github.com/minio/minio/cmd/config" xhttp "github.com/minio/minio/cmd/http" + xjwt "github.com/minio/minio/cmd/jwt" "github.com/minio/minio/cmd/logger" ) @@ -54,11 +56,29 @@ const DefaultSkewTime = 15 * time.Minute // Authenticates storage client's requests and validates for skewed time. func storageServerRequestValidate(r *http.Request) error { - _, owner, err := webRequestAuthenticate(r) + token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(r) if err != nil { + if err == jwtreq.ErrNoTokenInRequest { + return errNoAuthToken + } return err } - if !owner { // Disable access for non-admin users. + + claims := xjwt.NewStandardClaims() + if err = xjwt.ParseWithStandardClaims(token, claims, []byte(globalActiveCred.SecretKey)); err != nil { + return errAuthentication + } + + owner := claims.AccessKey == globalActiveCred.AccessKey || claims.Subject == globalActiveCred.AccessKey + if !owner { + return errAuthentication + } + + if claims.Audience != r.URL.Query().Encode() { + return errAuthentication + } + + if claims.Issuer != ReleaseTag { return errAuthentication } @@ -70,11 +90,12 @@ func storageServerRequestValidate(r *http.Request) error { utcNow := UTCNow() delta := requestTime.Sub(utcNow) if delta < 0 { - delta = delta * -1 + delta *= -1 } if delta > DefaultSkewTime { return fmt.Errorf("client time %v is too apart with server time %v", requestTime, utcNow) } + return nil } diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index 5ccb015b6..d98bd5250 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -101,7 +101,7 @@ func (web *webAPIHandlers) ServerInfo(r *http.Request, args *WebGenericArgs, rep } if !owner { - creds, ok := globalIAMSys.GetUser(claims.AccessKey()) + creds, ok := globalIAMSys.GetUser(claims.AccessKey) if ok && creds.SessionToken != "" { reply.MinioUserInfo["isTempUser"] = true } @@ -154,10 +154,10 @@ func (web *webAPIHandlers) MakeBucket(r *http.Request, args *MakeBucketArgs, rep // For authenticated users apply IAM policy. if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.CreateBucketAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, Claims: claims.Map(), }) { @@ -216,10 +216,10 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs, // For authenticated users apply IAM policy. if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.DeleteBucketAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, Claims: claims.Map(), }) { @@ -319,10 +319,10 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re } if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.ListBucketAction, BucketName: dnsRecord.Key, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: "", Claims: claims.Map(), @@ -342,10 +342,10 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re } for _, bucket := range buckets { if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.ListBucketAction, BucketName: bucket.Name, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: "", Claims: claims.Map(), @@ -495,19 +495,19 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r r.Header.Set("delimiter", SlashSeparator) readable := globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.ListBucketAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, Claims: claims.Map(), }) writable := globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.PutObjectAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: args.Prefix + SlashSeparator, Claims: claims.Map(), @@ -675,10 +675,10 @@ next: govBypassPerms := ErrAccessDenied if authErr != errNoAuthToken { if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.DeleteObjectAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: objectName, Claims: claims.Map(), @@ -686,10 +686,10 @@ next: return toJSONError(ctx, errAccessDenied) } if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.BypassGovernanceRetentionAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: objectName, Claims: claims.Map(), @@ -719,10 +719,10 @@ next: } if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.DeleteObjectAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: objectName, Claims: claims.Map(), @@ -844,8 +844,8 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se } // for IAM users, access key cannot be updated - // claims.AccessKey() is used instead of accesskey from args - prevCred, ok := globalIAMSys.GetUser(claims.AccessKey()) + // claims.AccessKey is used instead of accesskey from args + prevCred, ok := globalIAMSys.GetUser(claims.AccessKey) if !ok { return errInvalidAccessKeyID } @@ -855,7 +855,7 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se return errIncorrectCreds } - creds, err := auth.CreateCredentials(claims.AccessKey(), args.NewSecretKey) + creds, err := auth.CreateCredentials(claims.AccessKey, args.NewSecretKey) if err != nil { return toJSONError(ctx, err) } @@ -892,7 +892,7 @@ func (web *webAPIHandlers) CreateURLToken(r *http.Request, args *WebGenericArgs, creds := globalActiveCred if !owner { var ok bool - creds, ok = globalIAMSys.GetUser(claims.AccessKey()) + creds, ok = globalIAMSys.GetUser(claims.AccessKey) if !ok { return toJSONError(ctx, errInvalidAccessKeyID) } @@ -954,10 +954,10 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) { // For authenticated users apply IAM policy. if authErr == nil { if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.PutObjectAction, BucketName: bucket, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: object, Claims: claims.Map(), @@ -966,10 +966,10 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) { return } if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.PutObjectRetentionAction, BucketName: bucket, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: object, Claims: claims.Map(), @@ -977,10 +977,10 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) { retPerms = ErrNone } if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.PutObjectLegalHoldAction, BucketName: bucket, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: object, Claims: claims.Map(), @@ -1188,10 +1188,10 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) { // For authenticated users apply IAM policy. if authErr == nil { if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetObjectAction, BucketName: bucket, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: object, Claims: claims.Map(), @@ -1200,10 +1200,10 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) { return } if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetObjectRetentionAction, BucketName: bucket, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: object, Claims: claims.Map(), @@ -1211,10 +1211,10 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) { getRetPerms = ErrNone } if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetObjectLegalHoldAction, BucketName: bucket, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: object, Claims: claims.Map(), @@ -1390,10 +1390,10 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) { if authErr == nil { for _, object := range args.Objects { if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetObjectAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: pathJoin(args.Prefix, object), Claims: claims.Map(), @@ -1403,10 +1403,10 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) { } retentionPerm := ErrAccessDenied if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetObjectRetentionAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: pathJoin(args.Prefix, object), Claims: claims.Map(), @@ -1417,10 +1417,10 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) { legalHoldPerm := ErrAccessDenied if globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetObjectLegalHoldAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, ObjectName: pathJoin(args.Prefix, object), Claims: claims.Map(), @@ -1570,10 +1570,10 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic // For authenticated users apply IAM policy. if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetBucketPolicyAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, Claims: claims.Map(), }) { @@ -1668,10 +1668,10 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB // For authenticated users apply IAM policy. if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.GetBucketPolicyAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, Claims: claims.Map(), }) { @@ -1759,10 +1759,10 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic // For authenticated users apply IAM policy. if !globalIAMSys.IsAllowed(iampolicy.Args{ - AccountName: claims.AccessKey(), + AccountName: claims.AccessKey, Action: iampolicy.PutBucketPolicyAction, BucketName: args.BucketName, - ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), + ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()), IsOwner: owner, Claims: claims.Map(), }) { @@ -1905,7 +1905,7 @@ func (web *webAPIHandlers) PresignedGet(r *http.Request, args *PresignedGetArgs, var creds auth.Credentials if !owner { var ok bool - creds, ok = globalIAMSys.GetUser(claims.AccessKey()) + creds, ok = globalIAMSys.GetUser(claims.AccessKey) if !ok { return toJSONError(ctx, errInvalidAccessKeyID) } diff --git a/cmd/web-handlers_test.go b/cmd/web-handlers_test.go index c66611c60..00cbdc2ae 100644 --- a/cmd/web-handlers_test.go +++ b/cmd/web-handlers_test.go @@ -36,6 +36,7 @@ import ( jwtgo "github.com/dgrijalva/jwt-go" humanize "github.com/dustin/go-humanize" miniogopolicy "github.com/minio/minio-go/v6/pkg/policy" + xjwt "github.com/minio/minio/cmd/jwt" "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/policy/condition" @@ -753,12 +754,10 @@ func TestWebCreateURLToken(t *testing.T) { } func getTokenString(accessKey, secretKey string) (string, error) { - utcNow := UTCNow() - mapClaims := jwtgo.MapClaims{} - mapClaims["exp"] = utcNow.Add(defaultJWTExpiry).Unix() - mapClaims["sub"] = accessKey - mapClaims["accessKey"] = accessKey - token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, mapClaims) + claims := xjwt.NewMapClaims() + claims.SetExpiry(UTCNow().Add(defaultJWTExpiry)) + claims.SetAccessKey(accessKey) + token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims) return token.SignedString([]byte(secretKey)) } @@ -1002,7 +1001,7 @@ func testDownloadWebHandler(obj ObjectLayer, instanceType string, t TestErrHandl } if !bytes.Equal(bodyContent, bytes.NewBufferString("Authentication failed, check your access credentials").Bytes()) { - t.Fatalf("Expected authentication error message, got %v", bodyContent) + t.Fatalf("Expected authentication error message, got %s", string(bodyContent)) } // Unauthenticated download should fail.