public async Task <IssuedToken> GetTokenAsync(VssTraceActivity traceActivity) { IssuedToken token = null; try { VssHttpEventSource.Log.IssuedTokenAcquiring(traceActivity, this.Provider); if (this.Provider.InvokeRequired) { // Post to the UI thread using the scheduler. This may return a new task object which needs // to be awaited, since once we get to the UI thread there may be nothing to do if someone else // preempts us. // The cancellation token source is used to handle race conditions between scheduling and // waiting for the UI task to begin execution. The callback is responsible for disposing of // the token source, since the thought here is that the callback will run eventually as the // typical reason for not starting execution within the timeout is due to a deadlock with // the scheduler being used. var timerTask = new TaskCompletionSource <Object>(); var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); timeoutTokenSource.Token.Register(() => timerTask.SetResult(null), false); var uiTask = Task.Factory.StartNew((state) => PostCallback(state, timeoutTokenSource), this, this.CancellationToken, TaskCreationOptions.None, this.Provider.Credential.Scheduler).Unwrap(); var completedTask = await Task.WhenAny(timerTask.Task, uiTask).ConfigureAwait(false); if (completedTask == uiTask) { token = uiTask.Result; } } else { token = await this.Provider.OnGetTokenAsync(this.FailedToken, this.CancellationToken).ConfigureAwait(false); } CompletionSource.TrySetResult(token); return(token); } catch (Exception exception) { // Mark our completion source as failed so other waiters will get notified in all cases CompletionSource.TrySetException(exception); throw; } finally { this.Provider.CurrentToken = token ?? this.FailedToken; VssHttpEventSource.Log.IssuedTokenAcquired(traceActivity, this.Provider, token); } }
/// <summary> /// Performs a token exchange using the specified authorization grant and client credentials. /// </summary> /// <param name="grant">The authorization grant for the token request</param> /// <param name="credential">The credentials to present to the secure token service as proof of identity</param> /// <param name="tokenParameters">An collection of additional parameters to provide for the token request</param> /// <param name="cancellationToken">A token for signalling cancellation</param> /// <returns>A <c>Task<VssOAuthTokenResponse></c> which may be used to track progress of the token request</returns> public async Task <VssOAuthTokenResponse> GetTokenAsync( VssOAuthGrant grant, VssOAuthClientCredential credential, VssOAuthTokenParameters tokenParameters = null, CancellationToken cancellationToken = default(CancellationToken)) { VssTraceActivity traceActivity = VssTraceActivity.Current; using (HttpClient client = new HttpClient(CreateMessageHandler(this.AuthorizationUrl))) { var requestMessage = new HttpRequestMessage(HttpMethod.Post, this.AuthorizationUrl); requestMessage.Content = CreateRequestContent(grant, credential, tokenParameters); requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (VssClientHttpRequestSettings.Default.UseHttp11) { requestMessage.Version = HttpVersion.Version11; } foreach (var headerVal in VssClientHttpRequestSettings.Default.UserAgent) { if (!requestMessage.Headers.UserAgent.Contains(headerVal)) { requestMessage.Headers.UserAgent.Add(headerVal); } } using (var response = await client.SendAsync(requestMessage, cancellationToken: cancellationToken).ConfigureAwait(false)) { string correlationId = "Unknown"; if (response.Headers.TryGetValues("x-ms-request-id", out IEnumerable <string> requestIds)) { correlationId = string.Join(",", requestIds); } VssHttpEventSource.Log.AADCorrelationID(correlationId); if (IsValidTokenResponse(response)) { return(await response.Content.ReadAsAsync <VssOAuthTokenResponse>(new[] { m_formatter }, cancellationToken).ConfigureAwait(false)); } else { var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); throw new VssServiceResponseException(response.StatusCode, responseContent, null); } } } }
private void ApplyHeaders(HttpRequestMessage request) { if (this.Settings.ApplyTo(request)) { VssTraceActivity activity = request.GetActivity(); if (activity != null && activity != VssTraceActivity.Empty && !request.Headers.Contains(HttpHeaders.TfsSessionHeader)) { request.Headers.Add(HttpHeaders.TfsSessionHeader, activity.Id.ToString("D")); } request.Headers.ExpectContinue = this.ExpectContinue; } }
public GetTokenOperation( VssTraceActivity activity, IssuedTokenProvider provider, IssuedToken failedToken, CancellationToken cancellationToken, DisposableTaskCompletionSource <IssuedToken> completionSource, Boolean ownsCompletionSource = false) { this.Provider = provider; this.ActivityId = activity?.Id ?? Guid.Empty; this.FailedToken = failedToken; this.CancellationToken = cancellationToken; this.CompletionSource = completionSource; this.OwnsCompletionSource = ownsCompletionSource; }
protected async Task <T> SendAsync <T>( HttpMethod method, IEnumerable <KeyValuePair <String, String> > additionalHeaders, Guid locationId, Object routeValues = null, ApiResourceVersion version = null, HttpContent content = null, IEnumerable <KeyValuePair <String, String> > queryParameters = null, Object userState = null, CancellationToken cancellationToken = default(CancellationToken), Func <HttpResponseMessage, CancellationToken, Task <T> > processResponse = null) { using (VssTraceActivity.GetOrCreate().EnterCorrelationScope()) using (HttpRequestMessage requestMessage = await CreateRequestMessageAsync(method, additionalHeaders, locationId, routeValues, version, content, queryParameters, userState, cancellationToken).ConfigureAwait(false)) { return(await SendAsync <T>(requestMessage, userState, cancellationToken, processResponse).ConfigureAwait(false)); } }
public async Task <IssuedToken> WaitForTokenAsync( VssTraceActivity traceActivity, CancellationToken cancellationToken) { IssuedToken token = null; try { VssHttpEventSource.Log.IssuedTokenWaitStart(traceActivity, this.Provider, this.ActivityId); token = await Task.Factory.ContinueWhenAll <IssuedToken>(new Task[] { CompletionSource.Task }, (x) => CompletionSource.Task.Result, cancellationToken).ConfigureAwait(false); } finally { VssHttpEventSource.Log.IssuedTokenWaitStop(traceActivity, this.Provider, token); } return(token); }
/// <summary> /// Creates a token provider for the configured issued token credentials. /// </summary> /// <param name="serverUrl">The targeted server</param> /// <param name="webResponse">The failed web response</param> /// <param name="failedToken">The failed token</param> /// <returns>A provider for retrieving tokens for the configured credential</returns> internal IssuedTokenProvider CreateTokenProvider( Uri serverUrl, IHttpResponse webResponse, IssuedToken failedToken) { ArgumentUtility.CheckForNull(serverUrl, "serverUrl"); IssuedTokenProvider tokenProvider = null; VssTraceActivity traceActivity = VssTraceActivity.Current; lock (m_thisLock) { tokenProvider = m_currentProvider; if (tokenProvider == null || !tokenProvider.IsAuthenticationChallenge(webResponse)) { // Prefer federated authentication over Windows authentication. if (m_federatedCredential != null && m_federatedCredential.IsAuthenticationChallenge(webResponse)) { if (tokenProvider != null) { VssHttpEventSource.Log.IssuedTokenProviderRemoved(traceActivity, tokenProvider); } // TODO: This needs to be refactored or renamed to be more generic ... this.TryGetValidAdalToken(m_federatedCredential.Prompt); tokenProvider = m_federatedCredential.CreateTokenProvider(serverUrl, webResponse, failedToken); if (tokenProvider != null) { VssHttpEventSource.Log.IssuedTokenProviderCreated(traceActivity, tokenProvider); } } m_currentProvider = tokenProvider; } return(tokenProvider); } }
/// <summary> /// Handles the authentication hand-shake for a Visual Studio service. /// </summary> /// <param name="request">The HTTP request message</param> /// <param name="cancellationToken">The cancellation token used for cooperative cancellation</param> /// <returns>A new <c>Task<HttpResponseMessage></c> which wraps the response from the remote service</returns> protected override async Task <HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { VssTraceActivity traceActivity = VssTraceActivity.Current; var traceInfo = VssHttpMessageHandlerTraceInfo.GetTraceInfo(request); traceInfo?.TraceHandlerStartTime(); if (!m_appliedClientCertificatesToTransportHandler && request.RequestUri.Scheme == "https") { HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler; if (httpClientHandler != null && this.Settings.ClientCertificateManager != null && this.Settings.ClientCertificateManager.ClientCertificates != null && this.Settings.ClientCertificateManager.ClientCertificates.Count > 0) { httpClientHandler.ClientCertificates.AddRange(this.Settings.ClientCertificateManager.ClientCertificates); } m_appliedClientCertificatesToTransportHandler = true; } if (!m_appliedServerCertificateValidationCallbackToTransportHandler && request.RequestUri.Scheme == "https") { HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler; if (httpClientHandler != null && this.Settings.ServerCertificateValidationCallback != null) { httpClientHandler.ServerCertificateCustomValidationCallback = this.Settings.ServerCertificateValidationCallback; } m_appliedServerCertificateValidationCallbackToTransportHandler = true; } // The .NET Core 2.1 runtime switched its HTTP default from HTTP 1.1 to HTTP 2. // This causes problems with some versions of the Curl handler on Linux. // See GitHub issue https://github.com/dotnet/corefx/issues/32376 if (Settings.UseHttp11) { request.Version = HttpVersion.Version11; } IssuedToken token = null; IssuedTokenProvider provider; if (this.Credentials.TryGetTokenProvider(request.RequestUri, out provider)) { token = provider.CurrentToken; } // Add ourselves to the message so the underlying token issuers may use it if necessary request.Options.Set(new HttpRequestOptionsKey <VssHttpMessageHandler>(VssHttpMessageHandler.PropertyName), this); Boolean succeeded = false; Boolean lastResponseDemandedProxyAuth = false; Int32 retries = m_maxAuthRetries; HttpResponseMessage response = null; HttpResponseMessageWrapper responseWrapper; CancellationTokenSource tokenSource = null; try { tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); if (this.Settings.SendTimeout > TimeSpan.Zero) { tokenSource.CancelAfter(this.Settings.SendTimeout); } do { if (response != null) { response.Dispose(); } ApplyHeaders(request); // In the case of a Windows token, only apply it to the web proxy if it // returned a 407 Proxy Authentication Required. If we didn't get this // status code back, then the proxy (if there is one) is clearly working fine, // so we shouldn't mess with its credentials. ApplyToken(request, token, applyICredentialsToWebProxy: lastResponseDemandedProxyAuth); lastResponseDemandedProxyAuth = false; // The WinHttpHandler will chunk any content that does not have a computed length which is // not what we want. By loading into a buffer up-front we bypass this behavior and there is // no difference in the normal HttpClientHandler behavior here since this is what they were // already doing. await BufferRequestContentAsync(request, tokenSource.Token).ConfigureAwait(false); traceInfo?.TraceBufferedRequestTime(); // ConfigureAwait(false) enables the continuation to be run outside any captured // SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking... response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false); traceInfo?.TraceRequestSendTime(); // Now buffer the response content if configured to do so. In general we will be buffering // the response content in this location, except in the few cases where the caller has // specified HttpCompletionOption.ResponseHeadersRead. // Trace content type in case of error await BufferResponseContentAsync(request, response, () => $"[ContentType: {response.Content.GetType().Name}]", tokenSource.Token).ConfigureAwait(false); traceInfo?.TraceResponseContentTime(); responseWrapper = new HttpResponseMessageWrapper(response); if (!this.Credentials.IsAuthenticationChallenge(responseWrapper)) { // Validate the token after it has been successfully authenticated with the server. if (provider != null) { provider.ValidateToken(token, responseWrapper); } // Make sure that once we can authenticate with the service that we turn off the // Expect100Continue behavior to increase performance. this.ExpectContinue = false; succeeded = true; break; } else { // In the case of a Windows token, only apply it to the web proxy if it // returned a 407 Proxy Authentication Required. If we didn't get this // status code back, then the proxy (if there is one) is clearly working fine, // so we shouldn't mess with its credentials. lastResponseDemandedProxyAuth = responseWrapper.StatusCode == HttpStatusCode.ProxyAuthenticationRequired; // Invalidate the token and ensure that we have the correct token provider for the challenge // which we just received VssHttpEventSource.Log.AuthenticationFailed(traceActivity, response); if (provider != null) { provider.InvalidateToken(token); } // Ensure we have an appropriate token provider for the current challenge provider = this.Credentials.CreateTokenProvider(request.RequestUri, responseWrapper, token); // Make sure we don't invoke the provider in an invalid state if (provider == null) { VssHttpEventSource.Log.IssuedTokenProviderNotFound(traceActivity); break; } else if (provider.GetTokenIsInteractive && this.Credentials.PromptType == CredentialPromptType.DoNotPrompt) { VssHttpEventSource.Log.IssuedTokenProviderPromptRequired(traceActivity, provider); break; } // If the user has already tried once but still unauthorized, stop retrying. The main scenario for this condition // is a user typed in a valid credentials for a hosted account but the associated identity does not have // access. We do not want to continually prompt 3 times without telling them the failure reason. In the // next release we should rethink about presenting user the failure and options between retries. IEnumerable <String> headerValues; Boolean hasAuthenticateError = response.Headers.TryGetValues(HttpHeaders.VssAuthenticateError, out headerValues) && !String.IsNullOrEmpty(headerValues.FirstOrDefault()); if (retries == 0 || (retries < m_maxAuthRetries && hasAuthenticateError)) { break; } // Now invoke the provider and await the result token = await provider.GetTokenAsync(token, tokenSource.Token).ConfigureAwait(false); // I always see 0 here, but the method above could take more time so keep for now traceInfo?.TraceGetTokenTime(); // If we just received a token, lets ask the server for the VSID request.Headers.Add(HttpHeaders.VssUserData, String.Empty); retries--; } }while (retries >= 0); if (traceInfo != null) { traceInfo.TokenRetries = m_maxAuthRetries - retries; } // We're out of retries and the response was an auth challenge -- then the request was unauthorized // and we will throw a strongly-typed exception with a friendly error message. if (!succeeded && response != null && this.Credentials.IsAuthenticationChallenge(responseWrapper)) { String message = null; IEnumerable <String> serviceError; if (response.Headers.TryGetValues(HttpHeaders.TfsServiceError, out serviceError)) { message = UriUtility.UrlDecode(serviceError.FirstOrDefault()); } else { message = CommonResources.VssUnauthorized(request.RequestUri.GetLeftPart(UriPartial.Authority)); } // Make sure we do not leak the response object when raising an exception if (response != null) { response.Dispose(); } VssHttpEventSource.Log.HttpRequestUnauthorized(traceActivity, request, message); VssUnauthorizedException unauthorizedException = new VssUnauthorizedException(message); if (provider != null) { unauthorizedException.Data.Add(CredentialsType, provider.CredentialType); } throw unauthorizedException; } return(response); } catch (OperationCanceledException ex) { if (cancellationToken.IsCancellationRequested) { VssHttpEventSource.Log.HttpRequestCancelled(traceActivity, request); throw; } else { VssHttpEventSource.Log.HttpRequestTimedOut(traceActivity, request, this.Settings.SendTimeout); throw new TimeoutException(CommonResources.HttpRequestTimeout(this.Settings.SendTimeout), ex); } } finally { // We always dispose of the token source since otherwise we leak resources if there is a timer pending if (tokenSource != null) { tokenSource.Dispose(); } traceInfo?.TraceTrailingTime(); } }
protected override async Task <HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Int32 attempt = 1; HttpResponseMessage response = null; HttpRequestException exception = null; VssTraceActivity traceActivity = VssTraceActivity.Current; // Allow overriding default retry options per request VssHttpRetryOptions retryOptions = m_retryOptions; object retryOptionsObject; if (request.Options.TryGetValue(HttpRetryOptionsKey, out retryOptionsObject)) // NETSTANDARD compliant, TryGetValue<T> is not { // Fallback to default options if object of unexpected type was passed retryOptions = retryOptionsObject as VssHttpRetryOptions ?? m_retryOptions; } TimeSpan minBackoff = retryOptions.MinBackoff; Int32 maxAttempts = retryOptions.MaxRetries + 1; IVssHttpRetryInfo retryInfo = null; object retryInfoObject; if (request.Options.TryGetValue(HttpRetryInfoKey, out retryInfoObject)) // NETSTANDARD compliant, TryGetValue<T> is not { retryInfo = retryInfoObject as IVssHttpRetryInfo; } if (IsLowPriority(request)) { // Increase the backoff and retry count, low priority requests can be retried many times if the server is busy. minBackoff = TimeSpan.FromSeconds(minBackoff.TotalSeconds * 2); maxAttempts = maxAttempts * 10; } TimeSpan backoff = minBackoff; while (attempt <= maxAttempts) { // Reset the exception so we don't have a lingering variable exception = null; Boolean canRetry = false; SocketError? socketError = null; HttpStatusCode? statusCode = null; WebExceptionStatus?webExceptionStatus = null; WinHttpErrorCode? winHttpErrorCode = null; CurlErrorCode? curlErrorCode = null; string afdRefInfo = null; try { if (attempt == 1) { retryInfo?.InitialAttempt(request); } response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); if (attempt > 1) { TraceHttpRequestSucceededWithRetry(traceActivity, response, attempt); } // Verify the response is successful or the status code is one that may be retried. if (response.IsSuccessStatusCode) { break; } else { statusCode = response.StatusCode; afdRefInfo = response.Headers.TryGetValues(HttpHeaders.AfdResponseRef, out var headers) ? headers.First() : null; canRetry = m_retryOptions.IsRetryableResponse(response); } } catch (HttpRequestException ex) { exception = ex; canRetry = VssNetworkHelper.IsTransientNetworkException(exception, m_retryOptions, out statusCode, out webExceptionStatus, out socketError, out winHttpErrorCode, out curlErrorCode); } catch (TimeoutException) { throw; } if (attempt < maxAttempts && canRetry) { backoff = BackoffTimerHelper.GetExponentialBackoff(attempt, minBackoff, m_retryOptions.MaxBackoff, m_retryOptions.BackoffCoefficient); retryInfo?.Retry(backoff); TraceHttpRequestRetrying(traceActivity, request, attempt, backoff, statusCode, webExceptionStatus, socketError, winHttpErrorCode, curlErrorCode, afdRefInfo); } else { if (attempt < maxAttempts) { if (exception == null) { TraceHttpRequestFailed(traceActivity, request, statusCode != null ? statusCode.Value : (HttpStatusCode)0, afdRefInfo); } else { TraceHttpRequestFailed(traceActivity, request, exception); } } else { TraceHttpRequestFailedMaxAttempts(traceActivity, request, attempt, statusCode, webExceptionStatus, socketError, winHttpErrorCode, curlErrorCode, afdRefInfo); } break; } // Make sure to dispose of this so we don't keep the connection open if (response != null) { response.Dispose(); } attempt++; TraceRaw(request, 100011, TraceLevel.Error, "{{ \"Client\":\"{0}\", \"Endpoint\":\"{1}\", \"Attempt\":{2}, \"MaxAttempts\":{3}, \"Backoff\":{4} }}", m_clientName, request.RequestUri.Host, attempt, maxAttempts, backoff.TotalMilliseconds); await Task.Delay(backoff, cancellationToken).ConfigureAwait(false); } if (exception != null) { throw exception; } return(response); }
protected virtual void TraceHttpRequestRetrying(VssTraceActivity activity, HttpRequestMessage request, Int32 attempt, TimeSpan backoffDuration, HttpStatusCode?httpStatusCode, WebExceptionStatus?webExceptionStatus, SocketError?socketErrorCode, WinHttpErrorCode?winHttpErrorCode, CurlErrorCode?curlErrorCode, string afdRefInfo) { VssHttpEventSource.Log.HttpRequestRetrying(activity, request, attempt, backoffDuration, httpStatusCode, webExceptionStatus, socketErrorCode, winHttpErrorCode, curlErrorCode, afdRefInfo); }
protected virtual void TraceHttpRequestSucceededWithRetry(VssTraceActivity activity, HttpResponseMessage response, Int32 attempt) { VssHttpEventSource.Log.HttpRequestSucceededWithRetry(activity, response, attempt); }
protected virtual void TraceHttpRequestFailedMaxAttempts(VssTraceActivity activity, HttpRequestMessage request, Int32 attempt, HttpStatusCode?httpStatusCode, WebExceptionStatus?webExceptionStatus, SocketError?socketErrorCode, WinHttpErrorCode?winHttpErrorCode, CurlErrorCode?curlErrorCode, string afdRefInfo) { VssHttpEventSource.Log.HttpRequestFailedMaxAttempts(activity, request, attempt, httpStatusCode, webExceptionStatus, socketErrorCode, winHttpErrorCode, curlErrorCode, afdRefInfo); }
protected virtual void TraceHttpRequestFailed(VssTraceActivity activity, HttpRequestMessage request, Exception exception) { VssHttpEventSource.Log.HttpRequestFailed(activity, request, exception); }
protected virtual void TraceHttpRequestFailed(VssTraceActivity activity, HttpRequestMessage request, HttpStatusCode statusCode, string afdRefInfo) { VssHttpEventSource.Log.HttpRequestFailed(activity, request, statusCode, afdRefInfo); }
/// <summary> /// Retrieves a token for the credentials. /// </summary> /// <param name="failedToken">The token which previously failed authentication, if available</param> /// <param name="cancellationToken">The <c>CancellationToken</c>that will be assigned to the new task</param> /// <returns>A security token for the current credentials</returns> public async Task <IssuedToken> GetTokenAsync( IssuedToken failedToken, CancellationToken cancellationToken) { IssuedToken currentToken = this.CurrentToken; VssTraceActivity traceActivity = VssTraceActivity.Current; Stopwatch aadAuthTokenTimer = Stopwatch.StartNew(); try { VssHttpEventSource.Log.AuthenticationStart(traceActivity); if (currentToken != null) { VssHttpEventSource.Log.IssuedTokenRetrievedFromCache(traceActivity, this, currentToken); return(currentToken); } else { GetTokenOperation operation = null; try { GetTokenOperation operationInProgress; operation = CreateOperation(traceActivity, failedToken, cancellationToken, out operationInProgress); if (operationInProgress == null) { return(await operation.GetTokenAsync(traceActivity).ConfigureAwait(false)); } else { return(await operationInProgress.WaitForTokenAsync(traceActivity, cancellationToken).ConfigureAwait(false)); } } finally { lock (m_thisLock) { m_operations.Remove(operation); } operation?.Dispose(); } } } finally { VssHttpEventSource.Log.AuthenticationStop(traceActivity); aadAuthTokenTimer.Stop(); TimeSpan getTokenTime = aadAuthTokenTimer.Elapsed; if (getTokenTime.TotalSeconds >= c_slowTokenAcquisitionTimeInSeconds) { // It may seem strange to pass the string value of TotalSeconds into this method, but testing // showed that ETW is persnickety when you register a method in an EventSource that doesn't // use strings or integers as its parameters. It is easier to simply give the method a string // than figure out to get ETW to reliably accept a double or TimeSpan. VssHttpEventSource.Log.AuthorizationDelayed(getTokenTime.TotalSeconds.ToString()); } } }