/*
* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Accounts
* File: AccountSecProvider.cs
*
* AccountSecProvider.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
/*
* Implements the IAccountSecurityProvider interface to provide the shared
* service to the host application for securing user/account based connections
* via authorization.
*
* This system is technically configurable and optionally loadable
*/
using System;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis;
using FluentValidation;
using VNLib.Hashing;
using VNLib.Hashing.IdentityUtility;
using VNLib.Net.Http;
using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Users;
using VNLib.Plugins.Essentials.Sessions;
using VNLib.Plugins.Essentials.Middleware;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Validation;
namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider
{
[ConfigurationName("account_security", Required = false)]
[MiddlewareImpl(MiddlewareImplOptions.SecurityCritical)]
internal sealed class AccountSecProvider : IAccountSecurityProvider, IHttpMiddleware
{
private const int PUB_KEY_JWT_NONCE_SIZE = 16;
//Session entry keys
private const string PUBLIC_KEY_SIG_KEY_ENTRY = "acnt.pbsk";
private const HashAlg ClientTokenHmacType = HashAlg.SHA256;
///
/// The client data encryption padding.
///
public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256;
private readonly AccountSecConfig _config;
private readonly SingleCookieController _statusCookie;
private readonly SingleCookieController _pubkeyCookie;
private readonly ILogProvider _logger;
public AccountSecProvider(PluginBase plugin)
:this(plugin, new AccountSecConfig())
{ }
public AccountSecProvider(PluginBase plugin, IConfigScope config)
:this(
plugin,
config.DeserialzeAndValidate()
)
{ }
private AccountSecProvider(PluginBase plugin, AccountSecConfig config)
{
//Parse config if defined
_config = config;
//Status cookie handler
_statusCookie = new(_config.ClientStatusCookieName, _config.AuthorizationValidFor)
{
Domain = _config.CookieDomain,
Path = _config.CookiePath,
SameSite = CookieSameSite.Strict,
HttpOnly = false, //allow javascript to read this cookie
Secure = true
};
//Public key cookie handler
_pubkeyCookie = new(_config.PubKeyCookieName, _config.AuthorizationValidFor)
{
Domain = _config.CookieDomain,
Path = _config.CookiePath,
SameSite = CookieSameSite.Strict,
HttpOnly = true,
Secure = true
};
_logger = plugin.Log.CreateScope("Acnt-Sec");
}
/*
* Middleware handler for reconciling client cookies for all connections
*/
///
ValueTask IHttpMiddleware.ProcessAsync(HttpEntity entity)
{
ref readonly SessionInfo session = ref entity.Session;
//Session must be set and web based for checks
if (session.IsSet && session.SessionType == SessionType.Web)
{
//Make sure the session has not expired yet
if (OnMwCheckSessionExpired(entity, in session))
{
//Expired
ExpireCookies(entity, true);
//Verbose because this is a normal occurance
if (_logger.IsEnabled(LogLevel.Verbose))
{
_logger.Verbose("Session {id} expired", session.SessionID[..8]);
}
}
else
{
//See if the session might be elevated
if (!string.IsNullOrWhiteSpace(session.Token))
{
//If the session stored a user-agent, make sure it matches the connection
if (session.UserAgent != null && !session.UserAgent.Equals(entity.Server.UserAgent, StringComparison.Ordinal))
{
_logger.Debug("Denied authorized connection from {ip} because user-agent changed", entity.TrustedRemoteIp);
return ValueTask.FromResult(FileProcessArgs.Deny);
}
}
//If the session is new, or not supposed to be logged in, clear the login cookies if they were set
if (session.IsNew || string.IsNullOrEmpty(session.Token))
{
ExpireCookies(entity, false);
}
}
}
//Always continue otherwise
return ValueTask.FromResult(FileProcessArgs.Continue);
}
/*
* Verify sessions on new connections to ensure they have not expired
* and need to be regnerated or invalidated. If they are expired
* we need to cleanup any internal security flags/keys
*/
private bool OnMwCheckSessionExpired(HttpEntity entity, ref readonly SessionInfo session)
{
if (session.Created.AddSeconds(_config.WebSessionValidForSeconds) < entity.RequestedTimeUtc)
{
//Invalidate the session, so its technically valid for this request, but will be cleared on this handle close cycle
session.Invalidate();
//Clear basic login status now so checks will fail later
session.Token = null!;
session.UserID = null!;
session.Privilages = 0;
session[PUBLIC_KEY_SIG_KEY_ENTRY] = null!;
return true;
}
//Not expired
return false;
}
#region Interface Impl
///
IClientAuthorization IAccountSecurityProvider.AuthorizeClient(HttpEntity entity, IClientSecInfo clientInfo, IUser user)
{
//Validate client info
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(clientInfo);
ArgumentNullException.ThrowIfNull(clientInfo.PublicKey, nameof(clientInfo.PublicKey));
ArgumentNullException.ThrowIfNull(clientInfo.ClientId, nameof(clientInfo.ClientId));
if (!entity.Session.IsSet || entity.Session.IsNew || entity.Session.SessionType != SessionType.Web)
{
throw new ArgumentException("The session is no configured for authorization");
}
return GenerateAuth(entity, clientInfo.PublicKey, user.IsLocalAccount());
}
///
IClientAuthorization IAccountSecurityProvider.ReAuthorizeClient(HttpEntity entity)
{
//Confirm session is configured
if (!entity.Session.IsSet || entity.Session.IsNew || entity.Session.SessionType != SessionType.Web)
{
throw new InvalidOperationException("The session is not configured for authorization");
}
//recover the client's public key
if (!TryGetPublicKey(entity, out string? pubKey))
{
throw new InvalidOperationException("The user does not have the required public key token stored");
}
return GenerateAuth(entity, pubKey, entity.Session.HasLocalAccount());
}
///
void IAccountSecurityProvider.InvalidateLogin(HttpEntity entity)
{
//Client should also destroy the session
ExpireCookies(entity, true);
//Clear known security keys
entity.Session.Token = null!;
entity.Session[PUBLIC_KEY_SIG_KEY_ENTRY] = null!;
}
///
bool IAccountSecurityProvider.IsClientAuthorized(HttpEntity entity, AuthorzationCheckLevel level)
{
//Session must be loaded and not-new for an authorization to exist
if(!entity.Session.IsSet || entity.Session.IsNew)
{
return false;
}
return level switch
{
//Accept the client token or the cookie as any/medium
AuthorzationCheckLevel.Any or AuthorzationCheckLevel.Medium => VerifyClientToken(entity) || TryGetPublicKey(entity, out _),
//Critical requires that the client cookie is set and the token is set
AuthorzationCheckLevel.Critical => TryGetPublicKey(entity, out _) && VerifyClientToken(entity),
//Default to false condition
_ => false,
};
}
///
ERRNO IAccountSecurityProvider.TryEncryptClientData(HttpEntity entity, ReadOnlySpan data, Span outputBuffer)
{
//Recover the signed public key, already does session checks
return TryGetPublicKey(entity, out string? pubKey) ? TryEncryptClientData(pubKey, data, outputBuffer) : ERRNO.E_FAIL;
}
///
ERRNO IAccountSecurityProvider.TryEncryptClientData(IClientSecInfo entity, ReadOnlySpan data, Span outputBuffer)
{
//Use the public key supplied by the csecinfo
return TryEncryptClientData(entity.PublicKey, data, outputBuffer);
}
private IClientAuthorization GenerateAuth(HttpEntity entity, string publicKey, bool localAccount)
{
//Try to generate a new authorization
GenerateToken(publicKey, out string serverToken, out string clientToken);
/*
* The user's public key will be stored via a jwt cookie
* signed by this specific signing key, we will save the signing key
* in the session
*/
entity.Session[PUBLIC_KEY_SIG_KEY_ENTRY] = SetPublicKeyCookie(entity, publicKey);
entity.Session.Token = serverToken;
//set client status cookie via handler
_statusCookie.SetCookie(entity, localAccount ? "1" : "2");
//Return the new authorzation
return new EncryptedTokenAuthorization(clientToken);
}
#endregion
#region Security Tokens
/*
* A client token was an older term used for a single random token generated
* by the server and sent by the client.
*
* The latest revision generates a keypair on authorization, the public key
* is stored id the client's session, and the private key gets encrypted
* and sent to the client. The client uses this ECDSA key to sign one time use
* JWT tokens
*
*/
private void GenerateToken(ReadOnlySpan publicKey, out string serverToken, out string clientToken)
{
//Alloc buffer for encode/decode
using IMemoryHandle buffer = MemoryUtil.SafeAllocNearestPage(4000, true);
try
{
Span secretBuffer = buffer.Span[.._config.TokenKeySize];
Span outputBuffer = buffer.Span[_config.TokenKeySize..];
//Computes a random shared key
RandomHash.GetRandomBytes(secretBuffer);
ERRNO bytesEncrypted = TryEncryptClientData(publicKey, secretBuffer, outputBuffer);
//Encyrpt the secret key to send to client
if (!bytesEncrypted)
{
throw new InternalBufferTooSmallException("The internal buffer used to store the encrypted token is too small");
}
//Client token is the encrypted secret key
clientToken = Convert.ToBase64String(outputBuffer[..(int)bytesEncrypted]);
//Encode base64 url safe
serverToken = VnEncoding.ToBase64UrlSafeString(secretBuffer, false);
}
finally
{
//Zero buffer when complete
MemoryUtil.InitializeBlock(ref buffer.GetReference(), buffer.GetIntLength());
}
}
private bool VerifyClientToken(HttpEntity entity)
{
//Get the token from the client header, the client should always sent this
string? signedMessage = entity.Server.Headers[_config.TokenHeaderName];
//Make sure a session is loaded
if (!entity.Session.IsSet || entity.Session.IsNew || string.IsNullOrWhiteSpace(signedMessage))
{
return false;
}
//Get the stored shared symetric key
string sharedKey = entity.Session.Token;
if (string.IsNullOrWhiteSpace(sharedKey))
{
return false;
}
/*
* The clients signed message is a json web token that includes basic information
* Clients may send bad data, so we should swallow exceptions and return false
*/
try
{
bool isValid = true;
//Parse the client jwt signed message
using JsonWebToken jwt = JsonWebToken.Parse(signedMessage);
using (UnsafeMemoryHandle decodeBuffer = MemoryUtil.UnsafeAllocNearestPage(_config.TokenKeySize, true))
{
//Recover the key from base32
ERRNO count = VnEncoding.Base64UrlDecode(sharedKey, decodeBuffer.Span);
if (!count)
{
return false;
}
//Verity the jwt against the store symmetric key
isValid &= jwt.Verify(decodeBuffer.AsSpan(0, count), ClientTokenHmacType);
}
//Get the message payload
using JsonDocument data = jwt.GetPayload();
//Get iat time
if (data.RootElement.TryGetProperty("iat", out JsonElement iatEl)
&& iatEl.ValueKind == JsonValueKind.Number)
{
//Try to get iat in unint seconds
isValid &= iatEl.TryGetInt64(out long iatSec);
//Recover dto from unix seconds regardless of int success
DateTimeOffset iat = DateTimeOffset.FromUnixTimeSeconds(iatSec);
//Verify iat against current time with allowed disparity
isValid &= iat.Add(_config.SignedTokenTimeDiff) > entity.RequestedTimeUtc;
//Message is too far into the future!
isValid &= iat.Subtract(_config.SignedTokenTimeDiff) < entity.RequestedTimeUtc;
}
else
{
//No time element provided
isValid = false;
}
if (_config.VerifyOrigin)
{
//Check the audience matches the request uri
if (data.RootElement.TryGetProperty("aud", out JsonElement tokenOriginEl)
&& tokenOriginEl.ValueKind == JsonValueKind.String)
{
string? unsafeUserOrigin = tokenOriginEl.GetString();
if(string.IsNullOrWhiteSpace(unsafeUserOrigin))
{
isValid = false;
}
else if (_config.EnforceSameOriginToken)
{
//enforce strict origin checking
string strictOrigin = entity.Server.RequestUri.GetLeftPart(UriPartial.Authority);
isValid &= string.Equals(unsafeUserOrigin, strictOrigin, StringComparison.OrdinalIgnoreCase);
if (!isValid)
{
_logger.Debug("Client security OTP JWT origin mismatch from {ip} : strict origin {current} != {token}",
entity.TrustedRemoteIp,
strictOrigin,
unsafeUserOrigin
);
}
}
else
{
//Verify against allow list
isValid &= _config.AllowedOrigins!.Contains(unsafeUserOrigin, StringComparer.OrdinalIgnoreCase);
if (!isValid)
{
_logger.Debug("CST origin not allowed {ip} : {token}",
entity.TrustedRemoteIp,
unsafeUserOrigin
);
}
}
}
else
{
isValid = false;
}
}
if (_config.VerifyPath)
{
//Check the subject (path) matches the request uri
if (data.RootElement.TryGetProperty("path", out JsonElement tokenPathEl)
&& tokenPathEl.ValueKind == JsonValueKind.String)
{
ReadOnlySpan unsafeUserPath = tokenPathEl.GetString();
/*
* Query parameters are optional, so we need to check if the path contains a
* query, if so we can compare the entire path and query, otherwise we need to
* compare the path only
*/
if (unsafeUserPath.Contains("?", StringComparison.OrdinalIgnoreCase))
{
//Compare path and query when possible
string requestPath = entity.Server.RequestUri.PathAndQuery;
isValid &= unsafeUserPath.Equals(requestPath, StringComparison.OrdinalIgnoreCase);
if (!isValid && _logger.IsEnabled(LogLevel.Debug))
{
_logger.Debug("Client security OTP JWT path mismatch from {ip} : {current} != {token}",
entity.TrustedRemoteIp,
requestPath,
unsafeUserPath.ToString()
);
}
}
else
{
//Use path only
string requestPath = entity.Server.RequestUri.LocalPath;
//Compare path only
isValid &= unsafeUserPath.Equals(requestPath, StringComparison.OrdinalIgnoreCase);
if (!isValid && _logger.IsEnabled(LogLevel.Debug))
{
_logger.Debug("Client security OTP JWT path mismatch from {ip} : {current} != {token}",
entity.TrustedRemoteIp,
requestPath,
unsafeUserPath.ToString()
);
}
}
}
else
{
isValid = false;
}
}
return isValid;
}
catch (FormatException)
{
//we may catch the format exception for a malformatted jwt
_logger.Debug("Client security OTP JWT not valid from {ip}", entity.TrustedRemoteIp);
return false;
}
}
#endregion
#region Cookies
private void ExpireCookies(HttpEntity entity, bool force)
{
//Do not force clear cookies (saves bandwidth)
_statusCookie.ExpireCookie(entity, force);
_pubkeyCookie.ExpireCookie(entity, force);
}
#endregion
#region Data Encryption
///
/// Tries to encrypt the specified data using the specified public key
///
/// A base64 encoded public key used to encrypt client data
/// Data to encrypt
/// The buffer to store encrypted data in
///
/// The number of encrypted bytes written to the output buffer,
/// or false (0) if the operation failed, or if no credential is
/// specified.
///
///
private static ERRNO TryEncryptClientData(ReadOnlySpan base64PubKey, ReadOnlySpan data, Span outputBuffer)
{
if (base64PubKey.IsEmpty)
{
return ERRNO.E_FAIL;
}
//Alloc a buffer for decoding the public key
using UnsafeMemoryHandle pubKeyBuffer = MemoryUtil.UnsafeAllocNearestPage(base64PubKey.Length, true);
//Decode the public key
ERRNO pbkBytesWritten = VnEncoding.TryFromBase64Chars(base64PubKey, pubKeyBuffer.Span);
//Try to encrypt the data
return pbkBytesWritten ? TryEncryptClientData(pubKeyBuffer.Span[..(int)pbkBytesWritten], data, outputBuffer) : ERRNO.E_FAIL;
}
///
/// Tries to encrypt the specified data using the specified public key
///
/// The raw SKI public key
/// Data to encrypt
/// The buffer to store encrypted data in
///
/// The number of encrypted bytes written to the output buffer,
/// or false (0) if the operation failed, or if no credential is
/// specified.
///
///
private static ERRNO TryEncryptClientData(ReadOnlySpan rawPubKey, ReadOnlySpan data, Span outputBuffer)
{
if (rawPubKey.IsEmpty)
{
return false;
}
//Setup new empty rsa
using RSA rsa = RSA.Create();
//Import the public key
rsa.ImportSubjectPublicKeyInfo(rawPubKey, out _);
//Encrypt data with OaepSha256 as configured in the browser
return rsa.TryEncrypt(data, outputBuffer, ClientEncryptonPadding, out int bytesWritten) ? bytesWritten : ERRNO.E_FAIL;
}
#endregion
#region Client Encryption Key
/*
* Stores the public key the client provided as a signed JWT a and sets
* it as a cookie in the user's browser.
*
* The signing key is randomly generated and stored in the client's session
* so it cannot "stolen"
*
* This was done mostly to save session storage space
*/
private string SetPublicKeyCookie(HttpEntity entity, string pubKey)
{
//generate a random nonce
string nonce = RandomHash.GetRandomHex(PUB_KEY_JWT_NONCE_SIZE);
//Generate signing key
using JsonWebToken jwt = new();
//No header to write, we know the format
//add the clients public key and set iat/exp
jwt.InitPayloadClaim()
.AddClaim("sub", pubKey)
.AddClaim("iat", entity.RequestedTimeUtc.ToUnixTimeSeconds())
.AddClaim("exp", entity.RequestedTimeUtc.Add(_config.AuthorizationValidFor).ToUnixTimeSeconds())
.AddClaim("nonce", nonce)
.CommitClaims();
//genreate random signing key to store in the user's session
byte[] signingKey = RandomHash.GetRandomBytes(_config.PubKeySigningKeySize);
//Sign jwt
jwt.Sign(signingKey, ClientTokenHmacType);
//base32 encode the signing key
string base32SigningKey = VnEncoding.ToBase32String(signingKey, false);
//Zero signing key now were done using it
MemoryUtil.InitializeBlock(signingKey.AsSpan());
//Compile the jwt for the cookie value
string jwtValue = jwt.Compile();
_pubkeyCookie.SetCookie(entity, jwtValue);
//Return the signing key
return base32SigningKey;
}
private bool TryGetPublicKey(HttpEntity entity, [NotNullWhen(true)] out string? pubKey)
{
pubKey = null;
//Check session is valid for use
if (!entity.Session.IsSet || entity.Session.IsNew || entity.Session.SessionType != SessionType.Web)
{
return false;
}
//Get the jwt cookie
string? pubKeyJwt = _pubkeyCookie.GetCookie(entity);
if (string.IsNullOrWhiteSpace(pubKeyJwt))
{
return false;
}
//Get the client signature
string? base32Sig = entity.Session[PUBLIC_KEY_SIG_KEY_ENTRY];
if (string.IsNullOrWhiteSpace(base32Sig))
{
return false;
}
try
{
//Parse the jwt
using JsonWebToken jwt = JsonWebToken.Parse(pubKeyJwt);
//Recover the signing key bytes
byte[] signingKey = VnEncoding.FromBase32String(base32Sig)!;
//verify the client signature
if (!jwt.Verify(signingKey, ClientTokenHmacType))
{
return false;
}
//Erase the signing key bytes
MemoryUtil.InitializeBlock(signingKey);
//Verify expiration
using JsonDocument payload = jwt.GetPayload();
//Get the expiration time from the jwt
long expTimeSec = payload.RootElement.GetProperty("exp").GetInt64();
DateTimeOffset expired = DateTimeOffset.FromUnixTimeSeconds(expTimeSec);
//Check if expired
if (expired.Ticks < entity.RequestedTimeUtc.Ticks)
{
return false;
}
//Store the public key
pubKey = payload.RootElement.GetProperty("sub").GetString()!;
return true;
}
catch (FormatException)
{
//JWT is invalid and could not be parsed
_logger.Debug("Client public key JWT or message body was not valid from {ip}", entity.TrustedRemoteIp);
}
return false;
}
#endregion
private sealed class AccountSecConfig : IOnConfigValidation
{
private static IValidator _validator { get; } = GetValidator();
private static IValidator GetValidator()
{
InlineValidator val = new();
//Cookie domain may be null/emmpty
val.RuleFor(c => c.CookieDomain);
//Cookie path may be empty or null
val.RuleFor(c => c.CookiePath);
val.RuleFor(c => c.AuthorizationValidFor)
.GreaterThan(TimeSpan.FromMinutes(1))
.WithMessage("The authorization should be valid for at-least 1 minute");
val.RuleFor(C => C.ClientStatusCookieName)
.Length(1, 50)
.AlphaNumericOnly();
//header name is required, but not allowed to contain "illegal" chars
val.RuleFor(c => c.TokenHeaderName)
.NotEmpty()
.IllegalCharacters();
val.RuleFor(c => c.PubKeyCookieName)
.Length(1, 50)
.IllegalCharacters();
//Signing keys are base32 encoded and stored in the session, we dont want to take up too much space
val.RuleFor(c => c.PubKeySigningKeySize)
.InclusiveBetween(8, 512)
.WithMessage("Your public key signing key should be between 8 and 512 bytes");
//Time difference doesnt need to be validated, it may be 0 to effectively disable it
val.RuleFor(c => c.SignedTokenTimeDiff);
val.RuleFor(c => c.TokenKeySize)
.InclusiveBetween(8, 512)
.WithMessage("You should choose an OTP symmetric key size between 8 and 512 bytes");
val.RuleFor(c => c.WebSessionValidForSeconds)
.InclusiveBetween((uint)1, uint.MaxValue)
.WithMessage("You must specify a valid value for a web session timeout in seconds");
val.RuleForEach(c => c.AllowedOrigins)
.Matches(@"^https?://[a-z0-9\-\.]+$")
.WithMessage("The allowed origins must be valid http(s) urls");
return val;
}
///
/// The domain all authoization cookies will be set for
///
[JsonPropertyName("cookie_domain")]
public string CookieDomain { get; set; } = "";
///
/// The path all authorization cookies will be set for
///
[JsonPropertyName("cookie_path")]
public string? CookiePath { get; set; } = "/";
///
/// The amount if time new authorizations are valid for. This also
/// sets the duration of client cookies.
///
[JsonIgnore]
internal TimeSpan AuthorizationValidFor { get; set; } = TimeSpan.FromMinutes(60);
///
/// The name of the cookie used to set the client's login status message
///
[JsonPropertyName("status_cookie_name")]
public string ClientStatusCookieName { get; set; } = "li";
///
/// The name of the header used by the client to send the one-time use
/// authorization token
///
[JsonPropertyName("otp_header_name")]
public string TokenHeaderName { get; set; } = "X-Web-Token";
///
/// The size (in bytes) of the symmetric key used
/// by the client to sign token messages
///
[JsonPropertyName("otp_key_size")]
public int TokenKeySize { get; set; } = 64;
///
/// The name of the cookie that stores the user's signed public encryption key
///
[JsonPropertyName("pubkey_cookie_name")]
public string PubKeyCookieName { get; set; } = "client_id";
///
/// The size (in bytes) of the randomly generated key
/// used to sign the user's public key
///
[JsonPropertyName("pubkey_signing_key_size")]
public int PubKeySigningKeySize { get; set; } = 32;
///
/// The allowed time difference in the issuance time of the client's signed
/// one time use tokens
///
[JsonIgnore]
internal TimeSpan SignedTokenTimeDiff { get; set; } = TimeSpan.FromSeconds(30);
///
/// The amount of time a web session is valid for
///
[JsonPropertyName("session_valid_for_sec")]
public uint WebSessionValidForSeconds { get; set; } = 3600;
[JsonPropertyName("otp_time_diff_sec")]
public uint SigTokenTimeDifSeconds
{
get => (uint)SignedTokenTimeDiff.TotalSeconds;
set => SignedTokenTimeDiff = TimeSpan.FromSeconds(value);
}
///
/// Enforce that the client's token is only valid for the origin
/// it was read from. Will break sites hosted from multiple origins
///
[JsonPropertyName("strict_origin")]
public bool EnforceSameOriginToken { get; set; } = true;
///
/// Enable/disable origin verification for the client's token
///
[JsonIgnore]
public bool VerifyOrigin => AllowedOrigins != null && AllowedOrigins.Length > 0;
///
/// The list of origins that are allowed to send requests to the server
///
[JsonPropertyName("allowed_origins")]
public string[]? AllowedOrigins { get; set; }
///
/// Enforce strict path checking for the client's token
///
[JsonPropertyName("strict_path")]
public bool VerifyPath { get; set; } = true;
void IOnConfigValidation.Validate()
{
//Validate the current instance
_validator.ValidateAndThrow(this);
}
}
private sealed class EncryptedTokenAuthorization(string ClientAuthToken) : IClientAuthorization
{
///
public object GetClientAuthData() => ClientAuthToken;
///
public string GetClientAuthDataString() => ClientAuthToken;
}
}
}