I am using IdentityServer3 to issue tokens and trying to use Thinktecture.IdentityModel.Owin.ResourceAuthorization.WebApi to authorize resource access of the web api.
I am using below code to Authorize an action of the controller.
[ResourceAuthorize("Read","UsersList")]
ResourceAuthorizationManager looks like below.
public class MyAuthorizationManager : ResourceAuthorizationManager
{
/// <summary>
/// Verify Access Rights
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "UsersList":
return AuthorizeUsersList(context);
default:
return Nok();
}
}
private Task<bool> AuthorizeUsersList(ResourceAuthorizationContext context)
{
switch (context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "User"));
case "Write":
return Eval(context.Principal.HasClaim("role", "Owner"));
default:
return Nok();
}
}
}
However, when control comes to AuhtorizeUsersList, the context.Principal has no role claims. I do not store the user claims when I register a user. How can I add claims for an authenticated user on the go ?
Maybe it will be helpful for others.
Basically, I was missing 'role' claim inside scope-claim mapping while defining the API as scope. You just have to list all the claims that you want as part of the scope, and IdentityServer will handle the rest.
On the identity server side:
new Scope
{
Enabled = true,
Name = "ScopeName",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
Related
I am trying to configure Auth0 as an external login provider in my ABP.IO application (MVC with integrated identity server). I've got it working so that I can log in fine, but what I can't figure out is how to set the tenant in the ABP side.
What I came up with is a rule on the Auth0 side to populate the TenantId as a claim in the id token, so I can parse that in my custom SingInManager in the GetExternalLoginInfoAsync method, like so:
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId");
I'm just having a hard time figuring out what to do with it from there. The assumption is that users will be configured to authenticate via Auth0 and the users will get created locally on first login (which, again, is working EXCEPT for the Tenant part).
Alright, here is the workaround I have in place, and it SHOULD be transferable to any external login system that you are depending on. I'm not sure if this is the correct way of doing this, so if anybody else wants to chip in with a more efficient system I am all ears.
Anyway, my workflow assumes that you have, like I did, created a mechanism for the TenantId to be sent from the external IDP. For this, I used the Organizations feature in Auth0 and added the TenantId as metadata, then I created an Action in Auth0 to attach that metadata as a claim to be used on the ABP side.
In ABP, I followed this article to override the SignInManager: https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753
As in the article, I overrode the GetExternalLoginInfoAsync method of the sign in manager and added the following lines to pull the TenantId out of the Auth0 claims and add it back in using the pre-defined AbpClaimTypes.TenantId value.
EDIT: I also had to override the ExternalLoginSignInAsync method to account for multi-tenancy (otherwise it kept trying to recreate the users and throwing duplicate email errors). I'll post the full class below with my added stuff in comments:
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>
{
private const string LoginProviderKey = "LoginProvider";
private const string XsrfKey = "XsrfId";
private readonly IDataFilter _dataFilter;
public CustomSignInManager(
IDataFilter dataFilter,
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager,
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor,
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_dataFilter = dataFilter;
}
/// <summary>
/// Gets the external login information for the current login, as an asynchronous operation.
/// </summary>
/// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/>
/// for the sign-in attempt.</returns>
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey(XsrfKey))
{
return null;
}
var userId = items[XsrfKey] as string;
if (userId != expectedXsrf)
{
return null;
}
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var provider = items[LoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
?? provider;
/* Begin tenantId claim search */
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId"); //pull the tenantId claim if it exists
if(!string.IsNullOrEmpty(tenantId))
{
auth.Principal.Identities.FirstOrDefault().AddClaim(new Claim(AbpClaimTypes.TenantId, tenantId)); //if there is a tenantId, add the AbpClaimTypes.TenantId claim back into the principal
}
/* End tenantId claim search */
var eli = new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens(),
AuthenticationProperties = auth.Properties
};
return eli;
}
/// <summary>
/// Signs in a user via a previously registered third party login, as an asynchronous operation.
/// </summary>
/// <param name="loginProvider">The login provider to use.</param>
/// <param name="providerKey">The unique provider identifier for the user.</param>
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
/// <param name="bypassTwoFactor">Flag indicating whether to bypass two factor authentication.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="SignInResult"/>
/// for the sign-in attempt.</returns>
public override async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
{
Volo.Abp.Identity.IdentityUser user = null; //stage the user variable as null
using (_dataFilter.Disable<IMultiTenant>()) //disable the tenantid filters so we can search all logins for the expected key
{
user = await UserManager.FindByLoginAsync(loginProvider, providerKey); //search logins for the expected key
}
if (user == null)
{
return SignInResult.Failed;
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
}
}
Once that was done, I tracked down where the GetExternalLoginInfoAsync was being utilized and figured out I had to override the CreateExternalUserAsync method inside of the LoginModel for the Login page. To that end, I followed the directions in this article for creating a CustomLoginModel.cs and Login.cshtml : https://community.abp.io/articles/hide-the-tenant-switch-of-the-login-page-4foaup7p
So, my Auth0LoginModel class looks like this:
public class Auth0LoginModel : LoginModel
{
public Auth0LoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<IdentityOptions> identityOptions) : base(schemeProvider, accountOptions, identityOptions)
{
}
protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
await IdentityOptions.SetAsync();
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);
/* Begin TenantId claim check */
var tenantId = info.Principal.FindFirstValue(AbpClaimTypes.TenantId);
if (!string.IsNullOrEmpty(tenantId))
{
try
{
CurrentTenant.Change(Guid.Parse(tenantId));
}
catch
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = "Unable to parse TenantId: " + tenantId
}) ;
}
}
/* End TenantId claim check */
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await UserManager.CreateAsync(user));
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
return user;
}
}
The code added is between the comments, the rest of the method was pulled from the source. So I look for the AbpClaimTypes.TenantId claim being present, and if it does I attempt to use the CurrentTenant.Change method to change the tenant prior to the call to create the new IdentityUser.
Once that is done, the user gets created in the correct tenant and everything flows like expected.
I've got a couple of authorization polcies registered:
ConfigureServices()
{
services.AddAuthorization(authorisationOptions =>
{
authorisationOptions.AddPolicy(StandardAuthorizationPolicy.Name, StandardAuthorizationPolicy.Value);
authorisationOptions.AddPolicy(MutatingActionAuthorizationPolicy.Name, MutatingActionAuthorizationPolicy.Value);
});
}
& then I set a default authorization policy across all endpoints:
Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints =>
{
endpoints
.MapControllers()
.RequireAuthorization(StandardAuthorizationPolicy.Name); // Declaratively require the standard authorization policy on all controller endpoints
});
}
On the endpoints where I want to specify the mutating policy, I currently do the following:
[HttpPut]
[Authorize(MutatingActionAuthorizationPolicy.Name)] // Because 'PUT'. NOT DECLARATIVE! :-(
public async Task<IActionResult> AddOrUpdateOverride(SourceOverride sourceOverride, CancellationToken cancellationToken)
{
// ..
}
What I really want is a bit more control to declaritively apply the mutating policy based on the HttpVerb (i.e. POST, PUT, PATCH, DELETE).
Any idea on how to achieve that? Bonus points for allowing me to use other attributes on the controller method/class, not just [HttpPost] etc.
NB: I've seen solutions floating around that involve casting the content (and seem to revolve around a single access policy). I'd really rather stick with multiple access policies.
If I get stuck, I might end up writing a convention test for it.
You can implement a custom RequireAuthorization extension that takes HTTP verb filtering function as an argument and checks each endpoint metadata for HttpMethodAttribute
public static class AuthorizationEndpointConventionBuilderExtensions
{
/// <summary>
/// Adds authorization policies with the specified <see cref="IAuthorizeData"/> to the endpoint(s) filtered by supplied filter function
/// </summary>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="filterOnHttpMethods">Filters http methods that we applying specific policies to</param>
/// <param name="authorizeData">
/// A collection of <paramref name="authorizeData"/>. If empty, the default authorization policy will be used.
/// </param>
/// <returns>The original convention builder parameter.</returns>
public static TBuilder RequireAuthorizationForHttpMethods<TBuilder>(this TBuilder builder, Func<IEnumerable<HttpMethod>, bool> filterOnHttpMethods, params IAuthorizeData[] authorizeData)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (authorizeData == null)
{
throw new ArgumentNullException(nameof(authorizeData));
}
if (authorizeData.Length == 0)
{
authorizeData = new IAuthorizeData[] { new AuthorizeAttribute(), };
}
builder.Add(endpointBuilder =>
{
var appliedHttpMethodAttributes = endpointBuilder.Metadata
.Where(x => x is HttpMethodAttribute)
.Cast<HttpMethodAttribute>();
if (appliedHttpMethodAttributes.Any(x => filterOnHttpMethods(x.HttpMethods
.Select(method => new HttpMethod(method)))))
{
foreach (var data in authorizeData)
{
endpointBuilder.Metadata.Add(data);
}
}
});
return builder;
}
/// <summary>
/// Adds authorization policies with the specified names to the endpoint(s) for filtered endpoints that return for filterOnHttpMethod
/// </summary>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="filterOnHttpMethods">Filters http methods that we applying specific policies to</param>
/// <param name="policyNames">A collection of policy names. If empty, the default authorization policy will be used.</param>
/// <returns>The original convention builder parameter.</returns>
public static TBuilder RequireAuthorizationForHttpMethods<TBuilder>(this TBuilder builder, Func<IEnumerable<HttpMethod>, bool> filterOnHttpMethods, params string[] policyNames)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (policyNames == null)
{
throw new ArgumentNullException(nameof(policyNames));
}
return builder.RequireAuthorizationForHttpMethods(filterOnHttpMethods, policyNames.Select(n => new AuthorizeAttribute(n)).ToArray());
}
}
And then use this extension next to the original one:
app.UseEndpoints(endpoints =>
{
var mutatingHttpMethods = new HashSet<HttpMethod>()
{
HttpMethod.Post,
HttpMethod.Put,
HttpMethod.Delete
};
endpoints
.MapControllers()
.RequireAuthorization(StandardAuthorizationPolicy.Name)
.RequireAuthorizationForHttpMethods(httpMethods =>
httpMethods.Any(httpMethod => mutatingHttpMethods.Contains(httpMethod)),
MutatingActionAuthorizationPolicy.Name);
});
}
I have an Identityserver4 that is providing access tokens to clients.
On my API, I want to be sure that client is allowed to access specific scope and that User belongs to a specific role before I give this user access to API.
To do that I am using Identityserver3.accesstokenvalidation package.
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "Authority",
RequiredScopes = new[] { "MyScope" },
});
This is blocking users that do not have access token from accessing my API, also it is checking if the provided scope is "MyScope".
My question is how do I also check that user has a specific role before allowing access to API.
You could put attribute [Authorize(Roles = "Admin")] for specific controller. If you need more advanced logic with claims you need to specify your own attribute e.g. AuthorizePermissionAttribute and use it with controller [AuthorizePermission("Preview")]:
public class AuthorizePermissionAttribute : AuthorizeAttribute
{
private readonly string grantedPermission;
public AuthorizePermissionAttribute(string permission)
{
this.grantedPermission = permission ?? throw new ArgumentNullException(nameof(permission));
}
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var claims = actionContext.ControllerContext.RequestContext.Principal as ClaimsPrincipal;
var permission = claims?.FindFirst(this.grantedPermission);
return permission != null && Convert.ToBoolean(permission.Value);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
var response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "insufficient_permissions");
actionContext.Response = response;
}
}
Also you need to put in Startup.cs:
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["IdentityProviderApi"],
PreserveAccessToken = true
});
Without JwtSecurityTokenHandler.InboundClaimTypeMap it will return always Unauthorized status code.
I'm implementing Asp.NET MVC application with Identity 2.x Authentication and Authorization model.
During LogIn process I add Custom Claims (not persisted in the DB!), deriving from data passed in the LogIn from, to the Identity and I can correctly access them later on, until the identity gets regenerated.
[HttpPost]
[AllowAnonymous]
[ValidateHeaderAntiForgeryToken]
[ActionName("LogIn")]
public async Task<JsonResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
return Json(GenericResponseViewModel.Failure(ModelState.GetErrors("Inavlid model", true)));
using (var AppLayer = new ApplicationLayer(new ApplicationDbContext(), System.Web.HttpContext.Current))
{
GenericResponseViewModel LogInResult = AppLayer.Users.ValidateLogInCredential(ref model);
if (!LogInResult.Status)
{
WebApiApplication.ApplicationLogger.ExtWarn((int)Event.ACC_LOGIN_FAILURE, string.Join(", ", LogInResult.Msg));
return Json(LogInResult);
}
ApplicationUser User = (ApplicationUser)LogInResult.ObjResult;
// In case of positive login I reset the failed login attempts count
if (UserManager.SupportsUserLockout && UserManager.GetAccessFailedCount(User.Id) > 0)
UserManager.ResetAccessFailedCount(User.Id);
//// Add profile claims for LogIn
User.Claims.Add(new ApplicationIdentityUserClaim() { ClaimType = "Culture", ClaimValue = model.Culture });
User.Claims.Add(new ApplicationIdentityUserClaim() { ClaimType = "CompanyId", ClaimValue = model.CompanyId });
ClaimsIdentity Identity = await User.GenerateUserIdentityAsync(UserManager, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, Identity);
WebApiApplication.ApplicationLogger.ExtInfo((int)Event.ACC_LOGIN_SUCCESS, "LogIn success", new { UserName = User.UserName, CompanyId = model.CompanyId, Culture = model.Culture });
return Json(GenericResponseViewModel.SuccessObj(new { ReturnUrl = returnUrl }));
}
}
The validation process is defined in the OnValidationIdentity which I havn't done much to customize. When the validationInterval goes by (...or better said the half way to the validationInterval) Identity gets re generatd and Custom Claims are lost.
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(1d),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager, DefaultAuthenticationTypes.ApplicationCookie))
},
/// TODO: Expire Time must be reduced in production do 2h
ExpireTimeSpan = TimeSpan.FromDays(100d),
SlidingExpiration = true,
CookieName = "RMC.AspNet",
});
I think I should some how be able to pass the current Claims to the GenerateUserIdentityAsync so that I can re add Custom Clims, but I don't know how to.
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager, string authenticationType)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
// Add custom user claims here
// ????????????????????????????
return userIdentity;
}
Any help is appreciated.
Thanks
Problem solved (it seemms), I post my solution since I havn't found may appropriate answers and I think it might be useful to others.
The right track was found in an answer to the question Reuse Claim in regenerateIdentityCallback in Owin Identity in MVC5
I just had modify a little the code since the UserId in my case is of type string and not Guid.
Here is my code:
In Startup.Auth.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
//OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
// validateInterval: TimeSpan.FromMinutes(1d),
// regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager, DefaultAuthenticationTypes.ApplicationCookie))
OnValidateIdentity = context => SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, string>(
validateInterval: TimeSpan.FromMinutes(1d),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager, context.Identity),
getUserIdCallback: (ci) => ci.GetUserId()).Invoke(context)
},
/// TODO: Expire Time must be reduced in production do 2h
//ExpireTimeSpan = TimeSpan.FromDays(100d),
ExpireTimeSpan = TimeSpan.FromMinutes(2d),
SlidingExpiration = true,
CookieName = "RMC.AspNet",
});
NOTE: Please note that in my sample ExpireTimeSpan and validateInterval are ridiculously short since the purpose here was to cause the most frequest re validation for testing purposes.
In IdentityModels.cs goes the overload of GenerateUserIdentityAsync that takes care of re attaching all custom claims to the Identity.
/// Generates user Identity based on Claims already defined for user.
/// Used fro Identity re validation !!!
/// </summary>
/// <param name="manager"></param>
/// <param name="CurrentIdentity"></param>
/// <returns></returns>
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager, ClaimsIdentity CurrentIdentity)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Re validate existing Claims here
userIdentity.AddClaims(CurrentIdentity.Claims);
return userIdentity;
}
It works. Not really sure if it is the best solution, but in case anyone has better approaches please feel free to improve my answer.
Thanks.
Lorenzo
ADDENDUM
After some time using it I found out that what implemented in GenerateUserIdentityAsync(...) might give problems if used in conjunction with #Html.AntiForgeryToken(). My previous implementation would keep adding already existing Claims at each revalidation. This confuses AntiForgery logic that throws error. To prevent that I've re implemnted it this way:
/// <summary>
/// Generates user Identity based on Claims already defined for user.
/// Used fro Identity re validation !!!
/// </summary>
/// <param name="manager"></param>
/// <param name="CurrentIdentity"></param>
/// <returns></returns>
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager, ClaimsIdentity CurrentIdentity)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Re validate existing Claims here
foreach (var Claim in CurrentIdentity.Claims) {
if (!userIdentity.HasClaim(Claim.Type, Claim.Value))
userIdentity.AddClaim(new Claim(Claim.Type, Claim.Value));
}
return userIdentity;
}
}
ADDENDUM 2
I had to refine further me mechanism because my previosu ADDENDUM would lead in some peculiar cases to same problem described during re-validation.
The key to the current definitive solution is to Add Claims that I can clearly identify and Add only those during re-validation, without having to try to distinguish betweeb native ones (ASP Identity) and mine.
So now during LogIn I add the following custom Claims:
User.Claims.Add(new ApplicationIdentityUserClaim() { ClaimType = "CustomClaim.CultureUI", ClaimValue = UserProfile.CultureUI });
User.Claims.Add(new ApplicationIdentityUserClaim() { ClaimType = "CustomClaim.CompanyId", ClaimValue = model.CompanyId });
Note the Claim Type which now starts with "CustomClaim.".
Then in re-validation I do the following:
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager, ClaimsIdentity CurrentIdentity)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Re validate existing Claims here
foreach (var Claim in CurrentIdentity.FindAll(i => i.Type.StartsWith("CustomClaim.")))
{
userIdentity.AddClaim(new Claim(Claim.Type, Claim.Value));
// TODO devo testare perché va in loop la pagina Err500 per cui provoco volontariamente la duplicazioen delle Claims
//userIdentity.AddClaims(CurrentIdentity.Claims);
}
return userIdentity;
}
userIdentity does not contain the Custom Claims, while CurrentIdentity does contain both, but the only one I have to "re attach" to the current Identity are my custom one.
So far it is working fine, so I'll mark this as teh answer.
Hope it helps !
Lorenzo
Ohh lord i got tired of trying to get this to work, i just modified the SecurityStampValidator to take a context that i could pull the Identity out of to update accordingly in my User class. as far as i can tell there is no way to directly extend it. Updating claims from manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); had no affect using GenerateUserIdentityAsync
var validator = MySecurityStampValidator
.OnValidateIdentity<ApplicationUserManager, ApplicationUser, Guid>(
validateInterval: TimeSpan.FromSeconds(2),
regenerateIdentityCallback: (manager, user, claims) => user.UpdateUserIdentityAsync(claims),
getUserIdCallback: (id) => id.GetUserGuid());
var cookieAuthenticationOptions = new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
Provider = new CookieAuthenticationProvider
{
// Not called on signin
OnValidateIdentity = validator
}
};
And then copied the owin class but added the context to it that would be passed into my regenerateIdentityCallback
static class MySecurityStampValidator
{
public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
TimeSpan validateInterval,
Func<TManager, TUser, ***ClaimsIdentity***, Task<ClaimsIdentity>> regenerateIdentityCallback,
Func<ClaimsIdentity, TKey> getUserIdCallback)
where TManager : UserManager<TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
......
And then in my user i just
public override async Task<ClaimsIdentity> UpdateUserIdentityAsync(ClaimsIdentity userIdentity)
{
userIdentity.RemoveClaim(CustomClaimTypes.CLAIM1);
userIdentity.RemoveClaim(CustomClaimTypes.CLAIM2);
if (Access1Service.GetService().UserHasAccess(Id))
{
userIdentity.AddClaim(new Claim(CustomClaimTypes.CLAIM1, "1"));
}
if (Access2Service.GetService().UserHasAccess(Id))
{
userIdentity.AddClaim(new Claim(CustomClaimTypes.CLAIM2, "1"));
}
return userIdentity;
}
I am using the SessionExpireFilter on each action to check if session has expired or not. If session has expired then it redirects the user to sessionTimeoutPage i.e. to membershipController and SessionTimeOut View
The filter looks like-
public class SessionExpireFilterAttribute : ActionFilterAttribute
{
/// <summary>
/// Called when [action executing].
/// </summary>
/// <param name="filterContext">The filter context.</param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContext ctx = HttpContext.Current;
// check if session is supported
if (ctx.Session != null)
{
// check if a new session id was generated
if (ctx.Session.IsNewSession)
{
// If it says it is a new session, but an existing cookie exists, then it must
// have timed out
string sessionCookie = ctx.Request.Headers["Cookie"];
if ((null != sessionCookie) && (sessionCookie.IndexOf("ASP.NET_SessionId") >= 0))
{
if (ctx.Request.IsAuthenticated)
{
FormsAuthentication.SignOut();
}
//HttpCookie mycookie = new HttpCookie("ASP.NET_SessionId");
//mycookie.Expires = DateTime.MinValue;
//ctx.Response.Cookies.Add(mycookie);
//ctx.Session.Clear();
RouteValueDictionary redirectTargetDictionary = new RouteValueDictionary();
redirectTargetDictionary.Add("action", "SessionTimeOut");
redirectTargetDictionary.Add("controller", "Membership");
filterContext.Result = new RedirectToRouteResult(redirectTargetDictionary);
}
}
}
base.OnActionExecuting(filterContext);
}
}
The problem is I have a Membership controller login action method which also has that filter. It checks for the session expire and always find the ASP.NET_SessionId cookie and redirects to sessionTimeout page(Which has a link to login page) again and again
It would be great of somebody can help.
For this solution to work, you need to wireup the Session_Start event in Global.asax. If you wont do it, the Session.IsNewSession will always be true (that's the way the ASP.NET runtime works). Sometimes it is also necessary to store something (anything) in the Session.