1
0
mirror of https://github.com/XFox111/SimpleOTP.git synced 2026-04-22 08:00:45 +03:00

Major 2.0 (#20)

* New 2.0 version + DependencyInjection library

* Updated docs and repo settings (devcontainers, vscode, github, etc.)

* Added tests

* Fixed bugs

* Minor test project refactoring

* Updated projects
- Added symbol packages
- Updated package versions to pre-release

* Updated SECURITY.md

* Added GitHub Actions workflows
This commit is contained in:
2024-09-26 03:20:30 +03:00
committed by GitHub
parent 42f968171b
commit 1b989e7b35
87 changed files with 4076 additions and 2532 deletions
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SimpleOTP.Tests")]
@@ -0,0 +1,33 @@
namespace SimpleOTP.DependencyInjection;
/// <summary>
/// Provides methods for generating and validating One-Time Passwords.
/// </summary>
public interface IOtpService
{
/// <summary>
/// Creates an OTP URI for specified user and secret.
/// </summary>
/// <param name="username">The username of the user.</param>
/// <param name="secret">The secret to use.</param>
/// <param name="counter">(only for HOTP) The counter to use.</param>
/// <returns>The generated URI.</returns>
public Uri CreateUri(string username, OtpSecret secret, long counter = 0);
/// <summary>
/// Creates an OTP code for specified user and secret.
/// </summary>
/// <param name="secret">The secret to use.</param>
/// <param name="counter">(only for HOTP) The counter to use.</param>
public OtpCode GenerateCode(OtpSecret secret, long counter = 0);
/// <summary>
/// Validates an OTP code for specified user and secret.
/// </summary>
/// <param name="code">The code to validate.</param>
/// <param name="secret">The secret to use.</param>
/// <param name="resyncValue">The resync value. Shows how much the code is ahead or behind the current counter value.</param>
/// <param name="counter">(only for HOTP) The counter to use.</param>
/// <returns><c>true</c> if the code is valid; otherwise, <c>false</c>.</returns>
public bool ValidateCode(OtpCode code, OtpSecret secret, out int resyncValue, long counter = 0);
}
@@ -0,0 +1,57 @@
using System.Collections.Specialized;
namespace SimpleOTP.DependencyInjection;
/// <summary>
/// Provides options for the One-Time Password service.
/// </summary>
public class OtpOptions
{
/// <summary>
/// The name of the issuer.
/// </summary>
public required string Issuer { get; set; }
/// <summary>
/// The issuer domain.
/// </summary>
/// <remarks>
/// <b>IMPORTANT:</b> Using this property will imply adherence to the Apple specification.
/// </remarks>
public string? IssuerDomain { get; set; }
/// <summary>
/// The algorithm to use.
/// </summary>
public OtpAlgorithm Algorithm { get; set; } = OtpAlgorithm.SHA1;
/// <summary>
/// The number of digits in the OTP code.
/// </summary>
public int Digits { get; set; } = 6;
/// <summary>
/// The number of seconds between each OTP code.
/// </summary>
public int Period { get; set; } = 30;
/// <summary>
/// The type of One-Time Password to generate.
/// </summary>
public OtpType Type { get; set; } = OtpType.Totp;
/// <summary>
/// The format of OTP URIs.
/// </summary>
public OtpUriFormat UriFormat { get; set; } = OtpUriFormat.Google | OtpUriFormat.Minimal;
/// <summary>
/// The tolerance span for the OTP codes validation.
/// </summary>
public ToleranceSpan ToleranceSpan { get; set; } = ToleranceSpan.Default;
/// <summary>
/// Custom properties to place in OTP URIs.
/// </summary>
public NameValueCollection CustomProperties { get; } = [];
}
@@ -0,0 +1,95 @@
using System.Collections.Specialized;
using SimpleOTP.Fluent;
namespace SimpleOTP.DependencyInjection;
/// <summary>
/// Provides methods for generating and validating One-Time Passwords.
/// </summary>
/// <param name="configuration">The configuration for the One-Time Password service.</param>
internal class OtpService(OtpOptions configuration) : IOtpService
{
private readonly string _issuerName = configuration.Issuer;
private readonly string? _issuerDomain = configuration.IssuerDomain;
private readonly OtpAlgorithm _algorithm = configuration.Algorithm;
private readonly OtpType _type = configuration.Type;
private readonly OtpUriFormat _format = configuration.UriFormat |
(string.IsNullOrWhiteSpace(configuration.IssuerDomain) ? 0 : OtpUriFormat.Apple);
private readonly int _digits = configuration.Digits;
private readonly int _period = configuration.Period;
private readonly NameValueCollection _customProperties = configuration.CustomProperties;
private readonly ToleranceSpan _tolerance = configuration.ToleranceSpan;
/// <summary>
/// Creates an OTP URI for specified user and secret.
/// </summary>
/// <param name="username">The username of the user.</param>
/// <param name="secret">The secret to use.</param>
/// <param name="counter">(only for HOTP) The counter to use.</param>
/// <returns>The generated URI.</returns>
public Uri CreateUri(string username, OtpSecret secret, long counter = 0)
{
OtpConfig config = new(username)
{
Algorithm = _algorithm,
Type = _type,
Issuer = _issuerName,
Digits = _digits,
Period = _period,
Secret = secret,
Counter = counter
};
if (!string.IsNullOrWhiteSpace(_issuerDomain))
config.WithAppleIssuer(_issuerName, _issuerDomain);
config.CustomProperties.Add(_customProperties);
return config.ToUri(_format);
}
/// <summary>
/// Creates an OTP code for specified user and secret.
/// </summary>
/// <param name="secret">The secret to use.</param>
/// <param name="counter">(only for HOTP) The counter to use.</param>
/// <returns>The generated code.</returns>
/// <exception cref="NotSupportedException">The service was not configured properly. Check the "Authenticator:Type" configuration.</exception>
public OtpCode GenerateCode(OtpSecret secret, long counter = 0)
{
using OtpSecret secretClone = OtpSecret.CreateCopy(secret);
Otp generator = _type switch
{
OtpType.Hotp => new Hotp(secret, counter, _algorithm, _digits),
OtpType.Totp => new Totp(secret, _period, _algorithm, _digits),
_ => throw new NotSupportedException("The service was not configured properly. Check the \"Authenticator:Type\" configuration.")
};
return generator.Generate();
}
/// <summary>
/// Validates an OTP code for specified user and secret.
/// </summary>
/// <param name="code">The code to validate.</param>
/// <param name="secret">The secret to use.</param>
/// <param name="resyncValue">The resync value. Shows how much the code is ahead or behind the current counter value.</param>
/// <param name="counter">(only for HOTP) The counter to use.</param>
/// <returns><c>true</c> if the code is valid; otherwise, <c>false</c>.</returns>
/// <exception cref="NotSupportedException">The service was not configured properly. Check the "Authenticator:Type" configuration.</exception>
public bool ValidateCode(OtpCode code, OtpSecret secret, out int resyncValue, long counter = 0)
{
using OtpSecret secretClone = OtpSecret.CreateCopy(secret);
Otp generator = _type switch
{
OtpType.Hotp => new Hotp(secret, counter, _algorithm, _digits),
OtpType.Totp => new Totp(secret, _period, _algorithm, _digits),
_ => throw new NotSupportedException("The service was not configured properly. Check the \"Authenticator:Type\" configuration.")
};
return generator.Validate(code, _tolerance, out resyncValue);
}
}
@@ -0,0 +1,76 @@
namespace SimpleOTP.DependencyInjection;
/// <summary>
/// Configuration for the One-Time Password service.
/// </summary>
public class OtpServiceConfig
{
/// <summary>
/// The name of the issuer.
/// </summary>
public string Issuer { get; set; } = null!;
/// <summary>
/// The issuer domain.
/// </summary>
/// <remarks>
/// <b>IMPORTANT:</b> Using this property will imply adherence to the Apple specification.
/// </remarks>
public string? IssuerDomain { get; set; }
/// <summary>
/// The algorithm to use.
/// </summary>
public OtpAlgorithm Algorithm { get; set; } = OtpAlgorithm.SHA1;
/// <summary>
/// The number of digits in the OTP code.
/// </summary>
public int Digits { get; set; } = 6;
/// <summary>
/// The number of seconds between each OTP code.
/// </summary>
public int Period { get; set; } = 30;
/// <summary>
/// The type of One-Time Password to generate.
/// </summary>
public OtpType Type { get; set; } = OtpType.Totp;
/// <summary>
/// The format of OTP URIs.
/// </summary>
public OtpUriFormat UriFormat { get; set; } = OtpUriFormat.Google;
/// <summary>
/// Whether to use minimal URI formatting (only required, or altered properties are included), or full URI formatting.
/// </summary>
public bool MinimalUri { get; set; } = true;
/// <summary>
/// The tolerance span for the OTP codes validation.
/// </summary>
public ToleranceSpanConfig ToleranceSpan { get; set; } = new();
/// <summary>
/// Custom properties to place in OTP URIs.
/// </summary>
public Dictionary<string, string> CustomProperties { get; } = [];
}
/// <summary>
/// Configuration for the tolerance span.
/// </summary>
public class ToleranceSpanConfig
{
/// <summary>
/// The number of periods/counter values behind the current value.
/// </summary>
public int Behind { get; set; } = ToleranceSpan.Default.Behind;
/// <summary>
/// The number of periods/counter values ahead of the current value.
/// </summary>
public int Ahead { get; set; } = ToleranceSpan.Default.Ahead;
}
@@ -0,0 +1,96 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace SimpleOTP.DependencyInjection;
/// <summary>
/// Extension methods for the One-Time Password service.
/// </summary>
public static class OtpServiceExtensions
{
/// <summary>
/// Adds the One-Time Password service to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="issuerName">The issuer/application/service name.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddAuthenticator(this IServiceCollection services, string issuerName) =>
AddAuthenticator(services, issuerName);
/// <summary>
/// Adds the One-Time Password service to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="issuerName">The issuer/application/service name.</param>
/// <param name="configure">The configuration for the One-Time Password service.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddAuthenticator(this IServiceCollection services, string issuerName, Action<OtpOptions>? configure = null)
{
OtpOptions options = new()
{
Issuer = issuerName
};
configure?.Invoke(options);
services.AddTransient<IOtpService, OtpService>(_ => new OtpService(options));
return services;
}
/// <summary>
/// Adds the One-Time Password service to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">The configuration for the One-Time Password service.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddAuthenticator(this IServiceCollection services, IConfiguration configuration) =>
AddAuthenticator(services, configuration);
/// <summary>
/// Adds the One-Time Password service to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">The configuration for the One-Time Password service.</param>
/// <param name="configure">The configuration for the One-Time Password service.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddAuthenticator(this IServiceCollection services, IConfiguration configuration, Action<OtpOptions>? configure = null)
{
OtpOptions? options = GetOptionsFromConfiguration(configuration);
if (options is null)
return services;
configure?.Invoke(options);
services.AddTransient<IOtpService, OtpService>(_ => new OtpService(options));
return services;
}
private static OtpOptions? GetOptionsFromConfiguration(IConfiguration configuration)
{
OtpServiceConfig config = new();
IConfigurationSection configSection = configuration.GetSection("Authenticator");
if (!configSection.Exists() || string.IsNullOrWhiteSpace(configuration["Authenticator:Issuer"]))
return null;
configSection.Bind(config);
OtpOptions options = new()
{
Issuer = config.Issuer,
Algorithm = config.Algorithm,
Type = config.Type,
Digits = config.Digits,
Period = config.Period,
IssuerDomain = config.IssuerDomain,
ToleranceSpan = (config.ToleranceSpan.Behind, config.ToleranceSpan.Ahead),
UriFormat = config.UriFormat
};
options.UriFormat |= config.MinimalUri ? OtpUriFormat.Minimal : OtpUriFormat.Full;
foreach (KeyValuePair<string, string> pair in config.CustomProperties)
options.CustomProperties.Add(pair.Key, pair.Value);
return options;
}
}
@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<PackageId>EugeneFox.SimpleOTP.DependencyInjection</PackageId>
<Version>8.0.0.0-rc1</Version>
<Authors>Eugene Fox</Authors>
<Copyright>Copyright © Eugene Fox 2024</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/XFox111/SimpleOTP.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/XFox111/SimpleOTP</PackageProjectUrl>
</PropertyGroup>
<PropertyGroup>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup>
<PackageTags>
otp;totp;hotp;authenticator;authentication;one-time;2fa;mfa;security;otpauth;services;dependency-injection;di</PackageTags>
<Description>
Dependency Injection implementation for SimpleOTP library. Allows to use SimpleOTP as DI
service in your application.
</Description>
<PackageReleaseNotes>
Initial release. See README.md for details.
</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\assets\icon.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
<None Include="..\..\README.md">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SimpleOTP\SimpleOTP.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="8.0.*" />
</ItemGroup>
</Project>