Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #387 from whoisj/fix-vsts-detect
Browse files Browse the repository at this point in the history
Fix vsts detect
  • Loading branch information
J Wyman authored Mar 10, 2017
2 parents 46dca66 + e541b02 commit 74737f8
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 54 deletions.
9 changes: 8 additions & 1 deletion Cli-Shared/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,15 @@ private static async Task<BaseAuthentication> 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);
Expand Down
124 changes: 76 additions & 48 deletions Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO.Compression;

namespace Microsoft.Alm.Authentication
{
Expand Down Expand Up @@ -114,7 +115,7 @@ public override void DeleteCredentials(TargetUri targetUri)
/// <param name="tenantId">The identity of the authority tenant; <see cref="Guid.Empty"/> otherwise.</param>
/// <returns><see langword="true"/> if the authority is Visual Studio Online; <see langword="false"/> otherwise</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
public static async Task<Guid> DetectAuthority(TargetUri targetUri)
public static async Task<KeyValuePair<bool, Guid>> DetectAuthority(TargetUri targetUri)
{
const string VstsBaseUrlHost = "visualstudio.com";
const string VstsResourceTenantHeader = "X-VSS-ResourceTenant";
Expand All @@ -141,7 +142,7 @@ public static async Task<Guid> DetectAuthority(TargetUri targetUri)

// Check the cache for an existing value
if (cache.TryGetValue(tenantUrl, out tenantId))
return tenantId;
return new KeyValuePair<bool, Guid>(true, tenantId);

try
{
Expand Down Expand Up @@ -174,7 +175,7 @@ public static async Task<Guid> DetectAuthority(TargetUri targetUri)
await SerializeTenantCache(cache);

// Success, notify the caller
return tenantId;
return new KeyValuePair<bool, Guid>(true, tenantId);
}
}
}
Expand All @@ -185,7 +186,7 @@ public static async Task<Guid> DetectAuthority(TargetUri targetUri)
}

// if all else fails, fallback to basic authentication
return tenantId;
return new KeyValuePair<bool, Guid>(false, tenantId);
}

/// <summary>
Expand Down Expand Up @@ -215,8 +216,13 @@ public static async Task<BaseAuthentication> 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)
Expand Down Expand Up @@ -303,13 +309,15 @@ protected async Task<Credential> 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<Dictionary<string, Guid>> 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<string, Guid> cache = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);

// Attempt up to five times to read from the cache
Expand All @@ -320,33 +328,10 @@ private static async Task<Dictionary<string, Guid>> 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)
Expand All @@ -356,14 +341,68 @@ private static async Task<Dictionary<string, Guid>> 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<string, Guid> 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)
Expand All @@ -373,20 +412,9 @@ private static async Task SerializeTenantCache(Dictionary<string, Guid> 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);
}
}
Expand Down
21 changes: 16 additions & 5 deletions Microsoft.Vsts.Authentication/VstsAdalTokenCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
**/

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
Expand All @@ -32,23 +33,33 @@ 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<IReadOnlyList<string>> AdalCachePaths = new string[][]
{
new [] { @".IdentityService", @"IdentityServiceAdalCache.cache", }, // VS2017 Adal v3 cache
new [] { @"Microsoft\VSCommon\VSAccountManagement", @"AdalCache.cache", }, // VS2015 Adal v2 cache
};

/// <summary>
/// Default constructor.
/// </summary>
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);
}
Expand Down

0 comments on commit 74737f8

Please sign in to comment.