Miscelanious trials
This commit is contained in:
commit
f99c97f392
33 changed files with 881 additions and 0 deletions
6
IdentityShroud.Core/Contracts/ISecretProvider.cs
Normal file
6
IdentityShroud.Core/Contracts/ISecretProvider.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
public interface ISecretProvider
|
||||
{
|
||||
Task<string> GetSecretAsync(string name);
|
||||
}
|
||||
38
IdentityShroud.Core/Db.cs
Normal file
38
IdentityShroud.Core/Db.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using IdentityShroud.Core.Model;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public class DbConfiguration
|
||||
{
|
||||
public string ConnectionString { get; set; } = "";
|
||||
public bool LogSensitiveData { get; set; } = false;
|
||||
}
|
||||
|
||||
public class Db(
|
||||
IOptions<DbConfiguration> configuration,
|
||||
ILoggerFactory? loggerFactory)
|
||||
: DbContext
|
||||
{
|
||||
public virtual DbSet<Realm> Realms { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql("<connection string>");
|
||||
optionsBuilder.UseNpgsql(
|
||||
configuration.Value.ConnectionString,
|
||||
o => o.MigrationsAssembly("IdentityShroud.Migrations")); // , o => o.UseNodaTime().UseVector().MigrationsAssembly("Migrations.KnowledgeBaseDB"));
|
||||
optionsBuilder.UseSnakeCaseNamingConvention();
|
||||
|
||||
if (configuration.Value.LogSensitiveData)
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
|
||||
if (loggerFactory is { } )
|
||||
{
|
||||
optionsBuilder.UseLoggerFactory(loggerFactory);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
21
IdentityShroud.Core/IdentityShroud.Core.csproj
Normal file
21
IdentityShroud.Core/IdentityShroud.Core.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
|
||||
<PackageReference Include="jose-jwt" Version="5.2.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.WebUtilities">
|
||||
<HintPath>..\..\..\.nuget\packages\microsoft.aspnetcore.webutilities\10.0.2\lib\net10.0\Microsoft.AspNetCore.WebUtilities.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
34
IdentityShroud.Core/Messages/JsonWebKey.cs
Normal file
34
IdentityShroud.Core/Messages/JsonWebKey.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
public class JsonWebKey
|
||||
{
|
||||
[JsonPropertyName("kty")]
|
||||
public string KeyType { get; set; } = "RSA";
|
||||
|
||||
[JsonPropertyName("use")]
|
||||
public string Use { get; set; } = "sig"; // "sig" for signature, "enc" for encryption
|
||||
|
||||
[JsonPropertyName("alg")]
|
||||
public string Algorithm { get; set; } = "RS256";
|
||||
|
||||
[JsonPropertyName("kid")]
|
||||
public string KeyId { get; set; }
|
||||
|
||||
// RSA Public Key Components
|
||||
[JsonPropertyName("n")]
|
||||
public string Modulus { get; set; }
|
||||
|
||||
[JsonPropertyName("e")]
|
||||
public string Exponent { get; set; }
|
||||
|
||||
// Optional fields
|
||||
[JsonPropertyName("x5c")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public List<string> X509CertificateChain { get; set; }
|
||||
|
||||
[JsonPropertyName("x5t")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string X509CertificateThumbprint { get; set; }
|
||||
}
|
||||
9
IdentityShroud.Core/Messages/JsonWebKeySet.cs
Normal file
9
IdentityShroud.Core/Messages/JsonWebKeySet.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
public class JsonWebKeySet
|
||||
{
|
||||
[JsonPropertyName("keys")]
|
||||
public List<JsonWebKey> Keys { get; set; } = new List<JsonWebKey>();
|
||||
}
|
||||
39
IdentityShroud.Core/Messages/JsonWebToken.cs
Normal file
39
IdentityShroud.Core/Messages/JsonWebToken.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
public class JsonWebTokenHeader
|
||||
{
|
||||
[JsonPropertyName("alg")]
|
||||
public string Algorithm { get; set; } = "HS256";
|
||||
[JsonPropertyName("typ")]
|
||||
public string Type { get; set; } = "JWT";
|
||||
[JsonPropertyName("kid")]
|
||||
public string KeyId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class JsonWebTokenPayload
|
||||
{
|
||||
[JsonPropertyName("iss")]
|
||||
public string Issuer { get; set; }
|
||||
[JsonPropertyName("aud")]
|
||||
public string[] Audience { get; set; }
|
||||
[JsonPropertyName("sub")]
|
||||
public string Subject { get; set; }
|
||||
[JsonPropertyName("exp")]
|
||||
public long Expires { get; set; }
|
||||
[JsonPropertyName("iat")]
|
||||
public long IssuedAt { get; set; }
|
||||
[JsonPropertyName("nbf")]
|
||||
public long NotBefore { get; set; }
|
||||
[JsonPropertyName("jti")]
|
||||
public Guid JwtId { get; set; }
|
||||
}
|
||||
|
||||
public class JsonWebToken
|
||||
{
|
||||
public JsonWebTokenHeader Header { get; set; } = new();
|
||||
public JsonWebTokenPayload Payload { get; set; } = new();
|
||||
public byte[] Signature { get; set; } = [];
|
||||
}
|
||||
72
IdentityShroud.Core/Messages/OpenIdConfiguration.cs
Normal file
72
IdentityShroud.Core/Messages/OpenIdConfiguration.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
/// </summary>
|
||||
public class OpenIdConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// REQUIRED. URL using the https scheme with no query or fragment components that the OP asserts as its
|
||||
/// Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical to the
|
||||
/// issuer value returned by WebFinger. This also MUST be identical to the iss Claim value in ID Tokens issued
|
||||
/// from this Issuer.
|
||||
/// </summary>
|
||||
[JsonPropertyName("issuer")]
|
||||
public required string Issuer { get; set; }
|
||||
/// <summary>
|
||||
/// REQUIRED. URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core]. This URL MUST use the https scheme
|
||||
/// and MAY contain port, path, and query parameter components.
|
||||
/// </summary>
|
||||
[JsonPropertyName("authorization_endpoint")]
|
||||
public required string AuthorizationEndpoint { get; set; }
|
||||
/// <summary>
|
||||
/// URL of the OP's OAuth 2.0 Token Endpoint [OpenID.Core]. This is REQUIRED unless only the Implicit Flow is used.
|
||||
/// This URL MUST use the https scheme and MAY contain port, path, and query parameter components.
|
||||
/// </summary>
|
||||
[JsonPropertyName("token_endpoint")]
|
||||
public string? TokenEndpoint { get; set; }
|
||||
/// <summary>
|
||||
/// RECOMMENDED. URL of the OP's UserInfo Endpoint [OpenID.Core]. This URL MUST use the https scheme and MAY contain
|
||||
/// port, path, and query parameter components.
|
||||
/// </summary>
|
||||
[JsonPropertyName("userinfo_endpoint")]
|
||||
public string? UserInfoEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// REQUIRED. URL of the OP's JWK Set [JWK] document, which MUST use the https scheme. This contains the signing
|
||||
/// key(s) the RP uses to validate signatures from the OP. The JWK Set MAY also contain the Server's encryption
|
||||
/// key(s), which are used by RPs to encrypt requests to the Server. When both signing and encryption keys are made
|
||||
/// available, a use (public key use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate
|
||||
/// each key's intended usage. Although some algorithms allow the same key to be used for both signatures and
|
||||
/// encryption, doing so is NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide
|
||||
/// X.509 representations of keys provided. When used, the bare key values MUST still be present and MUST match
|
||||
/// those in the certificate. The JWK Set MUST NOT contain private or symmetric key values.
|
||||
/// </summary>
|
||||
[JsonPropertyName("jwks_uri")]
|
||||
public required string JwksUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// REQUIRED. JSON array containing a list of the OAuth 2.0 response_type values that this OP supports. Dynamic
|
||||
/// OpenID Providers MUST support the code, id_token, and the id_token token Response Type values.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response_types_supported")]
|
||||
public string[] ResponseTypesSupported { get; set; } = [ "code", "id_token", "id_token token"];
|
||||
|
||||
/// <summary>
|
||||
/// REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports. Valid types
|
||||
/// include pairwise and public.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subject_types_supported")]
|
||||
public string[] SubjectTypesSupported { get; set; } = [ "public" ];
|
||||
|
||||
/// <summary>
|
||||
/// REQUIRED. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the
|
||||
/// ID Token to encode the Claims in a JWT [JWT]. The algorithm RS256 MUST be included. The value none MAY be
|
||||
/// supported but MUST NOT be used unless the Response Type used returns no ID Token from the Authorization
|
||||
/// Endpoint (such as when using the Authorization Code Flow).
|
||||
/// </summary>
|
||||
[JsonPropertyName("id_token_signing_alg_values_supported")]
|
||||
public string[] IdTokenSigningAlgValuesSupported { get; set; } = [ "RS256" ];
|
||||
}
|
||||
7
IdentityShroud.Core/Model/Client.cs
Normal file
7
IdentityShroud.Core/Model/Client.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
public class Client
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
10
IdentityShroud.Core/Model/Realm.cs
Normal file
10
IdentityShroud.Core/Model/Realm.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
public class Realm
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Slug { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public List<Client> Clients { get; set; } = [];
|
||||
public byte[] PrivateKey { get; set; }
|
||||
}
|
||||
64
IdentityShroud.Core/Security/AesGcmHelper.cs
Normal file
64
IdentityShroud.Core/Security/AesGcmHelper.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public static class AesGcmHelper
|
||||
{
|
||||
|
||||
public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
|
||||
{
|
||||
using var aes = new AesGcm(key);
|
||||
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
|
||||
byte[] ciphertext = new byte[plaintext.Length];
|
||||
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
|
||||
|
||||
aes.Encrypt(nonce, plaintext, ciphertext, tag);
|
||||
// Return concatenated nonce|ciphertext|tag (or store separately)
|
||||
return nonce.Concat(ciphertext).Concat(tag).ToArray();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// DecryptAesGcm
|
||||
// • key – 32‑byte (256‑bit) secret key (same key used for encryption)
|
||||
// • payload – byte[] containing nonce‖ciphertext‖tag
|
||||
// • returns – the original plaintext bytes
|
||||
// --------------------------------------------------------------------
|
||||
public static byte[] DecryptAesGcm(byte[] payload, byte[] key)
|
||||
{
|
||||
if (payload == null) throw new ArgumentNullException(nameof(payload));
|
||||
if (key == null) throw new ArgumentNullException(nameof(key));
|
||||
if (key.Length != 32) // 256‑bit key
|
||||
throw new ArgumentException("Key must be 256 bits (32 bytes) for AES‑256‑GCM.", nameof(key));
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 1️⃣ Extract the three components.
|
||||
// ----------------------------------------------------------------
|
||||
// AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
|
||||
// AesGcm.TagByteSizes.MaxSize = 16 bytes (128‑bit authentication tag)
|
||||
int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12
|
||||
int tagSize = AesGcm.TagByteSizes.MaxSize; // 16
|
||||
|
||||
if (payload.Length < nonceSize + tagSize)
|
||||
throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
|
||||
|
||||
ReadOnlySpan<byte> nonce = new(payload, 0, nonceSize);
|
||||
ReadOnlySpan<byte> ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize);
|
||||
ReadOnlySpan<byte> tag = new(payload, payload.Length - tagSize, tagSize);
|
||||
|
||||
|
||||
byte[] plaintext = new byte[ciphertext.Length];
|
||||
|
||||
using var aes = new AesGcm(key);
|
||||
try
|
||||
{
|
||||
aes.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
// Tag verification failed → tampering or wrong key/nonce.
|
||||
throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
}
|
||||
38
IdentityShroud.Core/Security/JwtSignatureGenerator.cs
Normal file
38
IdentityShroud.Core/Security/JwtSignatureGenerator.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public class JwtSignatureGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a JWT signature using RS256 algorithm
|
||||
/// </summary>
|
||||
/// <param name="headerBase64Url">Base64Url encoded header</param>
|
||||
/// <param name="payloadBase64Url">Base64Url encoded payload</param>
|
||||
/// <param name="privateKey">RSA private key (PEM format or RSA parameters)</param>
|
||||
/// <returns>Base64Url encoded signature</returns>
|
||||
public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
|
||||
{
|
||||
// Combine header and payload with a period
|
||||
string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
|
||||
|
||||
// Convert to bytes
|
||||
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
|
||||
|
||||
// Sign the data using RSA-SHA256
|
||||
byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Convert signature to Base64Url encoding
|
||||
string signature = WebEncoders.Base64UrlEncode(signatureBytes);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
|
||||
{
|
||||
string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
|
||||
return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue