github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/sts-handlers.go (about) 1 /// Copyright (c) 2015-2021 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 cmd 19 20 import ( 21 "bytes" 22 "context" 23 "crypto/x509" 24 "encoding/base64" 25 "errors" 26 "fmt" 27 "net/http" 28 "strconv" 29 "strings" 30 "time" 31 32 "github.com/minio/madmin-go/v3" 33 "github.com/minio/minio/internal/auth" 34 "github.com/minio/minio/internal/config/identity/openid" 35 "github.com/minio/minio/internal/hash/sha256" 36 xhttp "github.com/minio/minio/internal/http" 37 "github.com/minio/minio/internal/logger" 38 "github.com/minio/mux" 39 "github.com/minio/pkg/v2/policy" 40 "github.com/minio/pkg/v2/wildcard" 41 ) 42 43 const ( 44 // STS API version. 45 stsAPIVersion = "2011-06-15" 46 stsVersion = "Version" 47 stsAction = "Action" 48 stsPolicy = "Policy" 49 stsToken = "Token" 50 stsRoleArn = "RoleArn" 51 stsWebIdentityToken = "WebIdentityToken" 52 stsWebIdentityAccessToken = "WebIdentityAccessToken" // only valid if UserInfo is enabled. 53 stsDurationSeconds = "DurationSeconds" 54 stsLDAPUsername = "LDAPUsername" 55 stsLDAPPassword = "LDAPPassword" 56 57 // STS API action constants 58 clientGrants = "AssumeRoleWithClientGrants" 59 webIdentity = "AssumeRoleWithWebIdentity" 60 ldapIdentity = "AssumeRoleWithLDAPIdentity" 61 clientCertificate = "AssumeRoleWithCertificate" 62 customTokenIdentity = "AssumeRoleWithCustomToken" 63 assumeRole = "AssumeRole" 64 65 stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB 66 67 // JWT claim keys 68 expClaim = "exp" 69 subClaim = "sub" 70 audClaim = "aud" 71 issClaim = "iss" 72 73 // JWT claim to check the parent user 74 parentClaim = "parent" 75 76 // LDAP claim keys 77 ldapUser = "ldapUser" 78 ldapUserN = "ldapUsername" 79 80 // Role Claim key 81 roleArnClaim = "roleArn" 82 ) 83 84 // stsAPIHandlers implements and provides http handlers for AWS STS API. 85 type stsAPIHandlers struct{} 86 87 // registerSTSRouter - registers AWS STS compatible APIs. 88 func registerSTSRouter(router *mux.Router) { 89 // Initialize STS. 90 sts := &stsAPIHandlers{} 91 92 // STS Router 93 stsRouter := router.NewRoute().PathPrefix(SlashSeparator).Subrouter() 94 95 // Assume roles with no JWT, handles AssumeRole. 96 stsRouter.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { 97 ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get(xhttp.ContentType)) 98 authOk := wildcard.MatchSimple(signV4Algorithm+"*", r.Header.Get(xhttp.Authorization)) 99 noQueries := len(r.URL.RawQuery) == 0 100 return ctypeOk && authOk && noQueries 101 }).HandlerFunc(httpTraceAll(sts.AssumeRole)) 102 103 // Assume roles with JWT handler, handles both ClientGrants and WebIdentity. 104 stsRouter.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { 105 ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get(xhttp.ContentType)) 106 noQueries := len(r.URL.RawQuery) == 0 107 return ctypeOk && noQueries 108 }).HandlerFunc(httpTraceAll(sts.AssumeRoleWithSSO)) 109 110 // AssumeRoleWithClientGrants 111 stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)). 112 Queries(stsAction, clientGrants). 113 Queries(stsVersion, stsAPIVersion). 114 Queries(stsToken, "{Token:.*}") 115 116 // AssumeRoleWithWebIdentity 117 stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)). 118 Queries(stsAction, webIdentity). 119 Queries(stsVersion, stsAPIVersion). 120 Queries(stsWebIdentityToken, "{Token:.*}") 121 122 // AssumeRoleWithLDAPIdentity 123 stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithLDAPIdentity)). 124 Queries(stsAction, ldapIdentity). 125 Queries(stsVersion, stsAPIVersion). 126 Queries(stsLDAPUsername, "{LDAPUsername:.*}"). 127 Queries(stsLDAPPassword, "{LDAPPassword:.*}") 128 129 // AssumeRoleWithCertificate 130 stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)). 131 Queries(stsAction, clientCertificate). 132 Queries(stsVersion, stsAPIVersion) 133 134 // AssumeRoleWithCustomToken 135 stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCustomToken)). 136 Queries(stsAction, customTokenIdentity). 137 Queries(stsVersion, stsAPIVersion) 138 } 139 140 func apiToSTSError(authErr APIErrorCode) (stsErrCode STSErrorCode) { 141 switch authErr { 142 case ErrSignatureDoesNotMatch, ErrInvalidAccessKeyID, ErrAccessKeyDisabled: 143 return ErrSTSAccessDenied 144 case ErrServerNotInitialized: 145 return ErrSTSNotInitialized 146 case ErrInternalError: 147 return ErrSTSInternalError 148 default: 149 return ErrSTSAccessDenied 150 } 151 } 152 153 func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (auth.Credentials, APIErrorCode) { 154 if !isRequestSignatureV4(r) { 155 return auth.Credentials{}, ErrAccessDenied 156 } 157 158 s3Err := isReqAuthenticated(ctx, r, globalSite.Region, serviceSTS) 159 if s3Err != ErrNone { 160 return auth.Credentials{}, s3Err 161 } 162 163 user, _, s3Err := getReqAccessKeyV4(r, globalSite.Region, serviceSTS) 164 if s3Err != ErrNone { 165 return auth.Credentials{}, s3Err 166 } 167 168 // Temporary credentials or Service accounts cannot generate further temporary credentials. 169 if user.IsTemp() || user.IsServiceAccount() { 170 return auth.Credentials{}, ErrAccessDenied 171 } 172 173 // Session tokens are not allowed in STS AssumeRole requests. 174 if getSessionToken(r) != "" { 175 return auth.Credentials{}, ErrAccessDenied 176 } 177 178 return user, ErrNone 179 } 180 181 func parseForm(r *http.Request) error { 182 if err := r.ParseForm(); err != nil { 183 return err 184 } 185 for k, v := range r.PostForm { 186 if _, ok := r.Form[k]; !ok { 187 r.Form[k] = v 188 } 189 } 190 return nil 191 } 192 193 // getTokenSigningKey returns secret key used to sign JWT session tokens 194 func getTokenSigningKey() (string, error) { 195 secret := globalActiveCred.SecretKey 196 if globalSiteReplicationSys.isEnabled() { 197 c, err := globalSiteReplicatorCred.Get(GlobalContext) 198 if err != nil { 199 return "", err 200 } 201 return c.SecretKey, nil 202 } 203 return secret, nil 204 } 205 206 // AssumeRole - implementation of AWS STS API AssumeRole to get temporary 207 // credentials for regular users on Minio. 208 // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html 209 func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) { 210 ctx := newContext(r, w, "AssumeRole") 211 212 claims := make(map[string]interface{}) 213 defer logger.AuditLog(ctx, w, r, claims) 214 215 // Check auth here (otherwise r.Form will have unexpected values from 216 // the call to `parseForm` below), but return failure only after we are 217 // able to validate that it is a valid STS request, so that we are able 218 // to send an appropriate audit log. 219 user, apiErrCode := checkAssumeRoleAuth(ctx, r) 220 221 if err := parseForm(r); err != nil { 222 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 223 return 224 } 225 226 if r.Form.Get(stsVersion) != stsAPIVersion { 227 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get(stsVersion), stsAPIVersion)) 228 return 229 } 230 231 action := r.Form.Get(stsAction) 232 switch action { 233 case assumeRole: 234 default: 235 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) 236 return 237 } 238 239 ctx = newContext(r, w, action) 240 241 // Validate the authentication result here so that failures will be audit-logged. 242 if apiErrCode != ErrNone { 243 stsErr := apiToSTSError(apiErrCode) 244 // Borrow the description error from the API error code 245 writeSTSErrorResponse(ctx, w, stsErr, fmt.Errorf(errorCodes[apiErrCode].Description)) 246 return 247 } 248 249 sessionPolicyStr := r.Form.Get(stsPolicy) 250 // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html 251 // The plain text that you use for both inline and managed session 252 // policies shouldn't exceed 2048 characters. 253 if len(sessionPolicyStr) > 2048 { 254 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, errSessionPolicyTooLarge) 255 return 256 } 257 258 if len(sessionPolicyStr) > 0 { 259 sessionPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr))) 260 if err != nil { 261 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 262 return 263 } 264 265 // Version in policy must not be empty 266 if sessionPolicy.Version == "" { 267 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Version cannot be empty expecting '2012-10-17'")) 268 return 269 } 270 } 271 272 duration, err := openid.GetDefaultExpiration(r.Form.Get(stsDurationSeconds)) 273 if err != nil { 274 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 275 return 276 } 277 278 claims[expClaim] = UTCNow().Add(duration).Unix() 279 claims[parentClaim] = user.AccessKey 280 281 // Validate that user.AccessKey's policies can be retrieved - it may not 282 // be in case the user is disabled. 283 if _, err = globalIAMSys.PolicyDBGet(user.AccessKey, user.Groups...); err != nil { 284 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 285 return 286 } 287 288 if len(sessionPolicyStr) > 0 { 289 claims[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicyStr)) 290 } 291 292 secret, err := getTokenSigningKey() 293 if err != nil { 294 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 295 return 296 } 297 298 cred, err := auth.GetNewCredentialsWithMetadata(claims, secret) 299 if err != nil { 300 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 301 return 302 } 303 304 // Set the parent of the temporary access key, so that it's access 305 // policy is inherited from `user.AccessKey`. 306 cred.ParentUser = user.AccessKey 307 308 // Set the newly generated credentials. 309 updatedAt, err := globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, "") 310 if err != nil { 311 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 312 return 313 } 314 315 // Call hook for site replication. 316 if cred.ParentUser != globalActiveCred.AccessKey { 317 logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ 318 Type: madmin.SRIAMItemSTSAcc, 319 STSCredential: &madmin.SRSTSCredential{ 320 AccessKey: cred.AccessKey, 321 SecretKey: cred.SecretKey, 322 SessionToken: cred.SessionToken, 323 ParentUser: cred.ParentUser, 324 }, 325 UpdatedAt: updatedAt, 326 })) 327 } 328 329 assumeRoleResponse := &AssumeRoleResponse{ 330 Result: AssumeRoleResult{ 331 Credentials: cred, 332 }, 333 } 334 335 assumeRoleResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) 336 writeSuccessResponseXML(w, encodeResponse(assumeRoleResponse)) 337 } 338 339 func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Request) { 340 ctx := newContext(r, w, "AssumeRoleSSOCommon") 341 342 claims := make(map[string]interface{}) 343 defer logger.AuditLog(ctx, w, r, claims) 344 345 // Parse the incoming form data. 346 if err := parseForm(r); err != nil { 347 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 348 return 349 } 350 351 if r.Form.Get(stsVersion) != stsAPIVersion { 352 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion)) 353 return 354 } 355 356 action := r.Form.Get(stsAction) 357 switch action { 358 case ldapIdentity: 359 sts.AssumeRoleWithLDAPIdentity(w, r) 360 return 361 case clientGrants, webIdentity: 362 default: 363 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) 364 return 365 } 366 367 ctx = newContext(r, w, action) 368 369 token := r.Form.Get(stsToken) 370 if token == "" { 371 token = r.Form.Get(stsWebIdentityToken) 372 } 373 374 accessToken := r.Form.Get(stsWebIdentityAccessToken) 375 376 // RoleARN parameter processing: If a role ARN is given in the request, we 377 // use that and validate the authentication request. If not, we assume this 378 // is an STS request for a claim based IDP (if one is present) and set 379 // roleArn = openid.DummyRoleARN. 380 // 381 // Currently, we do not support multiple claim based IDPs, as there is no 382 // defined parameter to disambiguate the intended IDP in this STS request. 383 roleArn := openid.DummyRoleARN 384 roleArnStr := r.Form.Get(stsRoleArn) 385 if roleArnStr != "" { 386 var err error 387 roleArn, _, err = globalIAMSys.GetRolePolicy(roleArnStr) 388 if err != nil { 389 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, 390 fmt.Errorf("Error processing %s parameter: %v", stsRoleArn, err)) 391 return 392 } 393 } 394 395 if !globalIAMSys.Initialized() { 396 writeSTSErrorResponse(ctx, w, ErrSTSIAMNotInitialized, errIAMNotInitialized) 397 return 398 } 399 400 // Validate JWT; check clientID in claims matches the one associated with the roleArn 401 if err := globalIAMSys.OpenIDConfig.Validate(r.Context(), roleArn, token, accessToken, r.Form.Get(stsDurationSeconds), claims); err != nil { 402 switch err { 403 case openid.ErrTokenExpired: 404 switch action { 405 case clientGrants: 406 writeSTSErrorResponse(ctx, w, ErrSTSClientGrantsExpiredToken, err) 407 case webIdentity: 408 writeSTSErrorResponse(ctx, w, ErrSTSWebIdentityExpiredToken, err) 409 } 410 return 411 case auth.ErrInvalidDuration: 412 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 413 return 414 } 415 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 416 return 417 } 418 419 var policyName string 420 if roleArnStr != "" && globalIAMSys.HasRolePolicy() { 421 // If roleArn is used, we set it as a claim, and use the 422 // associated policy when credentials are used. 423 claims[roleArnClaim] = roleArn.String() 424 } else { 425 // If no role policy is configured, then we use claims from the 426 // JWT. This is a MinIO STS API specific value, this value 427 // should be set and configured on your identity provider as 428 // part of JWT custom claims. 429 policySet, ok := policy.GetPoliciesFromClaims(claims, iamPolicyClaimNameOpenID()) 430 policies := strings.Join(policySet.ToSlice(), ",") 431 if ok { 432 policyName = globalIAMSys.CurrentPolicies(policies) 433 } 434 435 if newGlobalAuthZPluginFn() == nil { 436 if !ok { 437 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, 438 fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID())) 439 return 440 } else if policyName == "" { 441 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, 442 fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies)) 443 return 444 } 445 } 446 claims[iamPolicyClaimNameOpenID()] = policyName 447 } 448 449 sessionPolicyStr := r.Form.Get(stsPolicy) 450 // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html 451 // The plain text that you use for both inline and managed session 452 // policies shouldn't exceed 2048 characters. 453 if len(sessionPolicyStr) > 2048 { 454 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Session policy should not exceed 2048 characters")) 455 return 456 } 457 458 if len(sessionPolicyStr) > 0 { 459 sessionPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr))) 460 if err != nil { 461 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 462 return 463 } 464 465 // Version in policy must not be empty 466 if sessionPolicy.Version == "" { 467 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid session policy version")) 468 return 469 } 470 471 claims[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicyStr)) 472 } 473 474 secret, err := getTokenSigningKey() 475 if err != nil { 476 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 477 return 478 } 479 cred, err := auth.GetNewCredentialsWithMetadata(claims, secret) 480 if err != nil { 481 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 482 return 483 } 484 485 // https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability 486 // claim is only considered stable when subject and iss are used together 487 // this is to ensure that ParentUser doesn't change and we get to use 488 // parentUser as per the requirements for service accounts for OpenID 489 // based logins. 490 var subFromToken string 491 if v, ok := claims[subClaim]; ok { 492 subFromToken, _ = v.(string) 493 } 494 495 if subFromToken == "" { 496 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, 497 errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory")) 498 return 499 } 500 501 var issFromToken string 502 if v, ok := claims[issClaim]; ok { 503 issFromToken, _ = v.(string) 504 } 505 506 // Since issFromToken can have `/` characters (it is typically the 507 // provider URL), we hash and encode it to base64 here. This is needed 508 // because there will be a policy mapping stored on drives whose 509 // filename is this parentUser: therefore, it needs to have only valid 510 // filename characters and needs to have bounded length. 511 { 512 h := sha256.New() 513 h.Write([]byte("openid:" + subFromToken + ":" + issFromToken)) 514 bs := h.Sum(nil) 515 cred.ParentUser = base64.RawURLEncoding.EncodeToString(bs) 516 } 517 518 // Deny this assume role request if the policy that the user intends to bind 519 // has a sts:DurationSeconds condition, which is not satisfied as well 520 { 521 p := policyName 522 if p == "" { 523 var err error 524 _, p, err = globalIAMSys.GetRolePolicy(roleArnStr) 525 if err != nil { 526 writeSTSErrorResponse(ctx, w, ErrSTSAccessDenied, err) 527 return 528 } 529 } 530 531 if !globalIAMSys.doesPolicyAllow(p, policy.Args{ 532 DenyOnly: true, 533 Action: policy.AssumeRoleWithWebIdentityAction, 534 ConditionValues: getSTSConditionValues(r, "", cred), 535 Claims: cred.Claims, 536 }) { 537 writeSTSErrorResponse(ctx, w, ErrSTSAccessDenied, errors.New("this user does not have enough permission")) 538 return 539 } 540 } 541 542 // Set the newly generated credentials. 543 updatedAt, err := globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, policyName) 544 if err != nil { 545 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 546 return 547 } 548 549 // Call hook for site replication. 550 logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ 551 Type: madmin.SRIAMItemSTSAcc, 552 STSCredential: &madmin.SRSTSCredential{ 553 AccessKey: cred.AccessKey, 554 SecretKey: cred.SecretKey, 555 SessionToken: cred.SessionToken, 556 ParentUser: cred.ParentUser, 557 ParentPolicyMapping: policyName, 558 }, 559 UpdatedAt: updatedAt, 560 })) 561 562 var encodedSuccessResponse []byte 563 switch action { 564 case clientGrants: 565 clientGrantsResponse := &AssumeRoleWithClientGrantsResponse{ 566 Result: ClientGrantsResult{ 567 Credentials: cred, 568 SubjectFromToken: subFromToken, 569 }, 570 } 571 clientGrantsResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) 572 encodedSuccessResponse = encodeResponse(clientGrantsResponse) 573 case webIdentity: 574 webIdentityResponse := &AssumeRoleWithWebIdentityResponse{ 575 Result: WebIdentityResult{ 576 Credentials: cred, 577 SubjectFromWebIdentityToken: subFromToken, 578 }, 579 } 580 webIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) 581 encodedSuccessResponse = encodeResponse(webIdentityResponse) 582 } 583 584 writeSuccessResponseXML(w, encodedSuccessResponse) 585 } 586 587 // AssumeRoleWithWebIdentity - implementation of AWS STS API supporting OAuth2.0 588 // users from web identity provider such as Facebook, Google, or any OpenID 589 // Connect-compatible identity provider. 590 // 591 // Eg:- 592 // 593 // $ curl https://minio:9000/?Action=AssumeRoleWithWebIdentity&WebIdentityToken=<jwt> 594 func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) { 595 sts.AssumeRoleWithSSO(w, r) 596 } 597 598 // AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting 599 // OAuth2.0 client credential grants. 600 // 601 // Eg:- 602 // 603 // $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token=<jwt> 604 func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *http.Request) { 605 sts.AssumeRoleWithSSO(w, r) 606 } 607 608 // AssumeRoleWithLDAPIdentity - implements user auth against LDAP server 609 func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *http.Request) { 610 ctx := newContext(r, w, "AssumeRoleWithLDAPIdentity") 611 612 claims := make(map[string]interface{}) 613 defer logger.AuditLog(ctx, w, r, claims, stsLDAPPassword) 614 615 // Parse the incoming form data. 616 if err := parseForm(r); err != nil { 617 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 618 return 619 } 620 621 if r.Form.Get(stsVersion) != stsAPIVersion { 622 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, 623 fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion)) 624 return 625 } 626 627 ldapUsername := r.Form.Get(stsLDAPUsername) 628 ldapPassword := r.Form.Get(stsLDAPPassword) 629 630 if ldapUsername == "" || ldapPassword == "" { 631 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, fmt.Errorf("LDAPUsername and LDAPPassword cannot be empty")) 632 return 633 } 634 635 action := r.Form.Get(stsAction) 636 switch action { 637 case ldapIdentity: 638 default: 639 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) 640 return 641 } 642 643 sessionPolicyStr := r.Form.Get(stsPolicy) 644 // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html 645 // The plain text that you use for both inline and managed session 646 // policies shouldn't exceed 2048 characters. 647 if len(sessionPolicyStr) > 2048 { 648 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Session policy should not exceed 2048 characters")) 649 return 650 } 651 652 if len(sessionPolicyStr) > 0 { 653 sessionPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr))) 654 if err != nil { 655 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 656 return 657 } 658 659 // Version in policy must not be empty 660 if sessionPolicy.Version == "" { 661 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Version needs to be specified in session policy")) 662 return 663 } 664 } 665 666 if !globalIAMSys.Initialized() { 667 writeSTSErrorResponse(ctx, w, ErrSTSIAMNotInitialized, errIAMNotInitialized) 668 return 669 } 670 671 ldapUserDN, groupDistNames, err := globalIAMSys.LDAPConfig.Bind(ldapUsername, ldapPassword) 672 if err != nil { 673 err = fmt.Errorf("LDAP server error: %w", err) 674 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 675 return 676 } 677 678 // Check if this user or their groups have a policy applied. 679 ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, groupDistNames...) 680 if len(ldapPolicies) == 0 && newGlobalAuthZPluginFn() == nil { 681 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, 682 fmt.Errorf("expecting a policy to be set for user `%s` or one of their groups: `%s` - rejecting this request", 683 ldapUserDN, strings.Join(groupDistNames, "`,`"))) 684 return 685 } 686 687 expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration(r.Form.Get(stsDurationSeconds)) 688 if err != nil { 689 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 690 return 691 } 692 693 claims[expClaim] = UTCNow().Add(expiryDur).Unix() 694 claims[ldapUser] = ldapUserDN 695 claims[ldapUserN] = ldapUsername 696 697 if len(sessionPolicyStr) > 0 { 698 claims[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicyStr)) 699 } 700 701 secret, err := getTokenSigningKey() 702 if err != nil { 703 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 704 return 705 } 706 707 cred, err := auth.GetNewCredentialsWithMetadata(claims, secret) 708 if err != nil { 709 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 710 return 711 } 712 713 // Set the parent of the temporary access key, this is useful 714 // in obtaining service accounts by this cred. 715 cred.ParentUser = ldapUserDN 716 717 // Set this value to LDAP groups, LDAP user can be part 718 // of large number of groups 719 cred.Groups = groupDistNames 720 721 // Set the newly generated credentials, policyName is empty on purpose 722 // LDAP policies are applied automatically using their ldapUser, ldapGroups 723 // mapping. 724 updatedAt, err := globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, "") 725 if err != nil { 726 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 727 return 728 } 729 730 // Call hook for site replication. 731 logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ 732 Type: madmin.SRIAMItemSTSAcc, 733 STSCredential: &madmin.SRSTSCredential{ 734 AccessKey: cred.AccessKey, 735 SecretKey: cred.SecretKey, 736 SessionToken: cred.SessionToken, 737 ParentUser: cred.ParentUser, 738 }, 739 UpdatedAt: updatedAt, 740 })) 741 742 ldapIdentityResponse := &AssumeRoleWithLDAPResponse{ 743 Result: LDAPIdentityResult{ 744 Credentials: cred, 745 }, 746 } 747 ldapIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) 748 encodedSuccessResponse := encodeResponse(ldapIdentityResponse) 749 750 writeSuccessResponseXML(w, encodedSuccessResponse) 751 } 752 753 // AssumeRoleWithCertificate implements user authentication with client certificates. 754 // It verifies the client-provided X.509 certificate, maps the certificate to an S3 policy 755 // and returns temp. S3 credentials to the client. 756 // 757 // API endpoint: https://minio:9000?Action=AssumeRoleWithCertificate&Version=2011-06-15 758 func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *http.Request) { 759 ctx := newContext(r, w, "AssumeRoleWithCertificate") 760 761 claims := make(map[string]interface{}) 762 defer logger.AuditLog(ctx, w, r, claims) 763 764 if !globalIAMSys.Initialized() { 765 writeSTSErrorResponse(ctx, w, ErrSTSIAMNotInitialized, errIAMNotInitialized) 766 return 767 } 768 769 if !globalIAMSys.STSTLSConfig.Enabled { 770 writeSTSErrorResponse(ctx, w, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCertificate' is disabled")) 771 return 772 } 773 774 // We have to establish a TLS connection and the 775 // client must provide exactly one client certificate. 776 // Otherwise, we don't have a certificate to verify or 777 // the policy lookup would ambiguous. 778 if r.TLS == nil { 779 writeSTSErrorResponse(ctx, w, ErrSTSInsecureConnection, errors.New("No TLS connection attempt")) 780 return 781 } 782 783 // A client may send a certificate chain such that we end up 784 // with multiple peer certificates. However, we can only accept 785 // a single client certificate. Otherwise, the certificate to 786 // policy mapping would be ambiguous. 787 // However, we can filter all CA certificates and only check 788 // whether they client has sent exactly one (non-CA) leaf certificate. 789 peerCertificates := make([]*x509.Certificate, 0, len(r.TLS.PeerCertificates)) 790 for _, cert := range r.TLS.PeerCertificates { 791 if cert.IsCA { 792 continue 793 } 794 peerCertificates = append(peerCertificates, cert) 795 } 796 r.TLS.PeerCertificates = peerCertificates 797 798 // Now, we have to check that the client has provided exactly one leaf 799 // certificate that we can map to a policy. 800 if len(r.TLS.PeerCertificates) == 0 { 801 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, errors.New("No client certificate provided")) 802 return 803 } 804 if len(r.TLS.PeerCertificates) > 1 { 805 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, errors.New("More than one client certificate provided")) 806 return 807 } 808 809 certificate := r.TLS.PeerCertificates[0] 810 if !globalIAMSys.STSTLSConfig.InsecureSkipVerify { // Verify whether the client certificate has been issued by a trusted CA. 811 _, err := certificate.Verify(x509.VerifyOptions{ 812 KeyUsages: []x509.ExtKeyUsage{ 813 x509.ExtKeyUsageClientAuth, 814 }, 815 Roots: globalRootCAs, 816 }) 817 if err != nil { 818 writeSTSErrorResponse(ctx, w, ErrSTSInvalidClientCertificate, err) 819 return 820 } 821 } else { 822 // Technically, there is no security argument for verifying the key usage 823 // when we don't verify that the certificate has been issued by a trusted CA. 824 // Any client can create a certificate with arbitrary key usage settings. 825 // 826 // However, this check ensures that a certificate with an invalid key usage 827 // gets rejected even when we skip certificate verification. This helps 828 // clients detect malformed certificates during testing instead of e.g. 829 // a self-signed certificate that works while a comparable certificate 830 // issued by a trusted CA fails due to the MinIO server being less strict 831 // w.r.t. key usage verification. 832 // 833 // Basically, MinIO is more consistent (from a client perspective) when 834 // we verify the key usage all the time. 835 var validKeyUsage bool 836 for _, usage := range certificate.ExtKeyUsage { 837 if usage == x509.ExtKeyUsageAny || usage == x509.ExtKeyUsageClientAuth { 838 validKeyUsage = true 839 break 840 } 841 } 842 if !validKeyUsage { 843 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, errors.New("certificate is not valid for client authentication")) 844 return 845 } 846 } 847 848 // We map the X.509 subject common name to the policy. So, a client 849 // with the common name "foo" will be associated with the policy "foo". 850 // Other mapping functions - e.g. public-key hash based mapping - are 851 // possible but not implemented. 852 // 853 // Group mapping is not possible with standard X.509 certificates. 854 if certificate.Subject.CommonName == "" { 855 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, errors.New("certificate subject CN cannot be empty")) 856 return 857 } 858 859 expiry, err := globalIAMSys.STSTLSConfig.GetExpiryDuration(r.Form.Get(stsDurationSeconds)) 860 if err != nil { 861 writeSTSErrorResponse(ctx, w, ErrSTSMissingParameter, err) 862 return 863 } 864 865 // We set the expiry of the temp. credentials to the minimum of the 866 // configured expiry and the duration until the certificate itself 867 // expires. 868 // We must not issue credentials that out-live the certificate. 869 if validUntil := time.Until(certificate.NotAfter); validUntil < expiry { 870 expiry = validUntil 871 } 872 873 // Associate any service accounts to the certificate CN 874 parentUser := "tls:" + certificate.Subject.CommonName 875 876 claims[expClaim] = UTCNow().Add(expiry).Unix() 877 claims[subClaim] = certificate.Subject.CommonName 878 claims[audClaim] = certificate.Subject.Organization 879 claims[issClaim] = certificate.Issuer.CommonName 880 claims[parentClaim] = parentUser 881 secretKey, err := getTokenSigningKey() 882 if err != nil { 883 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 884 return 885 } 886 tmpCredentials, err := auth.GetNewCredentialsWithMetadata(claims, secretKey) 887 if err != nil { 888 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 889 return 890 } 891 892 tmpCredentials.ParentUser = parentUser 893 policyName := certificate.Subject.CommonName 894 updatedAt, err := globalIAMSys.SetTempUser(ctx, tmpCredentials.AccessKey, tmpCredentials, policyName) 895 if err != nil { 896 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 897 return 898 } 899 900 // Call hook for site replication. 901 logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ 902 Type: madmin.SRIAMItemSTSAcc, 903 STSCredential: &madmin.SRSTSCredential{ 904 AccessKey: tmpCredentials.AccessKey, 905 SecretKey: tmpCredentials.SecretKey, 906 SessionToken: tmpCredentials.SessionToken, 907 ParentUser: tmpCredentials.ParentUser, 908 ParentPolicyMapping: policyName, 909 }, 910 UpdatedAt: updatedAt, 911 })) 912 913 response := new(AssumeRoleWithCertificateResponse) 914 response.Result.Credentials = tmpCredentials 915 response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID) 916 writeSuccessResponseXML(w, encodeResponse(response)) 917 } 918 919 // AssumeRoleWithCustomToken implements user authentication with custom tokens. 920 // These tokens are opaque to MinIO and are verified by a configured (external) 921 // Identity Management Plugin. 922 // 923 // API endpoint: https://minio:9000?Action=AssumeRoleWithCustomToken&Token=xxx 924 func (sts *stsAPIHandlers) AssumeRoleWithCustomToken(w http.ResponseWriter, r *http.Request) { 925 ctx := newContext(r, w, "AssumeRoleWithCustomToken") 926 927 claims := make(map[string]interface{}) 928 defer logger.AuditLog(ctx, w, r, claims) 929 930 if !globalIAMSys.Initialized() { 931 writeSTSErrorResponse(ctx, w, ErrSTSIAMNotInitialized, errIAMNotInitialized) 932 return 933 } 934 935 authn := newGlobalAuthNPluginFn() 936 if authn == nil { 937 writeSTSErrorResponse(ctx, w, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCustomToken' is disabled")) 938 return 939 } 940 941 action := r.Form.Get(stsAction) 942 if action != customTokenIdentity { 943 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) 944 return 945 } 946 947 token := r.Form.Get(stsToken) 948 if token == "" { 949 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid empty `Token` parameter provided")) 950 return 951 } 952 953 durationParam := r.Form.Get(stsDurationSeconds) 954 var requestedDuration int 955 if durationParam != "" { 956 var err error 957 requestedDuration, err = strconv.Atoi(durationParam) 958 if err != nil { 959 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid requested duration: %s", durationParam)) 960 return 961 } 962 } 963 964 roleArnStr := r.Form.Get(stsRoleArn) 965 roleArn, _, err := globalIAMSys.GetRolePolicy(roleArnStr) 966 if err != nil { 967 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, 968 fmt.Errorf("Error processing parameter %s: %v", stsRoleArn, err)) 969 return 970 } 971 972 res, err := authn.Authenticate(roleArn, token) 973 if err != nil { 974 writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err) 975 return 976 } 977 978 // If authentication failed, return the error message to the user. 979 if res.Failure != nil { 980 writeSTSErrorResponse(ctx, w, ErrSTSUpstreamError, errors.New(res.Failure.Reason)) 981 return 982 } 983 984 // It is required that parent user be set. 985 if res.Success.User == "" { 986 writeSTSErrorResponse(ctx, w, ErrSTSUpstreamError, errors.New("A valid user was not returned by the authenticator.")) 987 return 988 } 989 990 // Expiry is set as minimum of requested value and value allowed by auth 991 // plugin. 992 expiry := res.Success.MaxValiditySeconds 993 if durationParam != "" && requestedDuration < expiry { 994 expiry = requestedDuration 995 } 996 997 parentUser := "custom:" + res.Success.User 998 999 // metadata map 1000 claims[expClaim] = UTCNow().Add(time.Duration(expiry) * time.Second).Unix() 1001 claims[subClaim] = parentUser 1002 claims[roleArnClaim] = roleArn.String() 1003 claims[parentClaim] = parentUser 1004 1005 // Add all other claims from the plugin **without** replacing any 1006 // existing claims. 1007 for k, v := range res.Success.Claims { 1008 if _, ok := claims[k]; !ok { 1009 claims[k] = v 1010 } 1011 } 1012 secretKey, err := getTokenSigningKey() 1013 if err != nil { 1014 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 1015 return 1016 } 1017 tmpCredentials, err := auth.GetNewCredentialsWithMetadata(claims, secretKey) 1018 if err != nil { 1019 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 1020 return 1021 } 1022 1023 tmpCredentials.ParentUser = parentUser 1024 updatedAt, err := globalIAMSys.SetTempUser(ctx, tmpCredentials.AccessKey, tmpCredentials, "") 1025 if err != nil { 1026 writeSTSErrorResponse(ctx, w, ErrSTSInternalError, err) 1027 return 1028 } 1029 1030 // Call hook for site replication. 1031 logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ 1032 Type: madmin.SRIAMItemSTSAcc, 1033 STSCredential: &madmin.SRSTSCredential{ 1034 AccessKey: tmpCredentials.AccessKey, 1035 SecretKey: tmpCredentials.SecretKey, 1036 SessionToken: tmpCredentials.SessionToken, 1037 ParentUser: tmpCredentials.ParentUser, 1038 }, 1039 UpdatedAt: updatedAt, 1040 })) 1041 1042 response := new(AssumeRoleWithCustomTokenResponse) 1043 response.Result.Credentials = tmpCredentials 1044 response.Result.AssumedUser = parentUser 1045 response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID) 1046 writeSuccessResponseXML(w, encodeResponse(response)) 1047 }