diff --git a/Fido2Demo/Controller.cs b/Fido2Demo/Controller.cs index ce7153f2..bad31d8a 100644 --- a/Fido2Demo/Controller.cs +++ b/Fido2Demo/Controller.cs @@ -22,7 +22,8 @@ public class MyController : Controller private Fido2 _lib; private IMetadataService _mds; private string _origin; - private static readonly DevelopmentInMemoryStore DemoStorage = new DevelopmentInMemoryStore(); + //private static readonly DevelopmentInMemoryStore DemoStorage = new DevelopmentInMemoryStore(); + private static readonly ActiveDirectoryStore adStore = new ActiveDirectoryStore(); public MyController(IConfiguration config) { @@ -49,10 +50,10 @@ private string FormatException(Exception e) public ContentResult Index(string username) { // 1. Get user from DB - var user = DemoStorage.GetUser(username + "@example.com"); + var user = adStore.GetUser(username + "@example.com"); // 2. Get registered credentials from database - var existingCredentials = DemoStorage.GetCredentialsByUser(user); + var existingCredentials = adStore.GetCredentialsByUser(user); var content = System.IO.File.ReadAllText("wwwroot/index.html"); @@ -130,15 +131,10 @@ public JsonResult MakeCredentialOptions([FromForm] string username, [FromForm] s try { // 1. Get user from DB by username (in our example, auto create missing users) - var user = DemoStorage.GetOrAddUser(username, () => new User - { - DisplayName = "Display " + username, - Name = username, - Id = Encoding.UTF8.GetBytes(username) // byte representation of userID is required - }); + var user = adStore.GetUser(username); // 2. Get user existing keys by username - var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); + var existingKeys = adStore.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); // 3. Create options var authenticatorSelection = new AuthenticatorSelection @@ -179,7 +175,7 @@ public async Task MakeCredential([FromBody] AuthenticatorAttestation // 2. Create callback so that lib can verify credential id is unique to this user IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) => { - var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId); + var users = await adStore.GetUsersByCredentialIdAsync(args.CredentialId); if (users.Count > 0) return false; return true; @@ -189,7 +185,7 @@ public async Task MakeCredential([FromBody] AuthenticatorAttestation var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback); // 3. Store the credentials in db - DemoStorage.AddCredentialToUser(options.User, new StoredCredential + adStore.AddCredentialToUser(options.User, new StoredCredential { Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), PublicKey = success.Result.PublicKey, @@ -216,11 +212,11 @@ public ActionResult AssertionOptionsPost([FromForm] string username) try { // 1. Get user from DB - var user = DemoStorage.GetUser(username); + var user = adStore.GetUser(username); if (user == null) throw new ArgumentException("Username was not registered"); // 2. Get registered credentials from database - var existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); + var existingCredentials = adStore.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); var exts = new AuthenticationExtensionsClientInputs() { AppID = _origin, SimpleTransactionAuthorization = "FIDO", GenericTransactionAuthorization = new TxAuthGenericArg { ContentType = "text/plain", Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } }, UserVerificationIndex = true, Location = true, UserVerificationMethod = true }; @@ -255,7 +251,7 @@ public async Task MakeAssertion([FromBody] AuthenticatorAssertionRaw var options = AssertionOptions.FromJson(jsonOptions); // 2. Get registered credential from database - var creds = DemoStorage.GetCredentialById(clientResponse.Id); + var creds = adStore.GetCredentialById(clientResponse.Id); // 3. Get credential counter from database var storedCounter = creds.SignatureCounter; @@ -263,7 +259,7 @@ public async Task MakeAssertion([FromBody] AuthenticatorAssertionRaw // 4. Create callback to check if userhandle owns the credentialId IsUserHandleOwnerOfCredentialIdAsync callback = async (args) => { - var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle); + var storedCreds = await adStore.GetCredentialsByUserHandleAsync(args.UserHandle); return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); }; @@ -271,7 +267,7 @@ public async Task MakeAssertion([FromBody] AuthenticatorAssertionRaw var res = await _lib.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback); // 6. Store the updated counter - DemoStorage.UpdateCounter(res.CredentialId, res.Counter); + adStore.UpdateCounter(res.CredentialId, res.Counter); // 7. return OK to client return Json(res); diff --git a/fido2-net-lib/ActiveDirectoryStore.cs b/fido2-net-lib/ActiveDirectoryStore.cs new file mode 100644 index 00000000..dc2738e3 --- /dev/null +++ b/fido2-net-lib/ActiveDirectoryStore.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using Fido2NetLib.Development; +using System.Linq; +using System.Threading.Tasks; + +/* + +dn: CN=fIDO-Authenticator-Aaguid, +changetype: ntdsSchemaAdd +adminDescription: fIDO-Authenticator-Aaguid +adminDisplayName: fIDO-Authenticator-Aaguid +attributeID: 1.2.840.113556.1.8000.2554.54579.64576.60639.19922.45518.10745386.112824.2.2 +attributeSyntax: 2.5.5.10 +isSingleValued: TRUE +lDAPDisplayName: fIDOAuthenticatorAaguid +name: fIDO-Authenticator-Aaguid +oMSyntax: 4 +objectCategory: CN=Attribute-Schema, +objectClass: attributeSchema +rangeLower: 16 +rangeUpper: 16 +schemaIdGuid:: 6mK5hwhZRTG0yl5t5AB3WQ== + + +dn: CN=fIDO-Authenticator-Credential-Id, +changetype: ntdsSchemaAdd +adminDescription: fIDO-Authenticator-Credential-Id +adminDisplayName: fIDO-Authenticator-Credential-Id +attributeID: 1.2.840.113556.1.8000.2554.54579.64576.60639.19922.45518.10745386.112824.2.1 +attributeSyntax: 2.5.5.10 +isSingleValued: TRUE +lDAPDisplayName: fIDOAuthenticatorCredentialId +name: fIDO-Authenticator-Credential-Id +oMSyntax: 4 +objectCategory: CN=Attribute-Schema, +objectClass: attributeSchema +rangeLower: 16 +rangeUpper: 128 +schemaIdGuid:: CW0AgPCsTwKMz0nVQKC3Xw== +searchFlags: 1 + + +dn: CN=fIDO-Authenticator-Devices, +changetype: ntdsSchemaAdd +adminDescription: fIDO-Authenticator-Devices +adminDisplayName: fIDO-Authenticator-Devices +defaultSecurityDescriptor: D:S: +governsID: 1.2.840.113556.1.8000.2554.54579.64576.60639.19922.45518.10745386.112824.1 +lDAPDisplayName: fIDOAuthenticatorDevices +name: fIDO-Authenticator-Devices +objectCategory: CN=Class-Schema, +objectClass: classSchema +objectClassCategory: 1 +rDNAttID: cn +schemaIdGuid:: loYx5wh5TNqHYH8lAqrQnQ== +subClassOf: top +possSuperiors: user + + +dn: +changetype: ntdsSchemaModify +replace: schemaUpdateNow +schemaUpdateNow: 1 +- + + +dn: CN=fIDO-Authenticator-Device, +changetype: ntdsSchemaAdd +adminDescription: fIDO-Authenticator-Device +adminDisplayName: fIDO-Authenticator-Device +defaultSecurityDescriptor: D:S: +governsID: 1.2.840.113556.1.8000.2554.54579.64576.60639.19922.45518.10745386.112824.1.2 +lDAPDisplayName: fIDOAuthenticatorDevice +name: fIDO-Authenticator-Device +objectCategory: CN=Class-Schema, +objectClass: classSchema +objectClassCategory: 1 +rDNAttID: cn +schemaIdGuid:: Pd68TF6uRXmql6LCWgtm0g== +subClassOf: top +possSuperiors: fIDOAuthenticatorDevices +mayContain: userCertificate +mayContain: logonCount +mayContain: fIDOAuthenticatorAaguid +mayContain: fIDOAuthenticatorCredentialId + + +dn: +changetype: ntdsSchemaModify +replace: schemaUpdateNow +schemaUpdateNow: 1 +- + +*/ + +namespace Fido2NetLib +{ + public class ActiveDirectoryStore + { + DirectoryEntry GetDevice(byte[] credentialId) + { + var queryGuid = ""; + foreach (var b in credentialId) + { + queryGuid += @"\" + b.ToString("x2"); + } + var deviceresult = GetObjectFromFilter("(&(objectCategory=fIDOAuthenticatorDevice)(fIDOAuthenticatorCredentialId=" + queryGuid + "))"); + if (null != deviceresult) + { + return deviceresult.GetDirectoryEntry(); + } + return null; + } + public void UpdateCounter(byte[] credentialId, uint counter) + { + var device = GetDevice(credentialId); + if (null != device) + { + device.Properties["logonCount"].Value = Convert.ToInt32(counter); + device.CommitChanges(); + } + } + DirectorySearcher GetSearcher(string filter, DirectoryEntry searchBase = null) + { + DirectoryEntry entry; + if (null == searchBase) + entry = new DirectoryEntry(); + else entry = searchBase; + + var search = new DirectorySearcher(entry) + { + Filter = filter + }; + search.PropertiesToLoad.Add("fIDOAuthenticatorCredentialId"); + search.PropertiesToLoad.Add("userCertificate"); + search.PropertiesToLoad.Add("logonCount"); + search.PropertiesToLoad.Add("displayName"); + search.PropertiesToLoad.Add("sAMAccountName"); + search.PropertiesToLoad.Add("objectGUID"); + + return search; + } + + SearchResult GetObjectFromFilter(string filter, DirectoryEntry searchBase = null) + { + var search = GetSearcher(filter, searchBase); + return search.FindOne(); + } + SearchResultCollection GetObjectsFromFilter(string filter, DirectoryEntry searchBase = null) + { + var search = GetSearcher(filter, searchBase); + return search.FindAll(); + } + public User GetUser(string upn) + { + var entry = GetUserEntry(upn); + return new User + { + DisplayName = entry.Properties["displayName"].Value.ToString(), + Id = entry.Guid.ToByteArray(), + Name = entry.Properties["sAMAccountName"].Value.ToString() + }; + } + public Task> GetCredentialsByUserHandleAsync(byte[] userHandle) + { + var storedCredentials = new List(); + var userGuid = new Guid(userHandle); + var userentry = new DirectoryEntry("LDAP://"); + var credentialCollection = GetObjectsFromFilter("(objectCategory=fIDOAuthenticatorDevice)", userentry); + foreach (SearchResult device in credentialCollection) + { + var storedCred = new StoredCredential() + { + Descriptor = new Objects.PublicKeyCredentialDescriptor() + { + Id = (byte[]) device.Properties["fIDOAuthenticatorCredentialId"][0], + Type = Objects.PublicKeyCredentialType.PublicKey + }, + PublicKey = (byte[]) device.Properties["userCertificate"][0], + SignatureCounter = Convert.ToUInt32(device.Properties["logonCount"][0]), + UserHandle = userHandle, + UserId = userHandle + }; + storedCredentials.Add(storedCred); + } + return Task.FromResult(storedCredentials.Where(c => c.UserHandle.SequenceEqual(userHandle)).ToList()); + } + public List GetCredentialsByUser(User user) + { + var results = GetCredentialsByUserHandleAsync(user.Id); + return results.Result; + } + public Task> GetUsersByCredentialIdAsync(byte[] credentialId) + { + var entry = GetCredentialOwnerById(credentialId); + if (null == entry) + return Task.FromResult(new List()); + + var storedUsers = new List() + { + new User() + { + DisplayName = entry.Properties["displayName"].Value.ToString(), + Id = entry.Guid.ToByteArray(), + Name = entry.Properties["sAMAccountName"].Value.ToString() + } + }; + return Task.FromResult(storedUsers); + } + private DirectoryEntry GetUserEntry(string upn) + { + var result = GetObjectFromFilter("(&(objectCategory=user)(userPrincipalName=" + upn + "))"); + if (null == result) + throw new Fido2VerificationException("User not found in active directory"); + else return result.GetDirectoryEntry(); + } + private DirectoryEntry GetUserEntry(byte[] objectGuid) + { + var userGuid = new Guid(objectGuid); + var userentry = new DirectoryEntry("LDAP://"); + if (null == userentry) + throw new Fido2VerificationException("User not found in active directory"); + else return userentry; + } + DirectoryEntry AddDevicesContainerIfNotExists(DirectoryEntry entry) + { + var devicesresult = GetObjectFromFilter("(objectCategory=fIDOAuthenticatorDevices)", entry); + if (null == devicesresult) + { + var devices = entry.Children.Add("CN=FIDO Authenticator Devices", "fIDOAuthenticatorDevices"); + devices.CommitChanges(); + return devices; + } + else return devicesresult.GetDirectoryEntry(); + } + public void AddCredentialToUser(User user, StoredCredential credential) + { + var result = GetUserEntry(user.Id); + if (null != result) + { + var devices = AddDevicesContainerIfNotExists(result); + if (null != devices) + { + if (null != GetDevice(credential.Descriptor.Id)) + throw new Fido2VerificationException("Device already registered to user"); + + else + { + var device = devices.Children.Add("CN=" + BitConverter.ToString(credential.Descriptor.Id, 0, 32).Replace("-", ""), "fIDOAuthenticatorDevice"); + device.CommitChanges(); + device.Properties["fIDOAuthenticatorCredentialId"].Value = credential.Descriptor.Id; + device.Properties["userCertificate"].Value = credential.PublicKey; + device.Properties["logonCount"].Value = Convert.ToInt32(credential.SignatureCounter); + device.Properties["fIDOAuthenticatorAaguid"].Value = credential.AaGuid.ToByteArray(); + device.CommitChanges(); + } + } + else throw new Fido2VerificationException("Unable to create devices container"); + } + else throw new Fido2VerificationException("User not found"); + } + public DirectoryEntry GetCredentialOwnerById(byte[] id) + { + var queryGuid = ""; + foreach (byte b in id) + { + queryGuid += @"\" + b.ToString("x2"); + } + + var result = GetObjectFromFilter("(&(objectCategory=fIDOAuthenticatorDevice)(fIDOAuthenticatorCredentialId=" + queryGuid + "))"); + + if (null == result) + return null; + + var device = result.GetDirectoryEntry(); + var devicecontainer = device.Parent; + var user = devicecontainer.Parent; + + return user; + } + public StoredCredential GetCredentialById(byte[] id) + { + try + { + var queryGuid = ""; + foreach (byte b in id) + { + queryGuid += @"\" + b.ToString("x2"); + } + + var result = GetObjectFromFilter("(&(objectCategory=fIDOAuthenticatorDevice)(fIDOAuthenticatorCredentialId=" + queryGuid + "))"); + + if (null != result) + { + var device = result.GetDirectoryEntry(); + var cred = new StoredCredential + { + Descriptor = new Objects.PublicKeyCredentialDescriptor() + { + Id = (byte[])device.Properties["fIDOAuthenticatorCredentialId"].Value, + Type = Objects.PublicKeyCredentialType.PublicKey + }, + PublicKey = (byte[])device.Properties["userCertificate"].Value, + SignatureCounter = Convert.ToUInt32(device.Properties["logonCount"].Value), + UserHandle = device.Parent.Parent.Guid.ToByteArray(), + UserId = device.Parent.Parent.Guid.ToByteArray() + }; + return cred; + } + else throw new Fido2VerificationException("User not found in active directory"); + } + catch (Exception e) + { + Console.WriteLine("Exception caught:\n\n" + e.ToString()); + } + return null; + } + } +} diff --git a/fido2-net-lib/Fido2NetLib.csproj b/fido2-net-lib/Fido2NetLib.csproj index 006ad5fd..efd75bbb 100644 --- a/fido2-net-lib/Fido2NetLib.csproj +++ b/fido2-net-lib/Fido2NetLib.csproj @@ -24,6 +24,7 @@ +