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 :)
Related
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();
}
}
I have a policy based .net core MVC application, in which only Authorized user has access to any particular menu. I used [Authorize(Policy = "MenuName")] attribute for every controllers. But I want to generalize it with one Enum, where all the Menus are listed in one Enum and use it in Authorize attribute instead of a static string ("MenuName").
public enum MenuEnum
{
[Description("Menu1")]
Dashboard,
[Description("Menu2")]
Help,
[Description("Menu3")]
About
}
and I want to use it like [Authorize(Policy = MenuEnum.Dashboard)] instead of static string [Authorize(Policy = "Dashboard")]. Can we have any way to generalize Authorize attribute with Enum?
I have a extensions method and I use it to read the name of display attribute
public static string ToDisplay(this Enum value, DisplayProperty property = DisplayProperty.Name)
{
var attribute = value.GetType().GetField(value.ToString())
.GetCustomAttributes<DisplayAttribute>(false).FirstOrDefault();
if (attribute == null)
return value.ToString();
var propValue = attribute.GetType().GetProperty(property.ToString()).GetValue(attribute, null);
return propValue.ToString();
}
And you can use it this way
replace Description attribute with DisplayAttribute and set propery Name
public enum MenuEnum
{
[Display(Name="Menu1")]
Dashboard,
[Display(Name="Menu2")]
Help,
[Display(Name="Menu3")]
About
}
[Authorize(Policy=MenuEnum.About.ToDisplay())]
You could implement your own AuthorizeAttribute.
1.AuthorizeMenuPolicyAttribute
public class AuthorizeMenuPolicyAttribute : TypeFilterAttribute
{
public AuthorizeMenuPolicyAttribute(MenuEnum Policy) : base(typeof(AuthorizeMenuPolicyFilter))
{
Arguments = new object[] { Policy };
}
}
2.AuthorizeMenuPolicyFilter
public class AuthorizeMenuPolicyFilter: IAsyncAuthorizationFilter
{
private readonly IAuthorizationService _authorization;
public MenuEnum _policy { get; set; }
public AuthorizeMenuPolicyFilter(MenuEnum policy, IAuthorizationService authorization)
{
_policy = policy;
_authorization = authorization;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
string description = GetEnumDescription(_policy);
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, description);
if (authorized.Succeeded)
{
return;
}
context.Result = new ForbidResult();
return;
}
public static string GetEnumDescription(Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes = fi.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
if (attributes != null && attributes.Any())
{
return attributes.First().Description;
}
return value.ToString();
}
}
3.Add Policy you want on Startup
services.AddAuthorization(options =>
{
options.AddPolicy("Menu1", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == "menu1")));
});
4.Authorization based on string value from Enum
[AuthorizeMenuPolicy(MenuEnum.Dashboard)]
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);
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.
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.