Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions BootstrapBlazor.Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.UniverIcon"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.ChatBot", "src\components\BootstrapBlazor.ChatBot\BootstrapBlazor.ChatBot.csproj", "{2F37FBF4-5C1C-4493-B614-0E8361432621}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Authenticator", "src\components\BootstrapBlazor.Authenticator\BootstrapBlazor.Authenticator.csproj", "{1FDDF0AD-7AB6-4706-A183-26C680817BB4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -492,6 +494,10 @@ Global
{2F37FBF4-5C1C-4493-B614-0E8361432621}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F37FBF4-5C1C-4493-B614-0E8361432621}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F37FBF4-5C1C-4493-B614-0E8361432621}.Release|Any CPU.Build.0 = Release|Any CPU
{1FDDF0AD-7AB6-4706-A183-26C680817BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FDDF0AD-7AB6-4706-A183-26C680817BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FDDF0AD-7AB6-4706-A183-26C680817BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FDDF0AD-7AB6-4706-A183-26C680817BB4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -577,6 +583,7 @@ Global
{E30AAB64-BF28-4960-89C1-1F521025F531} = {FF1089BE-C704-4374-B629-C57C08E1798F}
{A657E04C-1495-439E-BC2E-1EEAB2D1B4DA} = {FF1089BE-C704-4374-B629-C57C08E1798F}
{2F37FBF4-5C1C-4493-B614-0E8361432621} = {FF1089BE-C704-4374-B629-C57C08E1798F}
{1FDDF0AD-7AB6-4706-A183-26C680817BB4} = {FF1089BE-C704-4374-B629-C57C08E1798F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF}
Expand Down
1 change: 1 addition & 0 deletions exclusion.dic
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,4 @@ vimeo
scrlang
Validata
Validatable
Totp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.0.0</Version>
</PropertyGroup>

<PropertyGroup>
<PackageTags>Bootstrap Blazor WebAssembly wasm Authenticator 2FA MFA OTP TOTP HOTP</PackageTags>
<Description>Bootstrap UI components extensions of Authenticator</Description>
<RootNamespace>BootstrapBlazor.Components</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BootstrapBlazor" Version="9.5.12" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\BootstrapBlazor\src\BootstrapBlazor\BootstrapBlazor.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

internal static class OtpExtensions
{
public static OtpNet.OtpHashMode ToMode(this OtpHashMode mode) => mode switch
{
OtpHashMode.Sha256 => OtpNet.OtpHashMode.Sha256,
OtpHashMode.Sha512 => OtpNet.OtpHashMode.Sha512,
_ => OtpNet.OtpHashMode.Sha1
};

public static OtpNet.OtpType ToType(this OtpType type) => type switch
{
OtpType.Hotp => OtpNet.OtpType.Hotp,
_ => OtpNet.OtpType.Totp
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using Microsoft.Extensions.DependencyInjection;

namespace BootstrapBlazor.Components;

/// <summary>
/// BootstrapBlazor service extensions
/// </summary>
public static class ServiceCollectionExtension
{
/// <summary>
/// Inject <see cref="ITotpService"/> service extension method.
/// </summary>
/// <param name="services"></param>
/// <param name="configOptions"></param>
/// <returns></returns>
public static IServiceCollection AddBootstrapBlazorTotpService(this IServiceCollection services, Action<
OtpOptions>? configOptions = null)
{
services.AddSingleton<ITotpService, DefaultTotpService>();
services.Configure<OtpOptions>(options =>
{
configOptions?.Invoke(options);

options.AccountName ??= "BootstrapBlazor";
options.UserName ??= "Simulator";
options.IssuerName ??= options.AccountName;
options.SecretKey ??= "OMM2LVLFX6QJHMYI";
});
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using Microsoft.Extensions.Options;
using OtpNet;

namespace BootstrapBlazor.Components;

class DefaultTotpService(IOptionsMonitor<OtpOptions> optionsMonitor) : ITotpService
{
[NotNull]
public TotpInstanceBase? Instance { get; private set; }

public string GenerateOtpUri(OtpOptions? options = null)
{
options ??= optionsMonitor.CurrentValue;
var type = options.Type.ToType();
var mode = options.Algorithm.ToMode();
var uri = new OtpUri(type, options.SecretKey, options.UserName, options.IssuerName, mode, options.Digits, options.Period, options.Counter);
return uri.ToString();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): Consider thread-safety concerns with the mutable Instance property.

The Instance property is updated in the Compute method and then used in GetRemainingSeconds and Verify. In concurrent scenarios, this mutable field could lead to race conditions or unexpected behavior. It might be worthwhile to review thread-safety or consider a stateless approach.


public string Compute(string secretKey, int period = 30, OtpHashMode mode = OtpHashMode.Sha1, int digits = 6, DateTime? timestamp = null)
{
var instance = new Totp(Base32Encoding.ToBytes(secretKey), period, mode.ToMode(), digits, timeCorrection: null);
Instance = new DefaultTotpInstance(instance);
return timestamp == null ? instance.ComputeTotp() : instance.ComputeTotp(timestamp.Value);
}

public int GetRemainingSeconds(DateTime? timestamp = null)
{
if (Instance != null)
{
return timestamp == null ? Instance.GetRemainingSeconds() : Instance.GetRemainingSeconds(timestamp.Value);
}
var instance = new Totp(Base32Encoding.ToBytes("OMM2LVLFX6QJHMYI"));
return timestamp == null ? instance.RemainingSeconds() : instance.RemainingSeconds(timestamp.Value);
}

public string GenerateSecretKey(int length = 20)
{
var secretKey = KeyGeneration.GenerateRandomKey(length);
return Base32Encoding.ToString(secretKey);
}

public byte[] GetSecretKeyBytes(string input)
{
return Base32Encoding.ToBytes(input);
}

public bool Verify(string code, DateTime? timestamp = null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consistent use of configured secret key in Verify method.

Similar to GetRemainingSeconds, if Instance is null, Verify creates a Totp using a hard-coded secret. It might be beneficial to ensure that verification uses the same configuration from OtpOptions to avoid unexpected results.

Suggested implementation:

var instance = new Totp(GetSecretKeyBytes(OtpOptions.Secret));
return timestamp == null ? instance.RemainingSeconds() : instance.RemainingSeconds(timestamp.Value);

Ensure that the DefaultTotpService class has the OtpOptions (or similarly named options object) properly injected and that the property containing the secret key is named "Secret". If your property name is different, please update the field accordingly.

{
if (Instance != null)
{
return timestamp == null ? Instance.Verify(code) : Instance.Verify(code, timestamp.Value);
}
var instance = new Totp(Base32Encoding.ToBytes("OMM2LVLFX6QJHMYI"));
return timestamp == null ? instance.VerifyTotp(code, out _) : instance.VerifyTotp(timestamp.Value, code, out _);
}
}

class DefaultTotpInstance(Totp instance) : TotpInstanceBase
{
public override int GetRemainingSeconds(DateTime? timestamp = null)
{
return timestamp == null ? instance.RemainingSeconds() : instance.RemainingSeconds(timestamp.Value);
}

public override bool Verify(string code, DateTime? timestamp = null)
{
return timestamp == null ? instance.VerifyTotp(code, out _) : instance.VerifyTotp(timestamp.Value, code, out _);
}
}