private GetTokenOperation CreateOperation( VssTraceActivity traceActivity, IssuedToken failedToken, CancellationToken cancellationToken, out GetTokenOperation operationInProgress) { operationInProgress = null; GetTokenOperation operation = null; lock (m_thisLock) { if (m_operations == null) { m_operations = new List <GetTokenOperation>(); } // Grab the main operation which is doing the work (if any) if (m_operations.Count > 0) { operationInProgress = m_operations[0]; // Use the existing completion source when creating the new operation operation = new GetTokenOperation(traceActivity, this, failedToken, cancellationToken, operationInProgress.CompletionSource); } else { operation = new GetTokenOperation(traceActivity, this, failedToken, cancellationToken); } m_operations.Add(operation); } return(operation); }
private void ApplyToken( HttpRequestMessage request, IssuedToken token, bool applyICredentialsToWebProxy = false) { if (token == null) { return; } ICredentials credentialsToken = token as ICredentials; if (credentialsToken != null) { if (applyICredentialsToWebProxy) { HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler; if (httpClientHandler != null && httpClientHandler.Proxy != null) { httpClientHandler.Proxy.Credentials = credentialsToken; } } m_credentialWrapper.InnerCredentials = credentialsToken; } else { token.ApplyTo(new HttpRequestMessageWrapper(request)); } }
public GetTokenOperation( VssTraceActivity activity, IssuedTokenProvider provider, IssuedToken failedToken, CancellationToken cancellationToken) : this(activity, provider, failedToken, cancellationToken, new DisposableTaskCompletionSource <IssuedToken>(), true) { }
protected virtual void OnTokenInvalidated(IssuedToken token) { if (Credential.Storage != null && TokenStorageUrl != null) { Credential.Storage.RemoveTokenValue(TokenStorageUrl, token); } VssHttpEventSource.Log.IssuedTokenInvalidated(VssTraceActivity.Current, this, token); }
protected virtual void OnTokenValidated(IssuedToken token) { // Store the validated token to the token storage if it is not originally from there. if (!token.FromStorage && TokenStorageUrl != null) { Credential.Storage?.StoreToken(TokenStorageUrl, token); } VssHttpEventSource.Log.IssuedTokenValidated(VssTraceActivity.Current, this, token); }
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> /// 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> protected virtual Task <IssuedToken> OnGetTokenAsync( IssuedToken failedToken, CancellationToken cancellationToken) { if (this.Credential.Prompt != null) { return(this.Credential.Prompt.GetTokenAsync(this, failedToken)); } else { return(Task.FromResult <IssuedToken>(null)); } }
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; }
/// <summary> /// Invalidates the current token if the provided reference is the current token. /// </summary> /// <param name="token">The token reference which should be invalidated</param> internal void InvalidateToken(IssuedToken token) { bool invalidated = false; lock (m_thisLock) { if (token != null && object.ReferenceEquals(this.CurrentToken, token)) { this.CurrentToken = null; invalidated = true; } } if (invalidated) { OnTokenInvalidated(token); } }
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> /// Validates the current token if the provided reference is the current token and it /// has not been validated before. /// </summary> /// <param name="token">The token which should be validated</param> /// <param name="webResponse">The web response which used the token</param> internal void ValidateToken( IssuedToken token, IHttpResponse webResponse) { if (token == null) { return; } lock (m_thisLock) { IssuedToken tokenToValidate = OnValidatingToken(token, webResponse); if (tokenToValidate.IsAuthenticated) { return; } try { // Perform validation which may include matching user information from the response // with that from the stored connection. If user information mismatch, an exception // will be thrown and the token will not be authenticated, which means if the same // token is ever used again in a different request it will be revalidated and fail. tokenToValidate.GetUserData(webResponse); OnTokenValidated(tokenToValidate); // Set the token to be authenticated. tokenToValidate.Authenticated(); } finally { // When the token fails validation, we null its reference from the token provider so it // would not be used again by the consumers of both. Note that we only update the current // token of the provider if it is the original token being validated, because we do not // want to overwrite a different token. if (object.ReferenceEquals(this.CurrentToken, token)) { this.CurrentToken = tokenToValidate.IsAuthenticated ? tokenToValidate : null; } } } }
/// <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> /// Creates a token provider suitable for handling the challenge presented in the response. /// </summary> /// <param name="serverUrl">The targeted server</param> /// <param name="response">The challenge response</param> /// <param name="failedToken">The failed token</param> /// <returns>An issued token provider instance</returns> internal IssuedTokenProvider CreateTokenProvider( Uri serverUrl, IHttpResponse response, IssuedToken failedToken) { if (response != null && !IsAuthenticationChallenge(response)) { throw new InvalidOperationException(); } if (InitialToken == null && Storage != null) { if (TokenStorageUrl == null) { throw new InvalidOperationException($"The {nameof(TokenStorageUrl)} property must have a value if the {nameof(Storage)} property is set on this instance of {GetType().Name}."); } InitialToken = Storage.RetrieveToken(TokenStorageUrl, CredentialType); } IssuedTokenProvider provider = OnCreateTokenProvider(serverUrl, response); if (provider != null) { provider.TokenStorageUrl = TokenStorageUrl; } // If the initial token is the one which failed to authenticate, don't // use it again and let the token provider get a new token. if (provider != null) { if (InitialToken != null && !Object.ReferenceEquals(InitialToken, failedToken)) { provider.CurrentToken = InitialToken; } } return(provider); }
protected FederatedCredential(IssuedToken initialToken) : base(initialToken) { }
protected IssuedTokenCredential(IssuedToken initialToken) { InitialToken = initialToken; }
/// <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()); } } }
/// <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(); } }
/// <summary> /// Invoked when the current token is being validated. When overriden in a derived class, /// validate and return the validated token. /// </summary> /// <remarks>Is called inside a lock in <c>ValidateToken</c></remarks> /// <param name="token">The token to validate</param> /// <param name="webResponse">The web response which used the token</param> /// <returns>The validated token</returns> protected virtual IssuedToken OnValidatingToken( IssuedToken token, IHttpResponse webResponse) { return(token); }