diff --git a/Cli-Shared/Program.cs b/Cli-Shared/Program.cs index 3bf8f8034..6f1e2d634 100644 --- a/Cli-Shared/Program.cs +++ b/Cli-Shared/Program.cs @@ -420,8 +420,15 @@ private static async Task CreateAuthentication(OperationArgu case AuthorityType.AzureDirectory: Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Azure Directory."); + Guid tenantId = Guid.Empty; + // Get the identity of the tenant. - Guid tenantId = await BaseVstsAuthentication.DetectAuthority(operationArguments.TargetUri); + var result = await BaseVstsAuthentication.DetectAuthority(operationArguments.TargetUri); + + if (result.Key) + { + tenantId = result.Value; + } // return the allocated authority or a generic AAD backed VSTS authentication object return authority ?? new VstsAadAuthentication(tenantId, VstsCredentialScope, secrets); diff --git a/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs b/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs index d2685e27d..e57fd30f0 100644 --- a/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs +++ b/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs @@ -30,6 +30,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.IdentityModel.Clients.ActiveDirectory; +using System.IO.Compression; namespace Microsoft.Alm.Authentication { @@ -114,7 +115,7 @@ public override void DeleteCredentials(TargetUri targetUri) /// The identity of the authority tenant; otherwise. /// if the authority is Visual Studio Online; otherwise [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")] - public static async Task DetectAuthority(TargetUri targetUri) + public static async Task> DetectAuthority(TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; @@ -141,7 +142,7 @@ public static async Task DetectAuthority(TargetUri targetUri) // Check the cache for an existing value if (cache.TryGetValue(tenantUrl, out tenantId)) - return tenantId; + return new KeyValuePair(true, tenantId); try { @@ -174,7 +175,7 @@ public static async Task DetectAuthority(TargetUri targetUri) await SerializeTenantCache(cache); // Success, notify the caller - return tenantId; + return new KeyValuePair(true, tenantId); } } } @@ -185,7 +186,7 @@ public static async Task DetectAuthority(TargetUri targetUri) } // if all else fails, fallback to basic authentication - return tenantId; + return new KeyValuePair(false, tenantId); } /// @@ -215,8 +216,13 @@ public static async Task GetAuthentication( BaseAuthentication authentication = null; + var result = await DetectAuthority(targetUri); + + if (!result.Key) + return null; + // Query for the tenant's identity - Guid tenantId = await DetectAuthority(targetUri); + Guid tenantId = result.Value; // empty Guid is MSA, anything else is AAD if (tenantId == Guid.Empty) @@ -303,13 +309,15 @@ protected async Task GeneratePersonalAccessToken( private const char CachePairSeperator = '='; private const char CachePairTerminator = '\0'; + private const string CachePathDirectory = "GitCredentialManager"; + private const string CachePathFileName = "tenant.cache"; private static async Task> DeserializeTenantCache() { var encoding = new UTF8Encoding(false); - string path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - path = Path.Combine(path, "GCM", "tenants"); + var path = GetCachePath(); + string data = null; Dictionary cache = new Dictionary(StringComparer.OrdinalIgnoreCase); // Attempt up to five times to read from the cache @@ -320,33 +328,10 @@ private static async Task> DeserializeTenantCache() // Just open the file from disk, the tenant identities are not secret and therefore safely // left as unencrypted plain text. using (var stream = File.Open(path, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read)) - using (var reader = new StreamReader(stream, encoding)) + using (var inflate = new GZipStream(stream, CompressionMode.Decompress)) + using (var reader = new StreamReader(inflate, encoding)) { - string data = await reader.ReadToEndAsync(); - - if (data.Length > 0) - { - int last = 0; - int next = -1; - - while ((next = data.IndexOf(CachePairTerminator, last)) > 0) - { - int idx = data.IndexOf(CachePairSeperator, last, next - last); - if (idx > 0) - { - string key = data.Substring(last, idx - last); - string val = data.Substring(idx + 1, next - idx - 1); - - Guid id; - if (Guid.TryParse(val, out id)) - { - cache[key] = id; - } - - last = next + 1; - } - } - } + data = await reader.ReadToEndAsync(); } } catch when (i < 5) @@ -356,14 +341,68 @@ private static async Task> DeserializeTenantCache() } } + // Parse the inflated data + if (data.Length > 0) + { + int last = 0; + int next = -1; + + while ((next = data.IndexOf(CachePairTerminator, last)) > 0) + { + int idx = data.IndexOf(CachePairSeperator, last, next - last); + if (idx > 0) + { + string key = data.Substring(last, idx - last); + string val = data.Substring(idx + 1, next - idx - 1); + + Guid id; + if (Guid.TryParse(val, out id)) + { + cache[key] = id; + } + + last = next + 1; + } + } + } + return cache; } + private static string GetCachePath() + { + string path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + path = Path.Combine(path, CachePathDirectory); + + // Create the directory if necissary + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + // Append the file name to the path + path = Path.Combine(path, CachePathFileName); + + return path; + } + private static async Task SerializeTenantCache(Dictionary cache) { var encoding = new UTF8Encoding(false); - string path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - path = Path.Combine(path, "GCM", "tenants"); + string path = GetCachePath(); + + StringBuilder builder = new StringBuilder(); + + // Write each key/value pair as key=value\0 + foreach (var pair in cache) + { + builder.Append(pair.Key) + .Append('=') + .Append(pair.Value.ToString()) + .Append('\0'); + } + + string data = builder.ToString(); // Attempt up to five times to write to the cache for (int i = 0; i < 5; i += 1) @@ -373,20 +412,9 @@ private static async Task SerializeTenantCache(Dictionary cache) // Just open the file from disk, the tenant identities are not secret and therefore safely // left as unencrypted plain text. using (var stream = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) - using (var writer = new StreamWriter(stream, encoding)) + using (var deflate = new GZipStream(stream, CompressionMode.Compress)) + using (var writer = new StreamWriter(deflate, encoding)) { - StringBuilder builder = new StringBuilder(); - - foreach (var pair in cache) - { - builder.Append(pair.Key) - .Append('=') - .Append(pair.Value.ToString()) - .Append('\0'); - } - - string data = builder.ToString(); - await writer.WriteAsync(data); } } diff --git a/Microsoft.Vsts.Authentication/VstsAdalTokenCache.cs b/Microsoft.Vsts.Authentication/VstsAdalTokenCache.cs index 987574295..86698ea6d 100644 --- a/Microsoft.Vsts.Authentication/VstsAdalTokenCache.cs +++ b/Microsoft.Vsts.Authentication/VstsAdalTokenCache.cs @@ -24,6 +24,7 @@ **/ using System; +using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using Microsoft.IdentityModel.Clients.ActiveDirectory; @@ -32,8 +33,11 @@ namespace Microsoft.Alm.Authentication { internal class VstsAdalTokenCache : IdentityModel.Clients.ActiveDirectory.TokenCache { - private const string AdalCachePath = @"Microsoft\VSCommon\VSAccountManagement"; - private const string AdalCacheFile = @"AdalCache.cache"; + private readonly IReadOnlyList> AdalCachePaths = new string[][] + { + new [] { @".IdentityService", @"IdentityServiceAdalCache.cache", }, // VS2017 Adal v3 cache + new [] { @"Microsoft\VSCommon\VSAccountManagement", @"AdalCache.cache", }, // VS2015 Adal v2 cache + }; /// /// Default constructor. @@ -41,14 +45,21 @@ internal class VstsAdalTokenCache : IdentityModel.Clients.ActiveDirectory.TokenC public VstsAdalTokenCache() { string localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - string directoryPath = Path.Combine(localAppDataPath, AdalCachePath); AfterAccess = AfterAccessNotification; BeforeAccess = BeforeAccessNotification; - string filePath = Path.Combine(directoryPath, AdalCacheFile); + for (int i = 0; i < AdalCachePaths.Count; i += 1) + { + string directoryPath = Path.Combine(localAppDataPath, AdalCachePaths[i][0]); + string filePath = Path.Combine(directoryPath, AdalCachePaths[i][1]); - _cacheFilePath = filePath; + if (File.Exists(filePath)) + { + _cacheFilePath = filePath; + break; + } + } BeforeAccessNotification(null); }