Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,61 +222,69 @@ public override async Task<SqlAuthenticationToken> AcquireTokenAsync(SqlAuthenti
string[] scopes = [scope];
TokenRequestContext tokenRequestContext = new(scopes);

// We split audience from Authority URL here. Audience can be one of
// We split the tenant from the Authority URL here. The tenant can be one of
// the following:
//
// - The Entra ID authority audience enumeration
// - The tenant ID, which can be:
// - A GUID (the ID of your Entra ID instance), for
// single-tenant applications
// - A domain name associated with your Entra ID instance (also
// for single-tenant applications)
// - One of these placeholders as a tenant ID in place of the
// Entra ID authority audience enumeration:
// - One of these placeholders, which select an Entra ID authority
// audience instead of a specific tenant:
// - `organizations` for a multitenant application
// - `consumers` to sign in users only with their personal
// accounts
// - `common` to sign in users with their work and school
// accounts or their personal Microsoft accounts
//
// MSAL will throw a meaningful exception if you specify both the
// Entra ID authority audience and the tenant ID.
//
// If you don't specify an audience, your app will target Entra ID
// and personal Microsoft accounts as an audience. (That is, it
// will behave as though `common` were specified.)
// If no tenant is specified, the app targets Entra ID and personal
// Microsoft accounts as an audience. (That is, it behaves as though
// `common` were specified.) We always have a tenant here, because the
// server supplies one in the STSURL.
//
// More information:
//
// https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration
//
// The authority URL provided by the server may be a bare tenant endpoint
// ("https://login.microsoftonline.com/{tenantId}") or an ADAL v1 style endpoint
// ("https://login.microsoftonline.com/{tenantId}/oauth2/authorize"), so the tenant is
// taken from the first path segment rather than the last.

if (!TryParseAuthority(parameters.Authority, out string authorityHost, out string tenant, out string msalAuthority))
{
throw new Extensions.Azure.AuthenticationException(
Comment thread
cheenamalhotra marked this conversation as resolved.
parameters.AuthenticationMethod,
$"The authority '{parameters.Authority}' is not a valid Entra ID authority. " +
"Expected an absolute HTTPS URL containing a tenant, " +
"e.g. 'https://login.microsoftonline.com/<tenant>'.");
}

int separatorIndex = parameters.Authority.LastIndexOf('/');
string authority = parameters.Authority.Remove(separatorIndex + 1);
string audience = parameters.Authority.Substring(separatorIndex + 1);
string? clientId = string.IsNullOrWhiteSpace(parameters.UserId) ? null : parameters.UserId;

if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDefault)
{
// Cache DefaultAzureCredenial based on scope, authority, audience, and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authority, scope, audience, clientId);
// Cache DefaultAzureCredenial based on scope, authority host, tenant, and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authorityHost, scope, tenant, clientId);
AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false);
SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Default auth mode. Expiry Time: {0}", accessToken.ExpiresOn);
return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn);
}

if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryMSI)
{
// Cache ManagedIdentityCredential based on scope, authority, and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authority, scope, string.Empty, clientId);
// Cache ManagedIdentityCredential based on scope, authority host, and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authorityHost, scope, string.Empty, clientId);
AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false);
SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Managed Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn);
return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn);
}

if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)
{
// Cache ClientSecretCredential based on scope, authority, audience, and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authority, scope, audience, clientId);
// Cache ClientSecretCredential based on scope, authority host, tenant, and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authorityHost, scope, tenant, clientId);
string password = parameters.Password is null ? string.Empty : parameters.Password;
AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, password, tokenRequestContext, cts.Token).ConfigureAwait(false);
SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", accessToken.ExpiresOn);
Expand All @@ -285,8 +293,8 @@ public override async Task<SqlAuthenticationToken> AcquireTokenAsync(SqlAuthenti

if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)
{
// Cache WorkloadIdentityCredential based on authority and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authority, string.Empty, string.Empty, clientId);
// Cache WorkloadIdentityCredential based on authority host and clientId
TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authorityHost, string.Empty, string.Empty, clientId);
// If either tenant id, client id, or the token file path are not specified when fetching the token,
// a CredentialUnavailableException will be thrown instead
AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false);
Expand Down Expand Up @@ -316,9 +324,9 @@ public override async Task<SqlAuthenticationToken> AcquireTokenAsync(SqlAuthenti

PublicClientAppKey pcaKey =
#if NETFRAMEWORK
new(parameters.Authority, redirectUri, _applicationClientId, _iWin32WindowFunc);
new(msalAuthority, redirectUri, _applicationClientId, _iWin32WindowFunc);
#else
new(parameters.Authority, redirectUri, _applicationClientId);
new(msalAuthority, redirectUri, _applicationClientId);
#endif

AuthenticationResult? result = null;
Expand Down Expand Up @@ -435,6 +443,11 @@ previousPw is byte[] previousPwBytes &&

return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn);
}
catch (Extensions.Azure.AuthenticationException)
{
// Already shaped for the caller; don't re-wrap it below.
throw;
}
catch (MsalException ex)
{
// Check for an explicitly retryable error.
Expand Down Expand Up @@ -533,6 +546,68 @@ or AuthenticationRequiredException
}
}

/// <summary>
/// Splits an Entra ID authority URL (the STSURL provided by the server in the FEDAUTHINFO TDS
/// token) into the authority host and the tenant.
/// </summary>
/// <param name="authorityUrl">
/// The authority URL, e.g. <c>https://login.microsoftonline.com/{tenantId}</c>. Some services
/// (for example the Dataverse/Dynamics 365 TDS endpoint) return an ADAL v1 style URL such as
/// <c>https://login.microsoftonline.com/{tenantId}/oauth2/authorize</c>.
/// </param>
/// <param name="authorityHost">
/// Receives the authority host with a trailing slash, e.g. <c>https://login.microsoftonline.com/</c>.
/// </param>
/// <param name="tenant">
/// Receives the tenant (the first path segment of the authority URL), which may be a tenant id,
/// a domain name, or one of the <c>common</c>/<c>organizations</c>/<c>consumers</c> placeholders.
/// </param>
/// <param name="msalAuthority">
/// Receives the normalized authority (host + tenant) suitable for MSAL's <c>WithAuthority</c>.
/// </param>
/// <returns>
/// <c>true</c> if the authority URL is a well-formed, absolute HTTPS URL carrying a tenant
/// segment; otherwise <c>false</c>.
/// </returns>
/// <remarks>
/// <para>
/// The tenant is taken from the first path segment rather than the last so that trailing
/// endpoint suffixes (<c>/oauth2/authorize</c>, <c>/oauth2/v2.0/token</c>, etc.) are ignored.
/// </para>
/// <para>
/// Entra ID authorities are always absolute HTTPS URLs, so anything else is rejected rather
/// than guessed at. Both MSAL (<c>WithAuthority</c>) and Azure.Identity
/// (<c>TokenCredentialOptions.AuthorityHost</c>) require an absolute URI as well, so an
/// unparseable authority cannot produce a working credential.
/// </para>
/// </remarks>
internal static bool TryParseAuthority(
string authorityUrl,
out string authorityHost,
out string tenant,
out string msalAuthority)
{
if (Uri.TryCreate(authorityUrl, UriKind.Absolute, out Uri? uri) &&
uri.Scheme == Uri.UriSchemeHttps)
{
string path = uri.AbsolutePath.Trim('/');
int slashIndex = path.IndexOf('/');
tenant = slashIndex < 0 ? path : path.Substring(0, slashIndex);

if (tenant.Length > 0)
{
authorityHost = uri.GetLeftPart(UriPartial.Authority) + "/";
msalAuthority = authorityHost + tenant;
return true;
}
}

authorityHost = string.Empty;
tenant = string.Empty;
msalAuthority = string.Empty;
return false;
}

private static async Task<AuthenticationResult?> TryAcquireTokenSilent(IPublicClientApplication app, SqlAuthenticationParameters parameters,
string[] scopes, CancellationTokenSource cts)
{
Expand Down Expand Up @@ -864,8 +939,8 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential
{
DefaultAzureCredentialOptions defaultAzureCredentialOptions = new()
{
AuthorityHost = new Uri(tokenCredentialKey._authority),
TenantId = tokenCredentialKey._audience,
AuthorityHost = new Uri(tokenCredentialKey._authorityHost),
TenantId = tokenCredentialKey._tenant,
ExcludeInteractiveBrowserCredential = true // Force disabled, even though it's disabled by default to respect driver specifications.
};

Expand Down Expand Up @@ -909,23 +984,23 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential
: ManagedIdentityId.FromUserAssignedClientId(tokenCredentialKey._clientId);
ManagedIdentityCredentialOptions managedIdentityCredentialOptions = new(managedIdentityId)
{
AuthorityHost = new Uri(tokenCredentialKey._authority)
AuthorityHost = new Uri(tokenCredentialKey._authorityHost)
};

return new TokenCredentialData(new ManagedIdentityCredential(managedIdentityCredentialOptions), GetHash(secret));
}
else if (tokenCredentialKey._tokenCredentialType == typeof(ClientSecretCredential))
{
TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) };
TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authorityHost) };

return new TokenCredentialData(new ClientSecretCredential(tokenCredentialKey._audience, tokenCredentialKey._clientId, secret, tokenCredentialOptions), GetHash(secret));
return new TokenCredentialData(new ClientSecretCredential(tokenCredentialKey._tenant, tokenCredentialKey._clientId, secret, tokenCredentialOptions), GetHash(secret));
}
else if (tokenCredentialKey._tokenCredentialType == typeof(WorkloadIdentityCredential))
{
// The WorkloadIdentityCredentialOptions object initialization populates its instance members
// from the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE,
// and AZURE_ADDITIONALLY_ALLOWED_TENANTS. AZURE_CLIENT_ID may be overridden by the User Id.
WorkloadIdentityCredentialOptions options = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) };
WorkloadIdentityCredentialOptions options = new() { AuthorityHost = new Uri(tokenCredentialKey._authorityHost) };

if (tokenCredentialKey._clientId is not null)
{
Expand Down Expand Up @@ -1005,17 +1080,27 @@ public TokenCredentialData(TokenCredential tokenCredential, byte[] secretHash)
internal class TokenCredentialKey
{
public readonly Type _tokenCredentialType;
public readonly string _authority;

/// <summary>The authority host with a trailing slash, e.g. "https://login.microsoftonline.com/".</summary>
public readonly string _authorityHost;

public readonly string _scope;
public readonly string _audience;

/// <summary>
/// The tenant, which may be a tenant id, a domain name, or one of the
/// `common` / `organizations` / `consumers` placeholders. Empty when the credential
/// type doesn't take a tenant.
/// </summary>
public readonly string _tenant;

public readonly string? _clientId;

public TokenCredentialKey(Type tokenCredentialType, string authority, string scope, string audience, string? clientId)
public TokenCredentialKey(Type tokenCredentialType, string authorityHost, string scope, string tenant, string? clientId)
{
_tokenCredentialType = tokenCredentialType;
_authority = authority;
_authorityHost = authorityHost;
_scope = scope;
_audience = audience;
_tenant = tenant;
_clientId = clientId;
}

Expand All @@ -1024,15 +1109,15 @@ public override bool Equals(object? obj)
if (obj != null && obj is TokenCredentialKey tcKey)
{
return _tokenCredentialType == tcKey._tokenCredentialType
&& string.CompareOrdinal(_authority, tcKey._authority) == 0
&& string.CompareOrdinal(_authorityHost, tcKey._authorityHost) == 0
&& string.CompareOrdinal(_scope, tcKey._scope) == 0
&& string.CompareOrdinal(_audience, tcKey._audience) == 0
&& string.CompareOrdinal(_tenant, tcKey._tenant) == 0
&& string.CompareOrdinal(_clientId, tcKey._clientId) == 0
;
}
return false;
}

public override int GetHashCode() => Tuple.Create(_tokenCredentialType, _authority, _scope, _audience, _clientId).GetHashCode();
public override int GetHashCode() => Tuple.Create(_tokenCredentialType, _authorityHost, _scope, _tenant, _clientId).GetHashCode();
}
}
Loading
Loading