Permalink
Browse files

Add revocable, user-centric API Keys

Add a User controller for rudimentary profile information. Added APIKey and RevokedAIPKey identity types, ability to create an API Key and Revoke it (updated scheme). Assumption is that you would auth w/ both Id + Identity (Id + Secret) and only APIKey types woudl be checked
  • Loading branch information...
tarwn committed Apr 7, 2018
1 parent 1183f9b commit 1404bde5677301f385fe9189eb89ce710745685e
Showing with 360 additions and 32 deletions.
  1. +2 −1 SampleCosmosCore2App.Core/Users/AuthenticationSource.cs
  2. +34 −5 SampleCosmosCore2App.Core/Users/UserPersistence.cs
  3. +7 −1 SampleCosmosCore2App.sln
  4. +23 −2 SampleCosmosCore2App/Controllers/UserController.cs
  5. +60 −0 SampleCosmosCore2App/Membership/CosmosDBMembership.cs
  6. +16 −0 SampleCosmosCore2App/Membership/Data/AuthenticationDetails.cs
  7. +1 −1 SampleCosmosCore2App/Membership/{ → Data}/LoginResult.cs
  8. +1 −1 SampleCosmosCore2App/Membership/{ → Data}/RegisterResult.cs
  9. +30 −0 SampleCosmosCore2App/Membership/Data/RevocationDetails.cs
  10. +1 −1 SampleCosmosCore2App/Membership/{ → Data}/SessionDetails.cs
  11. +1 −1 SampleCosmosCore2App/Membership/{ → Data}/UserDetails.cs
  12. +6 −1 SampleCosmosCore2App/Membership/ICustomMembership.cs
  13. +0 −6 SampleCosmosCore2App/Membership/ValidateResult.cs
  14. +23 −0 SampleCosmosCore2App/Models/User/UserAuthenticationModel.cs
  15. +5 −0 SampleCosmosCore2App/SampleCosmosCore2App.v3.ncrunchproject
  16. +12 −0 SampleCosmosCore2App/Views/Shared/Layout.cshtml
  17. +26 −0 SampleCosmosCore2App/Views/User/AddKey.cshtml
  18. +16 −12 SampleCosmosCore2App/Views/User/Index.cshtml
  19. +22 −0 SampleCosmosCore2App/Views/User/ShowKey.cshtml
  20. +51 −0 SampleCosmosCore2AppTests/Membership/Data/UserAuthenticationModelTests.cs
  21. +15 −0 SampleCosmosCore2AppTests/SampleCosmosCore2AppTests.csproj
  22. +8 −0 SampleCosmosCore2AppTests/SampleCosmosCore2AppTests.v3.ncrunchproject
  23. BIN _NCrunch_SampleCosmosCore2App/SampleCosmosCore2App.crunchsolution.cache
@@ -7,6 +7,7 @@ namespace SampleCosmosCore2App.Core.Users
public enum AuthenticationScheme
{
Twitter = 1,
APIKey = 2
APIKey = 2,
RevokedAPIKey = 3
}
}
@@ -61,17 +61,24 @@ public async Task<LoginUser> GetUserAsync(string userId)
public async Task<LoginUser> GetUserBySessionIdAsync(string sessionId)
{
var query = _client.CreateDocumentQuery<LoginUser>(GetUsersCollectionUri(), new SqlQuerySpec()
var query = _client.CreateDocumentQuery<string>(GetSessionsCollectionUri(), new SqlQuerySpec()
{
QueryText = "SELECT U.* FROM Users U INNER JOIN Sessions S ON S.UserId = U.id WHERE S.id = @sessionId",
QueryText = "SELECT VALUE S.UserId FROM Sessions S WHERE S.id = @sessionId",
Parameters = new SqlParameterCollection()
{
new SqlParameter("@sessionId", sessionId)
}
});
var results = await query.AsDocumentQuery()
.ExecuteNextAsync<LoginUser>();
return results.FirstOrDefault();
.ExecuteNextAsync<string>();
if (results.Count == 0)
{
return null;
}
else
{
return await GetUserAsync(results.Single());
}
}
public async Task<LoginUser> GetUserByUsernameAsync(string userName)
@@ -142,11 +149,28 @@ public async Task<LoginUserAuthentication> CreateUserAuthenticationAsync(LoginUs
return JsonConvert.DeserializeObject<LoginUserAuthentication>(result.Resource.ToString());
}
public async Task<LoginUserAuthentication> GetUserAuthenticationAsync(string id)
{
var query = _client.CreateDocumentQuery<LoginUserAuthentication>(GetAuthenticationsCollectionUri(), new SqlQuerySpec()
{
QueryText = "SELECT * FROM UserAuthentications UA WHERE UA.id = @id",
Parameters = new SqlParameterCollection()
{
new SqlParameter("@id", id)
}
});
var result = await query.AsDocumentQuery()
.ExecuteNextAsync<LoginUserAuthentication>();
return result.SingleOrDefault();
}
public async Task<List<LoginUserAuthentication>> GetUserAuthenticationsAsync(string userId)
{
var query = _client.CreateDocumentQuery<int>(GetAuthenticationsCollectionUri(), new SqlQuerySpec()
{
QueryText = "SELECT * FROM UserAuthentications WHERE UA.UserId = @userId",
QueryText = "SELECT * FROM UserAuthentications UA WHERE UA.UserId = @userId",
Parameters = new SqlParameterCollection()
{
new SqlParameter("@userId", userId)
@@ -178,6 +202,11 @@ public async Task<bool> IsIdentityRegisteredAsync(AuthenticationScheme authentic
.ExecuteNextAsync<int>();
return result.Single() == 1;
}
public async Task UpdateUserAuthenticationAsync(LoginUserAuthentication userAuth)
{
await _client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(_databaseId, AUTHS_DOCUMENT_COLLECTION_ID, userAuth.Id), userAuth, new RequestOptions() { });
}
#endregion
View
@@ -5,7 +5,9 @@ VisualStudioVersion = 15.0.27428.2005
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleCosmosCore2App", "SampleCosmosCore2App\SampleCosmosCore2App.csproj", "{18786939-8496-48E1-AF99-2BCF84A1839C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleCosmosCore2App.Core", "SampleCosmosCore2App.Core\SampleCosmosCore2App.Core.csproj", "{8EC2A13F-E702-4508-9E67-DC5E7C5B19B9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleCosmosCore2App.Core", "SampleCosmosCore2App.Core\SampleCosmosCore2App.Core.csproj", "{8EC2A13F-E702-4508-9E67-DC5E7C5B19B9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleCosmosCore2AppTests", "SampleCosmosCore2AppTests\SampleCosmosCore2AppTests.csproj", "{FCC31B18-18F9-4F41-AAB6-C5EDF3899518}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,6 +23,10 @@ Global
{8EC2A13F-E702-4508-9E67-DC5E7C5B19B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EC2A13F-E702-4508-9E67-DC5E7C5B19B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EC2A13F-E702-4508-9E67-DC5E7C5B19B9}.Release|Any CPU.Build.0 = Release|Any CPU
{FCC31B18-18F9-4F41-AAB6-C5EDF3899518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCC31B18-18F9-4F41-AAB6-C5EDF3899518}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCC31B18-18F9-4F41-AAB6-C5EDF3899518}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCC31B18-18F9-4F41-AAB6-C5EDF3899518}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -22,8 +22,8 @@ public UserController(ICustomMembership membership, Persistence persistence)
_persistence = persistence;
}
[HttpGet("index")]
public async Task<IActionResult> IndexAsync()
[HttpGet("")]
public async Task<IActionResult> IndexAsync(string error)
{
var sessionId = _membership.GetSessionId(HttpContext.User);
var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);
@@ -49,6 +49,10 @@ public async Task<IActionResult> IndexAsync()
UserAuthentications = groupedAuths
};
if (!string.IsNullOrEmpty(error))
{
ModelState.AddModelError("", error);
}
return View("Index", model);
}
@@ -89,5 +93,22 @@ public async Task<IActionResult> PostAddKeyAsync(NewKeyModel model)
return View("ShowKey", resultModel);
}
[HttpGet("revoke")]
public async Task<IActionResult> Revoke(string id)
{
var sessionId = _membership.GetSessionId(HttpContext.User);
var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);
var result = await _membership.RevokeAuthenticationAsync(user.Id, id);
if (result.Failed)
{
return RedirectToAction("IndexAsync", new { error = result.Error });
}
else
{
return RedirectToAction("IndexAsync");
}
}
}
}
@@ -7,6 +7,8 @@
using System.Security.Claims;
using System.Threading.Tasks;
using SampleCosmosCore2App.Core.Users;
using System.Security.Cryptography;
using SampleCosmosCore2App.Membership.Data;
namespace SampleCosmosCore2App.Membership
{
@@ -104,6 +106,8 @@ private Core.Users.AuthenticationScheme StringToScheme(string scheme)
{
case "Twitter":
return Core.Users.AuthenticationScheme.Twitter;
case "APIKey":
return Core.Users.AuthenticationScheme.APIKey;
default:
throw new ArgumentException("Unrecognized sign-in scheme", scheme);
}
@@ -232,6 +236,62 @@ public async Task<SessionDetails> GetSessionDetailsAsync(ClaimsPrincipal princip
}
};
}
public string GenerateAPIKey(string userId)
{
var key = new byte[32];
using (var generator = RandomNumberGenerator.Create())
{
generator.GetBytes(key);
}
return Convert.ToBase64String(key);
}
public async Task<AuthenticationDetails> AddAuthenticationAsync(string userId, string scheme, string identity, string identityName)
{
var userAuth = new LoginUserAuthentication()
{
UserId = userId,
Scheme = StringToScheme(scheme),
Identity = identity,
Name = identityName,
CreationTime = DateTime.UtcNow
};
userAuth = await _persistence.Users.CreateUserAuthenticationAsync(userAuth);
return new AuthenticationDetails()
{
Id = userAuth.Id,
Scheme = userAuth.Scheme.ToString(),
Identity = userAuth.Identity,
Name = userAuth.Name,
CreationTime = userAuth.CreationTime
};
}
public async Task<RevocationDetails> RevokeAuthenticationAsync(string userId, string identity)
{
var userAuth = await _persistence.Users.GetUserAuthenticationAsync(identity);
if (!userAuth.UserId.Equals(userId))
{
return RevocationDetails.GetFailed("Could not find specified API Key for your account");
}
if (userAuth.Scheme == Core.Users.AuthenticationScheme.RevokedAPIKey)
{
return RevocationDetails.GetFailed("APIKey has already been revoked");
}
if (userAuth.Scheme != Core.Users.AuthenticationScheme.APIKey)
{
return RevocationDetails.GetFailed("Could not find specified API Key for your account");
}
userAuth.Scheme = Core.Users.AuthenticationScheme.RevokedAPIKey;
await _persistence.Users.UpdateUserAuthenticationAsync(userAuth);
return RevocationDetails.GetSuccess();
}
}
}
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SampleCosmosCore2App.Membership.Data
{
public class AuthenticationDetails
{
public string Id { get; set; }
public string Scheme { get; set; }
public string Identity { get; set; }
public string Name { get; set; }
public DateTime CreationTime { get; set; }
}
}
@@ -1,6 +1,6 @@
using System;
namespace SampleCosmosCore2App.Membership
namespace SampleCosmosCore2App.Membership.Data
{
public class LoginResult
{
@@ -3,7 +3,7 @@
using System.Linq;
using System.Threading.Tasks;
namespace SampleCosmosCore2App.Membership
namespace SampleCosmosCore2App.Membership.Data
{
public class RegisterResult
{
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SampleCosmosCore2App.Membership.Data
{
public class RevocationDetails
{
public bool Failed { get; set; }
public string Error { get; set; }
public static RevocationDetails GetSuccess()
{
return new RevocationDetails()
{
Failed = false
};
}
public static RevocationDetails GetFailed(string error)
{
return new RevocationDetails()
{
Failed = true,
Error = error
};
}
}
}
@@ -1,6 +1,6 @@
using System;
namespace SampleCosmosCore2App.Membership
namespace SampleCosmosCore2App.Membership.Data
{
public class SessionDetails
{
@@ -1,4 +1,4 @@
namespace SampleCosmosCore2App.Membership
namespace SampleCosmosCore2App.Membership.Data
{
public class UserDetails
{
@@ -1,4 +1,5 @@
using System;
using SampleCosmosCore2App.Membership.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
@@ -24,5 +25,9 @@ public interface ICustomMembership
string GetSessionId(ClaimsPrincipal principal);
Task<SessionDetails> GetSessionDetailsAsync(ClaimsPrincipal principal);
string GenerateAPIKey(string userId);
Task<AuthenticationDetails> AddAuthenticationAsync(string userId, string scheme, string identity, string identityName);
Task<RevocationDetails> RevokeAuthenticationAsync(string userId, string identity);
}
}

This file was deleted.

Oops, something went wrong.
@@ -1,15 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SampleCosmosCore2App.Models.User
{
public class UserAuthenticationModel
{
public const string EMPTY_MASKED_VALUE = "(empty)";
public string Id { get; set; }
public string Identity { get; set; }
public string Name { get; set; }
public DateTime CreationTime { get; set; }
public string GetMaskedIdentity()
{
if (string.IsNullOrEmpty(Identity))
{
return EMPTY_MASKED_VALUE;
}
else if (Identity.Length == 1)
{
return "X";
}
else if (Identity.Length < 4)
{
return Regex.Replace(Identity, "(?<=.).", "X");
}
else
{
return Regex.Replace(Identity, "(?<=..).", "X");
}
}
}
}
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>
Oops, something went wrong.

0 comments on commit 1404bde

Please sign in to comment.