Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic OAuth support #5638

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions cmd/server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion common/archiver/gcloud/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions common/archiver/gcloud/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 5 additions & 5 deletions common/authorization/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand All @@ -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},
}

Expand Down
139 changes: 109 additions & 30 deletions common/authorization/oauthAuthorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand Down
Loading