Merge pull request #30342 from AfsanehR/AccessTokenAAD

Azure Active Directory Authentication using Access Token
This commit is contained in:
Afsaneh Rafighi 2018-06-26 15:58:41 -07:00 коммит произвёл GitHub
Родитель 3035218ef5 da34501f05
Коммит a74bf42009
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 498 добавлений и 28 удалений

Просмотреть файл

@ -495,6 +495,7 @@ namespace System.Data.SqlClient
public System.Collections.IDictionary RetrieveStatistics() { throw null; }
public static void ChangePassword(string connectionString, string newPassword) { throw null; }
public static void ChangePassword(string connectionString, System.Data.SqlClient.SqlCredential credential, System.Security.SecureString newPassword) { throw null; }
public string AccessToken { get { throw null; } set { } }
}
public sealed partial class SqlConnectionStringBuilder : System.Data.Common.DbConnectionStringBuilder

Просмотреть файл

@ -1296,4 +1296,25 @@
<data name="SQL_ChangePasswordUseOfUnallowedKey" xml:space="preserve">
<value>The keyword '{0}' must not be specified in the connectionString argument to ChangePassword.</value>
</data>
<data name="SQL_ParsingErrorWithState" xml:space="preserve">
<value>Internal connection fatal error. Error state: {0}.</value>
</data>
<data name="SQL_ParsingErrorValue" xml:space="preserve">
<value>Internal connection fatal error. Error state: {0}, Value: {1}.</value>
</data>
<data name="ADP_InvalidMixedUsageOfAccessTokenAndIntegratedSecurity" xml:space="preserve">
<value>Cannot set the AccessToken property if the 'Integrated Security' connection string keyword has been set to 'true' or 'SSPI'.</value>
</data>
<data name="ADP_InvalidMixedUsageOfAccessTokenAndUserIDPassword" xml:space="preserve">
<value>Cannot set the AccessToken property if 'UserID', 'UID', 'Password', or 'PWD' has been specified in connection string.</value>
</data>
<data name="ADP_InvalidMixedUsageOfCredentialAndAccessToken" xml:space="preserve">
<value>Cannot set the Credential property if the AccessToken property is already set.</value>
</data>
<data name="SQL_ParsingErrorFeatureId" xml:space="preserve">
<value>Internal connection fatal error. Error state: {0}, Feature Id: {1}.</value>
</data>
<data name="SQL_ParsingErrorAuthLibraryType" xml:space="preserve">
<value>Internal connection fatal error. Error state: {0}, Authentication Library Type: {1}.</value>
</data>
</root>

Просмотреть файл

@ -910,5 +910,17 @@ namespace System.Data.Common
{
return Argument(SR.GetString(SR.ADP_InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity));
}
internal static InvalidOperationException InvalidMixedUsageOfAccessTokenAndIntegratedSecurity()
{
return ADP.InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfAccessTokenAndIntegratedSecurity));
}
static internal InvalidOperationException InvalidMixedUsageOfAccessTokenAndUserIDPassword()
{
return ADP.InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfAccessTokenAndUserIDPassword));
}
static internal Exception InvalidMixedUsageOfCredentialAndAccessToken()
{
return ADP.InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfCredentialAndAccessToken));
}
}
}

Просмотреть файл

@ -34,6 +34,7 @@ namespace System.Data.SqlClient
private SqlCredential _credential;
private string _connectionString;
private int _connectRetryCount;
private string _accessToken; // Access Token to be used for token based authententication
// connection resiliency
private object _reconnectLock = new object();
@ -97,6 +98,7 @@ namespace System.Data.SqlClient
_credential = new SqlCredential(connection._credential.UserId, password);
}
_accessToken = connection._accessToken;
CacheConnectionStringProperties();
}
@ -222,12 +224,19 @@ namespace System.Data.SqlClient
}
set
{
if (_credential != null)
if (_credential != null || _accessToken != null)
{
SqlConnectionString connectionOptions = new SqlConnectionString(value);
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions);
if (_credential != null)
{
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions);
}
else
{
CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken(connectionOptions);
}
}
ConnectionString_Set(new SqlConnectionPoolKey(value, _credential));
ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken));
_connectionString = value; // Change _connectionString value only after value is validated
CacheConnectionStringProperties();
}
@ -242,6 +251,37 @@ namespace System.Data.SqlClient
}
}
// AccessToken: To be used for token based authentication
public string AccessToken
{
get
{
string result = _accessToken;
// When a connection is connecting or is ever opened, make AccessToken available only if "Persist Security Info" is set to true
// otherwise, return null
SqlConnectionString connectionOptions = (SqlConnectionString)UserConnectionOptions;
return InnerConnection.ShouldHidePassword && connectionOptions != null && !connectionOptions.PersistSecurityInfo ? null : _accessToken;
}
set
{
// If a connection is connecting or is ever opened, AccessToken cannot be set
if (!InnerConnection.AllowSetConnectionString)
{
throw ADP.OpenConnectionPropertySet("AccessToken", InnerConnection.State);
}
if (value != null)
{
// Check if the usage of AccessToken has any conflict with the keys used in connection string and credential
CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken((SqlConnectionString)ConnectionOptions);
}
// Need to call ConnectionString_Set to do proper pool group check
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value));
_accessToken = value;
}
}
public override string Database
{
// if the connection is open, we need to ask the inner connection what it's
@ -396,12 +436,16 @@ namespace System.Data.SqlClient
if (value != null)
{
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions);
if (_accessToken != null)
{
throw ADP.InvalidMixedUsageOfCredentialAndAccessToken();
}
}
_credential = value;
// Need to call ConnectionString_Set to do proper pool group check
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential));
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken));
}
}
@ -422,6 +466,29 @@ namespace System.Data.SqlClient
}
}
// CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken: check if the usage of AccessToken has any conflict
// with the keys used in connection string and credential
// If there is any conflict, it throws InvalidOperationException
// This is to be used setter of ConnectionString and AccessToken properties
private void CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken(SqlConnectionString connectionOptions)
{
if (UsesClearUserIdOrPassword(connectionOptions))
{
throw ADP.InvalidMixedUsageOfAccessTokenAndUserIDPassword();
}
if (UsesIntegratedSecurity(connectionOptions))
{
throw ADP.InvalidMixedUsageOfAccessTokenAndIntegratedSecurity();
}
// Check if the usage of AccessToken has the conflict with credential
if (_credential != null)
{
throw ADP.InvalidMixedUsageOfCredentialAndAccessToken();
}
}
protected override DbProviderFactory DbProviderFactory
{
get => SqlClientFactory.Instance;
@ -654,6 +721,7 @@ namespace System.Data.SqlClient
private void DisposeMe(bool disposing)
{
_credential = null;
_accessToken = null;
if (!disposing)
{
@ -1360,7 +1428,7 @@ namespace System.Data.SqlClient
throw ADP.InvalidArgumentLength(nameof(newPassword), TdsEnums.MAXLEN_NEWPASSWORD);
}
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null);
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null);
SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key);
if (connectionOptions.IntegratedSecurity)
@ -1403,7 +1471,7 @@ namespace System.Data.SqlClient
throw ADP.InvalidArgumentLength(nameof(newSecurePassword), TdsEnums.MAXLEN_NEWPASSWORD);
}
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential);
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null);
SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key);
@ -1441,7 +1509,7 @@ namespace System.Data.SqlClient
if (con != null)
con.Dispose();
}
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential);
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null);
SqlConnectionFactory.SingletonInstance.ClearPool(key);
}

Просмотреть файл

@ -132,7 +132,7 @@ namespace System.Data.SqlClient
opt = new SqlConnectionString(opt, instanceName, userInstance: false, setEnlistValue: null);
poolGroupProviderInfo = null; // null so we do not pass to constructor below...
}
result = new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling);
result = new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling, key.AccessToken);
return result;
}

Просмотреть файл

@ -7,6 +7,7 @@
//------------------------------------------------------------------------------
using System.Data.Common;
using System.Diagnostics;
namespace System.Data.SqlClient
{
@ -16,16 +17,20 @@ namespace System.Data.SqlClient
{
private int _hashValue;
private SqlCredential _credential;
private readonly string _accessToken;
internal SqlConnectionPoolKey(string connectionString, SqlCredential credential) : base(connectionString)
internal SqlConnectionPoolKey(string connectionString, SqlCredential credential, string accessToken) : base(connectionString)
{
Debug.Assert(_credential == null || _accessToken == null, "Credential and AccessToken can't have the value at the same time.");
_credential = credential;
_accessToken = accessToken;
CalculateHashCode();
}
private SqlConnectionPoolKey(SqlConnectionPoolKey key) : base(key)
{
_credential = key.Credential;
_accessToken = key.AccessToken;
CalculateHashCode();
}
@ -50,12 +55,18 @@ namespace System.Data.SqlClient
internal SqlCredential Credential => _credential;
internal string AccessToken
{
get
{
return _accessToken;
}
}
public override bool Equals(object obj)
{
SqlConnectionPoolKey key = obj as SqlConnectionPoolKey;
return (key != null &&
ConnectionString == key.ConnectionString &&
Credential == key.Credential);
return (key != null && _credential == key._credential && ConnectionString == key.ConnectionString && Object.ReferenceEquals(_accessToken, key._accessToken));
}
public override int GetHashCode()
@ -74,6 +85,13 @@ namespace System.Data.SqlClient
_hashValue = _hashValue * 17 + _credential.GetHashCode();
}
}
else if (_accessToken != null)
{
unchecked
{
_hashValue = _hashValue * 17 + _accessToken.GetHashCode();
}
}
}
}
}

Просмотреть файл

@ -106,6 +106,7 @@ namespace System.Data.SqlClient
private TdsParser _parser;
private SqlLoginAck _loginAck;
private SqlCredential _credential;
private FederatedAuthenticationFeatureExtensionData? _fedAuthFeatureExtensionData;
// Connection Resiliency
private bool _sessionRecoveryRequested;
@ -113,6 +114,13 @@ namespace System.Data.SqlClient
internal SessionData _currentSessionData; // internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language
private SessionData _recoverySessionData;
// Federated Authentication
// Response obtained from the server for FEDAUTHREQUIRED prelogin option.
internal bool _fedAuthRequired;
internal bool _federatedAuthenticationRequested;
internal bool _federatedAuthenticationAcknowledged;
internal byte[] _accessTokenInBytes;
// The errors in the transient error set are contained in
// https://azure.microsoft.com/en-us/documentation/articles/sql-database-develop-error-messages/#transient-faults-connection-loss-and-other-temporary-errors
private static readonly HashSet<int> s_transientErrors = new HashSet<int>
@ -295,7 +303,7 @@ namespace System.Data.SqlClient
private RoutingInfo _routingInfo = null;
private Guid _originalClientConnectionId = Guid.Empty;
private string _routingDestination = null;
private readonly TimeoutTimer _timeout;
// although the new password is generally not used it must be passed to the ctor
// the new Login7 packet will always write out the new password (or a length of zero and no bytes if not present)
@ -310,7 +318,9 @@ namespace System.Data.SqlClient
bool redirectedUserInstance,
SqlConnectionString userConnectionOptions = null, // NOTE: userConnectionOptions may be different to connectionOptions if the connection string has been expanded (see SqlConnectionString.Expand)
SessionData reconnectSessionData = null,
bool applyTransientFaultHandling = false) : base(connectionOptions)
bool applyTransientFaultHandling = false,
string accessToken = null) : base(connectionOptions)
{
#if DEBUG
if (reconnectSessionData != null)
@ -335,6 +345,10 @@ namespace System.Data.SqlClient
}
}
if (accessToken != null)
{
_accessTokenInBytes = System.Text.Encoding.Unicode.GetBytes(accessToken);
}
_identity = identity;
Debug.Assert(newSecurePassword != null || newPassword != null, "cannot have both new secure change password and string based change password to be null");
@ -355,9 +369,10 @@ namespace System.Data.SqlClient
_parserLock.Wait(canReleaseFromAnyThread: false);
ThreadHasParserLockForClose = true; // In case of error, let ourselves know that we already own the parser lock
try
{
var timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout);
_timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout);
// If transient fault handling is enabled then we can retry the login up to the ConnectRetryCount.
int connectionEstablishCount = applyTransientFaultHandling ? connectionOptions.ConnectRetryCount + 1 : 1;
@ -366,7 +381,7 @@ namespace System.Data.SqlClient
{
try
{
OpenLoginEnlist(timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance);
OpenLoginEnlist(_timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance);
break;
}
@ -374,8 +389,8 @@ namespace System.Data.SqlClient
{
if (i + 1 == connectionEstablishCount
|| !applyTransientFaultHandling
|| timeout.IsExpired
|| timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds
|| _timeout.IsExpired
|| _timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds
|| !IsTransientError(sqlex))
{
throw sqlex;
@ -1017,6 +1032,10 @@ namespace System.Data.SqlClient
if (_routingInfo == null)
{ // ROR should not affect state of connection recovery
if (_federatedAuthenticationRequested && !_federatedAuthenticationAcknowledged)
{
throw SQL.ParsingError(ParsingErrorState.FedAuthNotAcknowledged);
}
if (!_sessionRecoveryAcknowledged)
{
_currentSessionData = null;
@ -1125,9 +1144,22 @@ namespace System.Data.SqlClient
_sessionRecoveryRequested = true;
}
if (_accessTokenInBytes != null)
{
requestedFeatures |= TdsEnums.FeatureExtension.FedAuth;
_fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData
{
libraryType = TdsEnums.FedAuthLibrary.SecurityToken,
fedAuthRequiredPreLoginResponse = _fedAuthRequired,
accessToken = _accessTokenInBytes
};
// No need any further info from the server for token based authentication. So set _federatedAuthenticationRequested to true
_federatedAuthenticationRequested = true;
}
// The GLOBALTRANSACTIONS feature is implicitly requested
requestedFeatures |= TdsEnums.FeatureExtension.GlobalTransactions;
_parser.TdsLogin(login, requestedFeatures, _recoverySessionData);
_parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData);
}
private void LoginFailure()
@ -1900,6 +1932,32 @@ namespace System.Data.SqlClient
}
break;
}
case TdsEnums.FEATUREEXT_FEDAUTH:
{
if (!_federatedAuthenticationRequested)
{
throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnrequestedFeatureAckReceived, featureId);
}
Debug.Assert(_fedAuthFeatureExtensionData != null, "_fedAuthFeatureExtensionData must not be null when _federatedAuthenticatonRequested == true");
switch (_fedAuthFeatureExtensionData.Value.libraryType)
{
case TdsEnums.FedAuthLibrary.SecurityToken:
// The server shouldn't have sent any additional data with the ack (like a nonce)
if (data.Length != 0)
{
throw SQL.ParsingError(ParsingErrorState.FedAuthFeatureAckContainsExtraData);
}
break;
default:
Debug.Assert(false, "Unknown _fedAuthLibrary type");
throw SQL.ParsingErrorLibraryType(ParsingErrorState.FedAuthFeatureAckUnknownLibraryType, (int)_fedAuthFeatureExtensionData.Value.libraryType);
}
_federatedAuthenticationAcknowledged = true;
break;
}
default:
{

Просмотреть файл

@ -188,6 +188,10 @@ namespace System.Data.SqlClient
{
return ADP.Argument(SR.GetString(SR.SQL_UserInstanceFailoverNotCompatible));
}
internal static Exception ParsingErrorLibraryType(ParsingErrorState state, int libraryType)
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorAuthLibraryType, ((int)state).ToString(CultureInfo.InvariantCulture), libraryType));
}
internal static Exception InvalidSQLServerVersionUnknown()
{
return ADP.DataAdapter(SR.GetString(SR.SQL_InvalidSQLServerVersionUnknown));
@ -407,6 +411,18 @@ namespace System.Data.SqlClient
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingError));
}
static internal Exception ParsingError(ParsingErrorState state)
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorWithState, ((int)state).ToString(CultureInfo.InvariantCulture)));
}
static internal Exception ParsingErrorValue(ParsingErrorState state, int value)
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorValue, ((int)state).ToString(CultureInfo.InvariantCulture), value));
}
static internal Exception ParsingErrorFeatureId(ParsingErrorState state, int featureId)
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorFeatureId, ((int)state).ToString(CultureInfo.InvariantCulture), featureId));
}
internal static Exception MoneyOverflow(string moneyValue)
{
return ADP.Overflow(SR.GetString(SR.SQL_MoneyOverflow, moneyValue));

Просмотреть файл

@ -202,15 +202,29 @@ namespace System.Data.SqlClient
public const byte FEATUREEXT_TERMINATOR = 0xFF;
public const byte FEATUREEXT_SRECOVERY = 0x01;
public const byte FEATUREEXT_GLOBALTRANSACTIONS = 0x05;
public const byte FEATUREEXT_FEDAUTH = 0x02;
[Flags]
public enum FeatureExtension : uint
{
None = 0,
SessionRecovery = 1,
FedAuth = 2,
GlobalTransactions = 8,
}
public const byte FEDAUTHLIB_LIVEID = 0X00;
public const byte FEDAUTHLIB_SECURITYTOKEN = 0x01;
public const byte FEDAUTHLIB_ADAL = 0x02;
public const byte FEDAUTHLIB_RESERVED = 0X7F;
public enum FedAuthLibrary : byte
{
LiveId = FEDAUTHLIB_LIVEID,
SecurityToken = FEDAUTHLIB_SECURITYTOKEN,
ADAL = FEDAUTHLIB_ADAL, // For later support
Default = FEDAUTHLIB_RESERVED
}
// Loginrec defines
public const byte MAX_LOG_NAME = 30; // TDS 4.2 login rec max name length
@ -927,5 +941,30 @@ namespace System.Data.SqlClient
Snix_Close,
Snix_SendRows,
}
internal enum ParsingErrorState
{
Undefined = 0,
FedAuthInfoLengthTooShortForCountOfInfoIds = 1,
FedAuthInfoLengthTooShortForData = 2,
FedAuthInfoFailedToReadCountOfInfoIds = 3,
FedAuthInfoFailedToReadTokenStream = 4,
FedAuthInfoInvalidOffset = 5,
FedAuthInfoFailedToReadData = 6,
FedAuthInfoDataNotUnicode = 7,
FedAuthInfoDoesNotContainStsurlAndSpn = 8,
FedAuthInfoNotReceived = 9,
FedAuthNotAcknowledged = 10,
FedAuthFeatureAckContainsExtraData = 11,
FedAuthFeatureAckUnknownLibraryType = 12,
UnrequestedFeatureAckReceived = 13,
UnknownFeatureAck = 14,
InvalidTdsTokenReceived = 15,
SessionStateLengthTooShort = 16,
SessionStateInvalidStatus = 17,
CorruptedTdsStream = 18,
ProcessSniPacketFailed = 19,
FedAuthRequiredPreLoginResponseInvalidValue = 20,
}
}

Просмотреть файл

@ -366,7 +366,7 @@ namespace System.Data.SqlClient
_physicalStateObj.SniContext = SniContext.Snix_PreLogin;
PreLoginHandshakeStatus status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable);
PreLoginHandshakeStatus status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable, out _connHandler._fedAuthRequired);
if (status == PreLoginHandshakeStatus.InstanceFailure)
{
@ -388,7 +388,7 @@ namespace System.Data.SqlClient
Debug.Assert(retCode == TdsEnums.SNI_SUCCESS, "Unexpected failure state upon calling SniGetConnectionId");
SendPreLoginHandshake(instanceName, encrypt);
status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable);
status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable, out _connHandler._fedAuthRequired);
// Don't need to check for Sphinx failure, since we've already consumed
// one pre-login packet and know we are connecting to Shiloh.
@ -632,6 +632,12 @@ namespace System.Data.SqlClient
optionDataSize += actIdSize;
break;
case (int)PreLoginOptions.FEDAUTHREQUIRED:
payload[payloadLength++] = 0x01;
offset += 1;
optionDataSize += 1;
break;
default:
Debug.Assert(false, "UNKNOWN option in SendPreLoginHandshake");
break;
@ -652,9 +658,10 @@ namespace System.Data.SqlClient
_physicalStateObj.WritePacket(TdsEnums.HARDFLUSH);
}
private PreLoginHandshakeStatus ConsumePreLoginHandshake(bool encrypt, bool trustServerCert, bool integratedSecurity, out bool marsCapable)
private PreLoginHandshakeStatus ConsumePreLoginHandshake(bool encrypt, bool trustServerCert, bool integratedSecurity, out bool marsCapable, out bool fedAuthRequired )
{
marsCapable = _fMARS; // Assign default value
fedAuthRequired = false;
bool isYukonOrLater = false;
Debug.Assert(_physicalStateObj._syncOverAsync, "Should not attempt pends in a synchronous call");
bool result = _physicalStateObj.TryReadNetworkPacket();
@ -772,7 +779,10 @@ namespace System.Data.SqlClient
_encryptionOption == EncryptionOptions.LOGIN)
{
uint error = 0;
uint info = ((encrypt && !trustServerCert) ? TdsEnums.SNI_SSL_VALIDATE_CERTIFICATE : 0)
// If we're using legacy server certificate validation behavior (not using access token), then validate if Encrypt=true and Trust Sever Certificate = false.
// If using access token, validate if Trust Server Certificate=false.
bool shouldValidateServerCert = (encrypt && !trustServerCert) || (_connHandler._accessTokenInBytes != null && !trustServerCert);
uint info = (shouldValidateServerCert ? TdsEnums.SNI_SSL_VALIDATE_CERTIFICATE : 0)
| (isYukonOrLater ? TdsEnums.SNI_SSL_USE_SCHANNEL_CACHE : 0);
if (encrypt && !integratedSecurity)
@ -835,6 +845,23 @@ namespace System.Data.SqlClient
offset += 4;
break;
case (int)PreLoginOptions.FEDAUTHREQUIRED:
payloadOffset = payload[offset++] << 8 | payload[offset++];
payloadLength = payload[offset++] << 8 | payload[offset++];
// Only 0x00 and 0x01 are accepted values from the server.
if (payload[payloadOffset] != 0x00 && payload[payloadOffset] != 0x01)
{
throw SQL.ParsingErrorValue(ParsingErrorState.FedAuthRequiredPreLoginResponseInvalidValue, (int)payload[payloadOffset]);
}
// We must NOT use the response for the FEDAUTHREQUIRED PreLogin option, if AccessToken is not null, meaning token based authentication is used.
if (_connHandler.ConnectionOptions != null || _connHandler._accessTokenInBytes != null)
{
fedAuthRequired = payload[payloadOffset] == 0x01 ? true : false;
}
break;
default:
Debug.Assert(false, "UNKNOWN option in ConsumePreLoginHandshake, option:" + option);
@ -5963,6 +5990,70 @@ namespace System.Data.SqlClient
return len;
}
internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionData fedAuthFeatureData,
bool write /* if false just calculates the length */)
{
Debug.Assert(fedAuthFeatureData.libraryType == TdsEnums.FedAuthLibrary.SecurityToken,
"only Security Token are supported in writing feature request");
int dataLen = 0;
int totalLen = 0;
// set dataLen and totalLen
switch (fedAuthFeatureData.libraryType)
{
case TdsEnums.FedAuthLibrary.SecurityToken:
Debug.Assert(fedAuthFeatureData.accessToken != null, "AccessToken should not be null.");
dataLen = 1 + sizeof(int) + fedAuthFeatureData.accessToken.Length; // length of feature data = 1 byte for library and echo, security token length and sizeof(int) for token lengh itself
break;
default:
Debug.Assert(false, "Unrecognized library type for fedauth feature extension request");
break;
}
totalLen = dataLen + 5; // length of feature id (1 byte), data length field (4 bytes), and feature data (dataLen)
// write feature id
if (write)
{
_physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_FEDAUTH);
// set options
byte options = 0x00;
// set upper 7 bits of options to indicate fed auth library type
switch (fedAuthFeatureData.libraryType)
{
case TdsEnums.FedAuthLibrary.SecurityToken:
Debug.Assert(_connHandler._federatedAuthenticationRequested == true, "_federatedAuthenticationRequested field should be true");
options |= TdsEnums.FEDAUTHLIB_SECURITYTOKEN << 1;
break;
default:
Debug.Assert(false, "Unrecognized FedAuthLibrary type for feature extension request");
break;
}
options |= (byte)(fedAuthFeatureData.fedAuthRequiredPreLoginResponse == true ? 0x01 : 0x00);
// write dataLen and options
WriteInt(dataLen, _physicalStateObj);
_physicalStateObj.WriteByte(options);
// write accessToken for FedAuthLibrary.SecurityToken
switch (fedAuthFeatureData.libraryType)
{
case TdsEnums.FedAuthLibrary.SecurityToken:
WriteInt(fedAuthFeatureData.accessToken.Length, _physicalStateObj);
_physicalStateObj.WriteByteArray(fedAuthFeatureData.accessToken, fedAuthFeatureData.accessToken.Length, 0);
break;
default:
Debug.Fail("Unrecognized FedAuthLibrary type for feature extension request");
break;
}
}
return totalLen;
}
internal int WriteGlobalTransactionsFeatureRequest(bool write /* if false just calculates the length */)
{
int len = 5; // 1byte = featureID, 4bytes = featureData length
@ -5977,13 +6068,17 @@ namespace System.Data.SqlClient
return len;
}
internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData)
internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData? fedAuthFeatureExtensionData)
{
_physicalStateObj.SetTimeoutSeconds(rec.timeout);
Debug.Assert(recoverySessionData == null || (requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0, "Recovery session data without session recovery feature request");
Debug.Assert(TdsEnums.MAXLEN_HOSTNAME >= rec.hostName.Length, "_workstationId.Length exceeds the max length for this value");
Debug.Assert(!rec.useSSPI || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Cannot use both SSPI and FedAuth");
Debug.Assert(fedAuthFeatureExtensionData == null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0, "fedAuthFeatureExtensionData provided without fed auth feature request");
Debug.Assert(fedAuthFeatureExtensionData != null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Fed Auth feature requested without specifying fedAuthFeatureExtensionData.");
Debug.Assert(rec.userName == null || (rec.userName != null && TdsEnums.MAXLEN_USERNAME >= rec.userName.Length), "_userID.Length exceeds the max length for this value");
Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_USERNAME >= rec.credential.UserId.Length), "_credential.UserId.Length exceeds the max length for this value");
@ -6060,12 +6155,12 @@ namespace System.Data.SqlClient
byte[] outSSPIBuff = null;
uint outSSPILength = 0;
// only add lengths of password and username if not using SSPI
if (!rec.useSSPI)
// only add lengths of password and username if not using SSPI or requesting federated authentication info
if (!rec.useSSPI && !_connHandler._federatedAuthenticationRequested)
{
checked
{
length += (userName.Length * 2) + encryptedPasswordLengthInBytes
length += (userName.Length * 2) + encryptedPasswordLengthInBytes
+ encryptedChangePasswordLengthInBytes;
}
}
@ -6110,6 +6205,11 @@ namespace System.Data.SqlClient
{
length += WriteGlobalTransactionsFeatureRequest(false);
}
if ((requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0)
{
Debug.Assert(fedAuthFeatureExtensionData != null, "fedAuthFeatureExtensionData should not null.");
length += WriteFedAuthFeatureRequest(fedAuthFeatureExtensionData.Value, write: false);
}
length++; // for terminator
}
@ -6325,7 +6425,6 @@ namespace System.Data.SqlClient
_physicalStateObj.WriteByteArray(outSSPIBuff, (int)outSSPILength, 0);
WriteString(rec.attachDBFilename, _physicalStateObj);
if (!rec.useSSPI)
{
if (rec.newSecurePassword != null)
@ -6348,6 +6447,11 @@ namespace System.Data.SqlClient
{
WriteGlobalTransactionsFeatureRequest(true);
}
if ((requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0)
{
Debug.Assert(fedAuthFeatureExtensionData != null, "fedAuthFeatureExtensionData should not null.");
WriteFedAuthFeatureRequest(fedAuthFeatureExtensionData.Value, write: true);
};
_physicalStateObj.WriteByte(0xFF); // terminator
}
}

Просмотреть файл

@ -46,6 +46,7 @@ namespace System.Data.SqlClient
THREADID,
MARS,
TRACEID,
FEDAUTHREQUIRED,
NUMOPT,
LASTOPT = 255
}
@ -66,6 +67,16 @@ namespace System.Data.SqlClient
Broken,
}
/// <summary>
/// Struct encapsulating the data to be sent to the server as part of Federated Authentication Feature Extension.
/// </summary>
internal struct FederatedAuthenticationFeatureExtensionData
{
internal TdsEnums.FedAuthLibrary libraryType;
internal bool fedAuthRequiredPreLoginResponse;
internal byte[] accessToken;
}
sealed internal class SqlCollation
{
// First 20 bits of info field represent the lcid, bits 21-25 are compare options

Просмотреть файл

@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Xunit;
namespace System.Data.SqlClient.Tests
{
public class AADAccessTokenTest
{
private SqlConnectionStringBuilder _builder;
private SqlCredential _credential = null;
[Theory]
[InlineData("Test combination of Access Token and IntegratedSecurity", new object[] { "Integrated Security", true })]
[InlineData("Test combination of Access Token and User Id", new object[] { "UID", "sampleUserId" })]
[InlineData("Test combination of Access Token and Password", new object[] { "PWD", "samplePassword" })]
[InlineData("Test combination of Access Token and Credentials", new object[] { "sampleUserId" })]
public void InvalidCombinationOfAccessToken(string description, object[] Params)
{
_builder = new SqlConnectionStringBuilder
{
["Data Source"] = "sample.database.windows.net"
};
if (Params.Length == 1)
{
Security.SecureString password = new Security.SecureString();
password.MakeReadOnly();
_credential = new SqlCredential(Params[0] as string, password);
}
else
{
_builder[Params[0] as string] = Params[1];
}
InvalidCombinationCheck(_credential);
}
private void InvalidCombinationCheck(SqlCredential credential)
{
using (SqlConnection connection = new SqlConnection(_builder.ConnectionString, credential))
{
Assert.Throws<InvalidOperationException>(() => connection.AccessToken = "SampleAccessToken");
}
}
}
}

Просмотреть файл

@ -9,6 +9,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Debug|AnyCPU'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Release|AnyCPU'" />
<ItemGroup>
<Compile Include="AADAccessTokenTest.cs" />
<Compile Include="CloneTests.cs" />
<Compile Include="BaseProviderAsyncTest\BaseProviderAsyncTest.cs" />
<Compile Include="BaseProviderAsyncTest\MockCommand.cs" />

Просмотреть файл

@ -17,6 +17,10 @@ namespace System.Data.SqlClient.ManualTesting.Tests
private static readonly Type s_tdsParserStateObjectFactory = s_systemDotData?.GetType("System.Data.SqlClient.TdsParserStateObjectFactory");
private static readonly PropertyInfo s_useManagedSNI = s_tdsParserStateObjectFactory?.GetProperty("UseManagedSNI", BindingFlags.Static | BindingFlags.Public);
private static readonly string[] s_azureSqlServerEndpoints = {".database.windows.net",
".database.cloudapi.de",
".database.usgovcloudapi.net",
".database.chinacloudapi.cn"};
static DataTestUtility()
{
NpConnStr = Environment.GetEnvironmentVariable("TEST_NP_CONN_STR");
@ -64,6 +68,42 @@ namespace System.Data.SqlClient.ManualTesting.Tests
public static bool IsIntegratedSecuritySetup() => int.TryParse(Environment.GetEnvironmentVariable("TEST_INTEGRATEDSECURITY_SETUP"), out int result) ? result == 1 : false;
public static string getAccessToken()
{
return Environment.GetEnvironmentVariable("TEST_ACCESSTOKEN_SETUP");
}
public static bool IsAccessTokenSetup() => string.IsNullOrEmpty(getAccessToken()) ? false : true;
// This method assumes dataSource parameter is in TCP connection string format.
public static bool IsAzureSqlServer(string dataSource)
{
int i = dataSource.LastIndexOf(',');
if (i >= 0)
{
dataSource = dataSource.Substring(0, i);
}
i = dataSource.LastIndexOf('\\');
if (i >= 0)
{
dataSource = dataSource.Substring(0, i);
}
// trim redundant whitespace
dataSource = dataSource.Trim();
// check if servername end with any azure endpoints
for (i = 0; i < s_azureSqlServerEndpoints.Length; i++)
{
if (dataSource.EndsWith(s_azureSqlServerEndpoints[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool CheckException<TException>(Exception ex, string exceptionMessage, bool innerExceptionMustBeNull) where TException : Exception
{
return ((ex != null) && (ex is TException) &&

Просмотреть файл

@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.RegularExpressions;
using Xunit;
namespace System.Data.SqlClient.ManualTesting.Tests
{
public class AADAccessTokenTest
{
private static bool IsAccessTokenSetup() => DataTestUtility.IsAccessTokenSetup();
private static bool IsAzureServer() => DataTestUtility.IsAzureSqlServer(GetDataSource());
[ConditionalFact(nameof(IsAccessTokenSetup), nameof(IsAzureServer))]
public static void AccessTokenTest()
{
using (SqlConnection connection = new SqlConnection(DataTestUtility.TcpConnStr))
{
connection.AccessToken = DataTestUtility.getAccessToken();
connection.Open();
}
}
private static string GetDataSource()
{
// Obtain Data source from connection string
string tcpConnStr = DataTestUtility.TcpConnStr.Replace(" ", string.Empty);
Regex regex = new Regex("DataSource=(.*?);");
Match match = regex.Match(tcpConnStr);
return match.Groups[1].ToString();
}
}
}

Просмотреть файл

@ -29,6 +29,7 @@
<Compile Include="DataCommon\DataTestUtility.cs" />
<Compile Include="DataCommon\ProxyServer.cs" />
<Compile Include="DataCommon\SystemDataResourceManager.cs" />
<Compile Include="SQL\ConnectivityTests\AADAccessTokenTest.cs" />
<Compile Include="SQL\ParameterTest\DateTimeVariantTest.cs" />
<Compile Include="SQL\ParameterTest\OutputParameter.cs" />
<Compile Include="SQL\ParameterTest\ParametersTest.cs" />