ASP.NET Core Authorize & UserRoles in Multy Tenancy - asp.net-core

We have am ASP.NET Core 6.0 multi tenant application with a single database. The tenant tables have a TenantId column to show only tenant data. We don't need to discuss whether is better or not to have multiple databases, for our purpose we need it this way.
All tenants share the users table and there is a UserTenant table to filter which users a tenant can see. But it is not relevant for this question.
We need to Authorize some controllers and for that we need to do this using roles:
[Authorize(Roles = "RoleA,RoleB,RoleC")]
public class SomeController : Controller
This way, a user can get into the controller only if belongs to one of those roles.
The problem is we need to assign roles to users in a certain tenant. So a "UserA" can have the "RoleA" in "TenantA", but not in "TenantB".
The Roles would be the same for all the tenants, so no tenantId in the table "aspnetroles", but we do need another property for table "aspnetuserroles" since the key sould be "UserId, RoleId,TenantId".
Our Application user is configured this way:
public class ApplicationUser : IdentityUser<int>
{
public string Name { get; set; } = String.Empty;
public string Surname { get; set; } = String.Empty;
public string? Alias { get; set; }
....
}
public class ApplicationRole : IdentityRole<int> { }
public class ApplicationUserLogin : IdentityRole<int> { }
How can we change the Identity UserRoles in order to, not only store the UserId, RoleId and TenantId but also to work with the Authorize in the controller?
Thanks in advance.
UPDATE 1
I have been able to customize the aspnetuserroles table, the Client is our Tenant. This way we have already the table modified:
public class ApplicationUserRole : IdentityUserRole<int>
{
public int ClientId { get; set; }
public Client Client { get; set; }
public static void OnModelCreating(ModelBuilder modelBuilder)
{
var e = modelBuilder.Entity<ApplicationUserRole>();
e.HasKey(p => new { p.UserId, p.RoleId, p.ClientId });
e.HasOne(p => p.Client).WithMany().HasForeignKey(tr => tr.ClientId).HasConstraintName("FK_UserRole_Client").IsRequired().OnDelete(DeleteBehavior.Cascade);
}
}
UPDATE 2
We have been able to modify the UserManager so we can add roles using the TenantId, in our case the ClientId.
public class ApplicationUserManager : UserManager<ApplicationUser>
{
private readonly UserStore<ApplicationUser, ApplicationRole, DataContext, int, IdentityUserClaim<int>, ApplicationUserRole, IdentityUserLogin<int>, IdentityUserToken<int>, IdentityRoleClaim<int>> _store;
public ApplicationUserManager(
IUserStore<ApplicationUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<ApplicationUser> passwordHasher,
IEnumerable<IUserValidator<ApplicationUser>> userValidators,
IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<ApplicationUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
_store = (UserStore<ApplicationUser, ApplicationRole, DataContext, int, IdentityUserClaim<int>, ApplicationUserRole, IdentityUserLogin<int>, IdentityUserToken<int>, IdentityRoleClaim<int>>)store;
}
public virtual async Task<IdentityResult> AddToRoleByClientAsync(ApplicationUser user, string role, int clientId)
{
ThrowIfDisposed();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrWhiteSpace(role))
throw new ArgumentNullException(nameof(role));
if (clientId == 0)
throw new ArgumentNullException(nameof(clientId));
var normalizedRole = NormalizeName(role);
var applicationRole = _store.Context.Roles.FirstOrDefault(p => p.NormalizedName == normalizedRole);
if (applicationRole == null)
return IdentityResult.Failed(ErrorDescriber.InvalidRoleName(normalizedRole));
if (await IsInRoleByRoleIdClientAsync(user, applicationRole.Id, clientId))
return IdentityResult.Failed(ErrorDescriber.UserAlreadyInRole(normalizedRole));
_store.Context.Set<ApplicationUserRole>().Add(new ApplicationUserRole { RoleId = applicationRole.Id, UserId = user.Id, ClientId = clientId });
return await UpdateUserAsync(user);
}
private async Task<bool> IsInRoleByRoleIdClientAsync(ApplicationUser user, int roleId, int clientId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (roleId == 0)
throw new ArgumentNullException(nameof(roleId));
if (clientId == 0)
throw new ArgumentNullException(nameof(clientId));
var userRole = await _store.Context.Set<ApplicationUserRole>().FindAsync(new object[] { user.Id, roleId, clientId }, cancellationToken);
return userRole != null;
}
public async Task<bool> IsInRoleByClientAsync(ApplicationUser user, string role, int clientId)
{
ThrowIfDisposed();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrWhiteSpace(role))
throw new ArgumentNullException(nameof(role));
if (clientId == 0)
throw new ArgumentNullException(nameof(clientId));
var normalizedRole = NormalizeName(role);
var applicationRole = _store.Context.Roles.FirstOrDefault(p => p.NormalizedName == normalizedRole);
if (applicationRole == null)
return false;
var userRole = await _store.Context.Set<ApplicationUserRole>().FindAsync(new object[] { user.Id, applicationRole.Id, clientId });
return userRole != null;
}
}
We still need to modify the [Authorize(Roles = "admin")] in order to check the roles of the current Tenant and not all of them. We are investigating this.

Related

How to create attributes on controller actions to check if user has claim

I have added a few custom claims to my user and I was wondering if I want to check if these claims exist on controller actions using attributes, I know that we can create a class and extend attribute from .Net and the general idea is to check if user has claim or not, I'm not really clear on the implementation.
Maybe something like this:
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public class ClaimRequirementAttribute : Attribute
{
public ClaimRequirementAttribute(string claimType)
{
new Claim(claimType, null);
}
}
public class ClaimRequirementFilter
{
public void OnAuthorization(HttpContext httpContext)
{
var hasClaim = httpContext.User.HasClaim(x => x.Type ==
CapabilityClaims.CanReadSpore);
if (!hasClaim)
{
}
}
}
You can get the Claims of a specific user, using the GetClaimsAsync method of UserManager.
You can use the following method:
public class TestController : Controller
{
private readonly UserManager<AppUser> _userManager;
public TestController(UserManager<AppUser> userManager)
{
_userManager = userManager;
}
public CheckIfClaimsExist(string email)
{
var user = await _userManager.FindByEmailAsync(email);
if(user != null)
{
var claims = await _userManager.GetClaimsAsync(user);
}
}
}
Note: AppUser class is a custom class which extends IdentityUser class from identity server.
After some long research i found this answer using filters
which ended up being the best approach
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class ClaimRequirementAttribute : TypeFilterAttribute
{
public ClaimRequirementAttribute(params string[] claimType) : base(typeof(ClaimRequirementFilter))
{
Arguments = new object[] { claimType };
}
}
public class ClaimRequirementFilter : IAuthorizationFilter
{
readonly string[] _claimTypes;
public ClaimRequirementFilter(string[] claimTypes)
{
_claimTypes = claimTypes;
}
public void OnAuthorization(AuthorizationFilterContext authContext)
{
if (authContext == null)
{
throw new ArgumentNullException(nameof(authContext));
}
var user = authContext.HttpContext.User;
var resourceId = authContext.RouteData.Values["id"].ToString();
var claimType = _claimTypes
.All(s => (user.Claims)
.Any(c => c.Type == s && (c.Value == resourceId || c.Value == string.Empty)));
if (user == null || !claimType)
{
authContext.Result = new ForbidResult();
}
}

Core 3.0 ApplicationUser is always empty

I have extended Identityuser
public class ApplicationUser : IdentityUser
{
[MaxLength(150)]
public string FirstName { get ; set ; }
[MaxLength(150)]
public string LastName { get ; set ; }
public int AlternateUserId { get ; set ; }
[MaxLength(150)]
public string CompanyName { get ; set ; }
[MaxLength(38)]
[Required]
public string ClientId { get ; set ; }
[Required]
public int ShortClient { set ; get ; }
public bool Locked { set ; get ; }
}
In Startup.cs i have:
services.AddIdentity<ApplicationUser, IdentityRole>().AddDefaultUI().AddEntityFrameworkStores<ApplicationDbContext>();
services.AddSingleton<ApplicationUser>();
But in
public static class IdentityExtentionMethods
{
public static string FirstName(this IIdentity identity)
{
var claim = ((ClaimsIdentity)identity).FindFirst(ClaimTypes.GivenName);
// Test for null to avoid issues during local testing
return (claim != null) ? claim.Value : string.Empty;
}
}
Claim is always null and anywhere I try to inject ApplicationUser the variable is available but it is not populated with the user information.
#inject ApplicationUser applicationUser
#inject SignInManager<ApplicationUser> signInManager;
Instead it has some dummy values in a few of the Guid fields and most everything else is null.
Yeah, you can't just inject ApplicationUser. After authentication, all you have is a ClaimsPrincipal, not an ApplicationUser instance. If you need an actual ApplicationUser instance, then you must query that out of the database based on the user id present in the ClaimsPrincipal (HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)).
As Chris Pratt points out you can't get ApplicationUser via injection in Core 3.x I am not sure about earlier versions. To bad its not in the documentation anywhere that I could see.
But you can get
SignInManager<ApplicationUser> _signInManager,
UserManager <ApplicationUser> _userManager
ApplicationDbContext _dbContext
And as Chris also points out you can get
ClaimsPrincipal
and
IPrincipal
I have IPrincipal and as the code shows below, that with SignInManager & UserManager is all you need to get ApplicationUser
public static class IdentityExtentionMethods
{
public static bool IsSysAdmin(this IPrincipal _principal,
SignInManager<ApplicationUser> _signInManager,
UserManager <ApplicationUser> _userManager)
{
var x = isSysAdmin(_principal, _signInManager, _userManager);
if (x.Result == false)
return false;
else
return true;
}
public static async Task<bool> isSysAdmin(this IPrincipal _principal,
SignInManager<ApplicationUser> _signInManager,
UserManager <ApplicationUser> _userManager)
{
var ci = _principal.Identity as ClaimsIdentity;
var userName = ci != null ? ci.FindFirst(ClaimTypes.Name) : null;
string username = userName?.Value;
// get ApplicationUser
var appUser = await _userManager.FindByNameAsync( username);
var _userClaims = await
_signInManager.ClaimsFactory.CreateAsync(appUser);
if (_userClaims.UserHasThisPermission(Permissions.AccessAll))
return true;
else
return false;
}
public static bool HasRole( this IPrincipal _principal,
string roleName,
SignInManager<ApplicationUser> _signInManager,
UserManager <ApplicationUser> _userManager,
ApplicationDbContext _dbContext)
{
var x = hasrole ( _principal , roleName , _signInManager , _userManager , _dbContext ) ;
if (x.Result == false)
return false;
else
return true;
}
private static async Task<bool> hasrole ( this IPrincipal _principal,
string roleName,
SignInManager<ApplicationUser> _signInManager,
UserManager <ApplicationUser> _userManager,
ApplicationDbContext _dbContext)
{
if (roleName == null)
throw new ArgumentNullException(nameof(roleName));
var ci = _principal.Identity as ClaimsIdentity;
var userName = ci != null ? ci.FindFirst(ClaimTypes.Name) : null;
string username = userName?.Value;
var appUser = await _userManager.FindByNameAsync( username);
if (_dbContext.Find<UserToRole>(appUser.Id, roleName) != null)
{
return true ;
}
return false ;
}
}
You access like this from _layout.cshtml
#using Microsoft.AspNetCore.Identity
#inject ApplicationDbContext dbcontext ;
#inject UserManager<ApplicationUser> userManager ;
#inject SignInManager<ApplicationUser> signInManager;
;;
;;
#if ( this.User.IsSysAdmin ( signInManager , userManager ) )
{
<!-- add menu stuff -->
}
#if ( this.User.HasRole ( signInManager , userManager,dbcontext ) )
{
<!-- add menu stuff -->
}
certainly seems like a lot of stuff to pass around but it gets the job done.
BTW, the claims stuff is from https://www.thereformedprogrammer.net/part-7-adding-the-better-asp-net-core-authorization-code-into-your-app/
Jon Smith has written a wonderful app which has an MIT open source license and allows you to use roles and permissions in Core 3.0/1 It is very complex but he provided a scaled down version https://github.com/JonPSmith/PermissionsOnlyApp that works well. Thanks Jon.

How do I get the current logged in user ID in the ApplicationDbContext using Identity?

I have created a .net core 2.1 MVC application using the template in Visual Studio with the Identity preset (user accounts stored in the application) and I am trying to automate some auditing fields.
Basically what I'm trying to do is overriding the SaveChangesAsync() method so that whenever changes are made to an entity the current logged in user ID is set to the auditing property of CreatedBy or ModifiedBy properties that are created as shadow properties on the entity.
I have looked at what seems to be tons of answers and surprisingly none of them work for me. I have tried injecting IHttpContext, HttpContext, UserManager, and I either can't seem to access a method that returns the user ID or I get a circular dependency error which I don't quite understand why it is happening.
I'm really running desperate with this one. I think something like this should be really straightforward to do, but I'm having a real hard time figuring out how to do it. There seem to be well documented solutions for web api controllers or for MVC controllers but not for use inside the ApplicationDbContext.
If someone can help me or at least point me into the right direction I'd be really grateful, thanks.
Let's call it DbContextWithUserAuditing
public class DBContextWithUserAuditing : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public string UserId { get; set; }
public int? TenantId { get; set; }
public DBContextWithUserAuditing(DbContextOptions<DBContextWithUserAuditing> options) : base(options) { }
// here we declare our db sets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.NamesToSnakeCase(); // PostgreSQL
modelBuilder.EnableSoftDelete();
}
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
ChangeTracker.ProcessModification(UserId);
ChangeTracker.ProcessDeletion(UserId);
ChangeTracker.ProcessCreation(UserId, TenantId);
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
ChangeTracker.DetectChanges();
ChangeTracker.ProcessModification(UserId);
ChangeTracker.ProcessDeletion(UserId);
ChangeTracker.ProcessCreation(UserId, TenantId);
return (await base.SaveChangesAsync(true, cancellationToken));
}
}
Then you have request pipeline and what you need - is a filter hook where you set your UserID
public class AppInitializerFilter : IAsyncActionFilter
{
private DBContextWithUserAuditing _dbContext;
public AppInitializerFilter(
DBContextWithUserAuditing dbContext
)
{
_dbContext = dbContext;
}
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next
)
{
string userId = null;
int? tenantId = null;
var claimsIdentity = (ClaimsIdentity)context.HttpContext.User.Identity;
var userIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (userIdClaim != null)
{
userId = userIdClaim.Value;
}
var tenantIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == CustomClaims.TenantId);
if (tenantIdClaim != null)
{
tenantId = !string.IsNullOrEmpty(tenantIdClaim.Value) ? int.Parse(tenantIdClaim.Value) : (int?)null;
}
_dbContext.UserId = userId;
_dbContext.TenantId = tenantId;
var resultContext = await next();
}
}
You activate this filter in the following way (Startup.cs file)
services
.AddMvc(options =>
{
options.Filters.Add(typeof(OnRequestInit));
})
Your app is then able to automatically set UserID & TenantID to newly created records
public static class ChangeTrackerExtensions
{
public static void ProcessCreation(this ChangeTracker changeTracker, string userId, int? tenantId)
{
foreach (var item in changeTracker.Entries<IHasCreationTime>().Where(e => e.State == EntityState.Added))
{
item.Entity.CreationTime = DateTime.Now;
}
foreach (var item in changeTracker.Entries<IHasCreatorUserId>().Where(e => e.State == EntityState.Added))
{
item.Entity.CreatorUserId = userId;
}
foreach (var item in changeTracker.Entries<IMustHaveTenant>().Where(e => e.State == EntityState.Added))
{
if (tenantId.HasValue)
{
item.Entity.TenantId = tenantId.Value;
}
}
}
I wouldn't recommend injecting HttpContext, UserManager or anything into your DbContext class because this way you violate Single Responsibility Principle.
Thanks to all the answers. In the end I decided to create a UserResolveService that receives through DI the HttpContextAccessor and can then get the current user's name. With the name I can then query the database to get whatever information I may need. I then inject this service on the ApplicationDbContext.
IUserResolveService.cs
public interface IUserResolveService
{
Task<string> GetCurrentSessionUserId(IdentityDbContext dbContext);
}
UserResolveService.cs
public class UserResolveService : IUserResolveService
{
private readonly IHttpContextAccessor httpContextAccessor;
public UserResolveService(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public async Task<string> GetCurrentSessionUserId(IdentityDbContext dbContext)
{
var currentSessionUserEmail = httpContextAccessor.HttpContext.User.Identity.Name;
var user = await dbContext.Users
.SingleAsync(u => u.Email.Equals(currentSessionUserEmail));
return user.Id;
}
}
You have to register the service on startup and inject it on the ApplicationDbContext and you can use it like this:
ApplicationDbContext.cs
var dbContext = this;
var currentSessionUserId = await userResolveService.GetCurrentSessionUserId(dbContext);

How to implement AD Group based authorization globally in asp.net core 2.x web application?

I am wondering if someone could point me a direction or an example which have the completed code for me to get an overall idea?
Thanks.
Update:
I only have following piece of code in Startup.cs and make sure windowsAutication is true in launchSettings.json.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
//.RequireRole(#"Departmental - Information Technology - Development") // Works
.RequireRole(#"*IT.Center of Excellence.Digital Workplace") // Error
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
}
I guess I have enabled Authentication and tries to authorize users who are within the specified AD group to have access to the application at global level.
If I use the commented RequireRole it works, but use the uncommented RequireRole it gives me this error:
Win32Exception: The trust relationship between the primary domain and the trusted domain failed.
The top line in the stack shows:
System.Security.Principal.NTAccount.TranslateToSids(IdentityReferenceCollection sourceAccounts, out bool someFailed)
Any idea why?
My understanding from update above
It seems the group name specified in RequireRole is an email distribution list not security group. If I use some other AD group it works but with this new error:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultForbidScheme found.
If I add IIS default authenticationScheme in ConfigureServices within Startup.cs
services.AddAuthentication(IISDefaults.AuthenticationScheme);
it gives me an HTTP 403 page: The website declined to show this webpage
So this is the final code:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole(#"Departmental - Information Technology - Development") // AD security group
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
}
Correct me if I understand wrongly. Thank you.
Option 1: Windows Authentication
You can turn on Windows Authentication for intranet applications. Read the docs here. You can check whether a user is in a role/group by doing something like this.
Before you do, you can check the groups information your computer joined by doing gpresult /R in the command prompt. See this post for more information.
User.IsInRole("xxxx") // this should return True for any group listed up there
You don't need to convert current principal to Windows principal if you don't need to get any information related to Windows.
If you want to get a list of all groups, you still need to query your AD.
warning:
Sometimes I see some groups are not showing up in the result using gpresult /R on the computer, comparing to the option 2 method. That's why sometimes when you do User.IsInRole() and it returns false. I still don't know why this happens.
Option 2: Form Authentication with AD lookup
The Windows Authentication offers just a little information about the user and the AD groups. Sometimes that's enough but most of the time it's not.
You can also use regular Form Authentication and talk to the AD underneath and issue a cookie. That way although the user needs to login to your app using their windows credential and password, you have full control on the AD information.
You don't want to write everything by hand. Luckily there is a library Novell.Directory.Ldap.NETStandard to help. You can find it in NuGet.
Interfaces to define what you need from the AD, as well as the login protocol:
namespace DL.SO.Services.Core
{
public interface IAppUser
{
string Username { get; }
string DisplayName { get; }
string Email { get; }
string[] Roles { get; }
}
public interface IAuthenticationService
{
IAppUser Login(string username, string password);
}
}
AppUser implementation:
using DL.SO.Services.Core;
namespace DL.SO.Services.Security.Ldap.Entities
{
public class AppUser : IAppUser
{
public string Username { get; set; }
public string DisplayName { get; set; }
public string Email { get; set; }
public string[] Roles { get; set; }
}
}
Ldap configuration object for mapping values from appsettings.json:
namespace DL.SO.Services.Security.Ldap
{
public class LdapConfig
{
public string Url { get; set; }
public string BindDn { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string SearchBase { get; set; }
public string SearchFilter { get; set; }
}
}
LdapAuthenticationService implementation:
using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using DL.SO.Services.Core;
using DL.SO.Services.Security.Ldap.Entities;
namespace DL.SO.Services.Security.Ldap
{
public class LdapAuthenticationService : IAuthenticationService
{
private const string MemberOfAttribute = "memberOf";
private const string DisplayNameAttribute = "displayName";
private const string SAMAccountNameAttribute = "sAMAccountName";
private const string MailAttribute = "mail";
private readonly LdapConfig _config;
private readonly LdapConnection _connection;
public LdapAuthenticationService(IOptions<LdapConfig> configAccessor)
{
_config = configAccessor.Value;
_connection = new LdapConnection();
}
public IAppUser Login(string username, string password)
{
_connection.Connect(_config.Url, LdapConnection.DEFAULT_PORT);
_connection.Bind(_config.Username, _config.Password);
var searchFilter = String.Format(_config.SearchFilter, username);
var result = _connection.Search(
_config.SearchBase,
LdapConnection.SCOPE_SUB,
searchFilter,
new[] {
MemberOfAttribute,
DisplayNameAttribute,
SAMAccountNameAttribute,
MailAttribute
},
false
);
try
{
var user = result.next();
if (user != null)
{
_connection.Bind(user.DN, password);
if (_connection.Bound)
{
var accountNameAttr = user.getAttribute(SAMAccountNameAttribute);
if (accountNameAttr == null)
{
throw new Exception("Your account is missing the account name.");
}
var displayNameAttr = user.getAttribute(DisplayNameAttribute);
if (displayNameAttr == null)
{
throw new Exception("Your account is missing the display name.");
}
var emailAttr = user.getAttribute(MailAttribute);
if (emailAttr == null)
{
throw new Exception("Your account is missing an email.");
}
var memberAttr = user.getAttribute(MemberOfAttribute);
if (memberAttr == null)
{
throw new Exception("Your account is missing roles.");
}
return new AppUser
{
DisplayName = displayNameAttr.StringValue,
Username = accountNameAttr.StringValue,
Email = emailAttr.StringValue,
Roles = memberAttr.StringValueArray
.Select(x => GetGroup(x))
.Where(x => x != null)
.Distinct()
.ToArray()
};
}
}
}
finally
{
_connection.Disconnect();
}
return null;
}
private string GetGroup(string value)
{
Match match = Regex.Match(value, "^CN=([^,]*)");
if (!match.Success)
{
return null;
}
return match.Groups[1].Value;
}
}
}
Configuration in appsettings.json (just an example):
{
"ldap": {
"url": "[YOUR_COMPANY].loc",
"bindDn": "CN=Users,DC=[YOUR_COMPANY],DC=loc",
"username": "[YOUR_COMPANY_ADMIN]",
"password": "xxx",
"searchBase": "DC=[YOUR_COMPANY],DC=loc",
"searchFilter": "(&(objectClass=user)(objectClass=person)(sAMAccountName={0}))"
},
"cookies": {
"cookieName": "cookie-name-you-want-for-your-app",
"loginPath": "/account/login",
"logoutPath": "/account/logout",
"accessDeniedPath": "/account/accessDenied",
"returnUrlParameter": "returnUrl"
}
}
Setup Authentication (maybe Authorization as well) for the app:
namespace DL.SO.Web.UI
{
public class Startup
{
private readonly IHostingEnvironment _currentEnvironment;
public IConfiguration Configuration { get; private set; }
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
_currentEnvironment = env;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// Authentication service
services.Configure<LdapConfig>(this.Configuration.GetSection("ldap"));
services.AddScoped<IAuthenticationService, LdapAuthenticationService>();
// MVC
services.AddMvc(config =>
{
// Requiring authenticated users on the site globally
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
// You can chain more requirements here
// .RequireRole(...) OR
// .RequireClaim(...) OR
// .Requirements.Add(...)
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Authentication
var cookiesConfig = this.Configuration.GetSection("cookies")
.Get<CookiesConfig>();
services.AddAuthentication(
CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = cookiesConfig.CookieName;
options.LoginPath = cookiesConfig.LoginPath;
options.LogoutPath = cookiesConfig.LogoutPath;
options.AccessDeniedPath = cookiesConfig.AccessDeniedPath;
options.ReturnUrlParameter = cookiesConfig.ReturnUrlParameter;
});
// Setup more authorization policies as an example.
// You can use them to protected more strict areas. Otherwise
// you don't need them.
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly",
policy => policy.RequireClaim(ClaimTypes.Role, "[ADMIN_ROLE_OF_YOUR_COMPANY]"));
// More on Microsoft documentation
// https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
});
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvc(...);
}
}
}
How to authenticate users using the authentication service:
namespace DL.SO.Web.UI.Controllers
{
public class AccountController : Controller
{
private readonly IAuthenticationService _authService;
public AccountController(IAuthenticationService authService)
{
_authService = authService;
}
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
try
{
var user = _authService.Login(model.Username, model.Password);
// If the user is authenticated, store its claims to cookie
if (user != null)
{
var userClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(CustomClaimTypes.DisplayName, user.DisplayName),
new Claim(ClaimTypes.Email, user.Email)
};
// Roles
foreach (var role in user.Roles)
{
userClaims.Add(new Claim(ClaimTypes.Role, role));
}
var principal = new ClaimsPrincipal(
new ClaimsIdentity(userClaims, _authService.GetType().Name)
);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties
{
IsPersistent = model.RememberMe
}
);
return Redirect(Url.IsLocalUrl(model.ReturnUrl)
? model.ReturnUrl
: "/");
}
ModelState.AddModelError("", #"Your username or password
is incorrect. Please try again.");
}
catch (Exception ex)
{
ModelState.AddModelError("", ex.Message);
}
}
return View(model);
}
}
}
How to read the information stored in the claims:
public class TopNavbarViewComponent : ViewComponent
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TopNavbarViewComponent(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<IViewComponentResult> InvokeAsync()
{
string loggedInUsername = _httpContextAccessor.HttpContext.User.Identity.Name;
string loggedInUserDisplayName = _httpContextAccessor.HttpContext.User.GetDisplayName();
...
return View(vm);
}
}
Extension method for ClaimsPrincipal:
namespace DL.SO.Framework.Mvc.Extensions
{
public static class ClaimsPrincipalExtensions
{
public static Claim GetClaim(this ClaimsPrincipal user, string claimType)
{
return user.Claims
.SingleOrDefault(c => c.Type == claimType);
}
public static string GetDisplayName(this ClaimsPrincipal user)
{
var claim = GetClaim(user, CustomClaimTypes.DisplayName);
return claim?.Value;
}
public static string GetEmail(this ClaimsPrincipal user)
{
var claim = GetClaim(user, ClaimTypes.Email);
return claim?.Value;
}
}
}
How to use policy authorization:
namespace DL.SO.Web.UI.Areas.Admin.Controllers
{
[Area("admin")]
[Authorize(Policy = "AdminOnly")]
public abstract class AdminControllerBase : Controller {}
}
Bonus
You can download the AD Explorer from Microsoft so that you can visualize your company AD.
Opps. I was planning to just give out something for head start but I ended up writing a very long post.

Custom authentication domain service - Silverlight and RIA

I'm trying to write custom authentication domain service. I think I understood all code which was written on this blog.
However I don't know how to specify which domain service application should use. I have one abstract domain service and second one is a concrete implementation of this service. If I build entire solution I get an error
'MainModule.Web.FormsAuthenticationService`1' is not a valid DomainService type. DomainService types cannot be abstract or generic.
I didn't find source code on blog which I mentioned before.
namespace MainModule.Web
{
using System;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
// TODO: Create methods containing your application logic.
[EnableClientAccess()]
public abstract class FormsAuthenticationService<TUser> : DomainService, IAuthentication<TUser> where TUser : UserBase
{
protected abstract TUser GetCurrentUser(string name, string userData);
protected abstract TUser ValidateCredentials(string name, string password, string customData, out string userData);
protected virtual TUser GetDefaultUser()
{
return null;
}
public TUser GetUser()
{
IPrincipal currentUser = ServiceContext.User;
if ((currentUser != null) && currentUser.Identity.IsAuthenticated)
{
FormsIdentity userIdentity = currentUser.Identity as FormsIdentity;
if (userIdentity != null)
{
FormsAuthenticationTicket ticket = userIdentity.Ticket;
if (ticket != null)
{
return GetCurrentUser(currentUser.Identity.Name, ticket.UserData);
}
}
}
return GetDefaultUser();
}
public TUser Login(string userName, string password, bool isPersistent, string customData)
{
string userData;
TUser user = ValidateCredentials(userName, password, customData, out userData);
if (user != null)
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(/* version */ 1, userName,
DateTime.Now, DateTime.Now.AddMinutes(30),
isPersistent,
userData,
FormsAuthentication.FormsCookiePath);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
HttpContextBase httpContext = (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.Response.Cookies.Add(authCookie);
}
else
{
HttpContextBase httpContext = (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.AddError(new FormsAuthenticationLogonException("Username or password is not correct."));
}
return user;
}
public TUser Logout()
{
FormsAuthentication.SignOut();
return GetDefaultUser();
}
public void UpdateUser(TUser user)
{
throw new NotImplementedException();
}
}
}
namespace MainModule.Web
{
using System.ServiceModel.DomainServices.Hosting;
// TODO: Create methods containing your application logic.
[EnableClientAccess()]
public class CustomAuthenticationService :FormsAuthenticationService<UserDTO>
{
protected override UserDTO GetCurrentUser(string name, string userData)
{
return new UserDTO {DisplayName = name, Name = name};
}
protected override UserDTO ValidateCredentials(string name, string password, string customData, out string userData)
{
userData = null;
UserDTO user = null;
if(name=="John" && password = "123")
{
userData = name;
user = new UserDTO {DisplayName = name, Email = "asdf"};
}
retrurn user;
}
}
}
This are classes I implemeted - it's the same code which is posted on blog. There is no exception so I can't paste an stackTrace. I just can't compile the solution
Make sure you are using the correct namespaces.
I noticed two small typos in the code that you pasted:
if(name=="John" && password = "123")
Should be:
if (name=="John" && password == "123")
retrurn user;
Should be:
return user;
Otherwise, it compiles without errors for me.
Create a new Web Application
Add a reference to System.ServiceModel.DomainServices.Hosting (ex. from "C:\Program Files (x86)\Microsoft SDKs\RIA Services\v1.0\Libraries\Server\System.ServiceModel.DomainServices.Hosting.dll")
Add a reference to System.ServiceModel.DomainServices.Server (ex. from "C:\Program Files (x86)\Microsoft SDKs\RIA Services\v1.0\Libraries\Server\System.ServiceModel.DomainServices.Server.dll")
Create a class called CustomAuthenticationService and insert the code below.
using System.ServiceModel.DomainServices.Hosting;
using System.Web;
using System.Web.Security;
using System;
using System.Security.Principal;
using System.ServiceModel.DomainServices.Server;
using System.ServiceModel.DomainServices.Server.ApplicationServices;
namespace WebApplication1.Services
{
public class UserDTO : UserBase
{
public string DisplayName { get; set; }
public string Email { get; set; }
}
public class FormsAuthenticationLogonException : System.Exception
{
public FormsAuthenticationLogonException(string message) : base(message) { }
}
// TODO: Create methods containing your application logic.
[EnableClientAccess()]
public abstract class FormsAuthenticationService<TUser> : DomainService, IAuthentication<TUser> where TUser : UserBase
{
protected abstract TUser GetCurrentUser(string name, string userData);
protected abstract TUser ValidateCredentials(string name, string password, string customData, out string userData);
protected virtual TUser GetDefaultUser()
{
return null;
}
public TUser GetUser()
{
IPrincipal currentUser = ServiceContext.User;
if ((currentUser != null) && currentUser.Identity.IsAuthenticated)
{
FormsIdentity userIdentity = currentUser.Identity as FormsIdentity;
if (userIdentity != null)
{
FormsAuthenticationTicket ticket = userIdentity.Ticket;
if (ticket != null)
{
return GetCurrentUser(currentUser.Identity.Name, ticket.UserData);
}
}
}
return GetDefaultUser();
}
public TUser Login(string userName, string password, bool isPersistent, string customData)
{
string userData;
TUser user = ValidateCredentials(userName, password, customData, out userData);
if (user != null)
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(/* version */ 1, userName,
DateTime.Now, DateTime.Now.AddMinutes(30),
isPersistent,
userData,
FormsAuthentication.FormsCookiePath);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
HttpContextBase httpContext = (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.Response.Cookies.Add(authCookie);
}
else
{
HttpContextBase httpContext = (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.AddError(new FormsAuthenticationLogonException("Username or password is not correct."));
}
return user;
}
public TUser Logout()
{
FormsAuthentication.SignOut();
return GetDefaultUser();
}
public void UpdateUser(TUser user)
{
throw new NotImplementedException();
}
}
// TODO: Create methods containing your application logic.
[EnableClientAccess()]
public class CustomAuthenticationService : FormsAuthenticationService<UserDTO>
{
protected override UserDTO GetCurrentUser(string name, string userData)
{
return new UserDTO { DisplayName = name, Name = name };
}
protected override UserDTO ValidateCredentials(string name, string password, string customData, out string userData)
{
userData = null;
UserDTO user = null;
if (name == "John" && password == "123")
{
userData = name;
user = new UserDTO { DisplayName = name, Email = "asdf" };
}
return user;
}
}
}
remove attribute [EnableClientAccess()] from the FormsAuthenticationService abstract class.
it will compile without any error