Skip to content
This repository was archived by the owner on Dec 5, 2024. It is now read-only.

Commit 9c57623

Browse files
authored
Merge pull request #33 from github-for-unity/fixes/secure-keychain-adapter
Fix for Keychain to validate credentials from CredentialManager
2 parents 8aa5b73 + 0da5532 commit 9c57623

File tree

3 files changed

+98
-13
lines changed

3 files changed

+98
-13
lines changed

src/GitHub.Api/Authentication/Keychain.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,26 @@ public IKeychainAdapter Connect(UriString host)
5252

5353
public async Task<IKeychainAdapter> Load(UriString host)
5454
{
55-
var keychainAdapter = FindOrCreateAdapter(host);
55+
KeychainAdapter keychainAdapter = FindOrCreateAdapter(host);
56+
var cachedConnection = connectionCache[host];
57+
58+
logger.Trace($@"Loading KeychainAdapter Host:""{host}"" Cached Username:""{cachedConnection.Username}""");
5659

57-
logger.Trace("Load KeychainAdapter Host:\"{0}\"", host);
5860
var keychainItem = await credentialManager.Load(host);
5961

6062
if (keychainItem == null)
6163
{
62-
logger.Warning("Cannot load host from credential manager; removing from cache");
64+
logger.Warning("Cannot load host from Credential Manager; removing from cache");
65+
await Clear(host, false);
66+
}
67+
else if (keychainItem.Username != cachedConnection.Username)
68+
{
69+
logger.Warning("Item loaded from credential manager does not match connection cache ; removing from cache");
6370
await Clear(host, false);
6471
}
6572
else
6673
{
67-
logger.Trace("Loading KeychainItem:{0}", keychainItem.ToString());
74+
logger.Trace($@"Loaded from Credential Manager Host:""{keychainItem.Host}"" Username:""{keychainItem.Username}""");
6875
keychainAdapter.Set(keychainItem);
6976
}
7077

@@ -113,7 +120,10 @@ private void ReadCacheFromDisk()
113120
if (connections != null)
114121
{
115122
connectionCache =
116-
connections.Select(item => new Connection { Host = new UriString(item.Host), Username = item.Username })
123+
connections.Select(item => {
124+
logger.Trace("ReadCacheFromDisk Item Host:{0} Username:{1}", item.Host, item.Username);
125+
return new Connection { Host = new UriString(item.Host), Username = item.Username };
126+
})
117127
.ToDictionary(connection => connection.Host);
118128
}
119129
else

src/tests/UnitTests/Authentication/KeychainTests.cs

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class KeychainTests
1818
private static readonly SubstituteFactory SubstituteFactory = new SubstituteFactory();
1919

2020
[Test]
21-
public void Should_Initialize_When_Cache_Does_Not_Exist()
21+
public void ShouldInitializeWhenCacheDoesNotExist()
2222
{
2323
const string connectionsCachePath = @"c:\UserCachePath\";
2424

@@ -52,7 +52,7 @@ public void Should_Initialize_When_Cache_Does_Not_Exist()
5252
}
5353

5454
[Test]
55-
public void Should_Initialize_When_Cache_Invalid()
55+
public void ShouldInitializeWhenCacheInvalid()
5656
{
5757
const string connectionsCachePath = @"c:\UserCachePath\";
5858
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";
@@ -92,7 +92,7 @@ public void Should_Initialize_When_Cache_Invalid()
9292
}
9393

9494
[Test]
95-
public void Should_Initialize_When_Cache_Exists()
95+
public void ShouldInitializeWhenCacheExists()
9696
{
9797
const string connectionsCachePath = @"c:\UserCachePath\";
9898
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";
@@ -134,7 +134,7 @@ public void Should_Initialize_When_Cache_Exists()
134134
}
135135

136136
[Test]
137-
public void Should_Load_From_ConnectionManager()
137+
public void ShouldLoadFromConnectionManager()
138138
{
139139
const string connectionsCachePath = @"c:\UserCachePath\";
140140
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";
@@ -194,7 +194,7 @@ public void Should_Load_From_ConnectionManager()
194194
}
195195

196196
[Test]
197-
public void Should_Delete_From_Cache_When_Load_Returns_Null_From_ConnectionManager()
197+
public void ShouldDeleteFromCacheWhenLoadReturnsNullFromConnectionManager()
198198
{
199199
const string connectionsCachePath = @"c:\UserCachePath\";
200200
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";
@@ -248,7 +248,82 @@ public void Should_Delete_From_Cache_When_Load_Returns_Null_From_ConnectionManag
248248
}
249249

250250
[Test]
251-
public void Should_Connect_Set_Credentials_Token_And_Save()
251+
public void ShouldDeleteFromCacheWhenLoadReturnsNullFromConnectionManagerDueToUserMismatch()
252+
{
253+
const string connectionsCachePath = @"c:\UserCachePath\";
254+
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";
255+
256+
const string cachedUsername = "SomeCachedUser";
257+
const string credentialedUsername = "SomeCredentialedUser";
258+
259+
const string token = "SomeToken";
260+
261+
var hostUri = new UriString("https://github.com/");
262+
263+
var fileSystem = SubstituteFactory.CreateFileSystem(new CreateFileSystemOptions
264+
{
265+
FilesThatExist = new List<string> { connectionsCacheFile },
266+
FileContents = new Dictionary<string, IList<string>> {
267+
{connectionsCacheFile, new List<string> {$@"[{{""Host"":""https://github.com/"",""Username"":""{cachedUsername}""}}]"
268+
}}
269+
}
270+
});
271+
272+
NPath.FileSystem = fileSystem;
273+
274+
var environment = SubstituteFactory.CreateEnvironment();
275+
environment.UserCachePath.Returns(info => connectionsCachePath.ToNPath());
276+
environment.FileSystem.Returns(fileSystem);
277+
278+
var credentialManager = Substitute.For<ICredentialManager>();
279+
credentialManager.Load(hostUri).Returns(info =>
280+
{
281+
var credential = Substitute.For<ICredential>();
282+
credential.Username.Returns(credentialedUsername);
283+
credential.Token.Returns(token);
284+
credential.Host.Returns(hostUri);
285+
return TaskEx.FromResult(credential);
286+
});
287+
288+
var keychain = new Keychain(environment, credentialManager);
289+
keychain.Initialize();
290+
291+
fileSystem.Received(1).FileExists(connectionsCacheFile);
292+
fileSystem.DidNotReceive().FileDelete(Args.String);
293+
fileSystem.Received(1).ReadAllText(connectionsCacheFile);
294+
fileSystem.DidNotReceive().ReadAllLines(Args.String);
295+
fileSystem.DidNotReceive().WriteAllText(Args.String, Args.String);
296+
fileSystem.DidNotReceive().WriteAllLines(Args.String, Arg.Any<string[]>());
297+
298+
credentialManager.DidNotReceive().Load(Args.UriString);
299+
credentialManager.DidNotReceive().HasCredentials();
300+
credentialManager.DidNotReceive().Delete(Args.UriString);
301+
credentialManager.DidNotReceive().Save(Arg.Any<ICredential>());
302+
303+
fileSystem.ClearReceivedCalls();
304+
305+
var uriString = keychain.Connections.FirstOrDefault();
306+
var keychainAdapter = keychain.Load(uriString).Result;
307+
keychainAdapter.Credential.Should().BeNull();
308+
309+
keychainAdapter.OctokitCredentials.AuthenticationType.Should().Be(AuthenticationType.Anonymous);
310+
keychainAdapter.OctokitCredentials.Login.Should().BeNull();
311+
keychainAdapter.OctokitCredentials.Password.Should().BeNull();
312+
313+
fileSystem.DidNotReceive().FileExists(Args.String);
314+
fileSystem.DidNotReceive().ReadAllText(Args.String);
315+
fileSystem.DidNotReceive().FileDelete(Args.String);
316+
fileSystem.Received(1).WriteAllText(connectionsCacheFile, "[]");
317+
fileSystem.DidNotReceive().WriteAllLines(Args.String, Arg.Any<string[]>());
318+
319+
credentialManager.Received(1).Load(hostUri);
320+
credentialManager.DidNotReceive().HasCredentials();
321+
credentialManager.DidNotReceive().Delete(Args.UriString);
322+
credentialManager.DidNotReceive().Save(Arg.Any<ICredential>());
323+
}
324+
325+
[Test]
326+
public void ShouldConnectSetCredentialsTokenAndSave()
252327
{
253328
const string connectionsCachePath = @"c:\UserCachePath\";
254329
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";
@@ -334,7 +409,7 @@ public void Should_Connect_Set_Credentials_Token_And_Save()
334409
}
335410

336411
[Test]
337-
public void Should_Connect_Set_Credentials_And_Clear()
412+
public void ShouldConnectSetCredentialsAndClear()
338413
{
339414
const string connectionsCachePath = @"c:\UserCachePath\";
340415
const string connectionsCacheFile = @"c:\UserCachePath\connections.json";

src/tests/UnitTests/SetUpFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class SetUpFixture
1010
[SetUp]
1111
public void SetUp()
1212
{
13-
//Logging.TracingEnabled = true;
13+
Logging.TracingEnabled = true;
1414

1515
Logging.LogAdapter = new MultipleLogAdapter(new FileLogAdapter($"..\\{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}-unit-tests.log"));
1616
}

0 commit comments

Comments
 (0)