I'm having an issue with a custom role provider in ASP.net MVC4. I implemented a very light weight RoleProvider which seems to work fine right up until I change
[Authorize]
public class BlahController:....
}
to
[Authorize(Roles="Administrator")]
public class BlahController:....
}
as soon as I make that change users are no longer authenticated and I get 401 errors. This is odd because my RoleProvider basically returns true for IsUSerInRole and a list containing "Administrator" for GetUserRoles. I had breakpoints in place on every method in my custom RoleProvider and found that none of them were being called.
Next I implemented my own authorize attribute which inherited from AuthorizeAttribute. In this I put in break points so I could see what was going on. It turned out that User.IsInRole(), which is called by the underlying attribute was returning false.
I am confident that the role provider is properly set up. I have this in my config file
<roleManager enabled="true" defaultProvider="SimplicityRoleProvider">
<providers>
<clear />
<add name="SimplicityRoleProvider" type="Simplicity.Authentication.SimplicityRoleProvider" applicationName="Simplicity" />
</providers>
</roleManager>
and checking which role provider is the current one using the method described here: Reference current RoleProvider instance? yields the correct result. However User.IsInRole persists in returning false.
I am using Azure Access Control Services but I don't see how that would be incompatible with a custom role provider.
What can I do to correct the IPrincipal User such that IsInRole returns the value from my custom RoleProvider?
RoleProvider source:
public class SimplicityRoleProvider : RoleProvider
{
private ILog log { get; set; }
public SimplicityRoleProvider()
{
log = LogManager.GetLogger("ff");
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
log.Warn(usernames);
log.Warn(roleNames);
}
public override string ApplicationName
{
get
{
return "Simplicity";
}
set
{
}
}
public override void CreateRole(string roleName)
{
}
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
return true;
}
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
log.Warn(roleName);
log.Warn(usernameToMatch);
return new string[0];
}
public override string[] GetAllRoles()
{
log.Warn("all roles");
return new string[0];
}
public override string[] GetRolesForUser(string username)
{
log.Warn(username);
return new String[] { "Administrator" };
}
public override string[] GetUsersInRole(string roleName)
{
log.Warn(roleName);
return new string[0];
}
public override bool IsUserInRole(string username, string roleName)
{
log.Warn(username);
log.Warn(roleName);
return true;
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
}
public override bool RoleExists(string roleName)
{
log.Warn(roleName);
return true;
}
}
It seems that System.Web.Security.Roles.GetRolesForUser(Username) does not get automatically hooked up when you have a custom AuthorizeAttribute and a custom RoleProvider.
So, in your custom AuthorizeAttribute you need to retrieve the list of roles from your data source and then compare them against the roles passed in as parameters to the AuthorizeAttribute.
I have seen in a couple blog posts comments that imply manually comparing roles is not necessary but when we override AuthorizeAttribute it seems that we are suppressing this behavior and need to provide it ourselves.
Anyway, I'll walk through what worked for me. Hopefully it will be of some assistance.
I welcome comments on whether there is a better way to accomplish this.
Note that in my case the AuthorizeAttribute is being applied to an ApiController although I'm not sure that is a relevant piece of information.
public class RequestHashAuthorizeAttribute : AuthorizeAttribute
{
bool requireSsl = true;
public bool RequireSsl
{
get { return requireSsl; }
set { requireSsl = value; }
}
bool requireAuthentication = true;
public bool RequireAuthentication
{
get { return requireAuthentication; }
set { requireAuthentication = value; }
}
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext ActionContext)
{
if (Authenticate(ActionContext) || !RequireAuthentication)
{
return;
}
else
{
HandleUnauthorizedRequest(ActionContext);
}
}
protected override void HandleUnauthorizedRequest(HttpActionContext ActionContext)
{
var challengeMessage = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Unauthorized);
challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
throw new HttpResponseException(challengeMessage);
}
private bool Authenticate(System.Web.Http.Controllers.HttpActionContext ActionContext)
{
if (RequireSsl && !HttpContext.Current.Request.IsSecureConnection && !HttpContext.Current.Request.IsLocal)
{
//TODO: Return false to require SSL in production - disabled for testing before cert is purchased
//return false;
}
if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")) return false;
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
IPrincipal principal;
if (TryGetPrincipal(authHeader, out principal))
{
HttpContext.Current.User = principal;
return true;
}
return false;
}
private bool TryGetPrincipal(string AuthHeader, out IPrincipal Principal)
{
var creds = ParseAuthHeader(AuthHeader);
if (creds != null)
{
if (TryGetPrincipal(creds[0], creds[1], creds[2], out Principal)) return true;
}
Principal = null;
return false;
}
private string[] ParseAuthHeader(string authHeader)
{
if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null;
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });
if (credentials.Length != 3 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[1]) || string.IsNullOrEmpty(credentials[2])) return null;
return credentials;
}
private bool TryGetPrincipal(string Username, string ApiKey, string RequestHash, out IPrincipal Principal)
{
Username = Username.Trim();
ApiKey = ApiKey.Trim();
RequestHash = RequestHash.Trim();
//is valid username?
IUserRepository userRepository = new UserRepository();
UserModel user = null;
try
{
user = userRepository.GetUserByUsername(Username);
}
catch (UserNotFoundException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
//is valid apikey?
IApiRepository apiRepository = new ApiRepository();
ApiModel api = null;
try
{
api = apiRepository.GetApi(new Guid(ApiKey));
}
catch (ApiNotFoundException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
if (user != null)
{
//check if in allowed role
bool isAllowedRole = false;
string[] userRoles = System.Web.Security.Roles.GetRolesForUser(user.Username);
string[] allowedRoles = Roles.Split(','); //Roles is the inherited AuthorizeAttribute.Roles member
foreach(string userRole in userRoles)
{
foreach (string allowedRole in allowedRoles)
{
if (userRole == allowedRole)
{
isAllowedRole = true;
}
}
}
if (!isAllowedRole)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
Principal = new GenericPrincipal(new GenericIdentity(user.Username), userRoles);
Thread.CurrentPrincipal = Principal;
return true;
}
else
{
Principal = null;
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
}
}
The custom authorize attribute is governing the following controller:
public class RequestKeyAuthorizeTestController : ApiController
{
[RequestKeyAuthorizeAttribute(Roles="Admin,Bob,Administrator,Clue")]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, "RequestKeyAuthorizeTestController");
}
}
In the custom RoleProvider, I have this method:
public override string[] GetRolesForUser(string Username)
{
IRoleRepository roleRepository = new RoleRepository();
RoleModel[] roleModels = roleRepository.GetRolesForUser(Username);
List<string> roles = new List<string>();
foreach (RoleModel roleModel in roleModels)
{
roles.Add(roleModel.Name);
}
return roles.ToArray<string>();
}
So the issue is not how you implement the role provider, but rather how you configure your application to use it. I could not find any issues in your configuration, though. Please make sure this is indeed how you configure your application. This post may help: http://brianlegg.com/post/2011/05/09/Implementing-your-own-RoleProvider-and-MembershipProvider-in-MVC-3.aspx. If you use the default MVC template to create the project, please check the AccountController. According to that post, you may need to do a few modifications to make a custom membership provider work. But that would not affect role providers.
Best Regards,
Ming Xu.
I don't like the custom authorization attribute because I have to remind people to use it. I chose to implement the my own IIdentity/IPrincipal class and wire it up on authorization.
The custom UserIdentity that calls the default RoleProvider:
public class UserIdentity : IIdentity, IPrincipal
{
private readonly IPrincipal _original;
public UserIdentity(IPrincipal original){
_original = original;
}
public string UserId
{
get
{
return _original.Identity.Name;
}
}
public string AuthenticationType
{
get
{
return _original.Identity.AuthenticationType;
}
}
public bool IsAuthenticated
{
get
{
return _original.Identity.IsAuthenticated;
}
}
public string Name
{
get
{
return _original.Identity.Name;
}
}
public IIdentity Identity
{
get
{
return this;
}
}
public bool IsInRole(string role){
return Roles.IsUserInRole(role);
}
}
and added this to global.asax.cs:
void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
if(false == HttpContext.Current.User is UserIdentity){
HttpContext.Current.User = new UserIdentity(HttpContext.Current.User);
}
}
What stimms wrote in his comment: "What I'm seeing is that the IPrincipal doesn't seem to have the correct RoleProvider set" got me looking at the implementation of my custom authentication attribute which inherits from Attribute and IAuthenticationFilter.
using System.Web.Security;
....
protected override async Task<IPrincipal> AuthenticateAsync(string userName, string password, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password))
{
// No user with userName/password exists.
return null;
}
var membershipProvider = Membership.Providers["CustomMembershipProvider"];
if (membershipProvider != null && membershipProvider.ValidateUser(userName, password))
{
ClaimsIdentity identity = new GenericIdentity(userName, "Basic");
return new RolePrincipal("CustomRoleProvider", identity);
}
return null;
}
The key is in returning RolePrincipal, which points to your custom role provider.
Initially I returned new ClaimsPrincipal(identity), which gave me the problem described in the OP.
Related
building new app on .net core 3 and Angular. Overall all works, but I want to add more intelligence to service/controller part. This is one of the api's, but this logic can be applied to others as as well.
Here's my Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var token = await _userService.LoginAsync(userLogin);
if (token != null)
{
return Ok(token);
}
else
{
return BadRequest("Something went wrong");
}
}
And here's my userService:
public async Task<string> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return tokenJson;
}
else
{
return null; // Return BadRequest and result reason (Failed, lockedout, etc)
}
}
else
{
return null; // User not found, return NotFound }
}
Here's my question - how should I return result from userService to Controller so, that I could respond to API call either with Ok(token) or BadRequest/NotFound with the reason.
If I keep all this LoginAsync code in controller, then it's easy, but I want to use service.
One option I was thinking was to introduce new class, something like:
public class BaseResult
{
public object Data { get; set; }
public long ResponseCode { get; set; }
public string ErrorMessage { get; set; }
}
then always return this class from service, but not fully like that idea either.
thanks!
Here is a working demo you could follow:
Model:
public class UserLoginDto
{
public string Email { get; set; }
public string Password { get; set; }
}
IUserService:
public interface IUserService
{
Task<IActionResult> LoginAsync(UserLoginDto userLogin);
}
UserService:
public class UserService: IUserService
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IJwtManager _jwtManager;
public UserService(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IJwtManager jwtManager)
{
_userManager = userManager;
_signInManager = signInManager;
_jwtManager = jwtManager;
}
public async Task<IActionResult> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return new OkObjectResult(tokenJson);
}
else
{
// Return BadRequest and result reason (Failed, lockedout, etc)
if (result.IsNotAllowed)
{
if (!await _userManager.IsEmailConfirmedAsync(user))
{
// Email isn't confirmed.
return new BadRequestObjectResult("Email isn't confirmed.");
}
if (!await _userManager.IsPhoneNumberConfirmedAsync(user))
{
// Phone Number isn't confirmed.
return new BadRequestObjectResult("Phone Number isn't confirmed.");
}
return new BadRequestObjectResult("Login IsNotAllowed");
}
else if (result.IsLockedOut)
{
// Account is locked out.
return new BadRequestObjectResult("Account is locked out.");
}
else if (result.RequiresTwoFactor)
{
// 2FA required.
return new BadRequestObjectResult("2FA required");
}
else
{
// Password is incorrect.
return new BadRequestObjectResult("Password is incorrect.");
}
}
}
else
{
return new NotFoundObjectResult("Username is incorrect"); // User not found, return NotFound }
}
}
}
Controller:
public class HomeController : Controller
{
private readonly IUserService _userService;
public HomeController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var result= await _userService.LoginAsync(userLogin);
return result;
}
}
Startup.cs:
Not sure what is _jwtManager.getJwtToken in your code,so I just guess it is an interface and owns a JwtManager class implemented this interface.And it contains a getJwtToken method which generated the token.
services.AddScoped<IUserService, UserService>();
services.AddScoped<IJwtManager, JwtManager>();
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.
I am using the opened connect middleware to authenticate with a third party oidc provider and everything is up and running as I would expect. During the token exchange I exchange my auth code for an access token which is successful but I then need to store this bearer token for use later in subsequent requests. The token exchange is done as part of my startup class (by overriding the OpenIdConnectEvents during the OnAuthorizationCodeReceived method) in the asp.net core project and I need to store and access that token in my controllers.
As there is no “session” per se yet, what is the most effective (or recommended way) to store this token value from the startup class and make it accessible in my controllers?
Ive tried to use IMemoryCache but despite putting the value in the cache during this startup phase, when I try and access that cache in my controller, it is always empty.
Is there a better/preferred way of persisting values form the startup class for later use in the lifecycle?
I can see in HttpContext.Authentication.HttpAuthenticationFeature.Handler.Options I have access to all the OpenIdConnectOptions properties and settings for oidc, but nowhere can I see the actual token value that I stored after the token exchange.
I use a similar approach with Auth0 and JWT. I store some app_metadata on the claims server, retrieve, and use these values in my controllers for every request.
Startup.cs Configure
var options = new JwtBearerOptions
{
Audience = AppSettings.Auth0ClientID,
Authority = AppSettings.Auth0Domain
};
app.UseJwtBearerAuthentication(options);
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new Auth0ClaimsTransformer()
});
AdminClaimType
public abstract class AdminClaimType : Enumeration
{
public static readonly AdminClaimType AccountId = new AccountIdType();
public static readonly AdminClaimType ClientId = new ClientIdType();
public static readonly AdminClaimType IsActive = new IsActiveType();
private AdminClaimType(int value, string displayName) : base(value, displayName)
{
}
public abstract string Auth0Key { get; }
public abstract string DefaultValue { get; }
private class AccountIdType : AdminClaimType
{
public AccountIdType() : base(1, "AccountId")
{
}
public override string Auth0Key => "accountId";
public override string DefaultValue => "0";
}
private class ClientIdType : AdminClaimType
{
public ClientIdType() : base(2, "ClientId")
{
}
public override string Auth0Key => "clientId";
public override string DefaultValue => "0";
}
private class IsActiveType : AdminClaimType
{
public IsActiveType() : base(3, "IsActive")
{
}
public override string Auth0Key => "isActive";
public override string DefaultValue => "false";
}
}
Auth0ClaimsTransformer
public class Auth0ClaimsTransformer : IClaimsTransformer
{
private string _accountId = AdminClaimType.AccountId.DefaultValue;
private string _clientId = AdminClaimType.ClientId.DefaultValue;
private string _isActive = AdminClaimType.IsActive.DefaultValue;
public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
//TODO: Clean up and simplify AdminClaimTypes Transformer
foreach (var claim in context.Principal.Claims)
{
switch (claim.Type)
{
case "accountId":
_accountId = claim.Value ?? _accountId;
break;
case "clientId":
_clientId = claim.Value ?? _clientId;
break;
case "isActive":
_isActive = claim.Value ?? _isActive;
break;
}
}
((ClaimsIdentity)context.Principal.Identity)
.AddClaims(new Claim[]
{
new Claim(AdminClaimType.AccountId.DisplayName, _accountId),
new Claim(AdminClaimType.ClientId.DisplayName, _clientId),
new Claim(AdminClaimType.IsActive.DisplayName, _isActive)
});
return Task.FromResult(context.Principal);
}
BaseAdminController
//[Authorize]
[ServiceFilter(typeof(ApiExceptionFilter))]
[Route("api/admin/[controller]")]
public class BaseAdminController : Controller
{
private long _accountId;
private long _clientId;
private bool _isActive;
protected long AccountId
{
get
{
var claim = GetClaim(AdminClaimType.AccountId);
if (claim == null)
return 0;
long.TryParse(claim.Value, out _accountId);
return _accountId;
}
}
public long ClientId
{
get
{
var claim = GetClaim(AdminClaimType.ClientId);
if (claim == null)
return 0;
long.TryParse(claim.Value, out _clientId);
return _clientId;
}
}
public bool IsActive
{
get
{
var claim = GetClaim(AdminClaimType.IsActive);
if (claim == null)
return false;
bool.TryParse(claim.Value, out _isActive);
return _isActive;
}
}
public string Auth0UserId
{
get
{
var claim = User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
return claim == null ? string.Empty : claim.Value;
}
}
private Claim GetClaim(AdminClaimType claim)
{
return User.Claims.FirstOrDefault(x => x.Type == claim.DisplayName);
}
}
Now in my controller classes that inherit from BaseAdminController I have access to:
AccountId
ClientId
IsActive
Auth0UserId
Anything else I want to add
Hope this helps.
So I figured it out. It is available on HttpContext via the AuthenticationManager:
var idToken = ((AuthenticateInfo)HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result).Properties.Items[".Token.id_token"];
Works a treat :)
Their use is described in at least a few articles (i.e. http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api) on WebApi 2 authorization, but I can't find what reference I need to have them included. I do have references to Microsoft.Owin/Owin.Security/Owin.Security.OAuth.
Help?
I have resolve HostAuthenticationFilter issue by installing Microsoft.AspNet.WebApi.Owin package.
Please also check
How do I get System.Web.Http.Owin?
by default it placed in Providers/ApplicationOAuthProvider.cs in root folder of your Project
ApplicationOAuthProvider inherit from OAuthAuthorizationServerProvider class.
here is it implemention :
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
//TODO: Pull from configuration
if (publicClientId == null)
{
throw new ArgumentNullException(nameof(publicClientId));
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager();
var user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
I have implemented a custom CredentialsAuthProvider for my authentication and used it with the default in memory session storage.
Now I tried to change the session storage to Redis and added this to my Configure() method in the AppHost:
container.Register<IRedisClientsManager>(c =>
new PooledRedisClientManager("localhost:6379"));
container.Register<ICacheClient>(c => (ICacheClient)c
.Resolve<IRedisClientsManager>()
.GetCacheClient()).ReusedWithin(Funq.ReuseScope.None);
Now when I authenticate, I can see that a key with urn:iauthsession:... is added to my Redis server. But all routes with the [Authenticate] attribute give a 401 Unauthorized error.
The CustomCredentialsAuthProvider is implemented like this:
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
if (userName != string.Empty && password != string.Empty)
{
// Database call ...
var session = (CustomSession)authService.GetSession();
session.ClientId = login.ClientId;
// Fill session...
authService.SaveSession(session, SessionExpiry);
return true;
}
return false;
}
}
ServiceStack Version: 3.9.71
EDIT :
I tried to override the CredentialsAuthProvider IsAuthorized method but without success.
But I'm inheriting my session object from AuthUserSession, which also has a IsAuthorized method. When I return true from this method the Redis session does work with the Authenticate Attribute.
public class CustomSession : AuthUserSession
{
public int ClientId { get; set; }
...
public override bool IsAuthorized(string provider)
{
return true;
}
}
The Authenticate attribute calls the IsAuthorized of the AuthUserSession class.
In my case to make it work with the Redis cache client, I've done the following
public override bool IsAuthorized(string provider)
{
string sessionKey = SessionFeature.GetSessionKey(this.Id);
ICacheClient cacheClient = AppHostBase.Resolve<ICacheClient>();
CustomUserSession session = cacheClient.Get<CustomUserSession>(sessionKey);
if (session == null)
{
return false;
}
return session.IsAuthenticated;
}
I couldn't figure out a way to get the [Authenticate] Attribute to work with Redis storage.
I had to write a custom [SessionAuth] Attribute
public class SessionAuthAttribute : RequestFilterAttribute
{
public ICacheClient cache { get; set; }
public string HtmlRedirect { get; set; }
public SessionAuthAttribute()
{
}
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
string sessionId = req.GetSessionId();
if (string.IsNullOrEmpty(sessionId))
{
HandleNoSession(req, res);
}
else
{
var session = cache.Get<CustomSession>("urn:iauthsession:" + sessionId);
if (session == null || !session.IsAuthenticated)
{
HandleNoSession(req, res);
}
}
}
private void HandleNoSession(IHttpRequest req, IHttpResponse res)
{
if (req.ResponseContentType.MatchesContentType(MimeTypes.Html))
{
res.RedirectToUrl(HtmlRedirect);
res.End();
}
res.StatusCode = (int)HttpStatusCode.Unauthorized;
res.Write("not authorized");
res.Close();
}
}
In my AppHost Configure() method I just register the SessionFeature and the IRedisClientsManager/ICacheClient:
Plugins.Add(new SessionFeature());
container.Register<IRedisClientsManager>(c => new PooledRedisClientManager("localhost:6379"));
container.Register<ICacheClient>(c => (ICacheClient)c.Resolve<IRedisClientsManager>()
.GetCacheClient()).ReusedWithin(Funq.ReuseScope.None);
The CustomSession class inherits from AuthUserSession:
public class CustomSession : AuthUserSession
{
public int ClientId { get; set; }
...
}
And I have a normal service route on /login/auth for the authentication part and a /login/logout route to remove the session:
public class LoginService : Service
{
public ICacheClient cache { get; set; }
public object Post(AuthRequest request)
{
string userName = request.UserName;
string password = request.Password;
// check login allowed
if (IsAllowed)
{
var session = SessionFeature.GetOrCreateSession<CustomSession>(cache);
session.ClientId = login.ClientId;
...
session.IsAuthenticated = true;
session.Id = SessionFeature.GetSessionId();
this.SaveSession(session, TimeSpan.FromSeconds(30 * 60));
return true;
}
return false;
}
[SessionAuth]
public object Any(LogoutRequest request)
{
this.RemoveSession();
return true;
}
}
}
I'm still interested in a solution that works with the normal [Authenticate] Attribute.