mirror of
https://github.com/XFox111/SimpleOTP.git
synced 2026-04-22 08:00:45 +03:00
Added tutorials
@@ -0,0 +1,92 @@
|
||||
This tutorial shows an example on how to implement two-factor authentication creation and validation in ASP.NET web services.
|
||||
|
||||
First, install `SimpleOTP.DependencyInjection` package:
|
||||
|
||||
```bash
|
||||
dotnet add package EugeneFox.SimpleOTP.DependencyInjection
|
||||
```
|
||||
|
||||
Register Authenticator service:
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
using SimpleOTP.DependencyInjection;
|
||||
...
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddAuthenticator("My service", options =>
|
||||
{
|
||||
// You can set custom options here
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
You can also define authenticator options in `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"Authenticator": {
|
||||
"Issuer": "My service",
|
||||
"Algorithm": "SHA512",
|
||||
"ToleranceSpan": {
|
||||
"Behind": 1,
|
||||
"Ahead": 1
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthenticator(builder.Configuration);
|
||||
```
|
||||
|
||||
Now you can use `IOtpService` in your controllers and services:
|
||||
|
||||
```csharp
|
||||
using SimpleOTP;
|
||||
using SimpleOTP.DependencyInjection;
|
||||
...
|
||||
|
||||
[ApiController, Route("[controller]")]
|
||||
public class MyController(IOtpService otpService) : ControllerBase
|
||||
{
|
||||
private readonly IOtpService _otpService = otpService;
|
||||
|
||||
[HttpPost, Route("enable2fa")]
|
||||
public IActionResult EnableTwoFactor()
|
||||
{
|
||||
var user = GetUser(); // Get current user
|
||||
|
||||
// Create new secret
|
||||
using OtpSecret secret = OtpSecret.CreateNew();
|
||||
|
||||
// Create configuration URI
|
||||
Uri uri = _otpService.CreateUri(user.Email, secret)
|
||||
|
||||
// Save secret
|
||||
user.AuthenticatorToken = secret;
|
||||
UpdateUser(user);
|
||||
|
||||
return Ok(uri.AbsoluteUri);
|
||||
}
|
||||
|
||||
[HttpPost, Route("login")]
|
||||
public IActionResult Login(string code)
|
||||
{
|
||||
var user = GetUser();
|
||||
|
||||
// Check if provided value is a valid code
|
||||
if (!OtpCode.TryParse(code, out OtpCode otpCode))
|
||||
return BadRequest();
|
||||
|
||||
// Validate code
|
||||
if (_otpService.Validate(otpCode, user.AuthenticatorToken))
|
||||
return Ok();
|
||||
|
||||
return Forbidden();
|
||||
}
|
||||
}
|
||||
```
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
This tutorial shows a basic example of how to use the library.
|
||||
|
||||
```csharp
|
||||
using SimpleOTP;
|
||||
|
||||
// Create a new secret
|
||||
// Make sure to dispose of the secret after use
|
||||
using OtpSecret secret = OtpSecret.CreateNew();
|
||||
|
||||
// Create a new generator
|
||||
Otp generator = new Totp(secret);
|
||||
|
||||
// Optionally, you can customize default options
|
||||
// generator.Period = 30;
|
||||
// generator.Digits = 6;
|
||||
// generator.Algorith = OtpAlgorithm.SHA1;
|
||||
|
||||
// Generate the code
|
||||
OtpCode code = generator.Generate(); // { _value = 1234, _digits = 6, CanExpire = true, ExpirationTime = <current date + 30 seconds> }
|
||||
Console.WriteLine(code); // 012345
|
||||
|
||||
// Verify the code
|
||||
generator.Validate(code); // True
|
||||
|
||||
await Task.Delay(30000); // 30 seconds
|
||||
generator.Validate(code); // False
|
||||
generator.Validate(code, new ToleranceSpan(behind: 1, ahead: 3)); // True
|
||||
|
||||
// Create new config
|
||||
OtpConfig config = new("user@example.com", "My app");
|
||||
|
||||
// Optionally, you can set pre-generated secret and customize other properties
|
||||
// OtpConfig config = new("user@example.com", "My app", secret);
|
||||
// config.Algorithm = OtpAlgorithm.SHA256;
|
||||
// config.Digits = 8;
|
||||
// ...
|
||||
|
||||
// Create a URI
|
||||
Uri uri = config.ToUri();
|
||||
Console.WriteLine(uri); // otpauth://totp/user@example.com?secret=KRUGKIDROVUWG2ZAMJZG653OEBTG66BO&issuer=My%20app
|
||||
|
||||
// Read config from URI
|
||||
OtpConfig config2 = OtpConfig.Parse(uri);
|
||||
Console.WriteLine(config == config2); // True
|
||||
|
||||
// Make sure to dispose of created secrets
|
||||
config.Secret.Dispose();
|
||||
config2.Secret.Dispose();
|
||||
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
> [!IMPORTANT]
|
||||
> This tutorial is made for educational purposes only! Do not use custom algorithms in public authenticator implementations!
|
||||
|
||||
## Justification
|
||||
This library is designed to be as flexible as possible to suit everyone's needs. This part is no exception, as it possibly can help to improve your service security.
|
||||
|
||||
In the current real-world scenarios it is unlikely that you ever need to implement custom algorithms.
|
||||
|
||||
## Best practicies
|
||||
### ✅ Do
|
||||
- Do consider creating custom hashing algorithms only for internal business apps implementations
|
||||
- Do consider creating custom hashing algorithms only if you believe that will improve your corporate app's security
|
||||
### ❌ Don't
|
||||
- **Do not ever** implement custom algorithms for creating `otpauth:` URIs in public services.
|
||||
- Do not override default algorithm providers.
|
||||
|
||||
## Creating new algorithms
|
||||
|
||||
The library utiliezes abstract `KeyedHashAlgorithm` class when managing different algorithms. If you need to implement a new one, or use one that is not included in the library by default (e.g. HMAC SHA-384), you can create a new class that inherits `KeyedHashAlgorithm` and override its methods.
|
||||
|
||||
## Registering provider
|
||||
|
||||
The library has a mechanism that detects hashing algorithms based on `OtpAlgorithm` value. If you need to implement a new algorithm, you can register it using `HashAlgorithmProviders.AddProvider` method:
|
||||
|
||||
```csharp
|
||||
HashAlgorithmProviders.AddProvider<HMACSHA384>((OtpAlgorithm)"SHA384");
|
||||
```
|
||||
|
||||
Once it has been registered it will be automatically recognized and used by the library.
|
||||
|
||||
Providers recognized by default and not required to be registered are:
|
||||
- HMAC SHA-1
|
||||
- HMAC SHA-256
|
||||
- HMAC SHA-512
|
||||
- HMAC MD5 (as per [IIJ specification][1])
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
using SimpleOTP;
|
||||
using SimpleOTP.Fluent;
|
||||
|
||||
HashAlgorithmProviders.AddProvider<HMACSHA384>((OtpAlgorithm)"SHA384");
|
||||
|
||||
string uri = "otpauth://totp/user@example.com?secret=KRUGKIDROVUWG2ZAMJZG653OEBTG66BO&algorithm=SHA384&issuer=example.com";
|
||||
|
||||
OtpConfig config = OtpConfig.Parse(uri);
|
||||
Console.WriteLine(config.Algorithm); // SHA384
|
||||
Console.WriteLine(config.ToUri()); // otpauth://totp/user@example.com?secret=KRUGKIDROVUWG2ZAMJZG653OEBTG66BO&algorithm=SHA384&issuer=example.com
|
||||
|
||||
Otp generator = OtpBuilder.FromConfig(config);
|
||||
generator.Generate(); // Will use HMACSHA384 algorithm
|
||||
|
||||
```
|
||||
|
||||
[1]: https://www1.auth.iij.jp/smartkey/en/uri_v1.html
|
||||
@@ -0,0 +1,194 @@
|
||||
> [!IMPORTANT]
|
||||
> This tutorial is made for educational purposes only! Do not use custom encodings in public authenticator implementations!
|
||||
|
||||
## Justification
|
||||
This library is designed to be as flexible as possible to suit everyone's needs. This part is no exception, as it possibly can help to improve your service security by, say, encrypting the secret.
|
||||
|
||||
Another reason why this was implemented is an [Internet Draft by Alexey Melnikov][1] which proposes to use "Extended Hex" Base32 alphabet to encode and decode secrets in OTP URIs.
|
||||
|
||||
In the current real-world scenarios it is unlikely that you ever need to implement custom encoders.
|
||||
|
||||
## Best practicies
|
||||
### ✅ Do
|
||||
- Do consider creating custom secret encoding only for internal business apps implementations
|
||||
- Do consider creating custom secret encoding only if you believe that will improve your corporate app's security
|
||||
- Do implement a mechanism that allows users to use default Base32 encoding as well.
|
||||
### ❌ Don't
|
||||
- **Do not ever** implement custom encoders for creating `otpauth:` URIs in public services.
|
||||
|
||||
## Scenario 1: Implementing custom Base32 alphabet
|
||||
Let's say, you want to adhere the [SCRAM specification][1], and instead of standard Base32, use Base32 "Extended Hex" alphabet.
|
||||
|
||||
<details>
|
||||
<summary>The Base 32 Alphabet (RFC 4648 §6)</summary>
|
||||
|
||||
Value Encoding Value Encoding Value Encoding Value Encoding
|
||||
0 A 9 J 18 S 27 3
|
||||
1 B 10 K 19 T 28 4
|
||||
2 C 11 L 20 U 29 5
|
||||
3 D 12 M 21 V 30 6
|
||||
4 E 13 N 22 W 31 7
|
||||
5 F 14 O 23 X
|
||||
6 G 15 P 24 Y (pad) =
|
||||
7 H 16 Q 25 Z
|
||||
8 I 17 R 26 2
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>The "Extended Hex" Base 32 Alphabet (RFC 4648 §7)</summary>
|
||||
|
||||
Value Encoding Value Encoding Value Encoding Value Encoding
|
||||
0 0 9 9 18 I 27 R
|
||||
1 1 10 A 19 J 28 S
|
||||
2 2 11 B 20 K 29 T
|
||||
3 3 12 C 21 L 30 U
|
||||
4 4 13 D 22 M 31 V
|
||||
5 5 14 E 23 N
|
||||
6 6 15 F 24 O (pad) =
|
||||
7 7 16 G 25 P
|
||||
8 8 17 H 26 Q
|
||||
</details>
|
||||
|
||||
This is relatively easy one. Default `Base32Encoder` provides override methods for character-to-value conversion and vice versa. All you need to do, is create a new class that will inherit `Base32Encoder` and override its methods:
|
||||
|
||||
```csharp
|
||||
// Base32HexEncoder.cs
|
||||
using SimpleOTP.Encoding;
|
||||
|
||||
namespace MyProgram;
|
||||
|
||||
/// <summary>
|
||||
/// Base32 "Extended Hex" encoder.
|
||||
/// </summary>
|
||||
public class Base32HexEncoder : Base32Encoder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string Scheme => "base32hex";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override int CharToValue(char c) =>
|
||||
(int)c switch
|
||||
{
|
||||
> 0x2F and < 0x3A => c - 0x30, // Digits
|
||||
> 0x40 and < 0x57 => c - 0x37, // Uppercase letters
|
||||
> 0x60 and < 0x77 => c - 0x57, // Lowercase letters
|
||||
_ => throw new ArgumentException("Character is not a Base32 Hex character.", nameof(c)),
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override char ValueToChar(int value) =>
|
||||
value switch
|
||||
{
|
||||
< 10 => (char)(value + 0x30), // Digits
|
||||
< 32 => (char)(value + 0x37), // Uppercase letters
|
||||
_ => throw new ArgumentException("Byte is not a Base32 Hex byte.", nameof(value)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Scenario 2: Implementing other encodings
|
||||
For example, you want to use any another encoding, or even add some additional encryption to the secret.
|
||||
|
||||
In this case, you need to create a new class that implements `SimpleOTP.Encoding.IEncoder` interface. Then you can use its constructor to pass any additional parameters, such as encryption key.
|
||||
|
||||
This example creates a new encoder that uses Base64 encoding with an addtional layer of AES encryption:
|
||||
|
||||
```csharp
|
||||
// Base64AesEncoder.cs
|
||||
using SimpleOTP.Encoding;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace MyProgram;
|
||||
|
||||
/// <summary>
|
||||
/// Base64 encoder with AES encryption.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key used for encryption.</param>
|
||||
public class Base64AesEncoder(byte[] key) : IEncoder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Scheme => "base64-aes";
|
||||
|
||||
/// <summary>
|
||||
/// The secret key used for encryption.
|
||||
/// </summary>
|
||||
public byte[] Key { get; } = key;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Base64AesEncoder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key used for encryption in Base64 encoding.</param>
|
||||
public Base64AesEncoder(string key) : this(Convert.FromBase64String(key)) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EncodeBytes(byte[] data)
|
||||
{
|
||||
using Aes aes = Aes.Create();
|
||||
|
||||
ICryptoTransform encryptor = aes.CreateEncryptor();
|
||||
return Convert.ToBase64String(encryptor.TransformFinalBlock(data, 0, data.Length));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GetBytes(string data)
|
||||
{
|
||||
byte[] dataBytes = Convert.FromBase64String(data);
|
||||
|
||||
using Aes aes = Aes.Create();
|
||||
|
||||
ICryptoTransform decryptor = aes.CreateDecryptor();
|
||||
return decryptor.TransformFinalBlock(dataBytes, 0, dataBytes.Length);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using the encoder
|
||||
|
||||
There are multiple ways you can use your custom encoder:
|
||||
|
||||
### Directly creating `OtpSecret` object
|
||||
|
||||
```csharp
|
||||
string base32 = "KRUGKIDROVUWG2ZAMJZG653OEBTG66BO"; // Base32
|
||||
string base32hex = "AHK6A83HELKM6QP0C9P6UTRE41J6UU1E"; // Base32 "Extended Hex"
|
||||
|
||||
// Decode
|
||||
IEncoder encoder = new Base32HexEncoder();
|
||||
OtpSecret secret = OtpSecret.Parse(base32);
|
||||
OtpSecret hexSecret = OtpSecret.Parse(base32hex, encoder);
|
||||
|
||||
// Encode
|
||||
Console.WriteLine(secret == hexSecret); // True
|
||||
Console.WriteLine(secret.ToString()); // KRUGKIDROVUWG2ZAMJZG653OEBTG66BO (Base32)
|
||||
Console.WriteLine(secret.ToString(encoder)); // AHK6A83HELKM6QP0C9P6UTRE41J6UU1E (Base32 "Extended Hex")
|
||||
```
|
||||
|
||||
### When parsing `OtpConfig`
|
||||
|
||||
```csharp
|
||||
// OTP URI with Base32 "Extended Hex" encoded secret
|
||||
string uri = "otpauth://totp/user@example.com?secret=AHK6A83HELKM6QP0C9P6UTRE41J6UU1E&issuer=example.com";
|
||||
|
||||
OtpConfig config = OtpConfig.Parse(uri, new Base32HexEncoder());
|
||||
|
||||
```
|
||||
|
||||
### Overriding default encoder
|
||||
|
||||
```csharp
|
||||
OtpSecret.DefaultEncoder = new Base32HexEncoder();
|
||||
|
||||
string base32hex = "AHK6A83HELKM6QP0C9P6UTRE41J6UU1E"; // Base32 "Extended Hex"
|
||||
|
||||
OtpSecret secret = OtpSecret.Parse(base32hex);
|
||||
|
||||
Console.WriteLine(secret.ToString()); // AHK6A83HELKM6QP0C9P6UTRE41J6UU1E (Base32 "Extended Hex")
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
JSON/XML Serialization will always use default encoder. You can override it globally via `OtpSecret.DefaultEncoder` property.
|
||||
|
||||
> [!IMPORTANT]
|
||||
There is no implemented mechanism to detect custom encodings.
|
||||
|
||||
[1]: https://datatracker.ietf.org/doc/draft-ietf-kitten-scram-2fa/
|
||||
+9
-14
@@ -1,14 +1,9 @@
|
||||
- [Home](https://github.com/XFox111/SimpleOTP/wiki/)
|
||||
- Get started
|
||||
- [OTP overview (Wiki)](https://en.wikipedia.org/wiki/Time-based_One-Time_Password)
|
||||
- [Configuration](https://github.com/XFox111/SimpleOTP/wiki/Configuration)
|
||||
- [Code generation/validation](https://github.com/XFox111/SimpleOTP/wiki/Code-generation-or-validation)
|
||||
- [OTPFactory](https://github.com/XFox111/SimpleOTP/wiki/OTPFactory-overview)
|
||||
- [API reference](https://github.com/XFox111/SimpleOTP/wiki/API-reference)
|
||||
- [OTPService](https://github.com/XFox111/SimpleOTP/wiki/OTPService)
|
||||
- [OTPFactory](https://github.com/XFox111/SimpleOTP/wiki/OTPFactory)
|
||||
- [OTPConfiguration](https://github.com/XFox111/SimpleOTP/wiki/OTPConfiguration)
|
||||
- [OTPCode](https://github.com/XFox111/SimpleOTP/wiki/OTPCode)
|
||||
- [OTPType](https://github.com/XFox111/SimpleOTP/wiki/OTPType)
|
||||
- [Algorithm](https://github.com/XFox111/SimpleOTP/wiki/Algorithm)
|
||||
- [SecretGenerator](https://github.com/XFox111/SimpleOTP/wiki/SecretGenerator)
|
||||
- [Get started](./Home)
|
||||
- [Contribution Guidelines](./Contribution-Guidelines)
|
||||
- [Code style](./Code-style)
|
||||
- Examples and tutorials
|
||||
- [Basic example](./Basic-example)
|
||||
- [ASP.NET example](./ASP.NET-code-validation)
|
||||
- [Implementing custom hashing algorithms](./Implementing-custom-hashing-algorithms)
|
||||
- [Implementing custom secret encoding](./Implementing-custom-secret-encoding)
|
||||
- [API reference](./API-reference)
|
||||
|
||||
Reference in New Issue
Block a user