Honor DurationSeconds properly for WebIdentity (#8581)
Also cleanup code to add various constants for verbatim strings across the code base. Fixes #8482
This commit is contained in:
committed by
Nitish Tiwari
parent
c7844fb1fb
commit
b21835f195
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
jwtgo "github.com/dgrijalva/jwt-go"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
@@ -107,36 +108,19 @@ type JWT struct {
|
||||
Config
|
||||
}
|
||||
|
||||
func expToInt64(expI interface{}) (expAt int64, err error) {
|
||||
switch exp := expI.(type) {
|
||||
case float64:
|
||||
expAt = int64(exp)
|
||||
case int64:
|
||||
expAt = exp
|
||||
case json.Number:
|
||||
expAt, err = exp.Int64()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
default:
|
||||
return 0, ErrInvalidDuration
|
||||
}
|
||||
return expAt, nil
|
||||
}
|
||||
|
||||
// GetDefaultExpiration - returns the expiration seconds expected.
|
||||
func GetDefaultExpiration(dsecs string) (time.Duration, error) {
|
||||
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
|
||||
if dsecs != "" {
|
||||
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidDuration
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
// The duration, in seconds, of the role session.
|
||||
// The value can range from 900 seconds (15 minutes)
|
||||
// to 12 hours.
|
||||
if expirySecs < 900 || expirySecs > 43200 {
|
||||
return 0, ErrInvalidDuration
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
|
||||
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
|
||||
@@ -144,6 +128,39 @@ func GetDefaultExpiration(dsecs string) (time.Duration, error) {
|
||||
return defaultExpiryDuration, nil
|
||||
}
|
||||
|
||||
func updateClaimsExpiry(dsecs string, claims map[string]interface{}) error {
|
||||
expStr := claims["exp"]
|
||||
if expStr == "" {
|
||||
return ErrTokenExpired
|
||||
}
|
||||
|
||||
// No custom duration requested, the claims can be used as is.
|
||||
if dsecs == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
expAt, err := auth.ExpToInt64(expStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultExpiryDuration, err := GetDefaultExpiration(dsecs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify if JWT expiry is lesser than default expiry duration,
|
||||
// if that is the case then set the default expiration to be
|
||||
// from the JWT expiry claim.
|
||||
if time.Unix(expAt, 0).UTC().Sub(time.Now().UTC()) < defaultExpiryDuration {
|
||||
defaultExpiryDuration = time.Unix(expAt, 0).UTC().Sub(time.Now().UTC())
|
||||
} // else honor the specified expiry duration.
|
||||
|
||||
expiry := time.Now().UTC().Add(defaultExpiryDuration).Unix()
|
||||
claims["exp"] = strconv.FormatInt(expiry, 10) // update with new expiry.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates the access token.
|
||||
func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
|
||||
jp := new(jwtgo.Parser)
|
||||
@@ -173,25 +190,10 @@ func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
|
||||
expAt, err := expToInt64(claims["exp"])
|
||||
if err != nil {
|
||||
if err = updateClaimsExpiry(dsecs, claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultExpiryDuration, err := GetDefaultExpiration(dsecs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if time.Unix(expAt, 0).UTC().Sub(time.Now().UTC()) < defaultExpiryDuration {
|
||||
defaultExpiryDuration = time.Unix(expAt, 0).UTC().Sub(time.Now().UTC())
|
||||
}
|
||||
|
||||
expiry := time.Now().UTC().Add(defaultExpiryDuration).Unix()
|
||||
if expAt < expiry {
|
||||
claims["exp"] = strconv.FormatInt(expAt, 64)
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2018-2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,6 +26,42 @@ import (
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
func TestUpdateClaimsExpiry(t *testing.T) {
|
||||
testCases := []struct {
|
||||
exp interface{}
|
||||
dsecs string
|
||||
expectedFailure bool
|
||||
}{
|
||||
{"", "", true},
|
||||
{"-1", "0", true},
|
||||
{"-1", "900", true},
|
||||
{"1574812326", "900", false},
|
||||
{1574812326, "900", false},
|
||||
{int64(1574812326), "900", false},
|
||||
{int(1574812326), "900", false},
|
||||
{uint(1574812326), "900", false},
|
||||
{uint64(1574812326), "900", false},
|
||||
{json.Number("1574812326"), "900", false},
|
||||
{1574812326.000, "900", false},
|
||||
{time.Duration(3) * time.Minute, "900", false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
claims := map[string]interface{}{}
|
||||
claims["exp"] = testCase.exp
|
||||
err := updateClaimsExpiry(testCase.dsecs, claims)
|
||||
if err != nil && !testCase.expectedFailure {
|
||||
t.Errorf("Expected success, got failure %s", err)
|
||||
}
|
||||
if err == nil && testCase.expectedFailure {
|
||||
t.Error("Expected failure, got success")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWT(t *testing.T) {
|
||||
const jsonkey = `{"keys":
|
||||
[
|
||||
|
||||
@@ -38,8 +38,7 @@ type Validator interface {
|
||||
|
||||
// ErrTokenExpired - error token expired
|
||||
var (
|
||||
ErrTokenExpired = errors.New("token expired")
|
||||
ErrInvalidDuration = errors.New("duration higher than token expiry")
|
||||
ErrTokenExpired = errors.New("token expired")
|
||||
)
|
||||
|
||||
// Validators - holds list of providers indexed by provider id.
|
||||
|
||||
@@ -36,7 +36,15 @@ import (
|
||||
|
||||
const (
|
||||
// STS API version.
|
||||
stsAPIVersion = "2011-06-15"
|
||||
stsAPIVersion = "2011-06-15"
|
||||
stsVersion = "Version"
|
||||
stsAction = "Action"
|
||||
stsPolicy = "Policy"
|
||||
stsToken = "Token"
|
||||
stsWebIdentityToken = "WebIdentityToken"
|
||||
stsDurationSeconds = "DurationSeconds"
|
||||
stsLDAPUsername = "LDAPUsername"
|
||||
stsLDAPPassword = "LDAPPassword"
|
||||
|
||||
// STS API action constants
|
||||
clientGrants = "AssumeRoleWithClientGrants"
|
||||
@@ -46,6 +54,10 @@ const (
|
||||
|
||||
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
|
||||
|
||||
// JWT claim keys
|
||||
expClaim = "exp"
|
||||
subClaim = "sub"
|
||||
|
||||
// LDAP claim keys
|
||||
ldapUser = "ldapUser"
|
||||
ldapGroups = "ldapGroups"
|
||||
@@ -71,30 +83,30 @@ func registerSTSRouter(router *mux.Router) {
|
||||
}).HandlerFunc(httpTraceAll(sts.AssumeRole))
|
||||
|
||||
// Assume roles with JWT handler, handles both ClientGrants and WebIdentity.
|
||||
stsRouter.Methods("POST").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
|
||||
stsRouter.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
|
||||
ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get(xhttp.ContentType))
|
||||
noQueries := len(r.URL.Query()) == 0
|
||||
return ctypeOk && noQueries
|
||||
}).HandlerFunc(httpTraceAll(sts.AssumeRoleWithJWT))
|
||||
|
||||
// AssumeRoleWithClientGrants
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
|
||||
Queries("Action", clientGrants).
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("Token", "{Token:.*}")
|
||||
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
|
||||
Queries(stsAction, clientGrants).
|
||||
Queries(stsVersion, stsAPIVersion).
|
||||
Queries(stsToken, "{Token:.*}")
|
||||
|
||||
// AssumeRoleWithWebIdentity
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)).
|
||||
Queries("Action", webIdentity).
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("WebIdentityToken", "{Token:.*}")
|
||||
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)).
|
||||
Queries(stsAction, webIdentity).
|
||||
Queries(stsVersion, stsAPIVersion).
|
||||
Queries(stsWebIdentityToken, "{Token:.*}")
|
||||
|
||||
// AssumeRoleWithLDAPIdentity
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithLDAPIdentity)).
|
||||
Queries("Action", ldapIdentity).
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("LDAPUsername", "{LDAPUsername:.*}").
|
||||
Queries("LDAPPassword", "{LDAPPassword:.*}")
|
||||
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithLDAPIdentity)).
|
||||
Queries(stsAction, ldapIdentity).
|
||||
Queries(stsVersion, stsAPIVersion).
|
||||
Queries(stsLDAPUsername, "{LDAPUsername:.*}").
|
||||
Queries(stsLDAPPassword, "{LDAPPassword:.*}")
|
||||
}
|
||||
|
||||
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, stsErr STSErrorCode) {
|
||||
@@ -141,12 +153,12 @@ func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if r.Form.Get("Version") != stsAPIVersion {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion))
|
||||
if r.Form.Get(stsVersion) != stsAPIVersion {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get(stsVersion), stsAPIVersion))
|
||||
return
|
||||
}
|
||||
|
||||
action := r.Form.Get("Action")
|
||||
action := r.Form.Get(stsAction)
|
||||
switch action {
|
||||
case assumeRole:
|
||||
default:
|
||||
@@ -157,7 +169,7 @@ func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) {
|
||||
ctx = newContext(r, w, action)
|
||||
defer logger.AuditLog(w, r, action, nil)
|
||||
|
||||
sessionPolicyStr := r.Form.Get("Policy")
|
||||
sessionPolicyStr := r.Form.Get(stsPolicy)
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
// The plain text that you use for both inline and managed session
|
||||
// policies shouldn't exceed 2048 characters.
|
||||
@@ -182,7 +194,7 @@ func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
m := make(map[string]interface{})
|
||||
m["exp"], err = openid.GetDefaultExpiration(r.Form.Get("DurationSeconds"))
|
||||
m[expClaim], err = openid.GetDefaultExpiration(r.Form.Get(stsDurationSeconds))
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err)
|
||||
return
|
||||
@@ -248,12 +260,12 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
if r.Form.Get("Version") != stsAPIVersion {
|
||||
if r.Form.Get(stsVersion) != stsAPIVersion {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion))
|
||||
return
|
||||
}
|
||||
|
||||
action := r.Form.Get("Action")
|
||||
action := r.Form.Get(stsAction)
|
||||
switch action {
|
||||
case clientGrants, webIdentity:
|
||||
default:
|
||||
@@ -275,12 +287,12 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
token := r.Form.Get("Token")
|
||||
token := r.Form.Get(stsToken)
|
||||
if token == "" {
|
||||
token = r.Form.Get("WebIdentityToken")
|
||||
token = r.Form.Get(stsWebIdentityToken)
|
||||
}
|
||||
|
||||
m, err := v.Validate(token, r.Form.Get("DurationSeconds"))
|
||||
m, err := v.Validate(token, r.Form.Get(stsDurationSeconds))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case openid.ErrTokenExpired:
|
||||
@@ -291,7 +303,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSWebIdentityExpiredToken, err)
|
||||
}
|
||||
return
|
||||
case openid.ErrInvalidDuration:
|
||||
case auth.ErrInvalidDuration:
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err)
|
||||
return
|
||||
}
|
||||
@@ -299,7 +311,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
sessionPolicyStr := r.Form.Get("Policy")
|
||||
sessionPolicyStr := r.Form.Get(stsPolicy)
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
|
||||
// The plain text that you use for both inline and managed session
|
||||
// policies shouldn't exceed 2048 characters.
|
||||
@@ -343,7 +355,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
var subFromToken string
|
||||
if v, ok := m["sub"]; ok {
|
||||
if v, ok := m[subClaim]; ok {
|
||||
subFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
@@ -415,12 +427,12 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
if r.Form.Get("Version") != stsAPIVersion {
|
||||
if r.Form.Get(stsVersion) != stsAPIVersion {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion))
|
||||
return
|
||||
}
|
||||
|
||||
action := r.Form.Get("Action")
|
||||
action := r.Form.Get(stsAction)
|
||||
switch action {
|
||||
case ldapIdentity:
|
||||
default:
|
||||
@@ -431,15 +443,15 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
|
||||
ctx = newContext(r, w, action)
|
||||
defer logger.AuditLog(w, r, action, nil)
|
||||
|
||||
ldapUsername := r.Form.Get("LDAPUsername")
|
||||
ldapPassword := r.Form.Get("LDAPPassword")
|
||||
ldapUsername := r.Form.Get(stsLDAPUsername)
|
||||
ldapPassword := r.Form.Get(stsLDAPPassword)
|
||||
|
||||
if ldapUsername == "" || ldapPassword == "" {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("LDAPUsername and LDAPPassword cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
sessionPolicyStr := r.Form.Get("Policy")
|
||||
sessionPolicyStr := r.Form.Get(stsPolicy)
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
// The plain text that you use for both inline and managed session
|
||||
// policies shouldn't exceed 2048 characters.
|
||||
@@ -516,9 +528,9 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
|
||||
}
|
||||
expiryDur := globalLDAPConfig.GetExpiryDuration()
|
||||
m := map[string]interface{}{
|
||||
"exp": UTCNow().Add(expiryDur).Unix(),
|
||||
"ldapUser": ldapUsername,
|
||||
"ldapGroups": groups,
|
||||
expClaim: UTCNow().Add(expiryDur).Unix(),
|
||||
ldapUser: ldapUsername,
|
||||
ldapGroups: groups,
|
||||
}
|
||||
|
||||
if len(sessionPolicyStr) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user