Add project files.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user