Repository: Azure/azure-sdk-for-go
Pull request #19529 (2022-11-09 19:23:12)

azidentity multitenant authentication API
Usage looks like this:
```go
// cred will request tokens from the configured tenant unless another is specified in GetToken args
cred, err := azidentity.NewClientSecretCredential("primary-tenant", "client-id", "secret", &azidentity.ClientSecretCredentialOptions{
	// cred will (attempt to) authenticate in any requested tenant; can also list specific tenants here
	AdditionallyAllowedTenants: []string{"*"},
})
```
`GetToken()` returns an error when its arguments specify a tenant the credential isn't configured to allow.


Patch:
diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md
index c2176107e0..273405e8cc 100644
--- a/sdk/azcore/CHANGELOG.md
+++ b/sdk/azcore/CHANGELOG.md
@@ -1,5 +1,22 @@
 # Release History
 
+## 1.4.0-beta.2 (Unreleased)
+
+### Features Added
+
+### Breaking Changes
+
+### Bugs Fixed
+
+### Other Changes
+
+## 1.4.0-beta.1 (2023-02-02)
+
+### Features Added
+* Added support for ARM cross-tenant authentication. Set the `AuxiliaryTenants` field of `arm.ClientOptions` to enable.
+* Added `Claims` and `TenantID` fields to `policy.TokenRequestOptions`.
+* ARM bearer token policy handles CAE challenges.
+
 ## 1.3.1 (2023-02-02)
 
 ### Other Changes
diff --git a/sdk/azcore/arm/policy/policy.go b/sdk/azcore/arm/policy/policy.go
index 098aa69934..8ab6aa9d9d 100644
--- a/sdk/azcore/arm/policy/policy.go
+++ b/sdk/azcore/arm/policy/policy.go
@@ -14,6 +14,12 @@ import (
 
 // BearerTokenOptions configures the bearer token policy's behavior.
 type BearerTokenOptions struct {
+	// AuxiliaryTenants are additional tenant IDs for authenticating cross-tenant requests.
+	// The policy will add a token from each of these tenants to every request. The
+	// authenticating user or service principal must be a guest in these tenants, and the
+	// policy's credential must support multitenant authentication.
+	AuxiliaryTenants []string
+
 	// Scopes contains the list of permission scopes required for the token.
 	Scopes []string
 }
@@ -44,6 +50,12 @@ type RegistrationOptions struct {
 type ClientOptions struct {
 	policy.ClientOptions
 
+	// AuxiliaryTenants are additional tenant IDs for authenticating cross-tenant requests.
+	// The client will add a token from each of these tenants to every request. The
+	// authenticating user or service principal must be a guest in these tenants, and the
+	// client's credential must support multitenant authentication.
+	AuxiliaryTenants []string
+
 	// DisableRPRegistration disables the auto-RP registration policy. Defaults to false.
 	DisableRPRegistration bool
 }
diff --git a/sdk/azcore/arm/runtime/pipeline.go b/sdk/azcore/arm/runtime/pipeline.go
index a2e897765d..57d8ea451a 100644
--- a/sdk/azcore/arm/runtime/pipeline.go
+++ b/sdk/azcore/arm/runtime/pipeline.go
@@ -28,7 +28,10 @@ func NewPipeline(module, version string, cred azcore.TokenCredential, plOpts azr
 	if err != nil {
 		return azruntime.Pipeline{}, err
 	}
-	authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{Scopes: []string{conf.Audience + "/.default"}})
+	authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{
+		AuxiliaryTenants: options.AuxiliaryTenants,
+		Scopes:           []string{conf.Audience + "/.default"},
+	})
 	perRetry := make([]azpolicy.Policy, 0, len(plOpts.PerRetry)+1)
 	copy(perRetry, plOpts.PerRetry)
 	plOpts.PerRetry = append(perRetry, authPolicy)
diff --git a/sdk/azcore/arm/runtime/policy_bearer_token.go b/sdk/azcore/arm/runtime/policy_bearer_token.go
index 10a3606e9d..83f1bf86e6 100644
--- a/sdk/azcore/arm/runtime/policy_bearer_token.go
+++ b/sdk/azcore/arm/runtime/policy_bearer_token.go
@@ -5,6 +5,7 @@ package runtime
 
 import (
 	"context"
+	"encoding/base64"
 	"fmt"
 	"net/http"
 	"strings"
@@ -14,9 +15,13 @@ import (
 	armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
 	azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+	azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/temporal"
 )
 
+const headerAuxiliaryAuthorization = "x-ms-authorization-auxiliary"
+
+// acquiringResourceState holds data for an auxiliary token request
 type acquiringResourceState struct {
 	ctx    context.Context
 	p      *BearerTokenPolicy
@@ -26,7 +31,10 @@ type acquiringResourceState struct {
 // acquire acquires or updates the resource; only one
 // thread/goroutine at a time ever calls this function
 func acquire(state acquiringResourceState) (newResource azcore.AccessToken, newExpiration time.Time, err error) {
-	tk, err := state.p.cred.GetToken(state.ctx, azpolicy.TokenRequestOptions{Scopes: state.p.options.Scopes})
+	tk, err := state.p.cred.GetToken(state.ctx, azpolicy.TokenRequestOptions{
+		Scopes:   state.p.scopes,
+		TenantID: state.tenant,
+	})
 	if err != nil {
 		return azcore.AccessToken{}, time.Time{}, err
 	}
@@ -35,13 +43,10 @@ func acquire(state acquiringResourceState) (newResource azcore.AccessToken, newE
 
 // BearerTokenPolicy authorizes requests with bearer tokens acquired from a TokenCredential.
 type BearerTokenPolicy struct {
-	// mainResource is the resource to be retreived using the tenant specified in the credential
-	mainResource *temporal.Resource[azcore.AccessToken, acquiringResourceState]
-	// auxResources are additional resources that are required for cross-tenant applications
 	auxResources map[string]*temporal.Resource[azcore.AccessToken, acquiringResourceState]
-	// the following fields are read-only
-	cred    azcore.TokenCredential
-	options armpolicy.BearerTokenOptions
+	btp          *azruntime.BearerTokenPolicy
+	cred         azcore.TokenCredential
+	scopes       []string
 }
 
 // NewBearerTokenPolicy creates a policy object that authorizes requests with bearer tokens.
@@ -51,36 +56,90 @@ func NewBearerTokenPolicy(cred azcore.TokenCredential, opts *armpolicy.BearerTok
 	if opts == nil {
 		opts = &armpolicy.BearerTokenOptions{}
 	}
-	p := &BearerTokenPolicy{
-		cred:         cred,
-		options:      *opts,
-		mainResource: temporal.NewResource(acquire),
+	p := &BearerTokenPolicy{cred: cred}
+	p.auxResources = make(map[string]*temporal.Resource[azcore.AccessToken, acquiringResourceState], len(opts.AuxiliaryTenants))
+	for _, t := range opts.AuxiliaryTenants {
+		p.auxResources[t] = temporal.NewResource(acquire)
 	}
+	p.scopes = make([]string, len(opts.Scopes))
+	copy(p.scopes, opts.Scopes)
+	p.btp = azruntime.NewBearerTokenPolicy(cred, opts.Scopes, &azpolicy.BearerTokenOptions{
+		AuthorizationHandler: azpolicy.AuthorizationHandler{
+			OnChallenge: p.onChallenge,
+			OnRequest:   p.onRequest,
+		},
+	})
 	return p
 }
 
-// Do authorizes a request with a bearer token
-func (b *BearerTokenPolicy) Do(req *azpolicy.Request) (*http.Response, error) {
+func (b *BearerTokenPolicy) onChallenge(req *azpolicy.Request, res *http.Response, authNZ func(azpolicy.TokenRequestOptions) error) error {
+	challenge := res.Header.Get(shared.HeaderWWWAuthenticate)
+	claims, err := parseChallenge(challenge)
+	if err != nil {
+		// the challenge contains claims we can't parse
+		return err
+	} else if claims != "" {
+		// request a new token having the specified claims, send the request again
+		return authNZ(azpolicy.TokenRequestOptions{Claims: claims, Scopes: b.scopes})
+	}
+	// auth challenge didn't include claims, so this is a simple authorization failure
+	return azruntime.NewResponseError(res)
+}
+
+// onRequest authorizes requests with one or more bearer tokens
+func (b *BearerTokenPolicy) onRequest(req *azpolicy.Request, authNZ func(azpolicy.TokenRequestOptions) error) error {
+	// authorize the request with a token for the primary tenant
+	err := authNZ(azpolicy.TokenRequestOptions{Scopes: b.scopes})
+	if err != nil || len(b.auxResources) == 0 {
+		return err
+	}
+	// add tokens for auxiliary tenants
 	as := acquiringResourceState{
 		ctx: req.Raw().Context(),
 		p:   b,
 	}
-	tk, err := b.mainResource.Get(as)
-	if err != nil {
-		return nil, err
-	}
-	req.Raw().Header.Set(shared.HeaderAuthorization, shared.BearerTokenPrefix+tk.Token)
-	auxTokens := []string{}
+	auxTokens := make([]string, 0, len(b.auxResources))
 	for tenant, er := range b.auxResources {
 		as.tenant = tenant
 		auxTk, err := er.Get(as)
 		if err != nil {
-			return nil, err
+			return err
 		}
 		auxTokens = append(auxTokens, fmt.Sprintf("%s%s", shared.BearerTokenPrefix, auxTk.Token))
 	}
-	if len(auxTokens) > 0 {
-		req.Raw().Header.Set(shared.HeaderAuxiliaryAuthorization, strings.Join(auxTokens, ", "))
+	req.Raw().Header.Set(headerAuxiliaryAuthorization, strings.Join(auxTokens, ", "))
+	return nil
+}
+
+// Do authorizes a request with a bearer token
+func (b *BearerTokenPolicy) Do(req *azpolicy.Request) (*http.Response, error) {
+	return b.btp.Do(req)
+}
+
+// parseChallenge parses claims from an authentication challenge issued by ARM so a client can request a token
+// that will satisfy conditional access policies. It returns a non-nil error when the given value contains
+// claims it can't parse. If the value contains no claims, it returns an empty string and a nil error.
+func parseChallenge(wwwAuthenticate string) (string, error) {
+	claims := ""
+	var err error
+	for _, param := range strings.Split(wwwAuthenticate, ",") {
+		if _, after, found := strings.Cut(param, "claims="); found {
+			if claims != "" {
+				// The header contains multiple challenges, at least two of which specify claims. The specs allow this
+				// but it's unclear what a client should do in this case and there's as yet no concrete example of it.
+				err = fmt.Errorf("found multiple claims challenges in %q", wwwAuthenticate)
+				break
+			}
+			// trim stuff that would get an error from RawURLEncoding; claims may or may not be padded
+			claims = strings.Trim(after, `\"=`)
+			// we don't return this error because it's something unhelpful like "illegal base64 data at input byte 42"
+			if b, decErr := base64.RawURLEncoding.DecodeString(claims); decErr == nil {
+				claims = string(b)
+			} else {
+				err = fmt.Errorf("failed to parse claims from %q", wwwAuthenticate)
+				break
+			}
+		}
 	}
-	return req.Next()
+	return claims, err
 }
diff --git a/sdk/azcore/internal/exported/exported.go b/sdk/azcore/internal/exported/exported.go
index 2ffbc0e474..2ef55e3677 100644
--- a/sdk/azcore/internal/exported/exported.go
+++ b/sdk/azcore/internal/exported/exported.go
@@ -71,8 +71,16 @@ type AccessToken struct {
 // TokenRequestOptions contain specific parameter that may be used by credentials types when attempting to get a token.
 // Exported as policy.TokenRequestOptions.
 type TokenRequestOptions struct {
+	// Claims are any additional claims required for the token to satisfy a conditional access policy, such as a
+	// service may return in a claims challenge following an authorization failure. If a service returned the
+	// claims value base64 encoded, it must be decoded before setting this field.
+	Claims string
 	// Scopes contains the list of permission scopes required for the token.
 	Scopes []string
+
+	// TenantID identifies the tenant from which to request the token. azidentity credentials authenticate in
+	// their configured default tenants when this field isn't set.
+	TenantID string
 }
 
 // TokenCredential represents a credential capable of providing an OAuth token.
diff --git a/sdk/azcore/internal/shared/constants.go b/sdk/azcore/internal/shared/constants.go
index 0fe2e2cb7f..f8fe3222e8 100644
--- a/sdk/azcore/internal/shared/constants.go
+++ b/sdk/azcore/internal/shared/constants.go
@@ -21,6 +21,7 @@ const (
 	HeaderOperationLocation      = "Operation-Location"
 	HeaderRetryAfter             = "Retry-After"
 	HeaderUserAgent              = "User-Agent"
+	HeaderWWWAuthenticate        = "WWW-Authenticate"
 )
 
 const BearerTokenPrefix = "Bearer "
@@ -30,5 +31,5 @@ const (
 	Module = "azcore"
 
 	// Version is the semantic version (see http://semver.org) of this module.
-	Version = "v1.3.1"
+	Version = "v1.4.0-beta.2"
 )
diff --git a/sdk/azidentity/azidentity.go b/sdk/azidentity/azidentity.go
index 907dd7dbda..f0deced24c 100644
--- a/sdk/azidentity/azidentity.go
+++ b/sdk/azidentity/azidentity.go
@@ -10,6 +10,7 @@ import (
 	"bytes"
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -26,16 +27,17 @@ import (
 )
 
 const (
-	azureAuthorityHost             = "AZURE_AUTHORITY_HOST"
-	azureClientCertificatePassword = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
-	azureClientCertificatePath     = "AZURE_CLIENT_CERTIFICATE_PATH"
-	azureClientID                  = "AZURE_CLIENT_ID"
-	azureClientSecret              = "AZURE_CLIENT_SECRET"
-	azureFederatedTokenFile        = "AZURE_FEDERATED_TOKEN_FILE"
-	azurePassword                  = "AZURE_PASSWORD"
-	azureRegionalAuthorityName     = "AZURE_REGIONAL_AUTHORITY_NAME"
-	azureTenantID                  = "AZURE_TENANT_ID"
-	azureUsername                  = "AZURE_USERNAME"
+	azureAdditionallyAllowedTenants = "AZURE_ADDITIONALLY_ALLOWED_TENANTS"
+	azureAuthorityHost              = "AZURE_AUTHORITY_HOST"
+	azureClientCertificatePassword  = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
+	azureClientCertificatePath      = "AZURE_CLIENT_CERTIFICATE_PATH"
+	azureClientID                   = "AZURE_CLIENT_ID"
+	azureClientSecret               = "AZURE_CLIENT_SECRET"
+	azureFederatedTokenFile         = "AZURE_FEDERATED_TOKEN_FILE"
+	azurePassword                   = "AZURE_PASSWORD"
+	azureRegionalAuthorityName      = "AZURE_REGIONAL_AUTHORITY_NAME"
+	azureTenantID                   = "AZURE_TENANT_ID"
+	azureUsername                   = "AZURE_USERNAME"
 
 	organizationsTenantID   = "organizations"
 	developerSignOnClientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
@@ -85,6 +87,41 @@ var getPublicClient = func(clientID, tenantID string, co *azcore.ClientOptions,
 	)
 }
 
+// resolveAdditionallyAllowedTenants returns a copy of tenants, simplified when tenants contains a wildcard
+func resolveAdditionallyAllowedTenants(tenants []string) []string {
+	if len(tenants) == 0 {
+		return nil
+	}
+	for _, t := range tenants {
+		// a wildcard makes all other values redundant
+		if t == "*" {
+			return []string{"*"}
+		}
+	}
+	cp := make([]string, len(tenants))
+	copy(cp, tenants)
+	return cp
+}
+
+// resolveTenant returns the correct tenant for a token request given a credential's configuration
+func resolveTenant(defaultTenant, reqTenant string, allowedTenants []string) (string, error) {
+	if reqTenant == "" || reqTenant == defaultTenant {
+		return defaultTenant, nil
+	}
+	if defaultTenant == "adfs" {
+		return "", errors.New("ADFS doesn't support tenants")
+	}
+	if !validTenantID(reqTenant) {
+		return "", errors.New(tenantIDValidationErr)
+	}
+	for _, tenant := range allowedTenants {
+		if tenant == "*" || tenant == reqTenant {
+			return reqTenant, nil
+		}
+	}
+	return "", fmt.Errorf(`this credential isn't configured to acquire tokens for tenant "%s". To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, reqTenant)
+}
+
 // setAuthorityHost initializes the authority host for credentials. Precedence is:
 // 1. cloud.Configuration.ActiveDirectoryAuthorityHost value set by user
 // 2. value of AZURE_AUTHORITY_HOST
diff --git a/sdk/azidentity/azure_cli_credential.go b/sdk/azidentity/azure_cli_credential.go
index 3b0083a9b1..68af66c9cc 100644
--- a/sdk/azidentity/azure_cli_credential.go
+++ b/sdk/azidentity/azure_cli_credential.go
@@ -30,6 +30,10 @@ type azureCLITokenProvider func(ctx context.Context, resource string, tenantID s
 
 // AzureCLICredentialOptions contains optional parameters for AzureCLICredential.
 type AzureCLICredentialOptions struct {
+	// AdditionallyAllowedTenants specifies tenants for which the credential may acquire tokens, in addition
+	// to TenantID. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the
+	// logged in account can access.
+	AdditionallyAllowedTenants []string
 	// TenantID identifies the tenant the credential should authenticate in.
 	// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
 	TenantID string
@@ -46,8 +50,9 @@ func (o *AzureCLICredentialOptions) init() {
 
 // AzureCLICredential authenticates as the identity logged in to the Azure CLI.
 type AzureCLICredential struct {
-	tokenProvider azureCLITokenProvider
-	tenantID      string
+	additionallyAllowedTenants []string
+	tenantID                   string
+	tokenProvider              azureCLITokenProvider
 }
 
 // NewAzureCLICredential constructs an AzureCLICredential. Pass nil to accept default options.
@@ -58,8 +63,9 @@ func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredent
 	}
 	cp.init()
 	return &AzureCLICredential{
-		tokenProvider: cp.tokenProvider,
-		tenantID:      cp.TenantID,
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(cp.AdditionallyAllowedTenants),
+		tenantID:                   cp.TenantID,
+		tokenProvider:              cp.tokenProvider,
 	}, nil
 }
 
@@ -69,9 +75,17 @@ func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequ
 	if len(opts.Scopes) != 1 {
 		return azcore.AccessToken{}, errors.New(credNameAzureCLI + ": GetToken() requires exactly one scope")
 	}
-	// CLI expects an AAD v1 resource, not a v2 scope
-	scope := strings.TrimSuffix(opts.Scopes[0], defaultSuffix)
-	at, err := c.authenticate(ctx, scope)
+	tenant, err := resolveTenant(c.tenantID, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	// CLI expects an AAD v1 resource, not a v2 resource
+	resource := strings.TrimSuffix(opts.Scopes[0], defaultSuffix)
+	b, err := c.tokenProvider(ctx, resource, tenant)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	at, err := c.createAccessToken(b)
 	if err != nil {
 		return azcore.AccessToken{}, err
 	}
@@ -81,15 +95,6 @@ func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequ
 
 const timeoutCLIRequest = 10 * time.Second
 
-func (c *AzureCLICredential) authenticate(ctx context.Context, resource string) (azcore.AccessToken, error) {
-	output, err := c.tokenProvider(ctx, resource, c.tenantID)
-	if err != nil {
-		return azcore.AccessToken{}, err
-	}
-
-	return c.createAccessToken(output)
-}
-
 func defaultTokenProvider() func(ctx context.Context, resource string, tenantID string) ([]byte, error) {
 	return func(ctx context.Context, resource string, tenantID string) ([]byte, error) {
 		match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", resource)
diff --git a/sdk/azidentity/client_assertion_credential.go b/sdk/azidentity/client_assertion_credential.go
index fc9e438973..15bebaa169 100644
--- a/sdk/azidentity/client_assertion_credential.go
+++ b/sdk/azidentity/client_assertion_credential.go
@@ -24,15 +24,21 @@ const credNameAssertion = "ClientAssertionCredential"
 //
 // [Azure AD documentation]: https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format
 type ClientAssertionCredential struct {
-	client confidentialClient
+	additionallyAllowedTenants []string
+	client                     confidentialClient
 	// name enables replacing "ClientAssertionCredential" with "WorkloadIdentityCredential" in log messages
-	name string
+	name   string
+	tenant string
 }
 
 // ClientAssertionCredentialOptions contains optional parameters for ClientAssertionCredential.
 type ClientAssertionCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
+	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
+	// application is registered.
+	AdditionallyAllowedTenants []string
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
 }
@@ -54,7 +60,12 @@ func NewClientAssertionCredential(tenantID, clientID string, getAssertion func(c
 	if err != nil {
 		return nil, err
 	}
-	return &ClientAssertionCredential{client: c, name: credNameAssertion}, nil
+	return &ClientAssertionCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(options.AdditionallyAllowedTenants),
+		client:                     c,
+		name:                       credNameAssertion,
+		tenant:                     tenantID,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -62,13 +73,17 @@ func (c *ClientAssertionCredential) GetToken(ctx context.Context, opts policy.To
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameAssertion + ": GetToken() requires at least one scope")
 	}
-	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
+	tenant, err := resolveTenant(c.tenant, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(tenant))
 	if err == nil {
 		logGetTokenSuccessImpl(c.name, opts)
 		return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
 	}
 
-	ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
+	ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(tenant))
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(c.name, err)
 	}
diff --git a/sdk/azidentity/client_certificate_credential.go b/sdk/azidentity/client_certificate_credential.go
index ba42259c90..4f0122f179 100644
--- a/sdk/azidentity/client_certificate_credential.go
+++ b/sdk/azidentity/client_certificate_credential.go
@@ -25,9 +25,12 @@ const credNameCert = "ClientCertificateCredential"
 type ClientCertificateCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
+	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
+	// application is registered.
+	AdditionallyAllowedTenants []string
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
-
 	// SendCertificateChain controls whether the credential sends the public certificate chain in the x5c
 	// header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication.
 	// Defaults to False.
@@ -36,7 +39,9 @@ type ClientCertificateCredentialOptions struct {
 
 // ClientCertificateCredential authenticates a service principal with a certificate.
 type ClientCertificateCredential struct {
-	client confidentialClient
+	additionallyAllowedTenants []string
+	client                     confidentialClient
+	tenant                     string
 }
 
 // NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults.
@@ -60,7 +65,11 @@ func NewClientCertificateCredential(tenantID string, clientID string, certs []*x
 	if err != nil {
 		return nil, err
 	}
-	return &ClientCertificateCredential{client: c}, nil
+	return &ClientCertificateCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(options.AdditionallyAllowedTenants),
+		client:                     c,
+		tenant:                     tenantID,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -68,13 +77,17 @@ func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts policy.
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameCert + ": GetToken() requires at least one scope")
 	}
-	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
+	tenant, err := resolveTenant(c.tenant, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(tenant))
 	if err == nil {
 		logGetTokenSuccess(c, opts)
 		return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
 	}
 
-	ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
+	ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(tenant))
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameCert, err)
 	}
diff --git a/sdk/azidentity/client_secret_credential.go b/sdk/azidentity/client_secret_credential.go
index 310527f9f0..bfdc353d26 100644
--- a/sdk/azidentity/client_secret_credential.go
+++ b/sdk/azidentity/client_secret_credential.go
@@ -21,13 +21,19 @@ const credNameSecret = "ClientSecretCredential"
 type ClientSecretCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
+	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
+	// application is registered.
+	AdditionallyAllowedTenants []string
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
 }
 
 // ClientSecretCredential authenticates an application with a client secret.
 type ClientSecretCredential struct {
-	client confidentialClient
+	additionallyAllowedTenants []string
+	client                     confidentialClient
+	tenant                     string
 }
 
 // NewClientSecretCredential constructs a ClientSecretCredential. Pass nil for options to accept defaults.
@@ -43,7 +49,11 @@ func NewClientSecretCredential(tenantID string, clientID string, clientSecret st
 	if err != nil {
 		return nil, err
 	}
-	return &ClientSecretCredential{client: c}, nil
+	return &ClientSecretCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(options.AdditionallyAllowedTenants),
+		client:                     c,
+		tenant:                     tenantID,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -51,13 +61,17 @@ func (c *ClientSecretCredential) GetToken(ctx context.Context, opts policy.Token
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameSecret + ": GetToken() requires at least one scope")
 	}
-	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
+	tenant, err := resolveTenant(c.tenant, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(tenant))
 	if err == nil {
 		logGetTokenSuccess(c, opts)
 		return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
 	}
 
-	ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
+	ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(tenant))
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameSecret, err)
 	}
diff --git a/sdk/azidentity/default_azure_credential.go b/sdk/azidentity/default_azure_credential.go
index 5d78f479e8..90ad3e6d60 100644
--- a/sdk/azidentity/default_azure_credential.go
+++ b/sdk/azidentity/default_azure_credential.go
@@ -23,9 +23,12 @@ import (
 type DefaultAzureCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens. Add
+	// the wildcard value "*" to allow the credential to acquire tokens for any tenant. This value can also be
+	// set as a semicolon delimited list of tenants in the environment variable AZURE_ADDITIONALLY_ALLOWED_TENANTS.
+	AdditionallyAllowedTenants []string
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
-
 	// TenantID identifies the tenant the Azure CLI should authenticate in.
 	// Defaults to the CLI's default tenant, which is typically the home tenant of the user logged in to the CLI.
 	TenantID string
@@ -58,8 +61,17 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default
 	if options == nil {
 		options = &DefaultAzureCredentialOptions{}
 	}
+	additionalTenants := options.AdditionallyAllowedTenants
+	if len(additionalTenants) == 0 {
+		if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
+			additionalTenants = strings.Split(tenants, ";")
+		}
+	}
+	additionalTenants = resolveAdditionallyAllowedTenants(additionalTenants)
 
-	envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery})
+	envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{
+		ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery, additionallyAllowedTenants: additionalTenants},
+	)
 	if err == nil {
 		creds = append(creds, envCred)
 	} else {
@@ -105,7 +117,7 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default
 		creds = append(creds, &defaultCredentialErrorReporter{credType: credNameManagedIdentity, err: err})
 	}
 
-	cliCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{TenantID: options.TenantID})
+	cliCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{AdditionallyAllowedTenants: additionalTenants, TenantID: options.TenantID})
 	if err == nil {
 		creds = append(creds, cliCred)
 	} else {
diff --git a/sdk/azidentity/device_code_credential.go b/sdk/azidentity/device_code_credential.go
index 5ed3c1379b..49c46a1552 100644
--- a/sdk/azidentity/device_code_credential.go
+++ b/sdk/azidentity/device_code_credential.go
@@ -22,13 +22,14 @@ const credNameDeviceCode = "DeviceCodeCredential"
 type DeviceCodeCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
+	// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
+	AdditionallyAllowedTenants []string
 	// ClientID is the ID of the application users will authenticate to.
 	// Defaults to the ID of an Azure development application.
 	ClientID string
-
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
-
 	// TenantID is the Azure Active Directory tenant the credential authenticates in. Defaults to the
 	// "organizations" tenant, which can authenticate work and school accounts. Required for single-tenant
 	// applications.
@@ -71,9 +72,11 @@ type DeviceCodeMessage struct {
 // If a web browser is available, InteractiveBrowserCredential is more convenient because it
 // automatically opens a browser to the login page.
 type DeviceCodeCredential struct {
-	client     publicClient
-	userPrompt func(context.Context, DeviceCodeMessage) error
-	account    public.Account
+	account                    public.Account
+	additionallyAllowedTenants []string
+	client                     publicClient
+	tenant                     string
+	userPrompt                 func(context.Context, DeviceCodeMessage) error
 }
 
 // NewDeviceCodeCredential creates a DeviceCodeCredential. Pass nil to accept default options.
@@ -87,7 +90,12 @@ func NewDeviceCodeCredential(options *DeviceCodeCredentialOptions) (*DeviceCodeC
 	if err != nil {
 		return nil, err
 	}
-	return &DeviceCodeCredential{userPrompt: cp.UserPrompt, client: c}, nil
+	return &DeviceCodeCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(cp.AdditionallyAllowedTenants),
+		client:                     c,
+		tenant:                     cp.TenantID,
+		userPrompt:                 cp.UserPrompt,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. It will begin the device code flow and poll until the user completes authentication.
@@ -96,11 +104,15 @@ func (c *DeviceCodeCredential) GetToken(ctx context.Context, opts policy.TokenRe
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameDeviceCode + ": GetToken() requires at least one scope")
 	}
-	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account))
+	tenant, err := resolveTenant(c.tenant, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account), public.WithTenantID(tenant))
 	if err == nil {
 		return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
 	}
-	dc, err := c.client.AcquireTokenByDeviceCode(ctx, opts.Scopes)
+	dc, err := c.client.AcquireTokenByDeviceCode(ctx, opts.Scopes, public.WithTenantID(tenant))
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameDeviceCode, err)
 	}
diff --git a/sdk/azidentity/environment_credential.go b/sdk/azidentity/environment_credential.go
index d43cf2506d..4d108d5837 100644
--- a/sdk/azidentity/environment_credential.go
+++ b/sdk/azidentity/environment_credential.go
@@ -26,6 +26,11 @@ type EnvironmentCredentialOptions struct {
 
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
+	// additionallyAllowedTenants is used only by NewDefaultAzureCredential() to enable that constructor's explicit
+	// option to override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS. Applications using EnvironmentCredential
+	// directly should set that variable instead. This field should remain unexported to preserve this credential's
+	// unambiguous "all configuration from environment variables" design.
+	additionallyAllowedTenants []string
 }
 
 // EnvironmentCredential authenticates a service principal with a secret or certificate, or a user with a password, depending
@@ -58,6 +63,12 @@ type EnvironmentCredentialOptions struct {
 // AZURE_USERNAME: a username (usually an email address)
 //
 // AZURE_PASSWORD: the user's password
+//
+// # Configuration for multitenant applications
+//
+// To enable multitenant authentication, set AZURE_ADDITIONALLY_ALLOWED_TENANTS with a semicolon delimited list of tenants
+// the credential may request tokens from in addition to the tenant specified by AZURE_TENANT_ID. Set
+// AZURE_ADDITIONALLY_ALLOWED_TENANTS to "*" to enable the credential to request a token from any tenant.
 type EnvironmentCredential struct {
 	cred azcore.TokenCredential
 }
@@ -75,9 +86,21 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
 	if clientID == "" {
 		return nil, errors.New("missing environment variable " + azureClientID)
 	}
+	// tenants set by NewDefaultAzureCredential() override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS
+	additionalTenants := options.additionallyAllowedTenants
+	if len(additionalTenants) == 0 {
+		if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
+			additionalTenants = strings.Split(tenants, ";")
+		}
+	}
+	additionalTenants = resolveAdditionallyAllowedTenants(additionalTenants)
 	if clientSecret := os.Getenv(azureClientSecret); clientSecret != "" {
 		log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientSecretCredential")
-		o := &ClientSecretCredentialOptions{ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery}
+		o := &ClientSecretCredentialOptions{
+			AdditionallyAllowedTenants: additionalTenants,
+			ClientOptions:              options.ClientOptions,
+			DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
+		}
 		cred, err := NewClientSecretCredential(tenantID, clientID, clientSecret, o)
 		if err != nil {
 			return nil, err
@@ -98,7 +121,11 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
 		if err != nil {
 			return nil, fmt.Errorf(`failed to load certificate from "%s": %v`, certPath, err)
 		}
-		o := &ClientCertificateCredentialOptions{ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery}
+		o := &ClientCertificateCredentialOptions{
+			AdditionallyAllowedTenants: additionalTenants,
+			ClientOptions:              options.ClientOptions,
+			DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
+		}
 		if v, ok := os.LookupEnv(envVarSendCertChain); ok {
 			o.SendCertificateChain = v == "1" || strings.ToLower(v) == "true"
 		}
@@ -111,7 +138,11 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
 	if username := os.Getenv(azureUsername); username != "" {
 		if password := os.Getenv(azurePassword); password != "" {
 			log.Write(EventAuthentication, "EnvironmentCredential will authenticate with UsernamePasswordCredential")
-			o := &UsernamePasswordCredentialOptions{ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery}
+			o := &UsernamePasswordCredentialOptions{
+				AdditionallyAllowedTenants: additionalTenants,
+				ClientOptions:              options.ClientOptions,
+				DisableInstanceDiscovery:   options.DisableInstanceDiscovery,
+			}
 			cred, err := NewUsernamePasswordCredential(tenantID, clientID, username, password, o)
 			if err != nil {
 				return nil, err
diff --git a/sdk/azidentity/go.mod b/sdk/azidentity/go.mod
index 75995dd984..2a7c5a3e3d 100644
--- a/sdk/azidentity/go.mod
+++ b/sdk/azidentity/go.mod
@@ -3,8 +3,8 @@ module github.com/Azure/azure-sdk-for-go/sdk/azidentity
 go 1.18
 
 require (
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0
-	github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0-beta.1
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1
 	github.com/golang-jwt/jwt/v4 v4.4.2
 	golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88
@@ -16,9 +16,9 @@ require (
 	github.com/google/uuid v1.1.1 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
-	golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
-	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
-	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/net v0.5.0 // indirect
+	golang.org/x/sys v0.4.0 // indirect
+	golang.org/x/text v0.6.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/sdk/azidentity/go.sum b/sdk/azidentity/go.sum
index 35014d4afb..1c6d2a75db 100644
--- a/sdk/azidentity/go.sum
+++ b/sdk/azidentity/go.sum
@@ -1,7 +1,7 @@
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 h1:sVPhtT2qjO86rTUaWMr4WoES4TkjGnzcioXcnHV9s5k=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0-beta.1 h1:gwgeyp9Xp0tqcSa1vLyMlem4UK5WGEHLAEi34dUVI+Q=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0-beta.1/go.mod h1:DffdKW9RFqa5VgmsjUOsS7UE7eiA5iAvYUs63bhKQ0M=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
 github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1 h1:oPdPEZFSbl7oSPEAIPMPBMUmiL+mqgzBJwM/9qYcwNg=
 github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -21,12 +21,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw=
 golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/sdk/azidentity/interactive_browser_credential.go b/sdk/azidentity/interactive_browser_credential.go
index 14edbf8820..b95f850fbe 100644
--- a/sdk/azidentity/interactive_browser_credential.go
+++ b/sdk/azidentity/interactive_browser_credential.go
@@ -21,6 +21,9 @@ const credNameBrowser = "InteractiveBrowserCredentiall"
 type InteractiveBrowserCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
+	// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
+	AdditionallyAllowedTenants []string
 	// ClientID is the ID of the application users will authenticate to.
 	// Defaults to the ID of an Azure development application.
 	ClientID string
@@ -51,9 +54,10 @@ func (o *InteractiveBrowserCredentialOptions) init() {
 
 // InteractiveBrowserCredential opens a browser to interactively authenticate a user.
 type InteractiveBrowserCredential struct {
-	account public.Account
-	client  publicClient
-	options InteractiveBrowserCredentialOptions
+	account                    public.Account
+	additionallyAllowedTenants []string
+	client                     publicClient
+	options                    InteractiveBrowserCredentialOptions
 }
 
 // NewInteractiveBrowserCredential constructs a new InteractiveBrowserCredential. Pass nil to accept default options.
@@ -67,7 +71,11 @@ func NewInteractiveBrowserCredential(options *InteractiveBrowserCredentialOption
 	if err != nil {
 		return nil, err
 	}
-	return &InteractiveBrowserCredential{options: cp, client: c}, nil
+	return &InteractiveBrowserCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(cp.AdditionallyAllowedTenants),
+		client:                     c,
+		options:                    cp,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -75,13 +83,21 @@ func (c *InteractiveBrowserCredential) GetToken(ctx context.Context, opts policy
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameBrowser + ": GetToken() requires at least one scope")
 	}
-	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account))
+	tenant, err := resolveTenant(c.options.TenantID, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account), public.WithTenantID(tenant))
 	if err == nil {
 		logGetTokenSuccess(c, opts)
 		return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
 	}
 
-	ar, err = c.client.AcquireTokenInteractive(ctx, opts.Scopes, public.WithLoginHint(c.options.LoginHint), public.WithRedirectURI(c.options.RedirectURL))
+	ar, err = c.client.AcquireTokenInteractive(ctx, opts.Scopes,
+		public.WithLoginHint(c.options.LoginHint),
+		public.WithRedirectURI(c.options.RedirectURL),
+		public.WithTenantID(tenant),
+	)
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameBrowser, err)
 	}
diff --git a/sdk/azidentity/on_behalf_of_credential.go b/sdk/azidentity/on_behalf_of_credential.go
index aff6ef2017..ac020d1fac 100644
--- a/sdk/azidentity/on_behalf_of_credential.go
+++ b/sdk/azidentity/on_behalf_of_credential.go
@@ -26,17 +26,21 @@ const credNameOBO = "OnBehalfOfCredential"
 //
 // [Azure Active Directory documentation]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
 type OnBehalfOfCredential struct {
-	assertion string
-	client    confidentialClient
+	additionallyAllowedTenants []string
+	assertion, tenant          string
+	client                     confidentialClient
 }
 
 // OnBehalfOfCredentialOptions contains optional parameters for OnBehalfOfCredential
 type OnBehalfOfCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
+	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
+	// application is registered.
+	AdditionallyAllowedTenants []string
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
-
 	// SendCertificateChain applies only when the credential is configured to authenticate with a certificate.
 	// This setting controls whether the credential sends the public certificate chain in the x5c header of each
 	// token request's JWT. This is required for, and only used in, Subject Name/Issuer (SNI) authentication.
@@ -75,7 +79,12 @@ func newOnBehalfOfCredential(tenantID, clientID, userAssertion string, cred conf
 	if err != nil {
 		return nil, err
 	}
-	return &OnBehalfOfCredential{assertion: userAssertion, client: c}, nil
+	return &OnBehalfOfCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(options.AdditionallyAllowedTenants),
+		assertion:                  userAssertion,
+		client:                     c,
+		tenant:                     tenantID,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -83,7 +92,11 @@ func (o *OnBehalfOfCredential) GetToken(ctx context.Context, opts policy.TokenRe
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameSecret + ": GetToken() requires at least one scope")
 	}
-	ar, err := o.client.AcquireTokenOnBehalfOf(ctx, o.assertion, opts.Scopes)
+	tenant, err := resolveTenant(o.tenant, opts.TenantID, o.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := o.client.AcquireTokenOnBehalfOf(ctx, o.assertion, opts.Scopes, confidential.WithTenantID(tenant))
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameOBO, err)
 	}
diff --git a/sdk/azidentity/username_password_credential.go b/sdk/azidentity/username_password_credential.go
index 256c7f4f79..b4a84199c2 100644
--- a/sdk/azidentity/username_password_credential.go
+++ b/sdk/azidentity/username_password_credential.go
@@ -21,6 +21,10 @@ const credNameUserPassword = "UsernamePasswordCredential"
 type UsernamePasswordCredentialOptions struct {
 	azcore.ClientOptions
 
+	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
+	// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
+	// application is registered.
+	AdditionallyAllowedTenants []string
 	// DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts.
 	DisableInstanceDiscovery bool
 }
@@ -30,10 +34,10 @@ type UsernamePasswordCredentialOptions struct {
 // with any form of multi-factor authentication, and the application must already have user or admin consent.
 // This credential can only authenticate work and school accounts; it can't authenticate Microsoft accounts.
 type UsernamePasswordCredential struct {
-	client   publicClient
-	username string
-	password string
-	account  public.Account
+	account                    public.Account
+	additionallyAllowedTenants []string
+	client                     publicClient
+	password, tenant, username string
 }
 
 // NewUsernamePasswordCredential creates a UsernamePasswordCredential. clientID is the ID of the application the user
@@ -46,7 +50,13 @@ func NewUsernamePasswordCredential(tenantID string, clientID string, username st
 	if err != nil {
 		return nil, err
 	}
-	return &UsernamePasswordCredential{username: username, password: password, client: c}, nil
+	return &UsernamePasswordCredential{
+		additionallyAllowedTenants: resolveAdditionallyAllowedTenants(options.AdditionallyAllowedTenants),
+		client:                     c,
+		password:                   password,
+		tenant:                     tenantID,
+		username:                   username,
+	}, nil
 }
 
 // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -54,12 +64,16 @@ func (c *UsernamePasswordCredential) GetToken(ctx context.Context, opts policy.T
 	if len(opts.Scopes) == 0 {
 		return azcore.AccessToken{}, errors.New(credNameUserPassword + ": GetToken() requires at least one scope")
 	}
-	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account))
+	tenant, err := resolveTenant(c.tenant, opts.TenantID, c.additionallyAllowedTenants)
+	if err != nil {
+		return azcore.AccessToken{}, err
+	}
+	ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account), public.WithTenantID(tenant))
 	if err == nil {
 		logGetTokenSuccess(c, opts)
 		return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
 	}
-	ar, err = c.client.AcquireTokenByUsernamePassword(ctx, opts.Scopes, c.username, c.password)
+	ar, err = c.client.AcquireTokenByUsernamePassword(ctx, opts.Scopes, c.username, c.password, public.WithTenantID(tenant))
 	if err != nil {
 		return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameUserPassword, err)
 	}


Test patch:
diff --git a/sdk/azcore/arm/runtime/policy_bearer_token_test.go b/sdk/azcore/arm/runtime/policy_bearer_token_test.go
index 04de55b595..1ab06ae00d 100644
--- a/sdk/azcore/arm/runtime/policy_bearer_token_test.go
+++ b/sdk/azcore/arm/runtime/policy_bearer_token_test.go
@@ -5,9 +5,9 @@ package runtime
 
 import (
 	"context"
+	"errors"
 	"strings"
 
-	"errors"
 	"net/http"
 	"testing"
 	"time"
@@ -17,7 +17,9 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
 	azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
+	"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -90,10 +92,10 @@ func TestBearerPolicy_SuccessGetToken(t *testing.T) {
 func TestBearerPolicy_CredentialFailGetToken(t *testing.T) {
 	srv, close := mock.NewTLSServer()
 	defer close()
-	expectedErr := errors.New("oops")
+	expectedErr := "oops"
 	failCredential := mockCredential{}
 	failCredential.getTokenImpl = func(ctx context.Context, options azpolicy.TokenRequestOptions) (azcore.AccessToken, error) {
-		return azcore.AccessToken{}, expectedErr
+		return azcore.AccessToken{}, errors.New(expectedErr)
 	}
 	b := NewBearerTokenPolicy(failCredential, nil)
 	pipeline := newTestPipeline(&azpolicy.ClientOptions{
@@ -104,16 +106,11 @@ func TestBearerPolicy_CredentialFailGetToken(t *testing.T) {
 		PerRetryPolicies: []azpolicy.Policy{b},
 	})
 	req, err := runtime.NewRequest(context.Background(), http.MethodGet, srv.URL())
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	resp, err := pipeline.Do(req)
-	if err != expectedErr {
-		t.Fatalf("unexpected error: %v", err)
-	}
-	if resp != nil {
-		t.Fatal("expected nil response")
-	}
+	require.EqualError(t, err, expectedErr)
+	require.Nil(t, resp)
+	require.Implements(t, (*errorinfo.NonRetriable)(nil), err)
 }
 
 func TestBearerTokenPolicy_TokenExpired(t *testing.T) {
@@ -165,41 +162,124 @@ func TestBearerPolicy_GetTokenFailsNoDeadlock(t *testing.T) {
 	}
 }
 
-func TestBearerTokenWithAuxiliaryTenants(t *testing.T) {
-	t.Skip("this feature isn't implemented yet")
+func TestAuxiliaryTenants(t *testing.T) {
 	srv, close := mock.NewTLSServer()
 	defer close()
-	srv.AppendResponse(mock.WithBody([]byte(accessTokenRespSuccess)))
-	srv.AppendResponse(mock.WithBody([]byte(accessTokenRespSuccess)))
-	srv.AppendResponse(mock.WithBody([]byte(accessTokenRespSuccess)))
-	srv.AppendResponse(mock.WithBody([]byte(accessTokenRespSuccess)))
-	srv.AppendResponse()
-	retryOpts := azpolicy.RetryOptions{
-		MaxRetryDelay: 500 * time.Millisecond,
-		RetryDelay:    50 * time.Millisecond,
-	}
+	srv.SetResponse(mock.WithStatusCode(http.StatusOK))
+	primary := "primary"
+	auxTenants := []string{"aux1", "aux2", "aux3"}
+	expectCache := false
 	b := NewBearerTokenPolicy(
-		mockCredential{},
-		&armpolicy.BearerTokenOptions{
-			Scopes: []string{scope},
-			//AuxiliaryTenants: []string{"tenant1", "tenant2", "tenant3"},
+		mockCredential{
+			// getTokenImpl returns a token whose value equals the requested tenant so the test can validate how the policy handles tenants
+			// i.e., primary tenant token goes in Authorization header and aux tenant tokens go in x-ms-authorization-auxiliary
+			getTokenImpl: func(ctx context.Context, options azpolicy.TokenRequestOptions) (azcore.AccessToken, error) {
+				require.False(t, expectCache, "client should have used a cached token instead of requesting another")
+				tenant := primary
+				if options.TenantID != "" {
+					tenant = options.TenantID
+				}
+				return azcore.AccessToken{Token: tenant, ExpiresOn: time.Now().Add(time.Hour).UTC()}, nil
+			},
 		},
+		&armpolicy.BearerTokenOptions{AuxiliaryTenants: auxTenants, Scopes: []string{scope}},
 	)
-	pipeline := newTestPipeline(&azpolicy.ClientOptions{Transport: srv, Retry: retryOpts, PerRetryPolicies: []azpolicy.Policy{b}})
-	req, err := runtime.NewRequest(context.Background(), http.MethodGet, srv.URL())
-	if err != nil {
-		t.Fatalf("Unexpected error: %v", err)
-	}
-	resp, err := pipeline.Do(req)
-	if err != nil {
-		t.Fatalf("Unexpected error: %v", err)
-	}
-	if resp.StatusCode != http.StatusOK {
-		t.Fatalf("unexpected status code: %d", resp.StatusCode)
+	pipeline := newTestPipeline(&azpolicy.ClientOptions{Transport: srv, PerRetryPolicies: []azpolicy.Policy{b}})
+	expected := strings.Split(shared.BearerTokenPrefix+strings.Join(auxTenants, ","+shared.BearerTokenPrefix), ",")
+	for i := 0; i < 3; i++ {
+		if i == 1 {
+			// policy should have a cached token after the first iteration
+			expectCache = true
+		}
+		req, err := runtime.NewRequest(context.Background(), http.MethodGet, srv.URL())
+		require.NoError(t, err)
+		resp, err := pipeline.Do(req)
+		require.NoError(t, err)
+		require.Equal(t, shared.BearerTokenPrefix+primary, resp.Request.Header.Get(shared.HeaderAuthorization), "Authorization header must contain primary tenant token")
+		actual := strings.Split(resp.Request.Header.Get(headerAuxiliaryAuthorization), ", ")
+		// auxiliary tokens may appear in arbitrary order
+		require.ElementsMatch(t, expected, actual)
 	}
-	expectedHeader := strings.Repeat(shared.BearerTokenPrefix+tokenValue+", ", 3)
-	expectedHeader = expectedHeader[:len(expectedHeader)-2]
-	if auxH := resp.Request.Header.Get(shared.HeaderAuxiliaryAuthorization); auxH != expectedHeader {
-		t.Fatalf("unexpected auxiliary authorization header %s", auxH)
+}
+
+func TestBearerTokenPolicyChallengeParsing(t *testing.T) {
+	for _, test := range []struct {
+		challenge, desc, expectedClaims string
+		err                             error
+	}{
+		{
+			desc: "no challenge",
+		},
+		{
+			desc:      "no claims",
+			challenge: `Bearer authorization_uri="https://login.windows.net/", error="invalid_token", error_description="The authentication failed because of missing 'Authorization' header."`,
+			err:       (*azcore.ResponseError)(nil),
+		},
+		{
+			desc:      "parsing error",
+			challenge: `Bearer claims="invalid"`,
+			// the specific error type isn't important but it must be nonretriable
+			err: (errorinfo.NonRetriable)(nil),
+		},
+		// CAE claims challenges. Position of the "claims" parameter within the challenge shouldn't affect parsing.
+		{
+			desc:           "insufficient claims",
+			challenge:      `Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", client_id="00000003-0000-0000-c000-000000000000", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0="`,
+			expectedClaims: `{"access_token": {"foo": "bar"}}`,
+		},
+		{
+			desc:           "insufficient claims",
+			challenge:      `Bearer claims="eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0=", realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", client_id="00000003-0000-0000-c000-000000000000", error="insufficient_claims"`,
+			expectedClaims: `{"access_token": {"foo": "bar"}}`,
+		},
+		{
+			desc:           "sessions revoked",
+			challenge:      `Bearer authorization_uri="https://login.windows.net/", error="invalid_token", error_description="User session has been revoked", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0="`,
+			expectedClaims: `{"access_token":{"nbf":{"essential":true, "value":"1603742800"}}}`,
+		},
+		{
+			desc:           "sessions revoked",
+			challenge:      `Bearer authorization_uri="https://login.windows.net/", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0=", error="invalid_token", error_description="User session has been revoked"`,
+			expectedClaims: `{"access_token":{"nbf":{"essential":true, "value":"1603742800"}}}`,
+		},
+		{
+			desc:           "IP policy",
+			challenge:      `Bearer authorization_uri="https://login.windows.net/", error="invalid_token", error_description="Tenant IP Policy validate failed.", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNjEwNTYzMDA2In0sInhtc19ycF9pcGFkZHIiOnsidmFsdWUiOiIxLjIuMy40In19fQ"`,
+			expectedClaims: `{"access_token":{"nbf":{"essential":true,"value":"1610563006"},"xms_rp_ipaddr":{"value":"1.2.3.4"}}}`,
+		},
+		{
+			desc:           "IP policy",
+			challenge:      `Bearer authorization_uri="https://login.windows.net/", error="invalid_token", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNjEwNTYzMDA2In0sInhtc19ycF9pcGFkZHIiOnsidmFsdWUiOiIxLjIuMy40In19fQ", error_description="Tenant IP Policy validate failed."`,
+			expectedClaims: `{"access_token":{"nbf":{"essential":true,"value":"1610563006"},"xms_rp_ipaddr":{"value":"1.2.3.4"}}}`,
+		},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			srv, close := mock.NewServer()
+			defer close()
+			srv.SetResponse(mock.WithHeader(shared.HeaderWWWAuthenticate, test.challenge), mock.WithStatusCode(http.StatusUnauthorized))
+			calls := 0
+			cred := mockCredential{
+				getTokenImpl: func(ctx context.Context, actual azpolicy.TokenRequestOptions) (azcore.AccessToken, error) {
+					calls += 1
+					if calls == 2 && test.expectedClaims != "" {
+						require.Equal(t, test.expectedClaims, actual.Claims)
+					}
+					return azcore.AccessToken{Token: "...", ExpiresOn: time.Now().Add(time.Hour).UTC()}, nil
+				},
+			}
+			b := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{Scopes: []string{scope}})
+			pipeline := newTestPipeline(&azpolicy.ClientOptions{Transport: srv, PerRetryPolicies: []azpolicy.Policy{b}})
+			req, err := runtime.NewRequest(context.Background(), http.MethodGet, srv.URL())
+			require.NoError(t, err)
+			_, err = pipeline.Do(req)
+			if test.err != nil {
+				require.ErrorAs(t, err, &test.err)
+			} else {
+				require.NoError(t, err)
+			}
+			if test.expectedClaims != "" {
+				require.Equal(t, 2, calls, "policy should have requested a new token upon receiving the challenge")
+			}
+		})
 	}
 }
diff --git a/sdk/azcore/runtime/policy_bearer_token_test.go b/sdk/azcore/runtime/policy_bearer_token_test.go
index 01ec151dd1..b9ba0175ca 100644
--- a/sdk/azcore/runtime/policy_bearer_token_test.go
+++ b/sdk/azcore/runtime/policy_bearer_token_test.go
@@ -149,7 +149,7 @@ func TestBearerTokenPolicy_AuthZHandler(t *testing.T) {
 	challenge := "Scheme parameters..."
 	srv, close := mock.NewTLSServer(mock.WithTransformAllRequestsToTestServerUrl())
 	defer close()
-	srv.AppendResponse(mock.WithStatusCode(401), mock.WithHeader("WWW-Authenticate", challenge))
+	srv.AppendResponse(mock.WithStatusCode(401), mock.WithHeader(shared.HeaderWWWAuthenticate, challenge))
 	srv.AppendResponse(mock.WithStatusCode(200))
 
 	req, err := NewRequest(context.Background(), "GET", "https://localhost")
@@ -167,7 +167,7 @@ func TestBearerTokenPolicy_AuthZHandler(t *testing.T) {
 	handler.OnChallenge = func(r *policy.Request, res *http.Response, f func(policy.TokenRequestOptions) error) error {
 		require.Equal(t, req.Raw().URL, r.Raw().URL)
 		handler.onChallengeCalls++
-		require.Equal(t, challenge, res.Header.Get("WWW-Authenticate"))
+		require.Equal(t, challenge, res.Header.Get(shared.HeaderWWWAuthenticate))
 		return nil
 	}
 
@@ -185,7 +185,7 @@ func TestBearerTokenPolicy_AuthZHandler(t *testing.T) {
 func TestBearerTokenPolicy_AuthZHandlerErrors(t *testing.T) {
 	srv, close := mock.NewTLSServer(mock.WithTransformAllRequestsToTestServerUrl())
 	defer close()
-	srv.SetResponse(mock.WithStatusCode(401), mock.WithHeader("WWW-Authenticate", "..."))
+	srv.SetResponse(mock.WithStatusCode(401), mock.WithHeader(shared.HeaderWWWAuthenticate, "..."))
 
 	req, err := NewRequest(context.Background(), "GET", "https://localhost")
 	require.NoError(t, err)
diff --git a/sdk/azidentity/azidentity_test.go b/sdk/azidentity/azidentity_test.go
index 3d662d0590..e1d9532292 100644
--- a/sdk/azidentity/azidentity_test.go
+++ b/sdk/azidentity/azidentity_test.go
@@ -36,7 +36,17 @@ const (
 
 var (
 	accessTokenRespSuccess    = []byte(fmt.Sprintf(`{"access_token": "%s", "expires_in": %d}`, tokenValue, tokenExpiresIn))
-	instanceDiscoveryResponse = []byte(strings.ReplaceAll(`{
+	instanceDiscoveryResponse = getInstanceDiscoveryResponse(fakeTenantID)
+	tenantDiscoveryResponse   = getTenantDiscoveryResponse(fakeTenantID)
+)
+
+// constants for this file
+const (
+	testHost = "https://localhost"
+)
+
+func getInstanceDiscoveryResponse(tenant string) []byte {
+	return []byte(strings.ReplaceAll(`{
 		"tenant_discovery_endpoint": "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
 		"api-version": "1.1",
 		"metadata": [
@@ -51,8 +61,11 @@ var (
 				]
 			}
 		]
-	}`, "{tenant}", fakeTenantID))
-	tenantDiscoveryResponse = []byte(strings.ReplaceAll(`{
+	}`, "{tenant}", tenant))
+}
+
+func getTenantDiscoveryResponse(tenant string) []byte {
+	return []byte(strings.ReplaceAll(`{
 		"token_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
 		"token_endpoint_auth_methods_supported": [
 			"client_secret_post",
@@ -118,13 +131,8 @@ var (
 		"cloud_graph_host_name": "graph.windows.net",
 		"msgraph_host": "graph.microsoft.com",
 		"rbac_url": "https://pas.windows.net"
-	}`, "{tenant}", fakeTenantID))
-)
-
-// constants for this file
-const (
-	testHost = "https://localhost"
-)
+	}`, "{tenant}", tenant))
+}
 
 func validateX5C(t *testing.T, certs []*x509.Certificate) mock.ResponsePredicate {
 	return func(req *http.Request) bool {
@@ -234,7 +242,7 @@ func Test_GetTokenRequiresScopes(t *testing.T) {
 			return NewClientCertificateCredential("tenantID", "clientID", allCertTests[0].certs, allCertTests[0].key, nil)
 		},
 		func() (azcore.TokenCredential, error) {
-			return NewClientSecretCredential("tenantID", "clientID", "secret", nil)
+			return NewClientSecretCredential("tenantID", "clientID", fakeSecret, nil)
 		},
 		func() (azcore.TokenCredential, error) { return NewDeviceCodeCredential(nil) },
 		func() (azcore.TokenCredential, error) { return NewInteractiveBrowserCredential(nil) },
@@ -266,33 +274,303 @@ func Test_NonHTTPSAuthorityHost(t *testing.T) {
 	}
 }
 
-func Test_ValidTenantIDFalse(t *testing.T) {
-	if validTenantID("bad@tenant") {
-		t.Fatal("Expected to receive false, but received true")
-	}
-	if validTenantID("bad/tenant") {
-		t.Fatal("Expected to receive false, but received true")
-	}
-	if validTenantID("bad(tenant") {
-		t.Fatal("Expected to receive false, but received true")
-	}
-	if validTenantID("bad)tenant") {
-		t.Fatal("Expected to receive false, but received true")
-	}
-	if validTenantID("bad:tenant") {
-		t.Fatal("Expected to receive false, but received true")
+func TestAdditionallyAllowedTenants(t *testing.T) {
+	tenantA := "A"
+	tenantB := "B"
+	for _, test := range []struct {
+		allowed                []string
+		desc, expected, tenant string
+		err                    bool
+	}{
+		{
+			desc:     "all tenants allowed",
+			allowed:  []string{"*"},
+			expected: tenantA,
+			tenant:   tenantA,
+		},
+		{
+			desc:     "tenant explicitly allowed",
+			allowed:  []string{tenantA, tenantB},
+			expected: tenantA,
+			tenant:   tenantA,
+		},
+		{
+			desc:     "tenant explicitly allowed",
+			allowed:  []string{tenantA, tenantB},
+			expected: tenantB,
+			tenant:   tenantB,
+		},
+		{
+			desc:    "tenant not allowed",
+			allowed: []string{tenantA},
+			tenant:  tenantB,
+			err:     true,
+		},
+		{
+			desc:   "no additional tenants allowed",
+			tenant: tenantA,
+			err:    true,
+		},
+	} {
+		tro := policy.TokenRequestOptions{Scopes: []string{liveTestScope}, TenantID: test.tenant}
+		for _, subtest := range []struct {
+			ctor func(azcore.ClientOptions) (azcore.TokenCredential, error)
+			env  map[string]string
+			name string
+		}{
+			{
+				name: credNameAssertion,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := ClientAssertionCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co}
+					return NewClientAssertionCredential(fakeTenantID, fakeClientID, func(context.Context) (string, error) { return "...", nil }, &o)
+				},
+			},
+			{
+				name: credNameAzureCLI,
+				ctor: func(azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := AzureCLICredentialOptions{
+						AdditionallyAllowedTenants: test.allowed,
+						tokenProvider: func(ctx context.Context, resource, tenantID string) ([]byte, error) {
+							if tenantID != test.expected {
+								t.Fatalf(`unexpected tenantID "%s"`, tenantID)
+							}
+							return mockCLITokenProviderSuccess(ctx, resource, tenantID)
+						},
+					}
+					return NewAzureCLICredential(&o)
+				},
+			},
+			{
+				name: credNameCert,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := ClientCertificateCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co}
+					return NewClientCertificateCredential(fakeTenantID, fakeClientID, allCertTests[0].certs, allCertTests[0].key, &o)
+				},
+			},
+			{
+				name: credNameDeviceCode,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := DeviceCodeCredentialOptions{
+						AdditionallyAllowedTenants: test.allowed,
+						ClientOptions:              co,
+						UserPrompt:                 func(context.Context, DeviceCodeMessage) error { return nil },
+					}
+					return NewDeviceCodeCredential(&o)
+				},
+			},
+			{
+				name: credNameOBO,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := OnBehalfOfCredentialOptions{
+						AdditionallyAllowedTenants: test.allowed,
+						ClientOptions:              co,
+					}
+					return NewOnBehalfOfCredentialFromSecret(fakeTenantID, fakeClientID, "assertion", fakeSecret, &o)
+				},
+			},
+			{
+				name: credNameSecret,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := ClientSecretCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co}
+					return NewClientSecretCredential(fakeTenantID, fakeClientID, fakeSecret, &o)
+				},
+			},
+			{
+				name: credNameUserPassword,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := UsernamePasswordCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co}
+					return NewUsernamePasswordCredential(fakeTenantID, fakeClientID, fakeUsername, "password", &o)
+				},
+			},
+			{
+				name: "DefaultAzureCredential/EnvironmentCredential",
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := DefaultAzureCredentialOptions{ClientOptions: co, TenantID: test.tenant}
+					return NewDefaultAzureCredential(&o)
+				},
+				env: map[string]string{
+					azureAdditionallyAllowedTenants: strings.Join(test.allowed, ";"),
+					azureClientID:                   fakeClientID,
+					azureClientSecret:               fakeSecret,
+					azureTenantID:                   fakeTenantID,
+				},
+			},
+			{
+				name: "DefaultAzureCredential/EnvironmentCredential/option-overrides-env",
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					o := DefaultAzureCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co, TenantID: test.tenant}
+					return NewDefaultAzureCredential(&o)
+				},
+				env: map[string]string{
+					azureAdditionallyAllowedTenants: "not-" + test.tenant,
+					azureClientID:                   fakeClientID,
+					azureClientSecret:               fakeSecret,
+					azureTenantID:                   fakeTenantID,
+				},
+			},
+			{
+				name: "EnvironmentCredential/" + credNameCert,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					return NewEnvironmentCredential(&EnvironmentCredentialOptions{ClientOptions: co})
+				},
+				env: map[string]string{
+					azureAdditionallyAllowedTenants: strings.Join(test.allowed, ";"),
+					azureClientCertificatePath:      "testdata/certificate.pem",
+					azureClientID:                   fakeClientID,
+					azureTenantID:                   fakeTenantID,
+				},
+			},
+			{
+				name: "EnvironmentCredential/" + credNameSecret,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					return NewEnvironmentCredential(&EnvironmentCredentialOptions{ClientOptions: co})
+				},
+				env: map[string]string{
+					azureAdditionallyAllowedTenants: strings.Join(test.allowed, ";"),
+					azureClientID:                   fakeClientID,
+					azureClientSecret:               fakeSecret,
+					azureTenantID:                   fakeTenantID,
+				},
+			},
+			{
+				name: "EnvironmentCredential/" + credNameUserPassword,
+				ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) {
+					return NewEnvironmentCredential(&EnvironmentCredentialOptions{ClientOptions: co})
+				},
+				env: map[string]string{
+					azureAdditionallyAllowedTenants: strings.Join(test.allowed, ";"),
+					azureClientID:                   fakeClientID,
+					azurePassword:                   "password",
+					azureTenantID:                   fakeTenantID,
+					azureUsername:                   fakeUsername,
+				},
+			},
+		} {
+			t.Run(fmt.Sprintf("%s/%s", subtest.name, test.desc), func(t *testing.T) {
+				for k, v := range subtest.env {
+					t.Setenv(k, v)
+				}
+				sts := mockSTS{
+					tenant: test.tenant,
+					tokenRequestCallback: func(r *http.Request) {
+						if actual := strings.Split(r.URL.Path, "/")[1]; actual != test.expected {
+							t.Fatalf("expected tenant %q, got %q", test.expected, actual)
+						}
+					},
+				}
+				c, err := subtest.ctor(policy.ClientOptions{Transport: &sts})
+				if err != nil {
+					t.Fatal(err)
+				}
+				tk, err := c.GetToken(context.Background(), tro)
+				if err != nil {
+					if test.err {
+						return
+					}
+					t.Fatal(err)
+				} else if test.err {
+					t.Fatal("expected an error")
+				}
+				// silent authentication should succeed
+				tk2, err := c.GetToken(context.Background(), tro)
+				if err != nil {
+					t.Fatalf(`silent authentication failed: "%v"`, err)
+				}
+				if tk.Token != tk2.Token {
+					t.Fatalf("expected %q, got %q", tk.Token, tk2.Token)
+				}
+				if !tk.ExpiresOn.Equal(tk2.ExpiresOn) {
+					t.Fatalf("expected %v, got %v", tk.ExpiresOn, tk2.ExpiresOn)
+				}
+			})
+		}
+		t.Run(fmt.Sprintf("DefaultAzureCredential/%s/%s", credNameAzureCLI, test.desc), func(t *testing.T) {
+			// mock IMDS failure because managed identity precedes CLI in the chain
+			srv, close := mock.NewTLSServer(mock.WithTransformAllRequestsToTestServerUrl())
+			defer close()
+			srv.SetResponse(mock.WithStatusCode(400))
+			o := DefaultAzureCredentialOptions{
+				AdditionallyAllowedTenants: test.allowed,
+				ClientOptions:              policy.ClientOptions{Transport: srv},
+			}
+			c, err := NewDefaultAzureCredential(&o)
+			if err != nil {
+				t.Fatal(err)
+			}
+			called := false
+			for _, source := range c.chain.sources {
+				if cli, ok := source.(*AzureCLICredential); ok {
+					cli.tokenProvider = func(ctx context.Context, resource, tenantID string) ([]byte, error) {
+						called = true
+						if tenantID != test.expected {
+							t.Fatalf(`unexpected tenantID "%s"`, tenantID)
+						}
+						return mockCLITokenProviderSuccess(ctx, resource, tenantID)
+					}
+					break
+				}
+			}
+			if _, err := c.GetToken(context.Background(), tro); err != nil {
+				if test.err {
+					return
+				}
+				t.Fatal(err)
+			} else if test.err {
+				t.Fatal("expected an error")
+			}
+			if !called {
+				t.Fatal("AzureCLICredential wasn't invoked")
+			}
+		})
 	}
 }
 
-func Test_ValidTenantIDTrue(t *testing.T) {
-	if !validTenantID("goodtenant") {
-		t.Fatal("Expected to receive true, but received false")
-	}
-	if !validTenantID("good-tenant") {
-		t.Fatal("Expected to receive true, but received false")
-	}
-	if !validTenantID("good.tenant") {
-		t.Fatal("Expected to receive true, but received false")
+func TestResolveTenant(t *testing.T) {
+	defaultTenant := "default-tenant"
+	otherTenant := "other-tenant"
+	for _, test := range []struct {
+		allowed          []string
+		expected, tenant string
+		expectError      bool
+	}{
+		// no alternate tenant specified -> should get default
+		{expected: defaultTenant},
+		{allowed: []string{""}, expected: defaultTenant},
+		{allowed: []string{"*"}, expected: defaultTenant},
+		{allowed: []string{otherTenant}, expected: defaultTenant},
+
+		// alternate tenant specified and allowed -> should get that tenant
+		{allowed: []string{"*"}, expected: otherTenant, tenant: otherTenant},
+		{allowed: []string{otherTenant}, expected: otherTenant, tenant: otherTenant},
+		{allowed: []string{"not-" + otherTenant, otherTenant}, expected: otherTenant, tenant: otherTenant},
+		{allowed: []string{"not-" + otherTenant, "*"}, expected: otherTenant, tenant: otherTenant},
+
+		// invalid or not allowed tenant -> should get an error
+		{tenant: otherTenant, expectError: true},
+		{allowed: []string{""}, tenant: otherTenant, expectError: true},
+		{allowed: []string{defaultTenant}, tenant: otherTenant, expectError: true},
+		{tenant: badTenantID, expectError: true},
+		{allowed: []string{""}, tenant: badTenantID, expectError: true},
+		{allowed: []string{"*", badTenantID}, tenant: badTenantID, expectError: true},
+		{tenant: "invalid@tenant", expectError: true},
+		{tenant: "invalid/tenant", expectError: true},
+		{tenant: "invalid(tenant", expectError: true},
+		{tenant: "invalid:tenant", expectError: true},
+	} {
+		t.Run("", func(t *testing.T) {
+			tenant, err := resolveTenant(defaultTenant, test.tenant, test.allowed)
+			if err != nil {
+				if test.expectError {
+					return
+				}
+				t.Fatal(err)
+			} else if test.expectError {
+				t.Fatal("expected an error")
+			}
+			if tenant != test.expected {
+				t.Fatalf(`expected "%s", got "%s"`, test.expected, tenant)
+			}
+		})
 	}
 }
 
diff --git a/sdk/azidentity/client_secret_credential_test.go b/sdk/azidentity/client_secret_credential_test.go
index e17fa13cdc..9e6b230522 100644
--- a/sdk/azidentity/client_secret_credential_test.go
+++ b/sdk/azidentity/client_secret_credential_test.go
@@ -16,10 +16,10 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/recording"
 )
 
-const secret = "secret"
+const fakeSecret = "secret"
 
 func TestClientSecretCredential_InvalidTenantID(t *testing.T) {
-	cred, err := NewClientSecretCredential(badTenantID, fakeClientID, secret, nil)
+	cred, err := NewClientSecretCredential(badTenantID, fakeClientID, fakeSecret, nil)
 	if err == nil {
 		t.Fatal("Expected an error but received none")
 	}
@@ -29,7 +29,7 @@ func TestClientSecretCredential_InvalidTenantID(t *testing.T) {
 }
 
 func TestClientSecretCredential_GetTokenSuccess(t *testing.T) {
-	cred, err := NewClientSecretCredential(fakeTenantID, fakeClientID, secret, nil)
+	cred, err := NewClientSecretCredential(fakeTenantID, fakeClientID, fakeSecret, nil)
 	if err != nil {
 		t.Fatalf("Unable to create credential. Received: %v", err)
 	}
diff --git a/sdk/azidentity/default_azure_credential_test.go b/sdk/azidentity/default_azure_credential_test.go
index 4f1906a9e6..5ffbb1c4d1 100644
--- a/sdk/azidentity/default_azure_credential_test.go
+++ b/sdk/azidentity/default_azure_credential_test.go
@@ -25,7 +25,7 @@ import (
 )
 
 func TestDefaultAzureCredential_GetTokenSuccess(t *testing.T) {
-	env := map[string]string{azureTenantID: fakeTenantID, azureClientID: fakeClientID, azureClientSecret: secret}
+	env := map[string]string{azureTenantID: fakeTenantID, azureClientID: fakeClientID, azureClientSecret: fakeSecret}
 	setEnvironmentVariables(t, env)
 	cred, err := NewDefaultAzureCredential(nil)
 	if err != nil {
diff --git a/sdk/azidentity/environment_credential_test.go b/sdk/azidentity/environment_credential_test.go
index 8dc42faa65..5fc46ae37e 100644
--- a/sdk/azidentity/environment_credential_test.go
+++ b/sdk/azidentity/environment_credential_test.go
@@ -30,7 +30,7 @@ func TestEnvironmentCredential_TenantIDNotSet(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Unexpected error when initializing environment variables: %v", err)
 	}
-	err = os.Setenv(azureClientSecret, secret)
+	err = os.Setenv(azureClientSecret, fakeSecret)
 	if err != nil {
 		t.Fatalf("Unexpected error when initializing environment variables: %v", err)
 	}
@@ -46,7 +46,7 @@ func TestEnvironmentCredential_ClientIDNotSet(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Unexpected error when initializing environment variables: %v", err)
 	}
-	err = os.Setenv(azureClientSecret, secret)
+	err = os.Setenv(azureClientSecret, fakeSecret)
 	if err != nil {
 		t.Fatalf("Unexpected error when initializing environment variables: %v", err)
 	}
@@ -82,7 +82,7 @@ func TestEnvironmentCredential_ClientSecretSet(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Unexpected error when initializing environment variables: %v", err)
 	}
-	err = os.Setenv(azureClientSecret, secret)
+	err = os.Setenv(azureClientSecret, fakeSecret)
 	if err != nil {
 		t.Fatalf("Unexpected error when initializing environment variables: %v", err)
 	}
diff --git a/sdk/azidentity/mock_test.go b/sdk/azidentity/mock_test.go
new file mode 100644
index 0000000000..04478c2626
--- /dev/null
+++ b/sdk/azidentity/mock_test.go
@@ -0,0 +1,58 @@
+//go:build go1.18
+// +build go1.18
+
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package azidentity
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"strings"
+)
+
+// mockSTS returns mock Azure AD responses so tests don't have to account for
+// MSAL metadata requests. All responses are success responses. Mock access
+// tokens expire in 1 hour and have the value of the "tokenValue" constant.
+type mockSTS struct {
+	// tenant to include in metadata responses. This value must match a test's
+	// expected tenant because metadata tells MSAL where to send token requests.
+	// Defaults to the "fakeTenantID" constant.
+	tenant string
+	// tokenRequestCallback is called for every token request
+	tokenRequestCallback func(*http.Request)
+}
+
+func (m *mockSTS) Do(req *http.Request) (*http.Response, error) {
+	res := http.Response{StatusCode: http.StatusOK}
+	tenant := m.tenant
+	if tenant == "" {
+		tenant = fakeTenantID
+	}
+	switch s := strings.Split(req.URL.Path, "/"); s[len(s)-1] {
+	case "instance":
+		res.Body = io.NopCloser(bytes.NewReader(getInstanceDiscoveryResponse(tenant)))
+	case "openid-configuration":
+		res.Body = io.NopCloser(bytes.NewReader(getTenantDiscoveryResponse(tenant)))
+	case "devicecode":
+		res.Body = io.NopCloser(strings.NewReader(`{"device_code":"...","expires_in":600,"interval":60}`))
+	case "token":
+		if m.tokenRequestCallback != nil {
+			m.tokenRequestCallback(req)
+		}
+		res.Body = io.NopCloser(bytes.NewReader(accessTokenRespSuccess))
+	default:
+		// User realm metadata request paths look like "/common/UserRealm/user@domain".
+		// Matching on the UserRealm segment avoids having to know the UPN.
+		if s[len(s)-2] == "UserRealm" {
+			res.Body = io.NopCloser(
+				strings.NewReader(`{"account_type":"Managed","cloud_audience_urn":"urn","cloud_instance_name":"...","domain_name":"..."}`),
+			)
+		} else {
+			panic("unexpected request " + req.URL.String())
+		}
+	}
+	return &res, nil
+}