go.temporal.io/server@v1.23.0/common/authorization/default_jwt_claim_mapper_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package authorization
    26  
    27  import (
    28  	"crypto/ecdsa"
    29  	"crypto/elliptic"
    30  	"crypto/rand"
    31  	"crypto/rsa"
    32  	"fmt"
    33  	"reflect"
    34  	"testing"
    35  	"time"
    36  
    37  	"github.com/golang-jwt/jwt/v4"
    38  	"github.com/golang/mock/gomock"
    39  	"github.com/stretchr/testify/require"
    40  	"github.com/stretchr/testify/suite"
    41  	"go.temporal.io/server/common/primitives"
    42  
    43  	"go.temporal.io/server/common/config"
    44  	"go.temporal.io/server/common/log"
    45  )
    46  
    47  type errorTestOptions int16
    48  
    49  const (
    50  	errorTestOptionNoKID = errorTestOptions(1 << iota)
    51  	errorTestOptionNoSubject
    52  	errorTestOptionNoAlgorithm
    53  	errorTestOptionNoError = errorTestOptions(0)
    54  )
    55  
    56  type keyAlgorithm int8
    57  
    58  const (
    59  	RSA keyAlgorithm = iota
    60  	ECDSA
    61  )
    62  
    63  const (
    64  	testSubject      = "test-user"
    65  	defaultNamespace = "default"
    66  )
    67  
    68  var (
    69  	permissionsAdmin              = []string{primitives.SystemLocalNamespace + ":admin", "default:read"}
    70  	permissionsReaderWriterWorker = []string{"default:read", "default:write", "default:worker"}
    71  )
    72  
    73  type (
    74  	defaultClaimMapperSuite struct {
    75  		suite.Suite
    76  		*require.Assertions
    77  
    78  		controller     *gomock.Controller
    79  		tokenGenerator *tokenGenerator
    80  		claimMapper    ClaimMapper
    81  		config         *config.Authorization
    82  		logger         log.Logger
    83  	}
    84  )
    85  
    86  func TestDefaultClaimMapperSuite(t *testing.T) {
    87  	s := new(defaultClaimMapperSuite)
    88  	suite.Run(t, s)
    89  }
    90  func (s *defaultClaimMapperSuite) SetupTest() {
    91  	s.Assertions = require.New(s.T())
    92  	s.controller = gomock.NewController(s.T())
    93  	s.tokenGenerator = newTokenGenerator()
    94  	s.config = &config.Authorization{}
    95  	s.logger = log.NewNoopLogger()
    96  	s.claimMapper = NewDefaultJWTClaimMapper(s.tokenGenerator, s.config, s.logger)
    97  }
    98  func (s *defaultClaimMapperSuite) TearDownTest() {
    99  	s.controller.Finish()
   100  }
   101  
   102  func (s *defaultClaimMapperSuite) TestTokenGeneratorRSA() {
   103  	s.testTokenGenerator(RSA)
   104  }
   105  func (s *defaultClaimMapperSuite) TestTokenGeneratorECDSA() {
   106  	s.testTokenGenerator(ECDSA)
   107  }
   108  func (s *defaultClaimMapperSuite) testTokenGenerator(alg keyAlgorithm) {
   109  	tokenString, err := s.tokenGenerator.generateToken(alg,
   110  		testSubject, permissionsAdmin, errorTestOptionNoError)
   111  	s.NoError(err)
   112  	claims, err := parseJWT(tokenString, s.tokenGenerator)
   113  	s.NoError(err)
   114  	s.Equal(testSubject, claims["sub"])
   115  }
   116  
   117  func (s *defaultClaimMapperSuite) TestTokenWithNoSubject() {
   118  	tokenString, err := s.tokenGenerator.generateRSAToken(
   119  		testSubject, permissionsAdmin, errorTestOptionNoSubject)
   120  	s.NoError(err)
   121  	claims, err := parseJWT(tokenString, s.tokenGenerator)
   122  	s.NoError(err)
   123  	subject := claims["sub"]
   124  	s.Nil(subject)
   125  }
   126  func (s *defaultClaimMapperSuite) TestTokenWithNoKID() {
   127  	tokenString, err := s.tokenGenerator.generateRSAToken(
   128  		testSubject, permissionsAdmin, errorTestOptionNoKID)
   129  	s.NoError(err)
   130  	_, err = parseJWT(tokenString, s.tokenGenerator)
   131  	s.Error(err, "malformed token - no \"kid\" header")
   132  }
   133  func (s *defaultClaimMapperSuite) TestTokenWithNoAlgorithm() {
   134  	tokenString, err := s.tokenGenerator.generateRSAToken(
   135  		testSubject, permissionsAdmin, errorTestOptionNoAlgorithm)
   136  	s.NoError(err)
   137  	_, err = parseJWT(tokenString, s.tokenGenerator)
   138  	s.Error(err, "signing method (alg) is unspecified.")
   139  }
   140  
   141  func (s *defaultClaimMapperSuite) TestTokenWithAdminPermissionsRSA() {
   142  	s.testTokenWithAdminPermissions(RSA)
   143  }
   144  func (s *defaultClaimMapperSuite) TestTokenWithAdminPermissionsECDSA() {
   145  	s.testTokenWithAdminPermissions(ECDSA)
   146  }
   147  func (s *defaultClaimMapperSuite) testTokenWithAdminPermissions(alg keyAlgorithm) {
   148  	tokenString, err := s.tokenGenerator.generateToken(alg,
   149  		testSubject, permissionsAdmin, errorTestOptionNoError)
   150  	s.NoError(err)
   151  	authInfo := &AuthInfo{
   152  		AddBearer(tokenString),
   153  		nil,
   154  		nil,
   155  		"",
   156  		"",
   157  	}
   158  	claims, err := s.claimMapper.GetClaims(authInfo)
   159  	s.NoError(err)
   160  	s.Equal(testSubject, claims.Subject)
   161  	s.Equal(RoleAdmin, claims.System)
   162  	s.Equal(1, len(claims.Namespaces))
   163  	defaultRole := claims.Namespaces[defaultNamespace]
   164  	s.Equal(RoleReader, defaultRole)
   165  }
   166  
   167  func (s *defaultClaimMapperSuite) TestNamespacePermissionCaseSensitive() {
   168  	tokenString, err := s.tokenGenerator.generateToken(RSA,
   169  		testSubject, []string{"Temporal-system:admin", "Foo:read"}, errorTestOptionNoError)
   170  	s.NoError(err)
   171  	authInfo := &AuthInfo{
   172  		AddBearer(tokenString),
   173  		nil,
   174  		nil,
   175  		"",
   176  		"",
   177  	}
   178  	claims, err := s.claimMapper.GetClaims(authInfo)
   179  	s.NoError(err)
   180  	s.Equal(testSubject, claims.Subject)
   181  	s.Equal(RoleUndefined, claims.System) // no system role
   182  	s.Equal(2, len(claims.Namespaces))
   183  	// claims contain namespace role for 'Foo', not for 'foo'.
   184  	s.Equal(RoleReader, claims.Namespaces["Foo"])
   185  	s.Equal(RoleAdmin, claims.Namespaces["Temporal-system"])
   186  }
   187  
   188  func (s *defaultClaimMapperSuite) TestTokenWithReaderWriterWorkerPermissionsRSA() {
   189  	s.testTokenWithReaderWriterWorkerPermissions(RSA)
   190  }
   191  func (s *defaultClaimMapperSuite) TestTokenWithReaderWriterWorkerPermissionsECDSA() {
   192  	s.testTokenWithReaderWriterWorkerPermissions(ECDSA)
   193  }
   194  func (s *defaultClaimMapperSuite) testTokenWithReaderWriterWorkerPermissions(alg keyAlgorithm) {
   195  	tokenString, err := s.tokenGenerator.generateToken(
   196  		alg, testSubject, permissionsReaderWriterWorker, errorTestOptionNoError)
   197  	s.NoError(err)
   198  	authInfo := &AuthInfo{
   199  		AddBearer(tokenString),
   200  		nil,
   201  		nil,
   202  		"",
   203  		"test-audience",
   204  	}
   205  	claims, err := s.claimMapper.GetClaims(authInfo)
   206  	s.NoError(err)
   207  	s.Equal(testSubject, claims.Subject)
   208  	s.Equal(RoleUndefined, claims.System)
   209  	s.Equal(1, len(claims.Namespaces))
   210  	defaultRole := claims.Namespaces[defaultNamespace]
   211  	s.Equal(RoleReader|RoleWriter|RoleWorker, defaultRole)
   212  }
   213  func (s *defaultClaimMapperSuite) TestGetClaimMapperFromConfigNoop() {
   214  	s.testGetClaimMapperFromConfig("", true, reflect.TypeOf(&noopClaimMapper{}))
   215  }
   216  func (s *defaultClaimMapperSuite) TestGetClaimMapperFromConfigDefault() {
   217  	s.testGetClaimMapperFromConfig("default", true, reflect.TypeOf(&defaultJWTClaimMapper{}))
   218  }
   219  
   220  func (s *defaultClaimMapperSuite) TestGetClaimMapperFromConfigUnknown() {
   221  	s.testGetClaimMapperFromConfig("foo", false, nil)
   222  }
   223  
   224  func (s *defaultClaimMapperSuite) TestWrongAudience() {
   225  	tokenString, err := s.tokenGenerator.generateRSAToken(testSubject, permissionsAdmin, errorTestOptionNoError)
   226  	s.NoError(err)
   227  	authInfo := &AuthInfo{
   228  		AddBearer(tokenString),
   229  		nil,
   230  		nil,
   231  		"",
   232  		"foo",
   233  	}
   234  	_, err = s.claimMapper.GetClaims(authInfo)
   235  	s.Error(err)
   236  }
   237  
   238  func (s *defaultClaimMapperSuite) TestCorrectAudience() {
   239  	tokenString, err := s.tokenGenerator.generateRSAToken(testSubject, permissionsAdmin, errorTestOptionNoError)
   240  	s.NoError(err)
   241  	authInfo := &AuthInfo{
   242  		AddBearer(tokenString),
   243  		nil,
   244  		nil,
   245  		"",
   246  		"test-audience",
   247  	}
   248  	_, err = s.claimMapper.GetClaims(authInfo)
   249  	s.NoError(err)
   250  }
   251  
   252  func (s *defaultClaimMapperSuite) TestIgnoreAudience() {
   253  	tokenString, err := s.tokenGenerator.generateRSAToken(testSubject, permissionsAdmin, errorTestOptionNoError)
   254  	s.NoError(err)
   255  	authInfo := &AuthInfo{
   256  		AddBearer(tokenString),
   257  		nil,
   258  		nil,
   259  		"",
   260  		"",
   261  	}
   262  	_, err = s.claimMapper.GetClaims(authInfo)
   263  	s.NoError(err)
   264  }
   265  
   266  func (s *defaultClaimMapperSuite) testGetClaimMapperFromConfig(name string, valid bool, cmType reflect.Type) {
   267  
   268  	cfg := config.Authorization{}
   269  	cfg.ClaimMapper = name
   270  	cm, err := GetClaimMapperFromConfig(&cfg, s.logger)
   271  	if valid {
   272  		s.NoError(err)
   273  		s.NotNil(cm)
   274  		t := reflect.TypeOf(cm)
   275  		s.True(t == cmType)
   276  	} else {
   277  		s.Error(err)
   278  		s.Nil(cm)
   279  	}
   280  }
   281  
   282  func AddBearer(token string) string {
   283  	return "Bearer " + token
   284  }
   285  
   286  type (
   287  	tokenGenerator struct {
   288  		rsaPrivateKey   *rsa.PrivateKey
   289  		rsaPublicKey    *rsa.PublicKey
   290  		ecdsaPrivateKey *ecdsa.PrivateKey
   291  		ecdsaPublicKey  *ecdsa.PublicKey
   292  	}
   293  )
   294  
   295  var _ TokenKeyProvider = (*tokenGenerator)(nil)
   296  
   297  func newTokenGenerator() *tokenGenerator {
   298  
   299  	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
   300  	if err != nil {
   301  		return nil
   302  	}
   303  	ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   304  	if err != nil {
   305  		return nil
   306  	}
   307  
   308  	return &tokenGenerator{
   309  		rsaPrivateKey:   rsaKey,
   310  		rsaPublicKey:    &rsaKey.PublicKey,
   311  		ecdsaPrivateKey: ecdsaKey,
   312  		ecdsaPublicKey:  &ecdsaKey.PublicKey,
   313  	}
   314  }
   315  
   316  type (
   317  	CustomClaims struct {
   318  		Permissions []string `json:"permissions"`
   319  		jwt.RegisteredClaims
   320  	}
   321  )
   322  
   323  func (CustomClaims) Valid() error {
   324  	return nil
   325  }
   326  
   327  func (tg *tokenGenerator) generateRSAToken(subject string, permissions []string, options errorTestOptions) (string, error) {
   328  	return tg.generateToken(RSA, subject, permissions, options)
   329  }
   330  
   331  func (tg *tokenGenerator) generateToken(alg keyAlgorithm, subject string, permissions []string, options errorTestOptions) (string, error) {
   332  	claims := CustomClaims{
   333  		permissions,
   334  		jwt.RegisteredClaims{
   335  			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
   336  			Issuer:    "test",
   337  			Audience:  []string{"test-audience"},
   338  		},
   339  	}
   340  	if options&errorTestOptionNoSubject == 0 {
   341  		claims.Subject = subject
   342  	}
   343  
   344  	var token *jwt.Token
   345  	switch alg {
   346  	case RSA:
   347  		token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
   348  	case ECDSA:
   349  		token = jwt.NewWithClaims(jwt.SigningMethodES256, claims)
   350  	default:
   351  		return "", fmt.Errorf("unsupported algorithm")
   352  	}
   353  
   354  	if options&errorTestOptionNoKID == 0 {
   355  		token.Header["kid"] = "test-key"
   356  	}
   357  	if options&errorTestOptionNoAlgorithm > 0 {
   358  		delete(token.Header, "alg")
   359  	}
   360  
   361  	switch alg {
   362  	case RSA:
   363  		return token.SignedString(tg.rsaPrivateKey)
   364  	case ECDSA:
   365  		return token.SignedString(tg.ecdsaPrivateKey)
   366  	}
   367  	return "", fmt.Errorf("unexpected condition")
   368  }
   369  
   370  func (tg *tokenGenerator) EcdsaKey(alg string, kid string) (*ecdsa.PublicKey, error) {
   371  	return tg.ecdsaPublicKey, nil
   372  }
   373  func (tg *tokenGenerator) HmacKey(alg string, kid string) ([]byte, error) {
   374  	return nil, fmt.Errorf("unsupported key type HMAC for: %s", alg)
   375  }
   376  func (tg *tokenGenerator) RsaKey(alg string, kid string) (*rsa.PublicKey, error) {
   377  	return tg.rsaPublicKey, nil
   378  }
   379  func (tg *tokenGenerator) SupportedMethods() []string {
   380  	return []string{jwt.SigningMethodRS256.Name, jwt.SigningMethodES256.Name}
   381  }
   382  func (tg *tokenGenerator) Close() {
   383  }