/// <summary> /// Deletes a `<see cref="Credential"/>` from the storage used by the authentication object. /// </summary> /// <param name="targetUri">The uniform resource indicator used to uniquely identify the credentials.</param> public override void DeleteCredentials(TargetUri targetUri) { BaseSecureStore.ValidateTargetUri(targetUri); Credential credentials = PersonalAccessTokenStore.ReadCredentials(targetUri); // This ought to be async, but the base type's method isn't. Task.Run(async() => { // Attempt to validate the credentials, if they're truly invalid delete them. if (!await ValidateCredentials(targetUri, credentials)) { PersonalAccessTokenStore.DeleteCredentials(targetUri); // Remove any related entries from the tenant cache because tenant change // could the be source of the invalidation, and not purging the cache will // trap the user in a limbo state of invalid credentials. // Deserialize the cache and remove any matching entry. string tenantUrl = targetUri.ToString(); var cache = await DeserializeTenantCache(); // Attempt to remove the URL entry, if successful serialize the cache. if (cache.Remove(tenantUrl)) { await SerializeTenantCache(cache); } } }).Wait(); }
internal async Task <TargetUri> GetIdentityServiceUri(TargetUri targetUri, Secret authorization) { const string LocationServiceUrlPathAndQuery = "_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381?api-version=1.0"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (authorization is null) { throw new ArgumentNullException(nameof(authorization)); } string tenantUrl = targetUri.ToString(false, true, false); if (targetUri.TargetUriContainsUsername) { string escapedUserInfo = Uri.EscapeUriString(targetUri.TargetUriUsername); tenantUrl = tenantUrl + escapedUserInfo + "/"; } var locationServiceUrl = tenantUrl + LocationServiceUrlPathAndQuery; var requestUri = new TargetUri(locationServiceUrl, targetUri.ProxyUri?.ToString()); var options = new NetworkRequestOptions(true) { Authorization = authorization, }; try { using (var response = await Network.HttpGetAsync(requestUri, options)) { if (response.IsSuccessStatusCode) { using (HttpContent content = response.Content) { string responseText = await content.ReadAsStringAsync(); Match match; if ((match = Regex.Match(responseText, @"\""location\""\:\""([^\""]+)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success) { string identityServiceUrl = match.Groups[1].Value; var idenitityServiceUri = new Uri(identityServiceUrl, UriKind.Absolute); return(new TargetUri(idenitityServiceUri, targetUri.ProxyUri)); } } } Trace.WriteLine($"failed to find Identity Service for '{targetUri}' via location service [{(int)response.StatusCode} {response.ReasonPhrase}]."); } } catch (Exception exception) { Trace.WriteException(exception); throw new VstsLocationServiceException($"Failed to find Identity Service for `{targetUri}`.", exception); } return(null); }
private StringContent GetAccessTokenRequestBody(TargetUri targetUri, VstsTokenScope tokenScope, TimeSpan?duration = null) { const string ContentBasicJsonFormat = "{{ \"scope\" : \"{0}\", \"displayName\" : \"Git: {1} on {2}\" }}"; const string ContentTimedJsonFormat = "{{ \"scope\" : \"{0}\", \"displayName\" : \"Git: {1} on {2}\", \"validTo\": \"{3:u}\" }}"; const string HttpJsonContentType = "application/json"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (tokenScope is null) { throw new ArgumentNullException(nameof(tokenScope)); } string tokenUrl = targetUri.ToString(username: false, port: true, path: false); if (targetUri.TargetUriContainsUsername) { string escapedUserInfo = Uri.EscapeUriString(targetUri.TargetUriUsername); tokenUrl = tokenUrl + escapedUserInfo + "/"; } Trace.WriteLine($"creating access token scoped to '{tokenScope}' for '{targetUri}'"); string jsonContent = (duration.HasValue && duration.Value > TimeSpan.FromHours(1)) ? string.Format(InvariantCulture, ContentTimedJsonFormat, tokenScope, tokenUrl, Environment.MachineName, DateTime.UtcNow + duration.Value) : string.Format(InvariantCulture, ContentBasicJsonFormat, tokenScope, tokenUrl, Environment.MachineName); StringContent content = new StringContent(jsonContent, Encoding.UTF8, HttpJsonContentType); return(content); }
/// <summary> /// Deletes a `<see cref="Credential"/>` from the storage used by the authentication object. /// </summary> /// <param name="targetUri">The uniform resource indicator used to uniquely identify the credentials.</param> public override async Task <bool> DeleteCredentials(TargetUri targetUri) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } Credential credentials = await PersonalAccessTokenStore.ReadCredentials(targetUri); // Attempt to validate the credentials, if they're truly invalid delete them. if (!await ValidateCredentials(targetUri, credentials)) { await PersonalAccessTokenStore.DeleteCredentials(targetUri); // Remove any related entries from the tenant cache because tenant change // could the be source of the invalidation, and not purging the cache will // trap the user in a limbo state of invalid credentials. // Deserialize the cache and remove any matching entry. string tenantUrl = targetUri.ToString(); var cache = await DeserializeTenantCache(Context); // Attempt to remove the URL entry, if successful serialize the cache. if (cache.Remove(tenantUrl)) { await SerializeTenantCache(Context, cache); } return(true); } return(false); }
public static string UriToUrl(TargetUri targetUri, string @namespace) { BaseSecureStore.ValidateTargetUri(targetUri); if (string.IsNullOrWhiteSpace(@namespace)) { throw new ArgumentNullException(@namespace); } string targetName = $"{@namespace}:{targetUri.ToString(false, true, true)}"; targetName = targetName.TrimEnd('/', '\\'); return(targetName); }
internal static string GetTargetUrl(TargetUri targetUri) { string requestUrl = targetUri.ToString(false, true, false); // Handle the Azure userinfo -> path conversion.AzureBaseUrlHost if (targetUri.Host.EndsWith(AzureBaseUrlHost, StringComparison.OrdinalIgnoreCase) && targetUri.ContainsUserInfo) { string escapedUserInfo = Uri.EscapeUriString(targetUri.UserInfo); requestUrl = requestUrl + escapedUserInfo + "/"; } return(requestUrl); }
/// <summary> /// Generate a key based on the `<paramref name="targetUri"/>`, may include username, port, etc. /// </summary> public static string UriToIdentityUrl(TargetUri targetUri, string @namespace) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (string.IsNullOrWhiteSpace(@namespace)) { throw new ArgumentNullException(@namespace); } string targetName = $"{@namespace}:{targetUri.ToString(true, true, true)}"; targetName = targetName.TrimEnd('/', '\\'); return(targetName); }
internal static TargetUri GetConnectionDataUri(TargetUri targetUri) { const string VstsValidationUrlPath = "_apis/connectiondata"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } string requestUrl = targetUri.ToString(false, true, false); if (targetUri.TargetUriContainsUsername) { string escapedUserInfo = Uri.EscapeUriString(targetUri.TargetUriUsername); requestUrl = requestUrl + escapedUserInfo + "/"; } // Create a URL to the connection data end-point, it's deployment level and "always on". string validationUrl = requestUrl + VstsValidationUrlPath; return(new TargetUri(validationUrl, targetUri.ProxyUri?.ToString())); }
public static async Task <KeyValuePair <bool, Guid> > DetectAuthority(TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { Git.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; WebResponse response; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "http") || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "https")) { // Query the cache first string tenantUrl = targetUri.ToString(); // Read the cache from disk var cache = await DeserializeTenantCache(); // Check the cache for an existing value if (cache.TryGetValue(tenantUrl, out tenantId)) { return(new KeyValuePair <bool, Guid>(true, tenantId)); } try { // Build a request that we expect to fail, do not allow redirect to sign in url var request = WebRequest.CreateHttp(targetUri); request.UserAgent = Global.UserAgent; request.Method = "HEAD"; request.AllowAutoRedirect = false; // Get the response from the server response = await request.GetResponseAsync(); } catch (WebException exception) { Git.Trace.WriteLine($"unable to get response from '{targetUri}' due to '{exception.Status}'."); // Given the number proxy related failures we see, emit a message about the failure potentially // being related to a proxy or gateway misconfiguration. switch (exception.Status) { case WebExceptionStatus.ConnectFailure: case WebExceptionStatus.PipelineFailure: case WebExceptionStatus.ProtocolError: case WebExceptionStatus.ReceiveFailure: case WebExceptionStatus.SecureChannelFailure: case WebExceptionStatus.SendFailure: case WebExceptionStatus.ServerProtocolViolation: case WebExceptionStatus.TrustFailure: { Git.Trace.WriteLine($"is there a proxy or gateway incorrectly configured?"); } break; } response = exception.Response; } // If the response exists and we have headers, parse them if (response != null && response.SupportsHeaders) { // Find the VSTS resource tenant entry tenant = response.Headers[VstsResourceTenantHeader]; if (!string.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(cache); // Success, notify the caller return(new KeyValuePair <bool, Guid>(true, tenantId)); } } } else { Git.Trace.WriteLine($"detected non-https based protocol: {targetUri.Scheme}."); } } // if all else fails, fallback to basic authentication return(new KeyValuePair <bool, Guid>(false, tenantId)); }
public static async Task <KeyValuePair <bool, Guid> > DetectAuthority(TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { Git.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; WebResponse response; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "http") || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, "https")) { // Query the cache first string tenantUrl = targetUri.ToString(); // Read the cache from disk var cache = await DeserializeTenantCache(); // Check the cache for an existing value if (cache.TryGetValue(tenantUrl, out tenantId)) { return(new KeyValuePair <bool, Guid>(true, tenantId)); } try { // build a request that we expect to fail, do not allow redirect to sign in url var request = WebRequest.CreateHttp(targetUri); request.UserAgent = Global.UserAgent; request.Method = "HEAD"; request.AllowAutoRedirect = false; // get the response from the server response = await request.GetResponseAsync(); } catch (WebException exception) { response = exception.Response; } // if the response exists and we have headers, parse them if (response != null && response.SupportsHeaders) { // find the VSTS resource tenant entry tenant = response.Headers[VstsResourceTenantHeader]; if (!String.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(cache); // Success, notify the caller return(new KeyValuePair <bool, Guid>(true, tenantId)); } } } else { Git.Trace.WriteLine($"detected non-https based protocol: {targetUri.Scheme}."); } } // if all else fails, fallback to basic authentication return(new KeyValuePair <bool, Guid>(false, tenantId)); }
public static async Task <Guid?> DetectAuthority(RuntimeContext context, TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { context.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttp) || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttps)) { // Query the cache first. string tenantUrl = targetUri.ToString(); // Read the cache from disk. var cache = await DeserializeTenantCache(context); // Check the cache for an existing value. if (cache.TryGetValue(tenantUrl, out tenantId)) { return(tenantId); } var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; try { using (var response = await context.Network.HttpGetAsync(targetUri, options)) { if (response.IsSuccessStatusCode) { if (response.Headers.TryGetValues(VstsResourceTenantHeader, out IEnumerable <string> values)) { tenant = System.Linq.Enumerable.First(values); if (!string.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Success, notify the caller return(tenantId); } } } else { context.Trace.WriteLine($"unable to get response from '{targetUri}', server responded with '{(int)response.StatusCode} {response.StatusCode}'."); } } } catch (HttpRequestException exception) { context.Trace.WriteLine($"unable to get response from '{targetUri}' due to '{exception.Message}'."); } } else { context.Trace.WriteLine($"detected non-https based protocol: '{targetUri.Scheme}'."); } } // Fallback to basic authentication. return(null); }