diff --git a/cmd/server/go.mod b/cmd/server/go.mod index ccf4e83624f..1530c689a12 100644 --- a/cmd/server/go.mod +++ b/cmd/server/go.mod @@ -60,7 +60,7 @@ require ( go.uber.org/zap v1.13.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.0 // indirect gonum.org/v1/gonum v0.7.0 // indirect google.golang.org/grpc v1.59.0 // indirect @@ -80,6 +80,7 @@ require ( cloud.google.com/go/iam v1.1.3 // indirect cloud.google.com/go/storage v1.30.1 // indirect github.com/BurntSushi/toml v0.4.1 // indirect + github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/apache/thrift v0.16.0 // indirect github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 // indirect diff --git a/cmd/server/go.sum b/cmd/server/go.sum index 04b2fe0db6a..7a050947fa1 100644 --- a/cmd/server/go.sum +++ b/cmd/server/go.sum @@ -13,6 +13,8 @@ cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7Biccwk github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= +github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.33.0 h1:2K4mB9M4fo46sAM7t6QTsmSO8dLX1OqznLM7vn3OjZ8= github.com/Shopify/sarama v1.33.0/go.mod h1:lYO7LwEBkE0iAeTl94UfPSrDaavFzSFlmn+5isARATQ= @@ -590,8 +592,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/common/archiver/gcloud/go.mod b/common/archiver/gcloud/go.mod index 032769464cf..c3b0b63dc77 100644 --- a/common/archiver/gcloud/go.mod +++ b/common/archiver/gcloud/go.mod @@ -40,7 +40,7 @@ require ( go.uber.org/zap v1.13.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.0 // indirect google.golang.org/grpc v1.59.0 // indirect gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 // indirect diff --git a/common/archiver/gcloud/go.sum b/common/archiver/gcloud/go.sum index f295ac28f39..5a9376f5247 100644 --- a/common/archiver/gcloud/go.sum +++ b/common/archiver/gcloud/go.sum @@ -444,8 +444,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/common/authorization/factory_test.go b/common/authorization/factory_test.go index d7c589d64eb..7f02250bc59 100644 --- a/common/authorization/factory_test.go +++ b/common/authorization/factory_test.go @@ -62,7 +62,7 @@ func cfgOAuth() config.Authorization { return config.Authorization{ OAuthAuthorizer: config.OAuthAuthorizer{ Enable: true, - JwtCredentials: config.JwtCredentials{ + JwtCredentials: &config.JwtCredentials{ Algorithm: jwt.SigningMethodRS256.Name, PublicKey: "../../config/credentials/keytest.pub", }, @@ -83,10 +83,10 @@ func (s *factorySuite) TestFactoryNoopAuthorizer() { }{ {cfgNoop(), &nopAuthority{}, nil}, {cfgOAuthVar, &oauthAuthority{ - authorizationCfg: cfgOAuthVar.OAuthAuthorizer, - log: s.logger, - publicKey: publicKey, - parser: jwt.NewParser(jwt.WithValidMethods([]string{cfgOAuthVar.OAuthAuthorizer.JwtCredentials.Algorithm}), jwt.WithIssuedAt()), + config: cfgOAuthVar.OAuthAuthorizer, + log: s.logger, + publicKey: publicKey, + parser: jwt.NewParser(jwt.WithValidMethods([]string{cfgOAuthVar.OAuthAuthorizer.JwtCredentials.Algorithm}), jwt.WithIssuedAt()), }, nil}, } diff --git a/common/authorization/oauthAuthorizer.go b/common/authorization/oauthAuthorizer.go index 47ce1bea575..b3c220ca306 100644 --- a/common/authorization/oauthAuthorizer.go +++ b/common/authorization/oauthAuthorizer.go @@ -27,7 +27,9 @@ import ( "strings" "time" + "github.com/MicahParks/keyfunc/v2" "github.com/golang-jwt/jwt/v5" + "github.com/jmespath/go-jmespath" "go.uber.org/yarpc" "github.com/uber/cadence/common" @@ -39,12 +41,18 @@ import ( var _ jwt.Claims = (*JWTClaims)(nil) +const ( + groupSeparator = " " + jwtInternalIssuer = "internal-jwt" +) + type oauthAuthority struct { - authorizationCfg config.OAuthAuthorizer - domainCache cache.DomainCache - log log.Logger - parser *jwt.Parser - publicKey interface{} + config config.OAuthAuthorizer + domainCache cache.DomainCache + log log.Logger + parser *jwt.Parser + publicKey interface{} + jwks *keyfunc.JWKS } // JWTClaims is a Cadence specific claim with embeded Claims defined https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 @@ -61,41 +69,51 @@ func (j JWTClaims) GetGroups() []string { return strings.Split(j.Groups, groupSeparator) } -const groupSeparator = " " - -// NewOAuthAuthorizer creates a oauth authority +// NewOAuthAuthorizer creates an oauth Authorizer func NewOAuthAuthorizer( - authorizationCfg config.OAuthAuthorizer, + oauthConfig config.OAuthAuthorizer, log log.Logger, domainCache cache.DomainCache, ) (Authorizer, error) { - - key, err := common.LoadRSAPublicKey(authorizationCfg.JwtCredentials.PublicKey) - if err != nil { - return nil, fmt.Errorf("loading RSA public key: %w", err) + var jwks *keyfunc.JWKS + var key interface{} + var err error + + if oauthConfig.JwtCredentials != nil { + if oauthConfig.JwtCredentials.Algorithm != jwt.SigningMethodRS256.Name { + return nil, fmt.Errorf("algorithm %q is not supported", oauthConfig.JwtCredentials.Algorithm) + } + + if key, err = common.LoadRSAPublicKey(oauthConfig.JwtCredentials.PublicKey); err != nil { + return nil, fmt.Errorf("loading RSA public key: %w", err) + } } - if authorizationCfg.JwtCredentials.Algorithm != jwt.SigningMethodRS256.Name { - return nil, fmt.Errorf("algorithm %q is not supported", authorizationCfg.JwtCredentials.Algorithm) + if oauthConfig.Provider != nil { + if oauthConfig.Provider.JWKSURL == "" { + return nil, fmt.Errorf("JWKSURL is not set") + } + // Create the JWKS from the resource at the given URL. + if jwks, err = keyfunc.Get(oauthConfig.Provider.JWKSURL, keyfunc.Options{}); err != nil { + return nil, fmt.Errorf("creating JWKS from resource: %s error: %w", oauthConfig.Provider.JWKSURL, err) + } } return &oauthAuthority{ - authorizationCfg: authorizationCfg, - domainCache: domainCache, - log: log, + config: oauthConfig, + domainCache: domainCache, + log: log, parser: jwt.NewParser( - jwt.WithValidMethods([]string{authorizationCfg.JwtCredentials.Algorithm}), + jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Name}), jwt.WithIssuedAt(), ), publicKey: key, + jwks: jwks, }, nil } // Authorize defines the logic to verify get claims from token -func (a *oauthAuthority) Authorize( - ctx context.Context, - attributes *Attributes, -) (Result, error) { +func (a *oauthAuthority) Authorize(ctx context.Context, attributes *Attributes) (Result, error) { call := yarpc.CallFromContext(ctx) token := call.Header(common.AuthorizationTokenHeaderName) @@ -105,14 +123,25 @@ func (a *oauthAuthority) Authorize( } var claims JWTClaims - - _, err := a.parser.ParseWithClaims(token, &claims, a.keyFunc) - + parsedToken, err := a.parser.ParseWithClaims(token, &claims, a.keyFunc) if err != nil { a.log.Debug("request is not authorized", tag.Error(err)) return Result{Decision: DecisionDeny}, nil } + if !isTokenInternal(parsedToken) { + parsed, _, err := a.parser.ParseUnverified(token, jwt.MapClaims{}) + if err != nil { + a.log.Debug("request is not authorized", tag.Error(err)) + return Result{Decision: DecisionDeny}, nil + } + + if err := a.parseExternal(parsed.Claims.(jwt.MapClaims), &claims); err != nil { + a.log.Debug("request is not authorized", tag.Error(err)) + return Result{Decision: DecisionDeny}, nil + } + } + if err := a.validateTTL(&claims); err != nil { a.log.Debug("request is not authorized", tag.Error(err)) return Result{Decision: DecisionDeny}, nil @@ -137,8 +166,16 @@ func (a *oauthAuthority) Authorize( // keyFunc returns correct key to check signature func (a *oauthAuthority) keyFunc(token *jwt.Token) (interface{}, error) { - // only local public key is supported currently - return a.publicKey, nil + if isTokenInternal(token) && a.publicKey != nil { + return a.publicKey, nil + } + // External provider with JWKS provided + // https://datatracker.ietf.org/doc/html/rfc7517 + if a.jwks != nil { + return a.jwks.Keyfunc(token) + } + + return nil, errors.New("no public key for verification") } func (a *oauthAuthority) validateTTL(claims *JWTClaims) error { @@ -158,8 +195,50 @@ func (a *oauthAuthority) validateTTL(claims *JWTClaims) error { return errors.New("token is expired") } - if timeLeft > a.authorizationCfg.MaxJwtTTL { - return fmt.Errorf("token TTL: %d is larger than MaxTTL allowed: %d", timeLeft, a.authorizationCfg.MaxJwtTTL) + if timeLeft > a.config.MaxJwtTTL { + return fmt.Errorf("token TTL: %d is larger than MaxTTL allowed: %d", timeLeft, a.config.MaxJwtTTL) + } + + return nil +} + +func isTokenInternal(token *jwt.Token) bool { + // external providers should set kid part always + if _, ok := token.Header["kid"]; !ok { + return true + } + + issuer, err := token.Claims.GetIssuer() + if err != nil { + return false + } + + return issuer == jwtInternalIssuer +} + +func (a *oauthAuthority) parseExternal(rawClaims map[string]interface{}, claims *JWTClaims) error { + if a.config.Provider.GroupsAttributePath != "" { + userGroups, err := jmespath.Search(a.config.Provider.GroupsAttributePath, rawClaims) + if err != nil { + return fmt.Errorf("extracting JWT Groups claim: %w", err) + } + + if _, ok := userGroups.(string); !ok { + return errors.New("cannot convert groups to string") + } + + claims.Groups = userGroups.(string) + } + + if a.config.Provider.AdminAttributePath != "" { + isAdmin, err := jmespath.Search(a.config.Provider.AdminAttributePath, rawClaims) + if err != nil { + return fmt.Errorf("extracting JWT Admin claim: %w", err) + } + if _, ok := isAdmin.(bool); !ok { + return errors.New("cannot convert isAdmin to bool") + } + claims.Admin = isAdmin.(bool) } return nil diff --git a/common/authorization/oauthAuthorizer_test.go b/common/authorization/oauthAuthorizer_test.go index bdf83cc0b3d..b1e318853e2 100644 --- a/common/authorization/oauthAuthorizer_test.go +++ b/common/authorization/oauthAuthorizer_test.go @@ -49,6 +49,7 @@ type ( suite.Suite logger *log.MockLogger cfg config.OAuthAuthorizer + providerCfg config.OAuthAuthorizer att Attributes token string controller *gomock.Controller @@ -66,12 +67,19 @@ func (s *oauthSuite) SetupTest() { s.logger = &log.MockLogger{} s.cfg = config.OAuthAuthorizer{ Enable: true, - JwtCredentials: config.JwtCredentials{ + JwtCredentials: &config.JwtCredentials{ Algorithm: jwt.SigningMethodRS256.Name, PublicKey: "../../config/credentials/keytest.pub", }, MaxJwtTTL: 300000001, } + s.providerCfg = config.OAuthAuthorizer{ + Enable: true, + Provider: &config.OAuthProvider{ + GroupsAttributePath: "tst_group", + AdminAttributePath: "tst_admin", + }, + } // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdWIiOiIxMjM0NTY3ODkwIiwiTmFtZSI6IkpvaG4gRG9lIiwiR3JvdXBzIjoiYSBiIGMiLCJBZG1pbiI6ZmFsc2UsIklhdCI6MTYyNzUzODcxMiwiVFRMIjozMDAwMDAwMDB9.bh4s8-l1bjG7-QFzuouPy9WPvkq3_9U2e815WFrN-M247NQROBii8ju_N21i6ixK0t-VZTgcJs2B4aN4w1uiCTCg6NyhdeeG8Xd8NcYw0Oq7fjSoFmOXzDzljY6oi9M1XXniNrDIMBLfKXx8tgseSBwOnWoT3vja3ioU6ReqD3Xsp-Wg_clDhb6vtA6pDtnaCVXJNStLSbgWyi-1Mxo9ar92zRDV5YsMaBdUjFUT2bW9QcFzMFAqpHin0QEIa6GPZezY-yn88k5S5cT6Yh7WA4C0Q6C3H1n3EOS05Phwpxt840w7zjh5XR0-rd8-kRX84pHMh0GwHfjV1K7jBQ2QnQ&publicKey=-----BEGIN%20PUBLIC%20KEY-----%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAscukltHilaq%2Bo5gIVE4P%0AGwWl%2BesvJ2EaEpWw6ogr98Un11YJ4oKkwIkLw4iIo0tveCINA3cZmxaW1RejRWKE%0AqYFtQ1rYd6BsnFAHXWh2R3A1FtpG6ANUEGkE7OAJe2%2FL42E%2FImJ%2BGQxRvartInDM%0AyfiRfB7%2BL2n3wG%2BNi%2BhBNMtAaX4Wwbj2hup21Jjuo96TuhcGImBFBATGWaYR2wqe%0A%2F6by9wJexPHlY%2F1uDp3SnzF1dCLjp76SGCfyYqOGC%2FPxhQi7mDxeH9%2FtIC%2Blt%2FSz%0Awc1n8gZLtlRlZHinvYa8lhWXqVYw6WD8h4LTgALq9iY%2BbeD1PFQSY1GkQtt0RhRw%0AeQIDAQAB%0A-----END%20PUBLIC%20KEY----- s.token = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdWIiOiIxMjM0NTY3ODkwIiwiTmFtZSI6IkpvaG4gRG9lIiwiR3JvdXBzIjoiYSBiIGMiLCJBZG1pbiI6ZmFsc2UsIklhdCI6MTYyNzUzODcxMiwiVFRMIjozMDAwMDAwMDB9.bh4s8-l1bjG7-QFzuouPy9WPvkq3_9U2e815WFrN-M247NQROBii8ju_N21i6ixK0t-VZTgcJs2B4aN4w1uiCTCg6NyhdeeG8Xd8NcYw0Oq7fjSoFmOXzDzljY6oi9M1XXniNrDIMBLfKXx8tgseSBwOnWoT3vja3ioU6ReqD3Xsp-Wg_clDhb6vtA6pDtnaCVXJNStLSbgWyi-1Mxo9ar92zRDV5YsMaBdUjFUT2bW9QcFzMFAqpHin0QEIa6GPZezY-yn88k5S5cT6Yh7WA4C0Q6C3H1n3EOS05Phwpxt840w7zjh5XR0-rd8-kRX84pHMh0GwHfjV1K7jBQ2QnQ` ctx := context.Background() @@ -239,6 +247,13 @@ func (s *oauthSuite) TestDifferentGroup() { s.Equal(result.Decision, DecisionDeny) } +func (s *oauthSuite) TestExternalProviderWithoutJWKSWillFail() { + authorizer, err := NewOAuthAuthorizer(s.providerCfg, s.logger, s.domainCache) + s.Error(err) + s.Equal(nil, authorizer) + +} + func (s *oauthSuite) TestIncorrectPermission() { s.domainCache.EXPECT().GetDomain(s.att.DomainName).Return(s.domainEntry, nil).Times(1) s.att.Permission = Permission(15) @@ -292,9 +307,169 @@ func Test_oauthAuthority_validateTTL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { validator := &oauthAuthority{ - authorizationCfg: config.OAuthAuthorizer{MaxJwtTTL: tt.ttlConfig}, + config: config.OAuthAuthorizer{MaxJwtTTL: tt.ttlConfig}, } tt.wantErr(t, validator.validateTTL(tt.claims), fmt.Sprintf("validateTTL(%v)", tt.claims)) }) } } + +func TestIsTokenInternal(t *testing.T) { + internalToken := &jwt.Token{ + Header: map[string]interface{}{}, + } + internalTokenWithKid := &jwt.Token{ + Header: map[string]interface{}{ + "kid": jwtInternalIssuer, + }, + Claims: JWTClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: jwtInternalIssuer, + }, + }, + } + externalToken := &jwt.Token{ + Header: map[string]interface{}{ + "kid": "3lkj323jkj3", + }, + Claims: JWTClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_hNqHxsxaM", + }, + }, + } + + tests := []struct { + name string + token *jwt.Token + want bool + }{ + { + name: "internal token w/o kid", + token: internalToken, + want: true, + }, + { + name: "internal token with kid", + token: internalTokenWithKid, + want: true, + }, + { + name: "external token with kid", + token: externalToken, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, isTokenInternal(tt.token), "isTokenInternal(%v)", tt.token) + }) + } +} + +func Test_oauthAuthority_parseExternal(t *testing.T) { + claim := map[string]interface{}{"cognito:groups": []interface{}{"domain2", "domain1", "group1"}} + + tests := []struct { + name string + config config.OAuthAuthorizer + mapToken map[string]interface{} + claims *JWTClaims + wantGroups string + wantAdmin bool + wantErr assert.ErrorAssertionFunc + }{ + { + name: "empty config will not alter token", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + GroupsAttributePath: "", + AdminAttributePath: "", + }, + }, + wantErr: assert.NoError, + }, + { + name: "groups incorrect path will result into an error", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + GroupsAttributePath: "/bad/path", + AdminAttributePath: "", + }, + }, + wantErr: assert.Error, + }, + { + name: "admin incorrect path will result into an error", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + GroupsAttributePath: "", + AdminAttributePath: "/bad/path", + }, + }, + wantErr: assert.Error, + }, + { + name: "correct groups path will fill claims", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + GroupsAttributePath: "\"cognito:groups\" | join(' ', @)", + }, + }, + mapToken: claim, + wantErr: assert.NoError, + wantGroups: "domain2 domain1 group1", + wantAdmin: false, + }, + { + name: "correct admin path will fill claims", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + AdminAttributePath: "\"cognito:groups\" | contains(@, 'group1')", + }, + }, + mapToken: claim, + wantErr: assert.NoError, + wantGroups: "", + wantAdmin: true, + }, + { + name: "non bool result for admin will result in error", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + AdminAttributePath: "\"cognito:groups\"", + }, + }, + mapToken: claim, + wantErr: assert.Error, + wantGroups: "", + wantAdmin: false, + }, + { + name: "non string result for groups will result in error", + config: config.OAuthAuthorizer{ + Provider: &config.OAuthProvider{ + GroupsAttributePath: "\"cognito:groups\" | contains(@, 'group1')", + }, + }, + mapToken: claim, + wantErr: assert.Error, + wantGroups: "", + wantAdmin: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &oauthAuthority{ + config: tt.config, + } + actualClaim := &JWTClaims{} + err := a.parseExternal(tt.mapToken, actualClaim) + tt.wantErr(t, err) + assert.Equal(t, tt.wantGroups, actualClaim.Groups) + assert.Equal(t, tt.wantAdmin, actualClaim.Admin) + }) + } +} diff --git a/common/config/authorization.go b/common/config/authorization.go index 113e331a7ab..f1f07f60cce 100644 --- a/common/config/authorization.go +++ b/common/config/authorization.go @@ -21,11 +21,47 @@ package config import ( + "errors" "fmt" "github.com/golang-jwt/jwt/v5" ) +type ( + Authorization struct { + OAuthAuthorizer OAuthAuthorizer `yaml:"oauthAuthorizer"` + NoopAuthorizer NoopAuthorizer `yaml:"noopAuthorizer"` + } + + NoopAuthorizer struct { + Enable bool `yaml:"enable"` + } + + OAuthAuthorizer struct { + Enable bool `yaml:"enable"` + // Max of TTL in the claim + MaxJwtTTL int64 `yaml:"maxJwtTTL"` + // Credentials to verify/create the JWT using public/private keys + JwtCredentials *JwtCredentials `yaml:"jwtCredentials"` + // Provider + Provider *OAuthProvider `yaml:"provider"` + } + + JwtCredentials struct { + // support: RS256 (RSA using SHA256) + Algorithm string `yaml:"algorithm"` + // Public Key Path for verifying JWT token passed in from external clients + PublicKey string `yaml:"publicKey"` + } + + // OAuthProvider is used to validate tokens provided by 3rd party Identity Provider service + OAuthProvider struct { + JWKSURL string `yaml:"jwksURL"` + GroupsAttributePath string `yaml:"groupsAttributePath"` + AdminAttributePath string `yaml:"adminAttributePath"` + } +) + // Validate validates the persistence config func (a *Authorization) Validate() error { if a.OAuthAuthorizer.Enable && a.NoopAuthorizer.Enable { @@ -47,11 +83,26 @@ func (a *Authorization) validateOAuth() error { if oauthConfig.MaxJwtTTL <= 0 { return fmt.Errorf("[OAuthConfig] MaxTTL must be greater than 0") } - if oauthConfig.JwtCredentials.PublicKey == "" { - return fmt.Errorf("[OAuthConfig] PublicKey can't be empty") + + if oauthConfig.JwtCredentials == nil && oauthConfig.Provider == nil { + return errors.New("jwtCredentials or provider must be provided") + } + + if oauthConfig.JwtCredentials != nil { + if oauthConfig.JwtCredentials.PublicKey == "" { + return fmt.Errorf("[OAuthConfig] PublicKey can't be empty") + } + + if oauthConfig.JwtCredentials.Algorithm != jwt.SigningMethodRS256.Name { + return fmt.Errorf("[OAuthConfig] The only supported Algorithm is RS256") + } } - if oauthConfig.JwtCredentials.Algorithm != jwt.SigningMethodRS256.Name { - return fmt.Errorf("[OAuthConfig] The only supported Algorithm is RS256") + + if oauthConfig.Provider != nil { + if oauthConfig.Provider.JWKSURL == "" { + return fmt.Errorf("[OAuthConfig] JWKSURL is not set") + } } + return nil } diff --git a/common/config/authorization_test.go b/common/config/authorization_test.go index cb083e22bea..9d3a04b4e24 100644 --- a/common/config/authorization_test.go +++ b/common/config/authorization_test.go @@ -44,7 +44,7 @@ func TestTTLIsZero(t *testing.T) { cfg := Authorization{ OAuthAuthorizer: OAuthAuthorizer{ Enable: true, - JwtCredentials: JwtCredentials{}, + JwtCredentials: &JwtCredentials{}, MaxJwtTTL: 0, }, NoopAuthorizer: NoopAuthorizer{ @@ -60,7 +60,7 @@ func TestPublicKeyIsEmpty(t *testing.T) { cfg := Authorization{ OAuthAuthorizer: OAuthAuthorizer{ Enable: true, - JwtCredentials: JwtCredentials{ + JwtCredentials: &JwtCredentials{ Algorithm: "", PublicKey: "", }, @@ -79,7 +79,7 @@ func TestAlgorithmIsInvalid(t *testing.T) { cfg := Authorization{ OAuthAuthorizer: OAuthAuthorizer{ Enable: true, - JwtCredentials: JwtCredentials{ + JwtCredentials: &JwtCredentials{ Algorithm: "SHA256", PublicKey: "public", }, @@ -98,7 +98,7 @@ func TestCorrectValidation(t *testing.T) { cfg := Authorization{ OAuthAuthorizer: OAuthAuthorizer{ Enable: true, - JwtCredentials: JwtCredentials{ + JwtCredentials: &JwtCredentials{ Algorithm: "RS256", PublicKey: "public", }, diff --git a/common/config/config.go b/common/config/config.go index a930bb657d1..37f859a9149 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -89,36 +89,12 @@ type ( Match *regexp.Regexp } - Authorization struct { - OAuthAuthorizer OAuthAuthorizer `yaml:"oauthAuthorizer"` - NoopAuthorizer NoopAuthorizer `yaml:"noopAuthorizer"` - } - DynamicConfig struct { Client string `yaml:"client"` ConfigStore c.ClientConfig `yaml:"configstore"` FileBased dynamicconfig.FileBasedClientConfig `yaml:"filebased"` } - NoopAuthorizer struct { - Enable bool `yaml:"enable"` - } - - OAuthAuthorizer struct { - Enable bool `yaml:"enable"` - // Credentials to verify/create the JWT using public/private keys - JwtCredentials JwtCredentials `yaml:"jwtCredentials"` - // Max of TTL in the claim - MaxJwtTTL int64 `yaml:"maxJwtTTL"` - } - - JwtCredentials struct { - // support: RS256 (RSA using SHA256) - Algorithm string `yaml:"algorithm"` - // Public Key Path for verifying JWT token passed in from external clients - PublicKey string `yaml:"publicKey"` - } - // Service contains the service specific config items Service struct { // TChannel is the tchannel configuration diff --git a/config/development_generic_oauth.yaml b/config/development_generic_oauth.yaml new file mode 100644 index 00000000000..d348fdc4dbc --- /dev/null +++ b/config/development_generic_oauth.yaml @@ -0,0 +1,62 @@ +persistence: + advancedVisibilityStore: es-visibility + datastores: + es-visibility: + elasticsearch: + version: "v7" + url: + scheme: "http" + host: "127.0.0.1:9200" + indices: + visibility: cadence-visibility-dev + +kafka: + tls: + enabled: false + clusters: + test: + brokers: + - 127.0.0.1:9092 + topics: + cadence-visibility-dev: + cluster: test + cadence-visibility-dev-dlq: + cluster: test + applications: + visibility: + topic: cadence-visibility-dev + dlq-topic: cadence-visibility-dev-dlq + +dynamicconfig: + client: filebased + filebased: + filepath: "config/dynamicconfig/development_es.yaml" + +authorization: + oauthAuthorizer: + enable: true + maxJwtTTL: 600000000 + jwtCredentials: + algorithm: "RS256" + publicKey: "config/credentials/keytest.pub" + # provider section can be used to validate token issued by 3rd party provider (Okta, AWS, Google, etc.) + provider: + jwksURL: # AWS cognito example: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_hNq90rT473/.well-known/jwks.json" + # Custom data is extracted from token using JMES Path query language: https://jmespath.org/tutorial.html + adminAttributePath: # AWS cognito example: "permissions | contains(@, 'admin:true')" + groupsAttributePath: # AWS cognito example: "\"cognito:groups\" | join(', ', @)" + +clusterGroupMetadata: + failoverVersionIncrement: 10 + masterClusterName: "cluster0" + currentClusterName: "cluster0" + clusterGroup: + cluster0: + enabled: true + initialFailoverVersion: 0 + rpcAddress: "localhost:7833" # this is to let worker service and XDC replicator connected to the frontend service. In cluster setup, localhost will not work + rpcTransport: "grpc" + authorizationProvider: + enable: true + type: "OAuthAuthorization" + privateKey: "config/credentials/keytest" diff --git a/go.mod b/go.mod index 456bb8b6602..c99af8e03cf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/uber/cadence go 1.20 require ( + github.com/MicahParks/keyfunc/v2 v2.1.0 github.com/Shopify/sarama v1.33.0 github.com/VividCortex/mysqlerr v1.0.0 github.com/aws/aws-sdk-go v1.44.180 @@ -22,6 +23,7 @@ require ( github.com/google/uuid v1.5.0 github.com/hashicorp/go-version v1.2.0 github.com/iancoleman/strcase v0.2.0 + github.com/jmespath/go-jmespath v0.4.0 github.com/jmoiron/sqlx v1.2.1-0.20200615141059-0794cb1f47ee github.com/jonboulle/clockwork v0.4.0 github.com/lib/pq v1.2.0 @@ -57,7 +59,7 @@ require ( golang.org/x/exp v0.0.0-20231226003508-02704c960a9b golang.org/x/net v0.19.0 golang.org/x/sync v0.5.0 - golang.org/x/time v0.3.0 + golang.org/x/time v0.5.0 golang.org/x/tools v0.16.0 gonum.org/v1/gonum v0.7.0 google.golang.org/grpc v1.59.0 @@ -94,7 +96,6 @@ require ( github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jessevdk/go-flags v1.4.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kisielk/errcheck v1.5.0 // indirect github.com/klauspost/compress v1.15.9 // indirect diff --git a/go.sum b/go.sum index e3c2acb010f..24d0d403407 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= +github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.33.0 h1:2K4mB9M4fo46sAM7t6QTsmSO8dLX1OqznLM7vn3OjZ8= github.com/Shopify/sarama v1.33.0/go.mod h1:lYO7LwEBkE0iAeTl94UfPSrDaavFzSFlmn+5isARATQ= @@ -650,8 +652,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=