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 }