diff --git a/Olive.MFA/IMFAManager.cs b/Olive.MFA/IMFAManager.cs new file mode 100644 index 00000000..2390e569 --- /dev/null +++ b/Olive.MFA/IMFAManager.cs @@ -0,0 +1,18 @@ +namespace Olive.MFA +{ + using Olive.Email; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public interface IMFAManager + { + Task SendOtpViaEmail(IEmailMessage emailMessage); + Task SendOtpViaSms(string otp, string phoneNumber); + string GetOtp(); + + + } +} diff --git a/Olive.MFA/ITemporaryLogin.cs b/Olive.MFA/ITemporaryLogin.cs new file mode 100644 index 00000000..bf8ff44e --- /dev/null +++ b/Olive.MFA/ITemporaryLogin.cs @@ -0,0 +1,19 @@ +namespace Olive.MFA +{ + using Olive.Entities; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + [LogEvents(false)] + [CacheObjects(false)] + public interface ITemporaryLogin : IEntity + { + DateTime CreatedAt { get; set; } + int ExpiryMinutes { get; set; } + string MFACode { get; set; } + + } +} diff --git a/Olive.MFA/ITemporaryLoginService.cs b/Olive.MFA/ITemporaryLoginService.cs new file mode 100644 index 00000000..34e07776 --- /dev/null +++ b/Olive.MFA/ITemporaryLoginService.cs @@ -0,0 +1,15 @@ +namespace Olive.MFA +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public interface ITemporaryLoginService + { + Task DeleteTemporaryLogin(ITemporaryLogin login); + Task IsSessionExpired(ITemporaryLogin login); + Task IsOtpValid(ITemporaryLogin login, string code); + } +} diff --git a/Olive.MFA/MFAExtensions.cs b/Olive.MFA/MFAExtensions.cs new file mode 100644 index 00000000..0770e866 --- /dev/null +++ b/Olive.MFA/MFAExtensions.cs @@ -0,0 +1,20 @@ +namespace Olive.MFA +{ + using Microsoft.Extensions.DependencyInjection; + using Olive.SMS; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public static class MFAExtensions + { + public static IServiceCollection AddMFA(this IServiceCollection @this) + { + return @this + .AddSingleton() + .AddSingleton(); + } + } +} diff --git a/Olive.MFA/MFAManager.cs b/Olive.MFA/MFAManager.cs new file mode 100644 index 00000000..b8ad444c --- /dev/null +++ b/Olive.MFA/MFAManager.cs @@ -0,0 +1,49 @@ +namespace Olive.MFA +{ + using Olive.Email; + using Olive.Entities; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Twilio; + using Twilio.Rest.Api.V2010.Account; + using Twilio.Types; + + public class MFAManager : IMFAManager + { + IDatabase Database => Context.Current.Database(); + public Task SendOtpViaEmail(IEmailMessage emailMessage) + { + return Database.Save(emailMessage); + } + + public Task SendOtpViaSms(string otp, string phoneNumber) + { + var accountId = Config.Get("TwilioAccountId"); + var authKey = Config.Get("TwilioAuthKey"); + var sender = Config.Get("TwilioSenderNumber"); + + if (accountId.IsEmpty() || authKey.IsEmpty() || sender.IsEmpty()) + throw new Exception("TwilioAccountId | TwilioAuthKey | TwilioSenderNumber configuration not set"); + + + + TwilioClient.Init(accountId, authKey); + var message = MessageResource.Create( + to: new PhoneNumber(phoneNumber), + from: new PhoneNumber(sender), + body: $"Your OTP code is: {otp}"); + + return Task.FromResult(message != null); + } + public string GetOtp() + { + var random = new Random(); + var otp = random.Next(100000, 999999).ToString(); + return otp; + + } + } +} diff --git a/Olive.MFA/Olive.MFA.csproj b/Olive.MFA/Olive.MFA.csproj new file mode 100644 index 00000000..0914c1f0 --- /dev/null +++ b/Olive.MFA/Olive.MFA.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Olive.MFA/TemporaryLoginService.cs b/Olive.MFA/TemporaryLoginService.cs new file mode 100644 index 00000000..b6c4f529 --- /dev/null +++ b/Olive.MFA/TemporaryLoginService.cs @@ -0,0 +1,29 @@ +namespace Olive.MFA +{ + using Olive.Entities; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public class TemporaryLoginService : ITemporaryLoginService + { + IDatabase Database => Context.Current.Database(); + public Task DeleteTemporaryLogin(ITemporaryLogin login) + { + return Database.Delete(login); + } + + public async Task IsOtpValid(ITemporaryLogin login, string code) + { + return login.MFACode.HasValue() && login.CreatedAt.AddMinutes((double)login.ExpiryMinutes) > LocalTime.Now && + login.MFACode.Equals(code); + } + + public async Task IsSessionExpired(ITemporaryLogin login) + { + return login.CreatedAt.AddMinutes(login.ExpiryMinutes) < LocalTime.Now; + } + } +} diff --git a/Olive.sln b/Olive.sln index e599dbfb..3c9cbf56 100644 --- a/Olive.sln +++ b/Olive.sln @@ -220,13 +220,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Olive.Entities.Data.EF.Repl EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenAI", "OpenAI", "{EA662106-3384-4A6F-83C4-83859789A6DD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Olive.OpenAI", "Olive.OpenAI\Olive.OpenAI\Olive.OpenAI.csproj", "{F7D375B0-4C0E-43F3-AE01-C35A0B01ED2C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Olive.OpenAI", "Olive.OpenAI\Olive.OpenAI\Olive.OpenAI.csproj", "{F7D375B0-4C0E-43F3-AE01-C35A0B01ED2C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Olive.OpenAI.Voice", "Olive.OpenAI.Voice\Olive.OpenAI.Voice\Olive.OpenAI.Voice.csproj", "{F96D0F3C-2F36-4897-A60B-B9F37793751D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Olive.OpenAI.Voice", "Olive.OpenAI.Voice\Olive.OpenAI.Voice\Olive.OpenAI.Voice.csproj", "{F96D0F3C-2F36-4897-A60B-B9F37793751D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Olive.Azure.DocumentAnalyzer", "Olive.Azure.DocumentAnalyzer\Olive.Azure.DocumentAnalyzer.csproj", "{1621EA95-F031-4FA2-BAA1-8CEDFA84D987}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Olive.Azure.DocumentAnalyzer", "Olive.Azure.DocumentAnalyzer\Olive.Azure.DocumentAnalyzer.csproj", "{1621EA95-F031-4FA2-BAA1-8CEDFA84D987}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Olive.Azure.DocumentClassification", "Olive.Azure.DocumentClassification\Olive.Azure.DocumentClassification.csproj", "{4CE3F810-F35F-40A2-83C5-529419ACEAD4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Olive.Azure.DocumentClassification", "Olive.Azure.DocumentClassification\Olive.Azure.DocumentClassification.csproj", "{4CE3F810-F35F-40A2-83C5-529419ACEAD4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Olive.MFA", "Olive.MFA\Olive.MFA.csproj", "{4D98F2B4-3B67-4A4D-8F90-7D4009F06EE3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -589,6 +591,10 @@ Global {4CE3F810-F35F-40A2-83C5-529419ACEAD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {4CE3F810-F35F-40A2-83C5-529419ACEAD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {4CE3F810-F35F-40A2-83C5-529419ACEAD4}.Release|Any CPU.Build.0 = Release|Any CPU + {4D98F2B4-3B67-4A4D-8F90-7D4009F06EE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D98F2B4-3B67-4A4D-8F90-7D4009F06EE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D98F2B4-3B67-4A4D-8F90-7D4009F06EE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D98F2B4-3B67-4A4D-8F90-7D4009F06EE3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -689,6 +695,7 @@ Global {F96D0F3C-2F36-4897-A60B-B9F37793751D} = {EA662106-3384-4A6F-83C4-83859789A6DD} {1621EA95-F031-4FA2-BAA1-8CEDFA84D987} = {B08CEA40-4865-41AF-94CA-AC59E2513A11} {4CE3F810-F35F-40A2-83C5-529419ACEAD4} = {B08CEA40-4865-41AF-94CA-AC59E2513A11} + {4D98F2B4-3B67-4A4D-8F90-7D4009F06EE3} = {B08CEA40-4865-41AF-94CA-AC59E2513A11} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B8459027-6FAF-42E6-B8A8-4887D7730704}