diff options
7 files changed, 76 insertions, 43 deletions
diff --git a/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs b/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs index ca6666c..18b0c29 100644 --- a/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs +++ b/TwoStepsAuthenticator.TestWebsite/Controllers/HomeController.cs @@ -45,7 +45,7 @@ namespace TwoStepsAuthenticator.TestWebsite.Controllers { WebsiteUser user = (WebsiteUser)Session["AuthenticatedUser"]; var auth = new TwoStepsAuthenticator.TimeAuthenticator(usedCodeManager: usedCodesManager); - if (auth.CheckCode(user.DoubleAuthKey, code)) + if (auth.CheckCode(user.DoubleAuthKey, code, user)) { FormsAuthentication.SetAuthCookie(user.Login, true); return RedirectToAction("Welcome"); diff --git a/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs b/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs index cb0065f..3ed8ba8 100644 --- a/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs +++ b/TwoStepsAuthenticator.UnitTests/MockUsedCodesManager.cs @@ -1,15 +1,15 @@ namespace TwoStepsAuthenticator.UnitTests { internal class MockUsedCodesManager : IUsedCodesManager { - public ulong? LastChallenge { get; private set; } + public long? LastChallenge { get; private set; } public string LastCode { get; private set; } - public void AddCode(ulong challenge, string code) { + public void AddCode(long challenge, string code, object user) { this.LastChallenge = challenge; this.LastCode = code; } - public bool IsCodeUsed(ulong challenge, string code) { + public bool IsCodeUsed(long challenge, string code, object user) { return false; } } diff --git a/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs index 99d1957..8aacfe1 100644 --- a/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs +++ b/TwoStepsAuthenticator.UnitTests/TimeAuthenticatorTests.cs @@ -46,7 +46,7 @@ namespace TwoStepsAuthenticator.UnitTests public void Prevent_code_reuse() { var date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); var usedCodesManager = new UsedCodesManager(); - var authenticator = new TimeAuthenticator(() => date, usedCodeManager: usedCodesManager); + var authenticator = new TimeAuthenticator(usedCodesManager, () => date); var secret = Authenticator.GenerateKey(); var code = authenticator.GetCode(secret); @@ -76,7 +76,7 @@ namespace TwoStepsAuthenticator.UnitTests DateTime usedTime; - Assert.True(authenticator.CheckCode("H22Q7WAMQYFZOJ2Q", "696227", out usedTime)); + Assert.True(authenticator.CheckCode("H22Q7WAMQYFZOJ2Q", "696227", null, out usedTime)); // 17:23:50 - 30s Assert.AreEqual(usedTime.Hour, 17); diff --git a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs index d72474c..ffe40ae 100644 --- a/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs +++ b/TwoStepsAuthenticator.UnitTests/UsedCodesManagerTests.cs @@ -14,9 +14,9 @@ namespace TwoStepsAuthenticator.UnitTests { public void Can_add_codes() { var manager = new UsedCodesManager(); - Assert.IsFalse(manager.IsCodeUsed(42L, "def")); - manager.AddCode(42L, "def"); - Assert.IsTrue(manager.IsCodeUsed(42L, "def")); + Assert.IsFalse(manager.IsCodeUsed(42L, "def","u")); + manager.AddCode(42L, "def", "u"); + Assert.IsTrue(manager.IsCodeUsed(42L, "def", "u")); } } diff --git a/TwoStepsAuthenticator/IUsedCodesManager.cs b/TwoStepsAuthenticator/IUsedCodesManager.cs index 8ff1fe3..9455d88 100644 --- a/TwoStepsAuthenticator/IUsedCodesManager.cs +++ b/TwoStepsAuthenticator/IUsedCodesManager.cs @@ -12,14 +12,16 @@ namespace TwoStepsAuthenticator { /// </summary> /// <param name="challenge">Used Challenge</param> /// <param name="code">Used Code</param> - void AddCode(long timestamp, string code); + /// <param name="user">The user</param> + void AddCode(long timestamp, string code, object user); /// <summary> /// Checks if code was previously used. /// </summary> /// <param name="challenge">Used Challenge</param> /// <param name="code">Used Code</param> - /// <returns></returns> - bool IsCodeUsed(long timestamp, string code); + /// <param name="user">The user</param> + /// <returns>True if the user as already used the code</returns> + bool IsCodeUsed(long timestamp, string code, object user); } } diff --git a/TwoStepsAuthenticator/TimeAuthenticator.cs b/TwoStepsAuthenticator/TimeAuthenticator.cs index 8c42e41..24441fe 100644 --- a/TwoStepsAuthenticator/TimeAuthenticator.cs +++ b/TwoStepsAuthenticator/TimeAuthenticator.cs @@ -4,19 +4,22 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace TwoStepsAuthenticator { - +namespace TwoStepsAuthenticator +{ + /// <summary> /// Implementation of rfc6238 Time-Based One-Time Password Algorithm /// </summary> - public class TimeAuthenticator : Authenticator { + public class TimeAuthenticator : Authenticator + { private static readonly Lazy<IUsedCodesManager> DefaultUsedCodeManager = new Lazy<IUsedCodesManager>(() => new UsedCodesManager()); private readonly Func<DateTime> NowFunc; private readonly IUsedCodesManager UsedCodeManager; private readonly int IntervalSeconds; - public TimeAuthenticator(IUsedCodesManager usedCodeManager = null, Func<DateTime> nowFunc = null, int intervalSeconds = 30) { + public TimeAuthenticator(IUsedCodesManager usedCodeManager = null, Func<DateTime> nowFunc = null, int intervalSeconds = 30) + { this.NowFunc = (nowFunc == null) ? () => DateTime.Now : nowFunc; this.UsedCodeManager = (usedCodeManager == null) ? DefaultUsedCodeManager.Value : usedCodeManager; this.IntervalSeconds = intervalSeconds; @@ -27,7 +30,8 @@ namespace TwoStepsAuthenticator { /// </summary> /// <param name="secret">Shared Secret</param> /// <returns>OTP</returns> - public string GetCode(string secret) { + public string GetCode(string secret) + { return GetCode(secret, NowFunc()); } @@ -37,7 +41,8 @@ namespace TwoStepsAuthenticator { /// <param name="secret">Shared Secret</param> /// <param name="date">Time to use as challenge</param> /// <returns>OTP</returns> - public string GetCode(string secret, DateTime date) { + public string GetCode(string secret, DateTime date) + { return GetCodeInternal(secret, (ulong)GetInterval(date)); } @@ -46,11 +51,13 @@ namespace TwoStepsAuthenticator { /// </summary> /// <param name="secret">Shared Secret</param> /// <param name="code">OTP</param> + /// <param name="user">The user</param> /// <returns>true if code matches</returns> - public bool CheckCode(string secret, string code) { + public bool CheckCode(string secret, string code, object user) + { DateTime successfulTime = DateTime.MinValue; - return CheckCode(secret, code, out successfulTime); + return CheckCode(secret, code, user, out successfulTime); } /// <summary> @@ -58,23 +65,27 @@ namespace TwoStepsAuthenticator { /// </summary> /// <param name="secret">Shared Secret</param> /// <param name="code">OTP</param> + /// <param name="user">The user</param> /// <param name="usedDateTime">Matching time if successful</param> /// <returns>true if code matches</returns> - public bool CheckCode(string secret, string code, out DateTime usedDateTime) { + public bool CheckCode(string secret, string code, object user, out DateTime usedDateTime) + { var baseTime = NowFunc(); DateTime successfulTime = DateTime.MinValue; // We need to do this in constant time var codeMatch = false; - for (int i = -2; i <= 1; i++) { + for (int i = -2; i <= 1; i++) + { var checkTime = baseTime.AddSeconds(IntervalSeconds * i); var checkInterval = GetInterval(checkTime); - if (ConstantTimeEquals(GetCode(secret, checkTime), code) && !UsedCodeManager.IsCodeUsed(checkInterval, code)) { + if (ConstantTimeEquals(GetCode(secret, checkTime), code) && (user == null || !UsedCodeManager.IsCodeUsed(checkInterval, code, user))) + { codeMatch = true; successfulTime = checkTime; - UsedCodeManager.AddCode(checkInterval, code); + UsedCodeManager.AddCode(checkInterval, code, user); } } @@ -82,7 +93,22 @@ namespace TwoStepsAuthenticator { return codeMatch; } - private long GetInterval(DateTime dateTime) { + + /// <summary> + /// Checks if the passed code is valid. + /// </summary> + /// <param name="secret">Shared Secret</param> + /// <param name="code">OTP</param> + /// <returns>true if code matches</returns> + public bool CheckCode(string secret, string code) + { + DateTime successfulTime = DateTime.MinValue; + + return CheckCode(secret, code, null, out successfulTime); + } + + private long GetInterval(DateTime dateTime) + { TimeSpan ts = (dateTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)); return (long)ts.TotalSeconds / IntervalSeconds; } diff --git a/TwoStepsAuthenticator/UsedCodesManager.cs b/TwoStepsAuthenticator/UsedCodesManager.cs index 13b3d61..dbd96a7 100644 --- a/TwoStepsAuthenticator/UsedCodesManager.cs +++ b/TwoStepsAuthenticator/UsedCodesManager.cs @@ -14,25 +14,28 @@ namespace TwoStepsAuthenticator { internal sealed class UsedCode { - public UsedCode(long timestamp, String code) + public UsedCode(long timestamp, String code, object user) { this.UseDate = DateTime.Now; this.Code = code; this.Timestamp = timestamp; + this.User = user; } internal DateTime UseDate { get; private set; } internal long Timestamp { get; private set; } internal String Code { get; private set; } + internal object User { get; private set; } public override bool Equals(object obj) { - if (Object.ReferenceEquals(this, obj)) { + if (Object.ReferenceEquals(this, obj)) + { return true; } var other = obj as UsedCode; - return (other != null) ? this.Code.Equals(other.Code) && this.Timestamp.Equals(other.Timestamp) : false; + return (other != null) && this.Code.Equals(other.Code) && this.Timestamp.Equals(other.Timestamp) && this.User.Equals(other.User); } public override string ToString() { @@ -40,7 +43,7 @@ namespace TwoStepsAuthenticator } public override int GetHashCode() { - return Code.GetHashCode() + Timestamp.GetHashCode() * 17; + return Code.GetHashCode() + (Timestamp.GetHashCode() + User.GetHashCode() * 17) * 17; } } @@ -61,42 +64,44 @@ namespace TwoStepsAuthenticator { var timeToClean = DateTime.Now.AddMinutes(-5); - try + try { rwlock.AcquireWriterLock(lockingTimeout); - while (codes.Count > 0 && codes.Peek().UseDate < timeToClean) { + while (codes.Count > 0 && codes.Peek().UseDate < timeToClean) + { codes.Dequeue(); } - } - finally + } + finally { rwlock.ReleaseWriterLock(); } } - public void AddCode(long timestamp, String code) + public void AddCode(long timestamp, String code, object user) { - try { + try + { rwlock.AcquireWriterLock(lockingTimeout); - codes.Enqueue(new UsedCode(timestamp, code)); - } - finally + codes.Enqueue(new UsedCode(timestamp, code, user)); + } + finally { rwlock.ReleaseWriterLock(); } } - public bool IsCodeUsed(long timestamp, String code) + public bool IsCodeUsed(long timestamp, String code, object user) { - try + try { rwlock.AcquireReaderLock(lockingTimeout); - return codes.Contains(new UsedCode(timestamp, code)); - } - finally + return codes.Contains(new UsedCode(timestamp, code, user)); + } + finally { rwlock.ReleaseReaderLock(); } |