-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ffcda7e
commit 198e586
Showing
13 changed files
with
288 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 3 additions & 3 deletions
6
src/OrchardCore/OrchardCore.Users.Core/Models/ChangeEmailTokenProviderOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,13 @@ | ||
using System; | ||
using Microsoft.AspNetCore.Identity; | ||
using OrchardCore.Users.Services; | ||
|
||
namespace OrchardCore.Users.Models; | ||
|
||
public sealed class ChangeEmailTokenProviderOptions : DataProtectionTokenProviderOptions | ||
public sealed class ChangeEmailTokenProviderOptions : TotpEmailTokenProviderOptions | ||
{ | ||
public ChangeEmailTokenProviderOptions() | ||
{ | ||
Name = "ChangeEmailDataProtectionTokenProvider"; | ||
Name = nameof(ChangeEmailTokenProvider); | ||
TokenLifespan = TimeSpan.FromMinutes(15); | ||
} | ||
} |
6 changes: 3 additions & 3 deletions
6
src/OrchardCore/OrchardCore.Users.Core/Models/EmailConfirmationTokenProviderOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,13 @@ | ||
using System; | ||
using Microsoft.AspNetCore.Identity; | ||
using OrchardCore.Users.Services; | ||
|
||
namespace OrchardCore.Users.Models; | ||
|
||
public sealed class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions | ||
public sealed class EmailConfirmationTokenProviderOptions : TotpEmailTokenProviderOptions | ||
{ | ||
public EmailConfirmationTokenProviderOptions() | ||
{ | ||
Name = "EmailConfirmationDataProtectorTokenProvider"; | ||
Name = nameof(EmailConfirmationTokenProvider); | ||
TokenLifespan = TimeSpan.FromHours(48); | ||
} | ||
} |
6 changes: 3 additions & 3 deletions
6
src/OrchardCore/OrchardCore.Users.Core/Models/PasswordResetTokenProviderOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,13 @@ | ||
using System; | ||
using Microsoft.AspNetCore.Identity; | ||
using OrchardCore.Users.Services; | ||
|
||
namespace OrchardCore.Users.Models; | ||
|
||
public sealed class PasswordResetTokenProviderOptions : DataProtectionTokenProviderOptions | ||
public sealed class PasswordResetTokenProviderOptions : TotpEmailTokenProviderOptions | ||
{ | ||
public PasswordResetTokenProviderOptions() | ||
{ | ||
Name = "PasswordResetDataProtectorTokenProvider"; | ||
Name = nameof(PasswordResetTokenProvider); | ||
TokenLifespan = TimeSpan.FromMinutes(15); | ||
} | ||
} |
11 changes: 4 additions & 7 deletions
11
src/OrchardCore/OrchardCore.Users.Core/Services/ChangeEmailTokenProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,15 @@ | ||
using Microsoft.AspNetCore.DataProtection; | ||
using Microsoft.AspNetCore.Identity; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using OrchardCore.Modules; | ||
using OrchardCore.Users.Models; | ||
|
||
namespace OrchardCore.Users.Services; | ||
|
||
public sealed class ChangeEmailTokenProvider : DataProtectorTokenProvider<IUser> | ||
public sealed class ChangeEmailTokenProvider : TotpEmailTokenProvider | ||
{ | ||
public ChangeEmailTokenProvider( | ||
IDataProtectionProvider dataProtectionProvider, | ||
IOptions<ChangeEmailTokenProviderOptions> options, | ||
ILogger<ChangeEmailTokenProvider> logger) | ||
: base(dataProtectionProvider, options, logger) | ||
IClock clock) | ||
: base(options, clock) | ||
{ | ||
} | ||
} |
11 changes: 4 additions & 7 deletions
11
src/OrchardCore/OrchardCore.Users.Core/Services/EmailConfirmationTokenProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,15 @@ | ||
using Microsoft.AspNetCore.DataProtection; | ||
using Microsoft.AspNetCore.Identity; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using OrchardCore.Modules; | ||
using OrchardCore.Users.Models; | ||
|
||
namespace OrchardCore.Users.Services; | ||
|
||
public sealed class EmailConfirmationTokenProvider : DataProtectorTokenProvider<IUser> | ||
public sealed class EmailConfirmationTokenProvider : TotpEmailTokenProvider | ||
{ | ||
public EmailConfirmationTokenProvider( | ||
IDataProtectionProvider dataProtectionProvider, | ||
IOptions<EmailConfirmationTokenProviderOptions> options, | ||
ILogger<EmailConfirmationTokenProvider> logger) | ||
: base(dataProtectionProvider, options, logger) | ||
IClock clock) | ||
: base(options, clock) | ||
{ | ||
} | ||
} |
12 changes: 4 additions & 8 deletions
12
src/OrchardCore/OrchardCore.Users.Core/Services/PasswordResetTokenProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,15 @@ | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.DataProtection; | ||
using Microsoft.AspNetCore.Identity; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using OrchardCore.Modules; | ||
using OrchardCore.Users.Models; | ||
|
||
namespace OrchardCore.Users.Services; | ||
|
||
public sealed class PasswordResetTokenProvider : DataProtectorTokenProvider<IUser> | ||
public sealed class PasswordResetTokenProvider : TotpEmailTokenProvider | ||
{ | ||
public PasswordResetTokenProvider( | ||
IDataProtectionProvider dataProtectionProvider, | ||
IOptions<PasswordResetTokenProviderOptions> options, | ||
ILogger<PasswordResetTokenProvider> logger) | ||
: base(dataProtectionProvider, options, logger) | ||
IClock clock) | ||
: base(options, clock) | ||
{ | ||
} | ||
} |
145 changes: 145 additions & 0 deletions
145
src/OrchardCore/OrchardCore.Users.Core/Services/Rfc6238AuthenticationService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
using System; | ||
using System.Diagnostics; | ||
using System.Globalization; | ||
using System.Net; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
using OrchardCore.Modules; | ||
|
||
namespace OrchardCore.Users.Services; | ||
|
||
/// <summary> | ||
/// The following code is influenced by <see href="https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs"/> | ||
/// </summary> | ||
public sealed class Rfc6238AuthenticationService | ||
{ | ||
private static readonly UTF8Encoding _encoding = new(false, true); | ||
|
||
private readonly TimeSpan _timeSpan; | ||
private readonly TwoFactorEmailTokenLength _length; | ||
private readonly IClock _clock; | ||
|
||
private int? _modulo; | ||
private string _format; | ||
|
||
public Rfc6238AuthenticationService( | ||
TimeSpan timeSpan, | ||
TwoFactorEmailTokenLength length, | ||
IClock clock) | ||
{ | ||
_timeSpan = timeSpan; | ||
_length = length; | ||
_clock = clock; | ||
} | ||
|
||
private int GetModuloValue() | ||
{ | ||
// Number of 0's is length of the generated PIN. | ||
_modulo ??= _length switch | ||
{ | ||
TwoFactorEmailTokenLength.Six => 1000000, | ||
TwoFactorEmailTokenLength.Seven => 10000000, | ||
TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => 100000000, | ||
_ => throw new NotSupportedException("Unsupported token length.") | ||
}; | ||
|
||
return _modulo.Value; | ||
} | ||
|
||
private string GetStringFormat() | ||
{ | ||
_format ??= _length switch | ||
{ | ||
TwoFactorEmailTokenLength.Six => "D6", | ||
TwoFactorEmailTokenLength.Seven => "D7", | ||
TwoFactorEmailTokenLength.Eight or TwoFactorEmailTokenLength.Default => "D8", | ||
_ => throw new NotSupportedException("Unsupported token length.") | ||
}; | ||
|
||
return _format; | ||
} | ||
|
||
public string GetString(int code) | ||
=> code.ToString(GetStringFormat(), CultureInfo.InvariantCulture); | ||
|
||
public int ComputeTOTP(byte[] key, ulong timestepNumber, byte[] modifierBytes) | ||
{ | ||
// See https://tools.ietf.org/html/rfc4226 | ||
// We can add an optional modifier. | ||
Span<byte> timestepAsBytes = stackalloc byte[sizeof(long)]; | ||
var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); | ||
Debug.Assert(res); | ||
|
||
var modifierCombinedBytes = timestepAsBytes; | ||
if (modifierBytes is not null) | ||
{ | ||
modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); | ||
} | ||
|
||
Span<byte> hash = stackalloc byte[HMACSHA256.HashSizeInBytes]; | ||
res = HMACSHA256.TryHashData(key, modifierCombinedBytes, hash, out var written); | ||
|
||
// Generate DT string. | ||
var offset = hash[hash.Length - 1] & 0xf; | ||
Debug.Assert(offset + 4 < hash.Length); | ||
var binaryCode = (hash[offset] & 0x7f) << 24 | ||
| (hash[offset + 1] & 0xff) << 16 | ||
| (hash[offset + 2] & 0xff) << 8 | ||
| (hash[offset + 3] & 0xff); | ||
|
||
return binaryCode % GetModuloValue(); | ||
} | ||
|
||
private static byte[] ApplyModifier(Span<byte> input, byte[] modifierBytes) | ||
{ | ||
var combined = new byte[checked(input.Length + modifierBytes.Length)]; | ||
input.CopyTo(combined); | ||
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); | ||
|
||
return combined; | ||
} | ||
|
||
/// <summary> | ||
/// More info: https://tools.ietf.org/html/rfc6238#section-4 | ||
/// </summary> | ||
private ulong GetCurrentTimeStepNumber() | ||
{ | ||
var delta = _clock.UtcNow - DateTimeOffset.UnixEpoch; | ||
|
||
return (ulong)(delta.Ticks / _timeSpan.Ticks); | ||
} | ||
|
||
public int GenerateCode(byte[] securityToken, string modifier = null) | ||
{ | ||
ArgumentNullException.ThrowIfNull(securityToken); | ||
|
||
var currentTimeStep = GetCurrentTimeStepNumber(); | ||
|
||
var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; | ||
|
||
return ComputeTOTP(securityToken, currentTimeStep, modifierBytes); | ||
} | ||
|
||
public bool ValidateCode(byte[] securityToken, int code, string modifier = null) | ||
{ | ||
ArgumentNullException.ThrowIfNull(securityToken); | ||
|
||
var currentTimeStep = GetCurrentTimeStepNumber(); | ||
|
||
var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; | ||
|
||
// Check the current, previous, and next time steps. | ||
for (var i = -1; i <= 1; i++) | ||
{ | ||
var computedTOTP = ComputeTOTP(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); | ||
|
||
if (computedTOTP == code) | ||
{ | ||
return true; | ||
} | ||
} | ||
|
||
// No match. | ||
return false; | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
src/OrchardCore/OrchardCore.Users.Core/Services/TotpEmailTokenProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Identity; | ||
using Microsoft.Extensions.Options; | ||
using OrchardCore.Modules; | ||
|
||
namespace OrchardCore.Users.Services; | ||
|
||
public class TotpEmailTokenProvider : IUserTwoFactorTokenProvider<IUser> | ||
{ | ||
private readonly TotpEmailTokenProviderOptions _options; | ||
private readonly IClock _clock; | ||
|
||
private Rfc6238AuthenticationService _service; | ||
|
||
public TotpEmailTokenProvider( | ||
IOptions<TotpEmailTokenProviderOptions> options, | ||
IClock clock) | ||
{ | ||
_options = options.Value; | ||
_clock = clock; | ||
} | ||
|
||
public virtual Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<IUser> manager, IUser user) | ||
=> Task.FromResult(false); | ||
|
||
public async Task<string> GenerateAsync(string purpose, UserManager<IUser> manager, IUser user) | ||
{ | ||
ArgumentNullException.ThrowIfNull(user); | ||
var token = await manager.CreateSecurityTokenAsync(user); | ||
var modifier = await GetUserModifierAsync(purpose, manager, user); | ||
|
||
var pin = _service.GenerateCode(token, modifier); | ||
|
||
_service ??= new Rfc6238AuthenticationService(_options.TokenLifespan, _options.TokenLength, _clock); | ||
|
||
return _service.GetString(pin); | ||
} | ||
|
||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<IUser> manager, IUser user) | ||
{ | ||
ArgumentNullException.ThrowIfNull(user); | ||
|
||
if (!int.TryParse(token, out var code)) | ||
{ | ||
return false; | ||
} | ||
|
||
var securityToken = await manager.CreateSecurityTokenAsync(user); | ||
var modifier = await GetUserModifierAsync(purpose, manager, user); | ||
|
||
_service ??= new Rfc6238AuthenticationService(_options.TokenLifespan, _options.TokenLength, _clock); | ||
|
||
return securityToken != null && | ||
_service.ValidateCode(securityToken, code, modifier); | ||
} | ||
|
||
private static async Task<string> GetUserModifierAsync(string purpose, UserManager<IUser> manager, IUser user) | ||
{ | ||
ArgumentNullException.ThrowIfNull(user); | ||
var userId = await manager.GetUserIdAsync(user); | ||
|
||
return $"Totp:{purpose}:{userId}"; | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/OrchardCore/OrchardCore.Users.Core/Services/TotpEmailTokenProviderOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using Microsoft.AspNetCore.Identity; | ||
|
||
namespace OrchardCore.Users.Services; | ||
|
||
public class TotpEmailTokenProviderOptions : DataProtectionTokenProviderOptions | ||
{ | ||
/// <summary> | ||
/// Gets or sets the generated token's length. Default value is 8 digits long. | ||
/// </summary> | ||
public TwoFactorEmailTokenLength TokenLength { get; set; } | ||
} |
9 changes: 9 additions & 0 deletions
9
src/OrchardCore/OrchardCore.Users.Core/Services/TwoFactorEmailTokenLength.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace OrchardCore.Users.Services; | ||
|
||
public enum TwoFactorEmailTokenLength | ||
{ | ||
Default, | ||
Six, | ||
Seven, | ||
Eight, | ||
} |
Oops, something went wrong.