github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/identity/openid/openid.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package openid 19 20 import ( 21 "crypto/sha1" 22 "encoding/base64" 23 "errors" 24 "io" 25 "net/http" 26 "sort" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 32 "github.com/minio/madmin-go/v3" 33 "github.com/minio/minio-go/v7/pkg/set" 34 "github.com/minio/minio/internal/arn" 35 "github.com/minio/minio/internal/auth" 36 "github.com/minio/minio/internal/config" 37 "github.com/minio/minio/internal/config/identity/openid/provider" 38 "github.com/minio/minio/internal/hash/sha256" 39 "github.com/minio/pkg/v2/env" 40 xnet "github.com/minio/pkg/v2/net" 41 "github.com/minio/pkg/v2/policy" 42 ) 43 44 // OpenID keys and envs. 45 const ( 46 ClientID = "client_id" 47 ClientSecret = "client_secret" 48 ConfigURL = "config_url" 49 ClaimName = "claim_name" 50 ClaimUserinfo = "claim_userinfo" 51 RolePolicy = "role_policy" 52 DisplayName = "display_name" 53 54 Scopes = "scopes" 55 RedirectURI = "redirect_uri" 56 RedirectURIDynamic = "redirect_uri_dynamic" 57 Vendor = "vendor" 58 59 // Vendor specific ENV only enabled if the Vendor matches == "vendor" 60 KeyCloakRealm = "keycloak_realm" 61 KeyCloakAdminURL = "keycloak_admin_url" 62 63 // Removed params 64 JwksURL = "jwks_url" 65 ClaimPrefix = "claim_prefix" 66 ) 67 68 // DefaultKVS - default config for OpenID config 69 var ( 70 DefaultKVS = config.KVS{ 71 config.KV{ 72 Key: config.Enable, 73 Value: "", 74 }, 75 config.KV{ 76 Key: DisplayName, 77 Value: "", 78 }, 79 config.KV{ 80 Key: ConfigURL, 81 Value: "", 82 }, 83 config.KV{ 84 Key: ClientID, 85 Value: "", 86 }, 87 config.KV{ 88 Key: ClientSecret, 89 Value: "", 90 }, 91 config.KV{ 92 Key: ClaimName, 93 Value: policy.PolicyName, 94 }, 95 config.KV{ 96 Key: ClaimUserinfo, 97 Value: "", 98 }, 99 config.KV{ 100 Key: RolePolicy, 101 Value: "", 102 }, 103 config.KV{ 104 Key: ClaimPrefix, 105 Value: "", 106 }, 107 config.KV{ 108 Key: RedirectURI, 109 Value: "", 110 }, 111 config.KV{ 112 Key: RedirectURIDynamic, 113 Value: "off", 114 }, 115 config.KV{ 116 Key: Scopes, 117 Value: "", 118 }, 119 config.KV{ 120 Key: Vendor, 121 Value: "", 122 }, 123 config.KV{ 124 Key: KeyCloakRealm, 125 Value: "", 126 }, 127 config.KV{ 128 Key: KeyCloakAdminURL, 129 Value: "", 130 }, 131 } 132 ) 133 134 var errSingleProvider = config.Errorf("Only one OpenID provider can be configured if not using role policy mapping") 135 136 // DummyRoleARN is used to indicate that the user associated with it was 137 // authenticated via policy-claim based OpenID provider. 138 var DummyRoleARN = func() arn.ARN { 139 v, err := arn.NewIAMRoleARN("dummy-internal", "") 140 if err != nil { 141 panic("should not happen!") 142 } 143 return v 144 }() 145 146 // Config - OpenID Config 147 type Config struct { 148 Enabled bool 149 150 // map of roleARN to providerCfg's 151 arnProviderCfgsMap map[arn.ARN]*providerCfg 152 153 // map of config names to providerCfg's 154 ProviderCfgs map[string]*providerCfg 155 156 pubKeys publicKeys 157 roleArnPolicyMap map[arn.ARN]string 158 159 transport http.RoundTripper 160 closeRespFn func(io.ReadCloser) 161 } 162 163 // Clone returns a cloned copy of OpenID config. 164 func (r *Config) Clone() Config { 165 if r == nil { 166 return Config{} 167 } 168 cfg := Config{ 169 Enabled: r.Enabled, 170 arnProviderCfgsMap: make(map[arn.ARN]*providerCfg, len(r.arnProviderCfgsMap)), 171 ProviderCfgs: make(map[string]*providerCfg, len(r.ProviderCfgs)), 172 pubKeys: r.pubKeys, 173 roleArnPolicyMap: make(map[arn.ARN]string, len(r.roleArnPolicyMap)), 174 transport: r.transport, 175 closeRespFn: r.closeRespFn, 176 } 177 for k, v := range r.arnProviderCfgsMap { 178 cfg.arnProviderCfgsMap[k] = v 179 } 180 for k, v := range r.ProviderCfgs { 181 cfg.ProviderCfgs[k] = v 182 } 183 for k, v := range r.roleArnPolicyMap { 184 cfg.roleArnPolicyMap[k] = v 185 } 186 return cfg 187 } 188 189 // LookupConfig lookup jwks from config, override with any ENVs. 190 func LookupConfig(s config.Config, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) { 191 openIDClientTransport := http.DefaultTransport 192 if transport != nil { 193 openIDClientTransport = transport 194 } 195 c = Config{ 196 Enabled: false, 197 arnProviderCfgsMap: map[arn.ARN]*providerCfg{}, 198 ProviderCfgs: map[string]*providerCfg{}, 199 pubKeys: publicKeys{ 200 RWMutex: &sync.RWMutex{}, 201 pkMap: map[string]interface{}{}, 202 }, 203 roleArnPolicyMap: map[arn.ARN]string{}, 204 transport: openIDClientTransport, 205 closeRespFn: closeRespFn, 206 } 207 208 seenClientIDs := set.NewStringSet() 209 210 deprecatedKeys := []string{JwksURL} 211 212 // remove this since we have removed support for this already. 213 for k := range s[config.IdentityOpenIDSubSys] { 214 for _, dk := range deprecatedKeys { 215 kvs := s[config.IdentityOpenIDSubSys][k] 216 kvs.Delete(dk) 217 s[config.IdentityOpenIDSubSys][k] = kvs 218 } 219 } 220 221 if err := s.CheckValidKeys(config.IdentityOpenIDSubSys, deprecatedKeys); err != nil { 222 return c, err 223 } 224 225 openIDTargets, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys) 226 if err != nil { 227 return c, err 228 } 229 230 for _, cfgName := range openIDTargets { 231 getCfgVal := func(cfgParam string) string { 232 // As parameters are already validated, we skip checking 233 // if the config param was found. 234 val, _, _ := s.ResolveConfigParam(config.IdentityOpenIDSubSys, cfgName, cfgParam, false) 235 return val 236 } 237 238 // In the past, when only one openID provider was allowed, there 239 // was no `enable` parameter - the configuration is turned off 240 // by clearing the values. With multiple providers, we support 241 // individually enabling/disabling provider configurations. If 242 // the enable parameter's value is non-empty, we use that 243 // setting, otherwise we treat it as enabled if some important 244 // parameters are non-empty. 245 var ( 246 cfgEnableVal = getCfgVal(config.Enable) 247 isExplicitlyEnabled = cfgEnableVal != "" 248 ) 249 250 var enabled bool 251 if isExplicitlyEnabled { 252 enabled, err = config.ParseBool(cfgEnableVal) 253 if err != nil { 254 return c, err 255 } 256 // No need to continue loading if the config is not enabled. 257 if !enabled { 258 continue 259 } 260 } 261 262 p := newProviderCfgFromConfig(getCfgVal) 263 configURL := getCfgVal(ConfigURL) 264 265 if !isExplicitlyEnabled { 266 enabled = true 267 if p.ClientID == "" && p.ClientSecret == "" && configURL == "" { 268 enabled = false 269 } 270 } 271 272 // No need to continue loading if the config is not enabled. 273 if !enabled { 274 continue 275 } 276 277 // Validate that client ID has not been duplicately specified. 278 if seenClientIDs.Contains(p.ClientID) { 279 return c, config.Errorf("Client ID %s is present with multiple OpenID configurations", p.ClientID) 280 } 281 seenClientIDs.Add(p.ClientID) 282 283 p.URL, err = xnet.ParseHTTPURL(configURL) 284 if err != nil { 285 return c, err 286 } 287 configURLDomain := p.URL.Hostname() 288 p.DiscoveryDoc, err = parseDiscoveryDoc(p.URL, transport, closeRespFn) 289 if err != nil { 290 return c, err 291 } 292 293 if p.ClaimUserinfo && configURL == "" { 294 return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint") 295 } 296 297 if scopeList := getCfgVal(Scopes); scopeList != "" { 298 var scopes []string 299 for _, scope := range strings.Split(scopeList, ",") { 300 scope = strings.TrimSpace(scope) 301 if scope == "" { 302 return c, config.Errorf("empty scope value is not allowed '%s', please refer to our documentation", scopeList) 303 } 304 scopes = append(scopes, scope) 305 } 306 // Replace the discovery document scopes by client customized scopes. 307 p.DiscoveryDoc.ScopesSupported = scopes 308 } 309 310 // Check if claim name is the non-default value and role policy is set. 311 if p.ClaimName != policy.PolicyName && p.RolePolicy != "" { 312 // In the unlikely event that the user specifies 313 // `policy.PolicyName` as the claim name explicitly and sets 314 // a role policy, this check is thwarted, but we will be using 315 // the role policy anyway. 316 return c, config.Errorf("Role Policy (=`%s`) and Claim Name (=`%s`) cannot both be set", p.RolePolicy, p.ClaimName) 317 } 318 319 jwksURL := p.DiscoveryDoc.JwksURI 320 if jwksURL == "" { 321 return c, config.Errorf("no JWKS URI found in your provider's discovery doc (config_url=%s)", configURL) 322 } 323 324 p.JWKS.URL, err = xnet.ParseHTTPURL(jwksURL) 325 if err != nil { 326 return c, err 327 } 328 329 if p.RolePolicy != "" { 330 // RolePolicy is validated by IAM System during its 331 // initialization. 332 333 // Generate role ARN as combination of provider domain and 334 // prefix of client ID. 335 domain := configURLDomain 336 if domain == "" { 337 // Attempt to parse the JWKs URI. 338 domain = p.JWKS.URL.Hostname() 339 if domain == "" { 340 return c, config.Errorf("unable to parse a domain from the OpenID config") 341 } 342 } 343 if p.ClientID == "" { 344 return c, config.Errorf("client ID must not be empty") 345 } 346 347 // We set the resource ID of the role arn as a hash of client 348 // ID, so we can get a short roleARN that stays the same on 349 // restart. 350 var resourceID string 351 { 352 h := sha1.New() 353 h.Write([]byte(p.ClientID)) 354 bs := h.Sum(nil) 355 resourceID = base64.RawURLEncoding.EncodeToString(bs) 356 } 357 p.roleArn, err = arn.NewIAMRoleARN(resourceID, serverRegion) 358 if err != nil { 359 return c, config.Errorf("unable to generate ARN from the OpenID config: %v", err) 360 } 361 362 c.roleArnPolicyMap[p.roleArn] = p.RolePolicy 363 } else if p.ClaimName == "" { 364 return c, config.Errorf("A role policy or claim name must be specified") 365 } 366 367 if err = p.initializeProvider(getCfgVal, c.transport); err != nil { 368 return c, err 369 } 370 371 arnKey := p.roleArn 372 if p.RolePolicy == "" { 373 arnKey = DummyRoleARN 374 // Ensure that at most one JWT policy claim based provider may be 375 // defined. 376 if _, ok := c.arnProviderCfgsMap[DummyRoleARN]; ok { 377 return c, errSingleProvider 378 } 379 } 380 381 c.arnProviderCfgsMap[arnKey] = &p 382 c.ProviderCfgs[cfgName] = &p 383 384 if err = c.PopulatePublicKey(arnKey); err != nil { 385 return c, err 386 } 387 } 388 389 c.Enabled = true 390 391 return c, nil 392 } 393 394 // ErrProviderConfigNotFound - represents a non-existing provider error. 395 var ErrProviderConfigNotFound = errors.New("provider configuration not found") 396 397 // GetConfigInfo - returns configuration and related info for the given IDP 398 // provider. 399 func (r *Config) GetConfigInfo(s config.Config, cfgName string) ([]madmin.IDPCfgInfo, error) { 400 openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys) 401 if err != nil { 402 return nil, err 403 } 404 405 present := false 406 for _, cfg := range openIDConfigs { 407 if cfg == cfgName { 408 present = true 409 break 410 } 411 } 412 413 if !present { 414 return nil, ErrProviderConfigNotFound 415 } 416 417 kvsrcs, err := s.GetResolvedConfigParams(config.IdentityOpenIDSubSys, cfgName, true) 418 if err != nil { 419 return nil, err 420 } 421 422 res := make([]madmin.IDPCfgInfo, 0, len(kvsrcs)+1) 423 for _, kvsrc := range kvsrcs { 424 // skip returning default config values. 425 if kvsrc.Src == config.ValueSourceDef { 426 if kvsrc.Key != madmin.EnableKey { 427 continue 428 } 429 // for EnableKey we set an explicit on/off from live configuration 430 // if it is present. 431 if _, ok := r.ProviderCfgs[cfgName]; !ok { 432 // No live config is present 433 continue 434 } 435 if r.Enabled { 436 kvsrc.Value = "on" 437 } else { 438 kvsrc.Value = "off" 439 } 440 } 441 res = append(res, madmin.IDPCfgInfo{ 442 Key: kvsrc.Key, 443 Value: kvsrc.Value, 444 IsCfg: true, 445 IsEnv: kvsrc.Src == config.ValueSourceEnv, 446 }) 447 } 448 449 if provCfg, exists := r.ProviderCfgs[cfgName]; exists && provCfg.RolePolicy != "" { 450 // Append roleARN 451 res = append(res, madmin.IDPCfgInfo{ 452 Key: "roleARN", 453 Value: provCfg.roleArn.String(), 454 IsCfg: false, 455 }) 456 } 457 458 // sort the structs by the key 459 sort.Slice(res, func(i, j int) bool { 460 return res[i].Key < res[j].Key 461 }) 462 463 return res, nil 464 } 465 466 // GetConfigList - list openID configurations 467 func (r *Config) GetConfigList(s config.Config) ([]madmin.IDPListItem, error) { 468 openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys) 469 if err != nil { 470 return nil, err 471 } 472 473 var res []madmin.IDPListItem 474 for _, cfg := range openIDConfigs { 475 pcfg, ok := r.ProviderCfgs[cfg] 476 if !ok { 477 res = append(res, madmin.IDPListItem{ 478 Type: "openid", 479 Name: cfg, 480 Enabled: false, 481 }) 482 } else { 483 var roleARN string 484 if pcfg.RolePolicy != "" { 485 roleARN = pcfg.roleArn.String() 486 } 487 res = append(res, madmin.IDPListItem{ 488 Type: "openid", 489 Name: cfg, 490 Enabled: r.Enabled, 491 RoleARN: roleARN, 492 }) 493 } 494 } 495 496 return res, nil 497 } 498 499 // Enabled returns if configURL is enabled. 500 func Enabled(kvs config.KVS) bool { 501 return kvs.Get(ConfigURL) != "" 502 } 503 504 // GetSettings - fetches OIDC settings for site-replication related validation. 505 // NOTE that region must be populated by caller as this package does not know. 506 func (r *Config) GetSettings() madmin.OpenIDSettings { 507 res := madmin.OpenIDSettings{} 508 if !r.Enabled { 509 return res 510 } 511 h := sha256.New() 512 for arn, provCfg := range r.arnProviderCfgsMap { 513 hashedSecret := "" 514 { 515 h.Reset() 516 h.Write([]byte(provCfg.ClientSecret)) 517 hashedSecret = base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 518 } 519 if arn != DummyRoleARN { 520 if res.Roles == nil { 521 res.Roles = make(map[string]madmin.OpenIDProviderSettings) 522 } 523 res.Roles[arn.String()] = madmin.OpenIDProviderSettings{ 524 ClaimUserinfoEnabled: provCfg.ClaimUserinfo, 525 RolePolicy: provCfg.RolePolicy, 526 ClientID: provCfg.ClientID, 527 HashedClientSecret: hashedSecret, 528 } 529 } else { 530 res.ClaimProvider = madmin.OpenIDProviderSettings{ 531 ClaimUserinfoEnabled: provCfg.ClaimUserinfo, 532 RolePolicy: provCfg.RolePolicy, 533 ClientID: provCfg.ClientID, 534 HashedClientSecret: hashedSecret, 535 } 536 } 537 538 } 539 540 return res 541 } 542 543 // GetIAMPolicyClaimName - returns the policy claim name for the (at most one) 544 // provider configured without a role policy. 545 func (r *Config) GetIAMPolicyClaimName() string { 546 pCfg, ok := r.arnProviderCfgsMap[DummyRoleARN] 547 if !ok { 548 return "" 549 } 550 return pCfg.ClaimPrefix + pCfg.ClaimName 551 } 552 553 // LookupUser lookup userid for the provider 554 func (r Config) LookupUser(roleArn, userid string) (provider.User, error) { 555 // Can safely ignore error here as empty or invalid ARNs will not be 556 // mapped. 557 arnVal, _ := arn.Parse(roleArn) 558 pCfg, ok := r.arnProviderCfgsMap[arnVal] 559 if ok { 560 user, err := pCfg.provider.LookupUser(userid) 561 if err != nil && err != provider.ErrAccessTokenExpired { 562 return user, err 563 } 564 if err == provider.ErrAccessTokenExpired { 565 if err = pCfg.provider.LoginWithClientID(pCfg.ClientID, pCfg.ClientSecret); err != nil { 566 return user, err 567 } 568 user, err = pCfg.provider.LookupUser(userid) 569 } 570 return user, err 571 } 572 // Without any specific logic for a provider, all accounts 573 // are always enabled. 574 return provider.User{ID: userid, Enabled: true}, nil 575 } 576 577 // ProviderEnabled returns true if any vendor specific provider is enabled. 578 func (r Config) ProviderEnabled() bool { 579 if !r.Enabled { 580 return false 581 } 582 for _, v := range r.arnProviderCfgsMap { 583 if v.provider != nil { 584 return true 585 } 586 } 587 return false 588 } 589 590 // GetRoleInfo - returns ARN to policies map if a role policy based openID 591 // provider is configured. Otherwise returns nil. 592 func (r Config) GetRoleInfo() map[arn.ARN]string { 593 for _, p := range r.arnProviderCfgsMap { 594 if p.RolePolicy != "" { 595 return r.roleArnPolicyMap 596 } 597 } 598 return nil 599 } 600 601 // GetDefaultExpiration - returns the expiration seconds expected. 602 func GetDefaultExpiration(dsecs string) (time.Duration, error) { 603 timeout := env.Get(config.EnvMinioStsDuration, "") 604 defaultExpiryDuration, err := time.ParseDuration(timeout) 605 if err != nil { 606 defaultExpiryDuration = time.Hour 607 } 608 if timeout == "" && dsecs != "" { 609 expirySecs, err := strconv.ParseInt(dsecs, 10, 64) 610 if err != nil { 611 return 0, auth.ErrInvalidDuration 612 } 613 614 // The duration, in seconds, of the role session. 615 // The value can range from 900 seconds (15 minutes) 616 // up to 365 days. 617 if expirySecs < config.MinExpiration || expirySecs > config.MaxExpiration { 618 return 0, auth.ErrInvalidDuration 619 } 620 621 defaultExpiryDuration = time.Duration(expirySecs) * time.Second 622 } else if timeout == "" && dsecs == "" { 623 return time.Hour, nil 624 } 625 626 if defaultExpiryDuration.Seconds() < config.MinExpiration || defaultExpiryDuration.Seconds() > config.MaxExpiration { 627 return 0, auth.ErrInvalidDuration 628 } 629 630 return defaultExpiryDuration, nil 631 }