/// <summary>
        /// Get the Federated Authentication Token.
        /// </summary>
        /// <param name="fedAuthInfo">Information obtained from server as Federated Authentication Info.</param>
        /// <returns>SqlFedAuthToken</returns>
        internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) {

            Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null.");

            // No:of milliseconds to sleep for the inital back off.
            int sleepInterval = 100;

            // No:of attempts, for tracing purposes, if we underwent retries.
            int numberOfAttempts = 0;

            // Object that will be returned to the caller, containing all required data about the token.
            SqlFedAuthToken fedAuthToken = new SqlFedAuthToken();

            // Username to use in error messages.
            string username = null;

            while (true) {
                numberOfAttempts++;
                try {
                    if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated) {
                        username = TdsEnums.NTAUTHORITYANONYMOUSLOGON;
                        fedAuthToken.accessToken = ADALNativeWrapper.ADALGetAccessTokenForWindowsIntegrated(fedAuthInfo.stsurl,
                                                                                                                fedAuthInfo.spn,
                                                                                                                _clientConnectionId, ActiveDirectoryAuthentication.AdoClientId,
                                                                                                                ref fedAuthToken.expirationFileTime);
                    }
                    else if (_credential != null) {
                        username = _credential.UserId;
                        fedAuthToken.accessToken = ADALNativeWrapper.ADALGetAccessToken(_credential.UserId,
                                                                                            _credential.Password,
                                                                                            fedAuthInfo.stsurl,
                                                                                            fedAuthInfo.spn,
                                                                                            _clientConnectionId,
                                                                                            ActiveDirectoryAuthentication.AdoClientId,
                                                                                            ref fedAuthToken.expirationFileTime);
                    }
                    else {
                        username = ConnectionOptions.UserID;
                        fedAuthToken.accessToken = ADALNativeWrapper.ADALGetAccessToken(ConnectionOptions.UserID,
                                                                                            ConnectionOptions.Password,
                                                                                            fedAuthInfo.stsurl,
                                                                                            fedAuthInfo.spn,
                                                                                            _clientConnectionId,
                                                                                            ActiveDirectoryAuthentication.AdoClientId,
                                                                                            ref fedAuthToken.expirationFileTime);
                    }

                    Debug.Assert(fedAuthToken.accessToken != null, "AccessToken should not be null.");
#if DEBUG
                    if (_forceAdalRetry) {
                        // 3399614468 is 0xCAA20004L just for testing.
                        throw new AdalException("Force retry in GetFedAuthToken", ActiveDirectoryAuthentication.GetAccessTokenTansisentError, 3399614468, 6);
                    }
#endif
                    // Break out of the retry loop in successful case.
                    break;
                }
                catch (AdalException adalException) {

                    uint errorCategory = adalException.GetCategory();

                    if (ActiveDirectoryAuthentication.GetAccessTokenTansisentError != errorCategory
                        || _timeout.IsExpired
                        || _timeout.MillisecondsRemaining <= sleepInterval) {

                        string errorStatus = adalException.GetStatus().ToString("X");

                        Bid.Trace("<sc.SqlInternalConnectionTds.GetFedAuthToken.ADALException category:> %d#  <error:> %s#\n", (int)errorCategory, errorStatus);

                        // Error[0]
                        SqlErrorCollection sqlErs = new SqlErrorCollection();
                        sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, ConnectionOptions.DataSource, Res.GetString(Res.SQL_ADALFailure, username, ConnectionOptions.Authentication.ToString("G")), ActiveDirectoryAuthentication.AdalGetAccessTokenFunctionName, 0));

                        // Error[1]
                        string errorMessage1 = Res.GetString(Res.SQL_ADALInnerException, errorStatus, adalException.GetState());
                        sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, ConnectionOptions.DataSource, errorMessage1, ActiveDirectoryAuthentication.AdalGetAccessTokenFunctionName, 0));

                        // Error[2]
                        if (!string.IsNullOrEmpty(adalException.Message)) {
                            sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, ConnectionOptions.DataSource, adalException.Message, ActiveDirectoryAuthentication.AdalGetAccessTokenFunctionName, 0));
                        }
                        SqlException exc = SqlException.CreateException(sqlErs, "", this);
                        throw exc;
                    }

                    Bid.Trace("<sc.SqlInternalConnectionTds.GetFedAuthToken|ADV> %d#, sleeping %d{Milliseconds}\n", ObjectID, sleepInterval);
                    Bid.Trace("<sc.SqlInternalConnectionTds.GetFedAuthToken|ADV> %d#, remaining %d{Milliseconds}\n", ObjectID, _timeout.MillisecondsRemaining);

                    Thread.Sleep(sleepInterval);
                    sleepInterval *= 2;
                }
            }

            Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null.");
            Debug.Assert(fedAuthToken.accessToken != null && fedAuthToken.accessToken.Length > 0, "fedAuthToken.accessToken should not be null or empty.");

            // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling.
            if (_dbConnectionPool != null) {
                DateTime expirationTime = DateTime.FromFileTimeUtc(fedAuthToken.expirationFileTime);
                _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(fedAuthToken.accessToken, expirationTime);
            }

            Bid.Trace("<sc.SqlInternalConnectionTds.GetFedAuthToken> %d#, Finished generating federated authentication token.\n", ObjectID);

            return fedAuthToken;
        }
        /// <summary>
        /// Generates (if appropriate) and sends a Federated Authentication Access token to the server, using the Federated Authentication Info.
        /// </summary>
        /// <param name="fedAuthInfo">Federated Authentication Info.</param>
        internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) {
            Debug.Assert((ConnectionOptions.HasUserIdKeyword && ConnectionOptions.HasPasswordKeyword)
                         || _credential != null
                         || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired),
                         "Credentials aren't provided for calling ADAL");
            Debug.Assert(fedAuthInfo != null, "info should not be null.");
            Debug.Assert(_dbConnectionPoolAuthenticationContextKey == null, "_dbConnectionPoolAuthenticationContextKey should be null.");

            Bid.Trace("<sc.SqlInternalConnectionTds.OnFedAuthInfo> %d#, Generating federated authentication token\n", ObjectID);

            DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext = null;

            // We want to refresh the token without taking a lock on the context, allowed when the access token is expiring within the next 10 mins.
            bool attemptRefreshTokenUnLocked = false;

            // We want to refresh the token, if taking the lock on the authentication context is successful.
            bool attemptRefreshTokenLocked = false;

            // The Federated Authentication returned by TryGetFedAuthTokenLocked or GetFedAuthToken.
            SqlFedAuthToken fedAuthToken = null;

            if (_dbConnectionPool != null) {
                Debug.Assert(_dbConnectionPool.AuthenticationContexts != null);

                // Construct the dbAuthenticationContextKey with information from FedAuthInfo and store for later use, when inserting in to the token cache.
                _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.stsurl, fedAuthInfo.spn);

                // Try to retrieve the authentication context from the pool, if one does exist for this key.
                if (_dbConnectionPool.AuthenticationContexts.TryGetValue(_dbConnectionPoolAuthenticationContextKey, out dbConnectionPoolAuthenticationContext)) {
                    Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null.");

                    // The timespan between UTCNow and the token expiry.
                    TimeSpan contextValidity = dbConnectionPoolAuthenticationContext.ExpirationTime.Subtract(DateTime.UtcNow);

                    // If the authentication context is expiring within next 10 minutes, lets just re-create a token for this connection attempt.
                    // And on successful login, try to update the cache with the new token.
                    if (contextValidity <= _dbAuthenticationContextUnLockedRefreshTimeSpan) {
                        Bid.Trace("<sc.SqlInternalConnectionTds.OnFedAuthInfo> %d#, The expiration time is less than 10 mins, so trying to get new access token regardless of if an other thread is also trying to update it.The expiration time is %s. Current Time is %s.\n", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString());

                        attemptRefreshTokenUnLocked = true;
                    }

#if DEBUG
                    // Checking if any failpoints are enabled.
                    else if (_forceExpiryUnLocked) {
                        attemptRefreshTokenUnLocked = true;
                    }
                    else if (_forceExpiryLocked) {
                        attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out fedAuthToken);
                    }
#endif

                    // If the token is expiring within the next 45 mins, try to fetch a new token, if there is no thread already doing it.
                    // If a thread is already doing the refresh, just use the existing token in the cache and proceed.
                    else if (contextValidity <= _dbAuthenticationContextLockedRefreshTimeSpan) {
                        if (Bid.AdvancedOn) {
                            Bid.Trace("<sc.SqlInternalConnectionTds.OnFedAuthInfo> %d#, The authentication context needs a refresh.The expiration time is %s. Current Time is %s.\n", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString());
                        }

                        // Call the function which tries to acquire a lock over the authentication context before trying to update.
                        // If the lock could not be obtained, it will return false, without attempting to fetch a new token.
                        attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out fedAuthToken);

                        // If TryGetFedAuthTokenLocked returns true, it means lock was obtained and fedAuthToken should not be null.
                        // If there was an exception in retrieving the new token, TryGetFedAuthTokenLocked should have thrown, so we won't be here.
                        Debug.Assert(!attemptRefreshTokenLocked || fedAuthToken != null, "Either Lock should not have been obtained or fedAuthToken should not be null.");
                        Debug.Assert(!attemptRefreshTokenLocked || _newDbConnectionPoolAuthenticationContext != null, "Either Lock should not have been obtained or _newDbConnectionPoolAuthenticationContext should not be null.");

                        // Indicate in Bid Trace that we are successful with the update.
                        if (attemptRefreshTokenLocked) {
                            Bid.Trace("<sc.SqlInternalConnectionTds.OnFedAuthInfo> %d#, The attempt to get a new access token succeeded under the locked mode.");
                        }

                    }
                    else if (Bid.AdvancedOn) {
                        Bid.Trace("<sc.SqlInternalConnectionTds.OnFedAuthInfo> %d#, Found an authentication context in the cache that does not need a refresh at this time. Re-using the cached token.\n", ObjectID);
                    }
                }
            }

            // dbConnectionPoolAuthenticationContext will be null if either this is the first connection attempt in the pool or pooling is disabled.
            if (dbConnectionPoolAuthenticationContext == null || attemptRefreshTokenUnLocked) {
                // Get the Federated Authentication Token.
                fedAuthToken = GetFedAuthToken(fedAuthInfo);
                Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null.");

                if (_dbConnectionPool != null) {
                    // GetFedAuthToken should have updated _newDbConnectionPoolAuthenticationContext.
                    Debug.Assert(_newDbConnectionPoolAuthenticationContext != null, "_newDbConnectionPoolAuthenticationContext should not be null.");
                }
            }
            else if (!attemptRefreshTokenLocked) {
                Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null.");
                Debug.Assert(fedAuthToken == null, "fedAuthToken should be null in this case.");
                Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, "_newDbConnectionPoolAuthenticationContext should be null.");

                fedAuthToken = new SqlFedAuthToken();

                // If the code flow is here, then we are re-using the context from the cache for this connection attempt and not
                // generating a new access token on this thread.
                fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken;
            }

            Debug.Assert(fedAuthToken != null && fedAuthToken.accessToken != null, "fedAuthToken and fedAuthToken.accessToken cannot be null.");
            _parser.SendFedAuthToken(fedAuthToken);
        }
        /// <summary>
        /// Tries to acquire a lock on the authentication context. If successful in acquiring the lock, gets a new token and assigns it in the out parameter. Else returns false.
        /// </summary>
        /// <param name="fedAuthInfo">Federated Authentication Info</param>
        /// <param name="dbConnectionPoolAuthenticationContext">Authentication Context cached in the connection pool.</param>
        /// <param name="fedAuthToken">Out parameter, carrying the token if we acquired a lock and got the token.</param>
        /// <returns></returns>
        internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext, out SqlFedAuthToken fedAuthToken) {

            Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null.");
            Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null.");

            fedAuthToken = null;

            // Variable which indicates if we did indeed manage to acquire the lock on the authentication context, to try update it.
            bool authenticationContextLocked = false;

            // Prepare CER to ensure the lock on authentication context is released.
            RuntimeHelpers.PrepareConstrainedRegions();
            try {
                // Try to obtain a lock on the context. If acquired, this thread got the opportunity to update.
                // Else some other thread is already updating it, so just proceed forward with the existing token in the cache.
                if (dbConnectionPoolAuthenticationContext.LockToUpdate()) {
                    Bid.Trace("<sc.SqlInternalConnectionTds.TryGetFedAuthTokenLocked> %d#, Acquired the lock to update the authentication context.The expiration time is %s. Current Time is %s.\n", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString());

                    authenticationContextLocked = true;
                }
                else {
                    Bid.Trace("<sc.SqlInternalConnectionTds.TryGetFedAuthTokenLocked> %d#, Refreshing the context is already in progress by another thread.\n", ObjectID);
                }

                if (authenticationContextLocked) {
                    // Get the Federated Authentication Token.
                    fedAuthToken = GetFedAuthToken(fedAuthInfo);
                    Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null.");
                }
            }
            finally {
                if (authenticationContextLocked) {
                    // Release the lock we took on the authentication context, even if we have not yet updated the cache with the new context. Login process can fail at several places after this step and so there is no guarantee that the new context will make it to the cache. So we shouldn't miss resetting the flag. With the reset, at-least another thread may have a chance to update it.
                    dbConnectionPoolAuthenticationContext.ReleaseLockToUpdate();
                }
            }

            return authenticationContextLocked;
        }
Пример #4
0
        private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, out SqlFedAuthInfo sqlFedAuthInfo) {
            sqlFedAuthInfo = null;
            SqlFedAuthInfo tempFedAuthInfo = new SqlFedAuthInfo();

            // Skip reading token length, since it has already been read in caller

            if (Bid.AdvancedOn) {
                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> FEDAUTHINFO token stream length = {0}\n", tokenLen);
            }

            if (tokenLen < sizeof(uint)) {
                // the token must at least contain a DWORD indicating the number of info IDs
                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> FEDAUTHINFO token stream length too short for CountOfInfoIDs.\n");
                throw SQL.ParsingErrorLength(ParsingErrorState.FedAuthInfoLengthTooShortForCountOfInfoIds, tokenLen);
            }

            // read how many FedAuthInfo options there are
            uint optionsCount;
            if (!stateObj.TryReadUInt32(out optionsCount)) {
                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> Failed to read CountOfInfoIDs in FEDAUTHINFO token stream.\n");
                throw SQL.ParsingError(ParsingErrorState.FedAuthInfoFailedToReadCountOfInfoIds);
            }
            tokenLen -= sizeof(uint); // remaining length is shortened since we read optCount

            if (Bid.AdvancedOn) {
                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> CountOfInfoIDs = {0}\n", optionsCount.ToString(CultureInfo.InvariantCulture));
            }

            if (tokenLen > 0) {
                // read the rest of the token
                byte[] tokenData = new byte[tokenLen];
                int totalRead = 0;
                bool successfulRead = stateObj.TryReadByteArray(tokenData, 0, tokenLen, out totalRead);

                if (Bid.AdvancedOn) {
                    Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> Read rest of FEDAUTHINFO token stream: {0}\n", BitConverter.ToString(tokenData, 0, totalRead));
                }

                if (!successfulRead || totalRead != tokenLen) {
                    Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> Failed to read FEDAUTHINFO token stream. Attempted to read {0} bytes, actually read {1}\n", tokenLen, totalRead);
                    throw SQL.ParsingError(ParsingErrorState.FedAuthInfoFailedToReadTokenStream);
                }

                // each FedAuthInfoOpt is 9 bytes:
                //    1 byte for FedAuthInfoID
                //    4 bytes for FedAuthInfoDataLen
                //    4 bytes for FedAuthInfoDataOffset
                // So this is the index in tokenData for the i-th option
                const uint optionSize = 9;

                // the total number of bytes for all FedAuthInfoOpts together
                uint totalOptionsSize = checked(optionsCount * optionSize);

                for (uint i = 0; i < optionsCount; i++) {
                    uint currentOptionOffset = checked(i * optionSize);

                    byte id = tokenData[currentOptionOffset];
                    uint dataLen = BitConverter.ToUInt32(tokenData, checked((int)(currentOptionOffset + 1)));
                    uint dataOffset = BitConverter.ToUInt32(tokenData, checked((int)(currentOptionOffset + 5)));

                    if (Bid.AdvancedOn) {
                        Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> FedAuthInfoOpt: ID={0}, DataLen={1}, Offset={2}\n", id, dataLen.ToString(CultureInfo.InvariantCulture), dataOffset.ToString(CultureInfo.InvariantCulture));
                    }

                    // offset is measured from optCount, so subtract to make offset measured
                    // from the beginning of tokenData
                    checked {
                        dataOffset -= sizeof(uint);
                    }

                    // if dataOffset points to a region within FedAuthInfoOpt or after the end of the token, throw
                    if (dataOffset < totalOptionsSize || dataOffset >= tokenLen) {
                        Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> FedAuthInfoDataOffset points to an invalid location.\n");
                        throw SQL.ParsingErrorOffset(ParsingErrorState.FedAuthInfoInvalidOffset, unchecked((int)dataOffset));
                    }

                    // try to read data and throw if the arguments are bad, meaning the server sent us a bad token
                    string data;
                    try {
                        data = System.Text.Encoding.Unicode.GetString(tokenData, checked((int)dataOffset), checked((int)dataLen));
                    }
                    catch (ArgumentOutOfRangeException e) {
                        Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> Failed to read FedAuthInfoData.\n");
                        throw SQL.ParsingError(ParsingErrorState.FedAuthInfoFailedToReadData, e);
                    }
                    catch (ArgumentException e) {
                        Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> FedAuthInfoData is not in unicode format.\n");
                        throw SQL.ParsingError(ParsingErrorState.FedAuthInfoDataNotUnicode, e);
                    }

                    if (Bid.AdvancedOn) {
                        Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> FedAuthInfoData: {0}\n", data);
                    }

                    // store data in tempFedAuthInfo
                    switch ((TdsEnums.FedAuthInfoId)id) {
                        case TdsEnums.FedAuthInfoId.Spn:
                            tempFedAuthInfo.spn = data;
                            break;
                        case TdsEnums.FedAuthInfoId.Stsurl:
                            tempFedAuthInfo.stsurl = data;
                            break;
                        default:
                            if (Bid.AdvancedOn) {
                                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> Ignoring unknown federated authentication info option: {0}\n", id);
                            }
                            break;
                    }
                }
            }
            else {
                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> FEDAUTHINFO token stream is not long enough to contain the data it claims to.\n");
                throw SQL.ParsingErrorLength(ParsingErrorState.FedAuthInfoLengthTooShortForData, tokenLen);
            }

            Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo> Processed FEDAUTHINFO token stream: {0}\n", tempFedAuthInfo.ToString());

            if (String.IsNullOrWhiteSpace(tempFedAuthInfo.stsurl) || String.IsNullOrWhiteSpace(tempFedAuthInfo.spn)) {
                // We should be receiving both stsurl and spn
                Bid.Trace("<sc.TdsParser.TryProcessFedAuthInfo|ERR> FEDAUTHINFO token stream does not contain both STSURL and SPN.\n");
                throw SQL.ParsingError(ParsingErrorState.FedAuthInfoDoesNotContainStsurlAndSpn);
            }

            sqlFedAuthInfo = tempFedAuthInfo;
            return true;
        }