diff --git a/src/customer-key-store/Controllers/KeysController.cs b/src/customer-key-store/Controllers/KeysController.cs index 60c7b35..1619a02 100644 --- a/src/customer-key-store/Controllers/KeysController.cs +++ b/src/customer-key-store/Controllers/KeysController.cs @@ -3,6 +3,7 @@ namespace Microsoft.InformationProtection.Web.Controllers { using System; + using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; @@ -39,11 +40,11 @@ public IActionResult GetKey(string keyName) [HttpPost] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - public IActionResult Decrypt(string keyName, string keyId, [FromBody] ippw.EncryptedData encryptedData) + public async Task Decrypt(string keyName, string keyId, [FromBody] ippw.EncryptedData encryptedData) { try { - var decryptedData = keyManager.Decrypt(HttpContext.User, keyName, keyId, encryptedData); + var decryptedData = await keyManager.Decrypt(HttpContext.User, keyName, keyId, encryptedData).ConfigureAwait(false); return Ok(decryptedData); } diff --git a/src/customer-key-store/Models/Authorizer.cs b/src/customer-key-store/Models/Authorizer.cs index 4aafa5b..b99463a 100644 --- a/src/customer-key-store/Models/Authorizer.cs +++ b/src/customer-key-store/Models/Authorizer.cs @@ -4,8 +4,9 @@ namespace Microsoft.InformationProtection.Web.Models { using System.Security.Claims; + using System.Threading.Tasks; public interface IAuthorizer { - void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key); + Task ProcessAccessRequest(ClaimsPrincipal user, KeyStoreData key); } } \ No newline at end of file diff --git a/src/customer-key-store/Models/EmailAuthorizer.cs b/src/customer-key-store/Models/EmailAuthorizer.cs index c1a12ce..08713c9 100644 --- a/src/customer-key-store/Models/EmailAuthorizer.cs +++ b/src/customer-key-store/Models/EmailAuthorizer.cs @@ -4,6 +4,7 @@ namespace Microsoft.InformationProtection.Web.Models { using System.Collections.Generic; using System.Security.Claims; + using System.Threading.Tasks; using Microsoft.InformationProtection.Web.Models.Extensions; public class EmailAuthorizer : IAuthorizer @@ -12,14 +13,7 @@ public class EmailAuthorizer : IAuthorizer private const string UpnClaim = ClaimTypes.Upn; private HashSet validEmails = new HashSet(System.StringComparer.OrdinalIgnoreCase); - public void AddEmail(string email) - { - email.ThrowIfNull(nameof(email)); - - validEmails.Add(email.Trim()); - } - - public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) + public static string GetEmailFromClaims(ClaimsPrincipal user) { string email = null; @@ -39,6 +33,20 @@ public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) } } + return email; + } + + public void AddEmail(string email) + { + email.ThrowIfNull(nameof(email)); + + validEmails.Add(email.Trim()); + } + + public Task ProcessAccessRequest(ClaimsPrincipal user, KeyStoreData key) + { + string email = EmailAuthorizer.GetEmailFromClaims(user); + if(email == null) { throw new System.ArgumentException("The email or upn claim is required"); @@ -48,6 +56,8 @@ public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) { throw new CustomerKeyStore.Models.KeyAccessException("User does not have access to the key"); } + + return Task.FromResult(true); } } } \ No newline at end of file diff --git a/src/customer-key-store/Models/KeyManager.cs b/src/customer-key-store/Models/KeyManager.cs index 09bf868..a5a1ec7 100644 --- a/src/customer-key-store/Models/KeyManager.cs +++ b/src/customer-key-store/Models/KeyManager.cs @@ -4,6 +4,7 @@ namespace Microsoft.InformationProtection.Web.Models { using System; using System.Security.Claims; + using System.Threading.Tasks; using Microsoft.InformationProtection.Web.Models.Extensions; using sg = System.Globalization; @@ -39,7 +40,7 @@ public KeyData GetPublicKey(Uri requestUri, string keyName) return new KeyData(publicKey, cache); } - public DecryptedData Decrypt(ClaimsPrincipal user, string keyName, string keyId, EncryptedData encryptedData) + public async Task Decrypt(ClaimsPrincipal user, string keyName, string keyId, EncryptedData encryptedData) { user.ThrowIfNull(nameof(user)); keyName.ThrowIfNull(nameof(keyName)); @@ -48,7 +49,7 @@ public DecryptedData Decrypt(ClaimsPrincipal user, string keyName, string keyId, var keyData = keyStore.GetKey(keyName, keyId); - keyData.KeyAuth.CanUserAccessKey(user, keyData); + await keyData.KeyAuth.ProcessAccessRequest(user, keyData).ConfigureAwait(false); if (encryptedData.Algorithm != "RSA-OAEP-256") { diff --git a/src/customer-key-store/Models/MultiFactorAuthorizer.cs b/src/customer-key-store/Models/MultiFactorAuthorizer.cs new file mode 100644 index 0000000..9922b12 --- /dev/null +++ b/src/customer-key-store/Models/MultiFactorAuthorizer.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +namespace Microsoft.InformationProtection.Web.Models +{ + using System.Security.Claims; + using System.Threading.Tasks; + using Microsoft.InformationProtection.Web.Models.Extensions; + + public class MultiFactorAuthorizer : IAuthorizer + { + private IAuthorizer mPrimaryAuthorizer; + private IAuthorizer mSecondaryAuthorizer; + + public MultiFactorAuthorizer(IAuthorizer primaryAuthorizer, IAuthorizer secondaryAuthorizer) + { + mPrimaryAuthorizer = primaryAuthorizer; + mSecondaryAuthorizer = secondaryAuthorizer; + } + + public async Task ProcessAccessRequest(ClaimsPrincipal user, KeyStoreData key) + { + user.ThrowIfNull(nameof(user)); + + await mPrimaryAuthorizer.ProcessAccessRequest(user, key).ConfigureAwait(false); + + await mSecondaryAuthorizer.ProcessAccessRequest(user, key).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/customer-key-store/Models/PushAuthorizer.cs b/src/customer-key-store/Models/PushAuthorizer.cs new file mode 100644 index 0000000..2d533ce --- /dev/null +++ b/src/customer-key-store/Models/PushAuthorizer.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +namespace Microsoft.InformationProtection.Web.Models +{ + using System.Security.Claims; + using System.Threading.Tasks; + using Microsoft.InformationProtection.Web.Models.Extensions; + + public class PushAuthorizer : IAuthorizer + { + public PushAuthorizer(string pushService) + { + PushService = pushService; + } + + public string PushService { get; private set; } + + public Task ProcessAccessRequest(ClaimsPrincipal user, KeyStoreData key) + { + user.ThrowIfNull(nameof(user)); + var email = EmailAuthorizer.GetEmailFromClaims(user); + + //send email to push notification service + return Task.FromResult(true); + } + } +} \ No newline at end of file diff --git a/src/customer-key-store/Models/RoleAuthorizer.cs b/src/customer-key-store/Models/RoleAuthorizer.cs index 914ec17..6918bfa 100644 --- a/src/customer-key-store/Models/RoleAuthorizer.cs +++ b/src/customer-key-store/Models/RoleAuthorizer.cs @@ -5,6 +5,7 @@ namespace Microsoft.InformationProtection.Web.Models using System.Collections.Generic; using System.DirectoryServices; using System.Security.Claims; + using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.InformationProtection.Web.Models.Extensions; public class RoleAuthorizer : IAuthorizer @@ -22,12 +23,18 @@ public RoleAuthorizer(IConfiguration configuration) ldapPath = configuration["RoleAuthorizer:LDAPPath"]; } + public static string GetRole(string memberOf) + { + memberOf.ThrowIfNull(nameof(memberOf)); + return ParseCN(memberOf); + } + public void AddRole(string role) { roles.Add(role); } - public void CanUserAccessKey(string sid) + public Task ProcessAccessRequest(string sid) { sid.ThrowIfNull(nameof(sid)); @@ -63,9 +70,11 @@ public void CanUserAccessKey(string sid) } } } + + return Task.FromResult(true); } - public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) + public Task ProcessAccessRequest(ClaimsPrincipal user, KeyStoreData key) { user.ThrowIfNull(nameof(user)); @@ -85,7 +94,7 @@ public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) throw new System.ArgumentException(SidClaim + " claim not found"); } - CanUserAccessKey(sid); + return ProcessAccessRequest(sid); } private static string ParseCN(string distinguishedName) @@ -133,11 +142,5 @@ private static string ParseCN(string distinguishedName) return role.ToString(); } - - public static string GetRole(string memberOf) - { - memberOf.ThrowIfNull(nameof(memberOf)); - return ParseCN(memberOf); - } } } \ No newline at end of file diff --git a/src/customer-key-store/Models/TestStore.cs b/src/customer-key-store/Models/TestStore.cs index 7547b90..4c7d5d8 100644 --- a/src/customer-key-store/Models/TestStore.cs +++ b/src/customer-key-store/Models/TestStore.cs @@ -57,6 +57,20 @@ public TestKeyStore(IConfiguration configuration) } } + var pushNotifications = testKey.GetSection("PushNotificationService"); + if(pushNotifications != null && pushNotifications.Exists()) + { + if(keyAuth == null) + { + keyAuth = new PushAuthorizer(pushNotifications.Value); + } + else + { + //Currently, push authorizer always comes second. + keyAuth = new MultiFactorAuthorizer(keyAuth, new PushAuthorizer(pushNotifications.Value)); + } + } + int? expirationTimeInDays = null; var cacheTime = testKey["CacheExpirationInDays"]; if(cacheTime != null) diff --git a/src/customer-key-store/appsettings.json b/src/customer-key-store/appsettings.json index 3974940..665dd5b 100644 --- a/src/customer-key-store/appsettings.json +++ b/src/customer-key-store/appsettings.json @@ -32,6 +32,7 @@ "Id": "GUID", "AuthorizedRoles": ["On premises Active Directory groups that you want to have access to this key. If you provide a value for AuthorizedRoles, then remove the line that starts with AuthorizedEmailAddress."], "AuthorizedEmailAddress": ["Email addresses of users that have access to this key. If you provide a value for AuthorizedEmailAddress, then remove the line that starts with AuthorizedRoles."], + "PushNotificationService":"Service that will handle push notifications for MFA to send an access request to a user's mobile device. This value can be present along with one other of 'AuthorizedRoles' or 'AuthorizedEmailAddress' for multifactor auth.", "PublicPem" : "The public key in PEM format. Do not include the BEGIN and END lines", "PrivatePem": "The private key in PEM format. Do not include the BEGIN and END lines" }