Add project files.

This commit is contained in:
Khwezi Mngoma
2026-06-05 22:07:25 +02:00
parent 59ea7de742
commit 437e8c5c08
978 changed files with 151911 additions and 0 deletions
@@ -0,0 +1,90 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// File: https://github.com/IdentityServer/IdentityServer4/blob/main/samples/Quickstarts/3_AspNetCoreAndApis/src/IdentityServer/Quickstart/Account/ExternalController.cs
// Modified by Jan Škoruba and J. Arturo
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public class ApplicationSignInManager<TUser> : SignInManager<TUser>
where TUser : class
{
private readonly IHttpContextAccessor _contextAccessor;
public ApplicationSignInManager(UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<TUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<TUser> confirmation) : base(userManager, contextAccessor,
claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_contextAccessor = contextAccessor;
}
public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable<Claim> additionalClaims)
{
var claims = additionalClaims.ToList();
var externalResult = await _contextAccessor.HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
if (externalResult != null && externalResult.Succeeded)
{
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
if (sid != null)
{
claims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
}
if (authenticationProperties != null)
{
// if the external provider issued an id_token, we'll keep it for sign out
var idToken = externalResult.Properties.GetTokenValue("id_token");
if (idToken != null)
{
authenticationProperties.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
}
}
var authenticationMethod = claims.FirstOrDefault(x => x.Type == ClaimTypes.AuthenticationMethod);
var idp = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.IdentityProvider);
if (authenticationMethod != null && idp == null)
{
claims.Add(new Claim(JwtClaimTypes.IdentityProvider, authenticationMethod.Value));
}
}
await base.SignInWithClaimsAsync(user, authenticationProperties, claims);
}
/// <summary>
/// Validates a passkey assertion and, if valid, signs the user in while also returning the signed-in user.
/// This is necessary because <see cref="PasskeySignInAsync"/> does not return the user object,
/// and the request principal is not updated after the auth cookie is issued in the same request.
/// </summary>
public async Task<(SignInResult Result, TUser User)> PasskeySignInWithUserAsync(string credentialJson)
{
var assertionResult = await PerformPasskeyAssertionAsync(credentialJson);
if (!assertionResult.Succeeded)
{
return (SignInResult.Failed, default);
}
var signInResult = await SignInOrTwoFactorAsync(assertionResult.User, isPersistent: false);
return (signInResult, assertionResult.User);
}
}
}
@@ -0,0 +1,43 @@
using System.Linq;
using System.Threading.Tasks;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
}
@@ -0,0 +1,48 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
// Original file: https://github.com/DuendeSoftware/IdentityServer.Quickstart.UI
// Modified by Jan Škoruba
using System;
using System.Threading.Tasks;
using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using LiteCharmsSecurity.STS.Identity.ViewModels.Account;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public static class Extensions
{
/// <summary>
/// Determines if the authentication scheme support signout.
/// </summary>
public static async Task<bool> GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme)
{
var provider = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await provider.GetHandlerAsync(context, scheme);
return (handler is IAuthenticationSignOutHandler);
}
/// <summary>
/// Checks if the redirect URI is for a native client.
/// </summary>
/// <returns></returns>
public static bool IsNativeClient(this AuthorizationRequest context)
{
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
}
public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri)
{
controller.HttpContext.Response.StatusCode = 200;
controller.HttpContext.Response.Headers["Location"] = "";
return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri });
}
}
}
@@ -0,0 +1,6 @@
namespace LiteCharmsSecurity.STS.Identity.Helpers;
public class IdentityProvidersHelpers
{
public const string OpenIdConnect = "oidc";
}
@@ -0,0 +1,188 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Skoruba.Duende.IdentityServer.Shared.Configuration.Configuration.Common;
using Skoruba.Duende.IdentityServer.Shared.Configuration.Helpers;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public static class IdentityServerBuilderExtensions
{
private const string CertificateNotFound = "Certificate not found";
private const string SigningCertificateThumbprintNotFound = "Signing certificate thumbprint not found";
private const string SigningCertificatePathIsNotSpecified = "Signing certificate file path is not specified";
private const string ValidationCertificateThumbprintNotFound = "Validation certificate thumbprint not found";
private const string ValidationCertificatePathIsNotSpecified = "Validation certificate file path is not specified";
/// <summary>
/// Add custom signing certificate from certification store according thumbprint or from file
/// </summary>
/// <param name="builder"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IIdentityServerBuilder AddCustomSigningCredential(this IIdentityServerBuilder builder, IConfiguration configuration)
{
var certificateConfiguration = configuration.GetSection(nameof(CertificateConfiguration)).Get<CertificateConfiguration>();
var azureKeyVaultConfiguration = configuration.GetSection(nameof(AzureKeyVaultConfiguration)).Get<AzureKeyVaultConfiguration>();
if (certificateConfiguration.UseSigningCertificateThumbprint)
{
if (string.IsNullOrWhiteSpace(certificateConfiguration.SigningCertificateThumbprint))
{
throw new Exception(SigningCertificateThumbprintNotFound);
}
StoreLocation storeLocation = StoreLocation.LocalMachine;
bool validOnly = certificateConfiguration.CertificateValidOnly;
// Parse the Certificate StoreLocation
string certStoreLocationLower = certificateConfiguration.CertificateStoreLocation.ToLower();
if (certStoreLocationLower == StoreLocation.CurrentUser.ToString().ToLower() ||
certificateConfiguration.CertificateStoreLocation == ((int)StoreLocation.CurrentUser).ToString())
{
storeLocation = StoreLocation.CurrentUser;
}
else if (certStoreLocationLower == StoreLocation.LocalMachine.ToString().ToLower() ||
certStoreLocationLower == ((int)StoreLocation.LocalMachine).ToString())
{
storeLocation = StoreLocation.LocalMachine;
}
else { storeLocation = StoreLocation.LocalMachine; validOnly = true; }
// Open Certificate
var certStore = new X509Store(StoreName.My, storeLocation);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certificateConfiguration.SigningCertificateThumbprint, validOnly);
if (certCollection.Count == 0)
{
throw new Exception(CertificateNotFound);
}
var certificate = certCollection[0];
builder.AddSigningCredential(certificate);
}
else if (certificateConfiguration.UseSigningCertificateForAzureKeyVault)
{
var x509Certificate2Certs = AzureKeyVaultHelpers.GetCertificates(azureKeyVaultConfiguration).GetAwaiter().GetResult();
builder.AddSigningCredential(x509Certificate2Certs.ActiveCertificate);
}
else if (certificateConfiguration.UseSigningCertificatePfxFile)
{
if (string.IsNullOrWhiteSpace(certificateConfiguration.SigningCertificatePfxFilePath))
{
throw new Exception(SigningCertificatePathIsNotSpecified);
}
if (File.Exists(certificateConfiguration.SigningCertificatePfxFilePath))
{
try
{
var bytes = File.ReadAllBytes(certificateConfiguration.SigningCertificatePfxFilePath);
var cert = X509CertificateLoader.LoadPkcs12(bytes, certificateConfiguration.SigningCertificatePfxFilePassword);
builder.AddSigningCredential(cert);
}
catch (Exception e)
{
throw new Exception("There was an error adding the key file - during the creation of the signing key", e);
}
}
else
{
throw new Exception($"Signing key file: {certificateConfiguration.SigningCertificatePfxFilePath} not found");
}
}
else if (certificateConfiguration.UseTemporarySigningKeyForDevelopment)
{
builder.AddDeveloperSigningCredential();
}
else
{
throw new Exception("Signing credential is not specified");
}
return builder;
}
/// <summary>
/// Add custom validation key for signing key rollover
/// http://docs.identityserver.io/en/latest/topics/crypto.html#signing-key-rollover
/// </summary>
/// <param name="builder"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IIdentityServerBuilder AddCustomValidationKey(this IIdentityServerBuilder builder, IConfiguration configuration)
{
var certificateConfiguration = configuration.GetSection(nameof(CertificateConfiguration)).Get<CertificateConfiguration>();
var azureKeyVaultConfiguration = configuration.GetSection(nameof(AzureKeyVaultConfiguration)).Get<AzureKeyVaultConfiguration>();
if (certificateConfiguration.UseValidationCertificateThumbprint)
{
if (string.IsNullOrWhiteSpace(certificateConfiguration.ValidationCertificateThumbprint))
{
throw new Exception(ValidationCertificateThumbprintNotFound);
}
var certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certificateConfiguration.ValidationCertificateThumbprint, false);
if (certCollection.Count == 0)
{
throw new Exception(CertificateNotFound);
}
var certificate = certCollection[0];
builder.AddValidationKey(certificate);
}
else if (certificateConfiguration.UseValidationCertificateForAzureKeyVault)
{
var x509Certificate2Certs = AzureKeyVaultHelpers.GetCertificates(azureKeyVaultConfiguration).GetAwaiter().GetResult();
if (x509Certificate2Certs.SecondaryCertificate != null)
{
builder.AddValidationKey(x509Certificate2Certs.SecondaryCertificate);
}
}
else if (certificateConfiguration.UseValidationCertificatePfxFile)
{
if (string.IsNullOrWhiteSpace(certificateConfiguration.ValidationCertificatePfxFilePath))
{
throw new Exception(ValidationCertificatePathIsNotSpecified);
}
if (File.Exists(certificateConfiguration.ValidationCertificatePfxFilePath))
{
try
{
var validationCertBytes = File.ReadAllBytes(certificateConfiguration.ValidationCertificatePfxFilePath);
var validationCertificate = X509CertificateLoader.LoadPkcs12(
validationCertBytes,
certificateConfiguration.ValidationCertificatePfxFilePassword);
builder.AddValidationKey(validationCertificate);
}
catch (Exception e)
{
throw new Exception("There was an error adding the key file - during the creation of the validation key", e);
}
}
else
{
throw new Exception($"Validation key file: {certificateConfiguration.ValidationCertificatePfxFilePath} not found");
}
}
return builder;
}
}
}
@@ -0,0 +1,12 @@
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public static class KeyValuePairExtensions
{
public static bool IsDefault<T>(this T value) where T : struct
{
var isDefault = value.Equals(default(T));
return isDefault;
}
}
}
@@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// https://github.com/aspnet/Extensions/blob/master/src/Localization/Abstractions/src/StringLocalizerOfT.cs
// Modified by Jan Škoruba
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Localization;
namespace LiteCharmsSecurity.STS.Identity.Helpers.Localization
{
public class GenericControllerLocalizer<TResourceSource> : IGenericControllerLocalizer<TResourceSource>
{
private IStringLocalizer _localizer;
/// <summary>
/// Creates a new <see cref="T:Microsoft.Extensions.Localization.StringLocalizer`1" />.
/// </summary>
/// <param name="factory">The <see cref="T:Microsoft.Extensions.Localization.IStringLocalizerFactory" /> to use.</param>
public GenericControllerLocalizer(IStringLocalizerFactory factory)
{
if (factory == null) throw new ArgumentNullException(nameof(factory));
var type = typeof(TResourceSource);
var assemblyName = type.GetTypeInfo().Assembly.GetName().Name;
var typeName = type.Name.Remove(type.Name.IndexOf('`'));
var baseName = (type.Namespace + "." + typeName).Substring(assemblyName.Length).Trim('.');
_localizer = factory.Create(baseName, assemblyName);
}
public virtual LocalizedString this[string name]
{
get
{
if (name == null)
throw new ArgumentNullException(nameof(name));
return _localizer[name];
}
}
public virtual LocalizedString this[string name, params object[] arguments]
{
get
{
if (name == null)
throw new ArgumentNullException(nameof(name));
return _localizer[name, arguments];
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return _localizer.GetAllStrings(includeParentCultures);
}
}
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
namespace LiteCharmsSecurity.STS.Identity.Helpers.Localization
{
public interface IGenericControllerLocalizer<out T>
{
LocalizedString this[string name] { get; }
LocalizedString this[string name, params object[] arguments] { get; }
IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using Skoruba.Duende.IdentityServer.Shared.Configuration.Configuration.Identity;
namespace LiteCharmsSecurity.STS.Identity.Helpers.Localization
{
public static class LoginPolicyResolutionLocalizer
{
public static string GetUserNameLocalizationKey(LoginResolutionPolicy policy)
{
switch (policy)
{
case LoginResolutionPolicy.Username:
return "Username";
case LoginResolutionPolicy.Email:
return "Email";
default:
return "Username";
}
}
}
}
@@ -0,0 +1,37 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using System.Security.Cryptography;
using System.Text;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
/// <summary>
/// Helper-class to create Md5hashes from strings
/// </summary>
public static class Md5HashHelper
{
/// <summary>
/// Computes a Md5-hash of the submitted string and returns the corresponding hash
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string GetHash(string input)
{
using (var md5 = MD5.Create())
{
var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
var sBuilder = new StringBuilder();
foreach (var dataByte in bytes)
{
sBuilder.Append(dataByte.ToString("x2"));
}
return sBuilder.ToString();
}
}
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public class OpenIdProfile
{
public string FullName { get; internal set; }
public string Website { get; internal set; }
public string Profile { get; internal set; }
public string StreetAddress { get; internal set; }
public string Locality { get; internal set; }
public string Region { get; internal set; }
public string PostalCode { get; internal set; }
public string Country { get; internal set; }
}
}
@@ -0,0 +1,231 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using IdentityModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LiteCharmsSecurity.STS.Identity.Configuration.Constants;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public static class OpenIdClaimHelpers
{
public static Claim ExtractAddressClaim(OpenIdProfile profile)
{
var addressJson = new JObject();
if (!string.IsNullOrWhiteSpace(profile.StreetAddress))
{
addressJson[AddressClaimConstants.StreetAddress] = profile.StreetAddress;
}
if (!string.IsNullOrWhiteSpace(profile.Locality))
{
addressJson[AddressClaimConstants.Locality] = profile.Locality;
}
if (!string.IsNullOrWhiteSpace(profile.Region))
{
addressJson[AddressClaimConstants.Region] = profile.Region;
}
if (!string.IsNullOrWhiteSpace(profile.PostalCode))
{
addressJson[AddressClaimConstants.PostalCode] = profile.PostalCode;
}
if (!string.IsNullOrWhiteSpace(profile.Country))
{
addressJson[AddressClaimConstants.Country] = profile.Country;
}
return new Claim(JwtClaimTypes.Address, addressJson.Count != 0 ? addressJson.ToString() : string.Empty);
}
/// <summary>
/// Map claims to OpenId Profile
/// </summary>
/// <param name="claims"></param>
/// <returns></returns>
public static OpenIdProfile ExtractProfileInfo(IList<Claim> claims)
{
var profile = new OpenIdProfile
{
FullName = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value,
Website = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.WebSite)?.Value,
Profile = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Profile)?.Value
};
var address = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Address)?.Value;
if (address == null) return profile;
try
{
var addressJson = JObject.Parse(address);
if (addressJson.ContainsKey(AddressClaimConstants.StreetAddress))
{
profile.StreetAddress = addressJson[AddressClaimConstants.StreetAddress].ToString();
}
if (addressJson.ContainsKey(AddressClaimConstants.Locality))
{
profile.Locality = addressJson[AddressClaimConstants.Locality].ToString();
}
if (addressJson.ContainsKey(AddressClaimConstants.Region))
{
profile.Region = addressJson[AddressClaimConstants.Region].ToString();
}
if (addressJson.ContainsKey(AddressClaimConstants.PostalCode))
{
profile.PostalCode = addressJson[AddressClaimConstants.PostalCode].ToString();
}
if (addressJson.ContainsKey(AddressClaimConstants.Country))
{
profile.Country = addressJson[AddressClaimConstants.Country].ToString();
}
}
catch (JsonReaderException)
{
}
return profile;
}
/// <summary>
/// Get claims to remove
/// </summary>
/// <param name="oldProfile"></param>
/// <param name="newProfile"></param>
/// <returns></returns>
public static IList<Claim> ExtractClaimsToRemove(OpenIdProfile oldProfile, OpenIdProfile newProfile)
{
var claimsToRemove = new List<Claim>();
if (string.IsNullOrWhiteSpace(newProfile.FullName) && !string.IsNullOrWhiteSpace(oldProfile.FullName))
{
claimsToRemove.Add(new Claim(JwtClaimTypes.Name, oldProfile.FullName));
}
if (string.IsNullOrWhiteSpace(newProfile.Website) && !string.IsNullOrWhiteSpace(oldProfile.Website))
{
claimsToRemove.Add(new Claim(JwtClaimTypes.WebSite, oldProfile.Website));
}
if (string.IsNullOrWhiteSpace(newProfile.Profile) && !string.IsNullOrWhiteSpace(oldProfile.Profile))
{
claimsToRemove.Add(new Claim(JwtClaimTypes.Profile, oldProfile.Profile));
}
var oldAddressClaim = ExtractAddressClaim(oldProfile);
var newAddressClaim = ExtractAddressClaim(newProfile);
if (string.IsNullOrWhiteSpace(newAddressClaim.Value) && !string.IsNullOrWhiteSpace(oldAddressClaim.Value))
{
claimsToRemove.Add(oldAddressClaim);
}
return claimsToRemove;
}
/// <summary>
/// Get claims to add
/// </summary>
/// <param name="oldProfile"></param>
/// <param name="newProfile"></param>
/// <returns></returns>
public static IList<Claim> ExtractClaimsToAdd(OpenIdProfile oldProfile, OpenIdProfile newProfile)
{
var claimsToAdd = new List<Claim>();
if (!string.IsNullOrWhiteSpace(newProfile.FullName) && string.IsNullOrWhiteSpace(oldProfile.FullName))
{
claimsToAdd.Add(new Claim(JwtClaimTypes.Name, newProfile.FullName));
}
if (!string.IsNullOrWhiteSpace(newProfile.Website) && string.IsNullOrWhiteSpace(oldProfile.Website))
{
claimsToAdd.Add(new Claim(JwtClaimTypes.WebSite, newProfile.Website));
}
if (!string.IsNullOrWhiteSpace(newProfile.Profile) && string.IsNullOrWhiteSpace(oldProfile.Profile))
{
claimsToAdd.Add(new Claim(JwtClaimTypes.Profile, newProfile.Profile));
}
var oldAddressClaim = ExtractAddressClaim(oldProfile);
var newAddressClaim = ExtractAddressClaim(newProfile);
if (!string.IsNullOrWhiteSpace(newAddressClaim.Value) && string.IsNullOrWhiteSpace(oldAddressClaim.Value))
{
claimsToAdd.Add(newAddressClaim);
}
return claimsToAdd;
}
/// <summary>
/// Get claims to replace
/// </summary>
/// <param name="oldClaims"></param>
/// <param name="newProfile"></param>
/// <returns></returns>
public static IList<Tuple<Claim,Claim>> ExtractClaimsToReplace(IList<Claim> oldClaims, OpenIdProfile newProfile)
{
var oldProfile = ExtractProfileInfo(oldClaims);
var claimsToReplace = new List<Tuple<Claim, Claim>>();
if (!string.IsNullOrWhiteSpace(newProfile.FullName) && !string.IsNullOrWhiteSpace(oldProfile.FullName))
{
if (newProfile.FullName != oldProfile.FullName)
{
var oldClaim = oldClaims.First(x => x.Type == JwtClaimTypes.Name);
var newClaim = new Claim(JwtClaimTypes.Name, newProfile.FullName);
claimsToReplace.Add(new Tuple<Claim, Claim>(oldClaim, newClaim));
}
}
if (!string.IsNullOrWhiteSpace(newProfile.Website) && !string.IsNullOrWhiteSpace(oldProfile.Website))
{
if (newProfile.Website != oldProfile.Website)
{
var oldClaim = oldClaims.First(x => x.Type == JwtClaimTypes.WebSite);
var newClaim = new Claim(JwtClaimTypes.WebSite, newProfile.Website);
claimsToReplace.Add(new Tuple<Claim, Claim>(oldClaim, newClaim));
}
}
if (!string.IsNullOrWhiteSpace(newProfile.Profile) && !string.IsNullOrWhiteSpace(oldProfile.Profile))
{
if (newProfile.Profile != oldProfile.Profile)
{
var oldClaim = oldClaims.First(x => x.Type == JwtClaimTypes.Profile);
var newClaim = new Claim(JwtClaimTypes.Profile, newProfile.Profile);
claimsToReplace.Add(new Tuple<Claim, Claim>(oldClaim, newClaim));
}
}
var oldAddressClaim = ExtractAddressClaim(oldProfile);
var newAddressClaim = ExtractAddressClaim(newProfile);
if (!string.IsNullOrWhiteSpace(newAddressClaim.Value) && !string.IsNullOrWhiteSpace(oldAddressClaim.Value))
{
if (newAddressClaim.Value != oldAddressClaim.Value)
{
var oldClaim = oldClaims.First(x => x.Type == JwtClaimTypes.Address);
claimsToReplace.Add(new Tuple<Claim, Claim>(oldClaim, newAddressClaim));
}
}
return claimsToReplace;
}
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
// Original file: https://github.com/DuendeSoftware/IdentityServer.Quickstart.UI
// Modified by Jan Škoruba
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public class SecurityHeadersAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var result = context.Result;
if (result is ViewResult)
{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options"))
{
context.HttpContext.Response.Headers.Append("X-Content-Type-Options", "nosniff");
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options"))
{
context.HttpContext.Response.Headers.Append("X-Frame-Options", "SAMEORIGIN");
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';";
// also consider adding upgrade-insecure-requests once you have HTTPS in place for production
//csp += "upgrade-insecure-requests;";
// also an example if you need client images to be displayed from twitter
// csp += "img-src 'self' https://pbs.twimg.com;";
// once for standards compliant browsers
if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy"))
{
context.HttpContext.Response.Headers.Append("Content-Security-Policy", csp);
}
// and once again for IE
if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy"))
{
context.HttpContext.Response.Headers.Append("X-Content-Security-Policy", csp);
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
var referrer_policy = "no-referrer";
if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy"))
{
context.HttpContext.Response.Headers.Append("Referrer-Policy", referrer_policy);
}
}
}
}
}
@@ -0,0 +1,579 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using NetIPNetwork = System.Net.IPNetwork;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.EntityFramework.Storage;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using IdentityModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Tokens;
using Skoruba.Duende.IdentityServer.Shared.Configuration.Constants;
using System.IO;
using Skoruba.Duende.IdentityServer.Admin.EntityFramework.Configuration.Configuration;
using Skoruba.Duende.IdentityServer.Admin.EntityFramework.Configuration.PostgreSQL;
using Skoruba.Duende.IdentityServer.Admin.EntityFramework.Configuration.SqlServer;
using Skoruba.Duende.IdentityServer.Admin.EntityFramework.Helpers;
using Skoruba.Duende.IdentityServer.Admin.EntityFramework.Interfaces;
using Skoruba.Duende.IdentityServer.Shared.Configuration.Authentication;
using Skoruba.Duende.IdentityServer.Shared.Configuration.Configuration.Identity;
using LiteCharmsSecurity.STS.Identity.Configuration;
using LiteCharmsSecurity.STS.Identity.Configuration.ApplicationParts;
using LiteCharmsSecurity.STS.Identity.Configuration.Constants;
using LiteCharmsSecurity.STS.Identity.Configuration.Interfaces;
using Configuration = LiteCharmsSecurity.STS.Identity.Configuration;
using LiteCharmsSecurity.STS.Identity.Helpers.Localization;
using LiteCharmsSecurity.STS.Identity.Services;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public static class StartupHelpers
{
/// <summary>
/// Register services for MVC and localization including available languages
/// </summary>
/// <param name="services"></param>
public static IMvcBuilder AddMvcWithLocalization<TUser, TKey>(this IServiceCollection services, IConfiguration configuration)
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
{
services.AddLocalization(opts => { opts.ResourcesPath = ConfigurationConsts.ResourcesPath; });
services.TryAddTransient(typeof(IGenericControllerLocalizer<>), typeof(GenericControllerLocalizer<>));
var mvcBuilder = services.AddControllersWithViews(o =>
{
o.Conventions.Add(new GenericControllerRouteConvention());
})
.AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix,
opts => { opts.ResourcesPath = ConfigurationConsts.ResourcesPath; })
.AddDataAnnotationsLocalization()
.ConfigureApplicationPartManager(m =>
{
m.FeatureProviders.Add(new GenericTypeControllerFeatureProvider<TUser, TKey>());
});
var cultureConfiguration = configuration.GetSection(nameof(CultureConfiguration)).Get<CultureConfiguration>();
services.Configure<RequestLocalizationOptions>(
opts =>
{
// If cultures are specified in the configuration, use them (making sure they are among the available cultures),
// otherwise use all the available cultures
var supportedCultureCodes = (cultureConfiguration?.Cultures?.Count > 0 ?
cultureConfiguration.Cultures.Intersect(CultureConfiguration.AvailableCultures) :
CultureConfiguration.AvailableCultures).ToArray();
if (!supportedCultureCodes.Any()) supportedCultureCodes = CultureConfiguration.AvailableCultures;
var supportedCultures = supportedCultureCodes.Select(c => new CultureInfo(c)).ToList();
// If the default culture is specified use it, otherwise use CultureConfiguration.DefaultRequestCulture ("en")
var defaultCultureCode = string.IsNullOrEmpty(cultureConfiguration?.DefaultCulture) ?
CultureConfiguration.DefaultRequestCulture : cultureConfiguration?.DefaultCulture;
// If the default culture is not among the supported cultures, use the first supported culture as default
if (!supportedCultureCodes.Contains(defaultCultureCode)) defaultCultureCode = supportedCultureCodes.FirstOrDefault();
opts.DefaultRequestCulture = new RequestCulture(defaultCultureCode);
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
return mvcBuilder;
}
/// <summary>
/// Using of Forwarded Headers and Referrer Policy
/// </summary>
/// <param name="app"></param>
/// <param name="configuration"></param>
public static void UseSecurityHeaders(this IApplicationBuilder app, IConfiguration configuration)
{
var forwardedHeadersConfig = configuration.GetSection(ConfigurationConsts.ForwardedHeadersConfigurationKey)
.Get<Configuration.ForwardedHeadersConfiguration>() ?? new Configuration.ForwardedHeadersConfiguration();
if (forwardedHeadersConfig.Enabled)
{
var forwardingOptions = new ForwardedHeadersOptions()
{
ForwardedHeaders = ForwardedHeaders.All,
ForwardLimit = forwardedHeadersConfig.ForwardLimit
};
if (forwardedHeadersConfig.AllowAll)
{
// Development mode: allow all proxies and networks (insecure)
forwardingOptions.KnownIPNetworks.Clear();
forwardingOptions.KnownProxies.Clear();
}
else
{
// Production mode: only trust configured proxies and networks
if (forwardedHeadersConfig.KnownProxies != null && forwardedHeadersConfig.KnownProxies.Count > 0)
{
forwardingOptions.KnownProxies.Clear();
foreach (var proxy in forwardedHeadersConfig.KnownProxies)
{
if (System.Net.IPAddress.TryParse(proxy, out var ipAddress))
{
forwardingOptions.KnownProxies.Add(ipAddress);
}
}
}
if (forwardedHeadersConfig.KnownNetworks != null && forwardedHeadersConfig.KnownNetworks.Count > 0)
{
forwardingOptions.KnownIPNetworks.Clear();
foreach (var network in forwardedHeadersConfig.KnownNetworks)
{
var parts = network.Split('/');
if (parts.Length == 2 &&
IPAddress.TryParse(parts[0], out var prefix) &&
int.TryParse(parts[1], out var prefixLength))
{
forwardingOptions.KnownIPNetworks.Add(new NetIPNetwork(prefix, prefixLength));
}
}
}
// If no proxies or networks configured, don't clear defaults (more secure)
// This means it will only trust the loopback by default
}
app.UseForwardedHeaders(forwardingOptions);
}
app.UseReferrerPolicy(options => options.NoReferrer());
// CSP Configuration to be able to use external resources
var cspTrustedDomains = new List<string>();
configuration.GetSection(ConfigurationConsts.CspTrustedDomainsKey).Bind(cspTrustedDomains);
if (cspTrustedDomains.Any())
{
app.UseCsp(csp =>
{
var imagesSources = new List<string> { "data:" };
imagesSources.AddRange(cspTrustedDomains);
csp.ImageSources(options =>
{
options.SelfSrc = true;
options.CustomSources = imagesSources;
options.Enabled = true;
});
csp.FontSources(options =>
{
options.SelfSrc = true;
options.CustomSources = cspTrustedDomains;
options.Enabled = true;
});
csp.ScriptSources(options =>
{
options.SelfSrc = true;
options.CustomSources = cspTrustedDomains;
options.Enabled = true;
});
csp.StyleSources(options =>
{
options.SelfSrc = true;
options.CustomSources = cspTrustedDomains;
options.Enabled = true;
options.UnsafeInlineSrc = false;
});
csp.Sandbox(options =>
{
options.AllowForms()
.AllowSameOrigin()
.AllowScripts();
});
csp.FrameAncestors(option =>
{
option.NoneSrc = true;
option.Enabled = true;
});
csp.BaseUris(options =>
{
options.SelfSrc = true;
options.Enabled = true;
});
csp.ObjectSources(options =>
{
options.NoneSrc = true;
options.Enabled = true;
});
csp.DefaultSources(options =>
{
options.Enabled = true;
options.SelfSrc = true;
options.CustomSources = cspTrustedDomains;
});
});
}
}
/// <summary>
/// Register DbContexts for IdentityServer ConfigurationStore, PersistedGrants, Identity and DataProtection
/// Configure the connection strings in AppSettings.json
/// </summary>
/// <typeparam name="TConfigurationDbContext"></typeparam>
/// <typeparam name="TPersistedGrantDbContext"></typeparam>
/// <typeparam name="TIdentityDbContext"></typeparam>
/// <typeparam name="TDataProtectionDbContext"></typeparam>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void RegisterDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext, TDataProtectionDbContext>(this IServiceCollection services, IConfiguration configuration)
where TIdentityDbContext : DbContext
where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
{
var databaseProvider = configuration.GetSection(nameof(DatabaseProviderConfiguration)).Get<DatabaseProviderConfiguration>();
var identityConnectionString = configuration.GetConnectionString(ConfigurationConsts.IdentityDbConnectionStringKey);
var configurationConnectionString = configuration.GetConnectionString(ConfigurationConsts.ConfigurationDbConnectionStringKey);
var persistedGrantsConnectionString = configuration.GetConnectionString(ConfigurationConsts.PersistedGrantDbConnectionStringKey);
var dataProtectionConnectionString = configuration.GetConnectionString(ConfigurationConsts.DataProtectionDbConnectionStringKey);
switch (databaseProvider.ProviderType)
{
case DatabaseProviderType.SqlServer:
services.RegisterSqlServerDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext, TDataProtectionDbContext>(identityConnectionString, configurationConnectionString, persistedGrantsConnectionString, dataProtectionConnectionString);
break;
case DatabaseProviderType.PostgreSQL:
services.RegisterNpgSqlDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext, TDataProtectionDbContext>(identityConnectionString, configurationConnectionString, persistedGrantsConnectionString, dataProtectionConnectionString);
break;
default:
throw new ArgumentOutOfRangeException(nameof(databaseProvider.ProviderType), $@"The value needs to be one of {string.Join(", ", Enum.GetNames(typeof(DatabaseProviderType)))}.");
}
}
/// <summary>
/// Register InMemory DbContexts for IdentityServer ConfigurationStore, PersistedGrants, Identity and DataProtection
/// Configure the connection strings in AppSettings.json
/// </summary>
/// <typeparam name="TConfigurationDbContext"></typeparam>
/// <typeparam name="TPersistedGrantDbContext"></typeparam>
/// <typeparam name="TIdentityDbContext"></typeparam>
/// <param name="services"></param>
public static void RegisterDbContextsStaging<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext, TDataProtectionDbContext>(
this IServiceCollection services)
where TIdentityDbContext : DbContext
where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
{
var identityDatabaseName = Guid.NewGuid().ToString();
services.AddDbContext<TIdentityDbContext>(optionsBuilder => optionsBuilder.UseInMemoryDatabase(identityDatabaseName));
var configurationDatabaseName = Guid.NewGuid().ToString();
var operationalDatabaseName = Guid.NewGuid().ToString();
var dataProtectionDatabaseName = Guid.NewGuid().ToString();
services.AddConfigurationDbContext<TConfigurationDbContext>(options =>
{
options.ConfigureDbContext = b => b.UseInMemoryDatabase(configurationDatabaseName);
});
services.AddOperationalDbContext<TPersistedGrantDbContext>(options =>
{
options.ConfigureDbContext = b => b.UseInMemoryDatabase(operationalDatabaseName);
});
services.AddDbContext<TDataProtectionDbContext>(options =>
{
options.UseInMemoryDatabase(dataProtectionDatabaseName);
});
}
/// <summary>
/// Add services for authentication, including Identity model, Duende IdentityServer and external providers
/// </summary>
/// <typeparam name="TIdentityDbContext">DbContext for Identity</typeparam>
/// <typeparam name="TUserIdentity">User Identity class</typeparam>
/// <typeparam name="TUserIdentityRole">User Identity Role class</typeparam>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void AddAuthenticationServices<TIdentityDbContext, TUserIdentity, TUserIdentityRole>(this IServiceCollection services, IConfiguration configuration) where TIdentityDbContext : DbContext
where TUserIdentity : class
where TUserIdentityRole : class
{
var loginConfiguration = GetLoginConfiguration(configuration);
var registrationConfiguration = GetRegistrationConfiguration(configuration);
var identityOptions = configuration.GetSection(nameof(IdentityOptions)).Get<IdentityOptions>();
services
.AddSingleton(registrationConfiguration)
.AddSingleton(loginConfiguration)
.AddSingleton(identityOptions)
.AddScoped<ApplicationSignInManager<TUserIdentity>>()
.AddScoped<UserResolver<TUserIdentity>>()
.AddIdentity<TUserIdentity, TUserIdentityRole>(options =>
{
configuration.GetSection(nameof(IdentityOptions)).Bind(options);
options.Stores.SchemaVersion = IdentityStoreDefaults.SchemaVersion;
options.Stores.MaxLengthForKeys = IdentityStoreDefaults.MaxLengthForKeys;
})
.AddEntityFrameworkStores<TIdentityDbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
options.Secure = CookieSecurePolicy.SameAsRequest;
options.OnAppendCookie = cookieContext =>
AuthenticationHelpers.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
AuthenticationHelpers.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
var authenticationBuilder = services.AddAuthentication();
AddExternalProviders(authenticationBuilder, configuration);
}
/// <summary>
/// Get configuration for login
/// </summary>
/// <param name="configuration"></param>
/// <returns></returns>
private static LoginConfiguration GetLoginConfiguration(IConfiguration configuration)
{
var loginConfiguration = configuration.GetSection(nameof(LoginConfiguration)).Get<LoginConfiguration>();
// Cannot load configuration - use default configuration values
if (loginConfiguration == null)
{
return new LoginConfiguration();
}
return loginConfiguration;
}
/// <summary>
/// Get configuration for registration
/// </summary>
/// <param name="configuration"></param>
/// <returns></returns>
private static RegisterConfiguration GetRegistrationConfiguration(IConfiguration configuration)
{
var registerConfiguration = configuration.GetSection(nameof(RegisterConfiguration)).Get<RegisterConfiguration>();
// Cannot load configuration - use default configuration values
if (registerConfiguration == null)
{
return new RegisterConfiguration();
}
return registerConfiguration;
}
/// <summary>
/// Add configuration for Duende IdentityServer
/// </summary>
/// <typeparam name="TUserIdentity"></typeparam>
/// <typeparam name="TConfigurationDbContext"></typeparam>
/// <typeparam name="TPersistedGrantDbContext"></typeparam>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static IIdentityServerBuilder AddIdentityServer<TConfigurationDbContext, TPersistedGrantDbContext, TUserIdentity>(
this IServiceCollection services,
IConfiguration configuration)
where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
where TUserIdentity : class
{
var configurationSection = configuration.GetSection(nameof(IdentityServerOptions));
var identityServerOptions = configurationSection.Get<IdentityServerOptions>();
var builder = services.AddIdentityServer(options =>
{
configurationSection.Bind(options);
options.DynamicProviders.SignInScheme = IdentityConstants.ExternalScheme;
options.DynamicProviders.SignOutScheme = IdentityConstants.ApplicationScheme;
})
.AddConfigurationStore<TConfigurationDbContext>()
.AddOperationalStore<TPersistedGrantDbContext>()
.AddAspNetIdentity<TUserIdentity>();
services.ConfigureOptions<OpenIdClaimsMappingConfig>();
if (!identityServerOptions.KeyManagement.Enabled)
{
builder.AddCustomSigningCredential(configuration);
builder.AddCustomValidationKey(configuration);
}
builder.AddExtensionGrantValidator<DelegationGrantValidator>();
// Check if server-side sessions should be enabled from configuration
var serverSideSessionsConfig = configuration.GetSection(Configuration.ServerSideSessionsConfiguration.SectionName).Get<Configuration.ServerSideSessionsConfiguration>() ?? new Configuration.ServerSideSessionsConfiguration();
var serverSideSessionsEnabled = serverSideSessionsConfig.Enabled;
if (serverSideSessionsEnabled)
{
builder.AddServerSideSessions();
services.Configure<IdentityServerOptions>(options =>
{
options.ServerSideSessions.UserDisplayNameClaimType = JwtClaimTypes.Name;
options.ServerSideSessions.RemoveExpiredSessions = true;
options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;
});
}
return builder;
}
/// <summary>
/// Add external providers
/// </summary>
/// <param name="authenticationBuilder"></param>
/// <param name="configuration"></param>
private static void AddExternalProviders(AuthenticationBuilder authenticationBuilder,
IConfiguration configuration)
{
var externalProviderConfiguration = configuration.GetSection(nameof(ExternalProvidersConfiguration)).Get<ExternalProvidersConfiguration>();
if (externalProviderConfiguration.UseGitHubProvider)
{
authenticationBuilder.AddGitHub(options =>
{
options.ClientId = externalProviderConfiguration.GitHubClientId;
options.ClientSecret = externalProviderConfiguration.GitHubClientSecret;
options.CallbackPath = externalProviderConfiguration.GitHubCallbackPath;
options.Scope.Add("user:email");
});
}
if (externalProviderConfiguration.UseAzureAdProvider)
{
authenticationBuilder.AddMicrosoftIdentityWebApp(options =>
{
options.ClientSecret = externalProviderConfiguration.AzureAdSecret;
options.ClientId = externalProviderConfiguration.AzureAdClientId;
options.TenantId = externalProviderConfiguration.AzureAdTenantId;
options.Instance = externalProviderConfiguration.AzureInstance;
options.Domain = externalProviderConfiguration.AzureDomain;
options.CallbackPath = externalProviderConfiguration.AzureAdCallbackPath;
}, cookieScheme: null);
}
}
/// <summary>
/// Register middleware for localization
/// </summary>
/// <param name="app"></param>
public static void UseMvcLocalizationServices(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
}
/// <summary>
/// Add authorization policies
/// </summary>
/// <param name="services"></param>
/// <param name="rootConfiguration"></param>
public static void AddAuthorizationPolicies(this IServiceCollection services,
IRootConfiguration rootConfiguration)
{
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationConsts.AdministrationPolicy,
policy => policy.RequireRole(rootConfiguration.AdminConfiguration.AdministrationRole));
});
}
public static void AddIdSHealthChecks<TConfigurationDbContext, TPersistedGrantDbContext, TIdentityDbContext, TDataProtectionDbContext>(this IServiceCollection services, IConfiguration configuration)
where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
where TIdentityDbContext : DbContext
where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
{
var configurationDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.ConfigurationDbConnectionStringKey);
var persistedGrantsDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.PersistedGrantDbConnectionStringKey);
var identityDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.IdentityDbConnectionStringKey);
var dataProtectionDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.DataProtectionDbConnectionStringKey);
var healthChecksBuilder = services.AddHealthChecks()
.AddDbContextCheck<TConfigurationDbContext>("ConfigurationDbContext")
.AddDbContextCheck<TPersistedGrantDbContext>("PersistedGrantsDbContext")
.AddDbContextCheck<TIdentityDbContext>("IdentityDbContext")
.AddDbContextCheck<TDataProtectionDbContext>("DataProtectionDbContext");
var serviceProvider = services.BuildServiceProvider();
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var configurationTableName = DbContextHelpers.GetEntityTable<TConfigurationDbContext>(scope.ServiceProvider);
var persistedGrantTableName = DbContextHelpers.GetEntityTable<TPersistedGrantDbContext>(scope.ServiceProvider);
var identityTableName = DbContextHelpers.GetEntityTable<TIdentityDbContext>(scope.ServiceProvider);
var dataProtectionTableName = DbContextHelpers.GetEntityTable<TDataProtectionDbContext>(scope.ServiceProvider);
var databaseProvider = configuration.GetSection(nameof(DatabaseProviderConfiguration)).Get<DatabaseProviderConfiguration>();
switch (databaseProvider.ProviderType)
{
case DatabaseProviderType.SqlServer:
healthChecksBuilder
.AddSqlServer(configurationDbConnectionString, name: "ConfigurationDb",
healthQuery: $"SELECT TOP 1 * FROM dbo.[{configurationTableName}]")
.AddSqlServer(persistedGrantsDbConnectionString, name: "PersistentGrantsDb",
healthQuery: $"SELECT TOP 1 * FROM dbo.[{persistedGrantTableName}]")
.AddSqlServer(identityDbConnectionString, name: "IdentityDb",
healthQuery: $"SELECT TOP 1 * FROM dbo.[{identityTableName}]")
.AddSqlServer(dataProtectionDbConnectionString, name: "DataProtectionDb",
healthQuery: $"SELECT TOP 1 * FROM dbo.[{dataProtectionTableName}]");
break;
case DatabaseProviderType.PostgreSQL:
healthChecksBuilder
.AddNpgSql(configurationDbConnectionString, name: "ConfigurationDb",
healthQuery: $"SELECT * FROM \"{configurationTableName}\" LIMIT 1")
.AddNpgSql(persistedGrantsDbConnectionString, name: "PersistentGrantsDb",
healthQuery: $"SELECT * FROM \"{persistedGrantTableName}\" LIMIT 1")
.AddNpgSql(identityDbConnectionString, name: "IdentityDb",
healthQuery: $"SELECT * FROM \"{identityTableName}\" LIMIT 1")
.AddNpgSql(dataProtectionDbConnectionString, name: "DataProtectionDb",
healthQuery: $"SELECT * FROM \"{dataProtectionTableName}\" LIMIT 1");
break;
default:
throw new NotImplementedException($"Health checks not defined for database provider {databaseProvider.ProviderType}");
}
}
}
}
}
@@ -0,0 +1,52 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace LiteCharmsSecurity.STS.Identity.Helpers.TagHelpers
{
[HtmlTargetElement("img-gravatar")]
public class GravatarTagHelper : TagHelper
{
[HtmlAttributeName("email")]
public string Email { get; set; }
[HtmlAttributeName("alt")]
public string Alt { get; set; }
[HtmlAttributeName("class")]
public string Class { get; set; }
[HtmlAttributeName("size")]
public int Size { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!string.IsNullOrWhiteSpace(Email))
{
var hash = Md5HashHelper.GetHash(Email);
output.TagName = "img";
if (!string.IsNullOrWhiteSpace(Class))
{
output.Attributes.Add("class", Class);
}
if (!string.IsNullOrWhiteSpace(Alt))
{
output.Attributes.Add("alt", Alt);
}
output.Attributes.Add("src", GetAvatarUrl(hash, Size));
output.TagMode = TagMode.SelfClosing;
}
}
private static string GetAvatarUrl(string hash, int size)
{
var sizeArg = size > 0 ? $"?s={size}" : "";
return $"https://www.gravatar.com/avatar/{hash}{sizeArg}";
}
}
}
@@ -0,0 +1,63 @@
#nullable enable
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;
namespace LiteCharmsSecurity.STS.Identity.Helpers.TagHelpers
{
[HtmlTargetElement("lucideicon", TagStructure = TagStructure.WithoutEndTag)]
public class LucideIconTagHelper : TagHelper
{
private readonly IFileProvider _fileProvider;
public LucideIconTagHelper(IWebHostEnvironment env)
{
_fileProvider = env.WebRootFileProvider;
}
[HtmlAttributeName("name")]
public string Name { get; set; } = string.Empty;
[HtmlAttributeName("class")]
public string? Class { get; set; }
[HtmlAttributeName("size")]
public int? Size { get; set; }
[HtmlAttributeName("base-path")]
public string BasePath { get; set; } = "icons/lucide";
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = null;
if (string.IsNullOrWhiteSpace(Name))
{
output.Content.SetHtmlContent("<!-- missing icon name -->");
return;
}
var file = _fileProvider.GetFileInfo($"{BasePath}/{Name}.svg");
if (!file.Exists)
{
output.Content.SetHtmlContent($"<!-- icon not found: {Name} -->");
return;
}
string svg;
using (var stream = file.CreateReadStream())
using (var reader = new StreamReader(stream))
svg = reader.ReadToEnd();
if (!string.IsNullOrWhiteSpace(Class))
svg = svg.Replace("<svg", $"<svg class=\"{Class}\"", StringComparison.OrdinalIgnoreCase);
if (Size.HasValue)
svg = svg.Replace("<svg", $"<svg width=\"{Size}\" height=\"{Size}\"", StringComparison.OrdinalIgnoreCase);
output.Content.SetHtmlContent(svg);
}
}
}
@@ -0,0 +1,27 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace LiteCharmsSecurity.STS.Identity.Helpers.TagHelpers
{
[HtmlTargetElement("toggle-button")]
public class SwitchTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
var divSlider = new TagBuilder("div");
divSlider.AddCssClass("slider round bg-primary");
output.TagName = "label";
output.Attributes.Add("class", "switch");
output.Content.AppendHtml(childContent);
output.Content.AppendHtml(divSlider);
output.TagMode = TagMode.StartTagAndEndTag;
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace LiteCharmsSecurity.STS.Identity.Helpers.TagHelpers;
[HtmlTargetElement("label", Attributes = "asp-for,tw-validation", TagStructure = TagStructure.NormalOrSelfClosing)]
public class TwValidationLabelTagHelper : TagHelper
{
[HtmlAttributeName("asp-for")]
public ModelExpression For { get; set; } = default!;
[HtmlAttributeName("tw-validation")]
public bool TwValidation { get; set; } = true;
[ViewContext]
public ViewContext ViewContext { get; set; } = default!;
public override int Order => 1000;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!TwValidation || ViewContext == null || For == null) return;
var key = For.Name;
var modelState = ViewContext.ViewData.ModelState;
if (modelState.TryGetValue(key, out var entry) && entry.Errors.Count > 0)
{
var existingClass = output.Attributes["class"]?.Value?.ToString() ?? string.Empty;
var mergedClass = string.IsNullOrWhiteSpace(existingClass)
? "form-label form-label-invalid"
: $"{existingClass} form-label-invalid";
output.Attributes.SetAttribute("class", mergedClass);
}
// odstraníme vlastní atribut, aby nešel do HTML
output.Attributes.RemoveAll("tw-validation");
}
}
@@ -0,0 +1,65 @@
#nullable enable
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace LiteCharmsSecurity.STS.Identity.Helpers.TagHelpers;
[HtmlTargetElement("input", Attributes = "asp-for,tw-validation")]
public class TwValidationTagHelper : TagHelper
{
public override int Order => 1000;
[HtmlAttributeName("asp-for")]
public ModelExpression AspFor { get; set; } = default!;
[HtmlAttributeName("tw-validation")]
public bool TwValidation { get; set; } = true;
[ViewContext, HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; } = default!;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("tw-validation");
if (!TwValidation || AspFor == null || ViewContext == null)
return;
var fieldName = AspFor.Name;
var hasError = ViewContext.ViewData.ModelState.TryGetValue(fieldName, out var entry)
&& entry is { Errors.Count: > 0 };
if (!hasError)
return;
var existing = output.Attributes.TryGetAttribute("class", out var classAttr)
? AsPlainText(classAttr.Value)
: string.Empty;
var merged = string.IsNullOrWhiteSpace(existing)
? "input-invalid"
: $"{existing} input-invalid";
output.Attributes.SetAttribute("class", merged.Trim());
output.Attributes.SetAttribute("aria-invalid", "true");
}
private static string AsPlainText(object? value)
{
if (value is null) return string.Empty;
if (value is string s) return s;
if (value is IHtmlContent html)
{
using var sw = new StringWriter();
html.WriteTo(sw, HtmlEncoder.Default);
return sw.ToString();
}
return value.ToString() ?? string.Empty;
}
}
@@ -0,0 +1,34 @@
// Copyright (c) Jan Škoruba. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Skoruba.Duende.IdentityServer.Shared.Configuration.Configuration.Identity;
namespace LiteCharmsSecurity.STS.Identity.Helpers
{
public class UserResolver<TUser> where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly LoginResolutionPolicy _policy;
public UserResolver(UserManager<TUser> userManager, LoginConfiguration configuration)
{
_userManager = userManager;
_policy = configuration.ResolutionPolicy;
}
public async Task<TUser> GetUserAsync(string login)
{
switch (_policy)
{
case LoginResolutionPolicy.Username:
return await _userManager.FindByNameAsync(login);
case LoginResolutionPolicy.Email:
return await _userManager.FindByEmailAsync(login);
default:
return null;
}
}
}
}