How to "hook" into the Authorize method in mvc6 - c#

In an asp.net5/mvc6 application I use Azure DocumentDB as a user store and authenticate users with CookieAuthentication like this:
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, account.UserName));
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));
await _httpContextAccessor.HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user);
I would like to persist the cookie so the user does not have to login every time. This is working correctly now. However when i was to delete a user the user will still have the cookie and thus access to the website.
I use for example on the controllers:
[Authorize("PolicyName")]
Policy configured like this:
services.Configure<AuthorizationOptions>(options => {
options.AddPolicy("PolicyName", policy =>
{ /* configuration */ });
});
There will be multiple policies so I'm looking for a central place to do the reauthentication. Is there a way to hook into the Authorize method?

In the cookie options there is a Events class. Within that is the OnValidatePrincipal event. If you override that it gets called every time cookie auth pulls in an identity.
Task ValidateAsync(CookieValidatePrincipalContext context);
Inside this function you can call context.RejectPrincipal() to invalidate the identity.
ASP.NET identity uses this. Identity has the concept of a security stamp. This is embedded as a claim inside the identity. When changes happen the security stamp is changed. A security stamp validator is wired up to OnValidatePrincipal which checks the membership database value and compares it to the database value.

Related

How do you use ProtectedBrowserStorage and BlazorServer AspNet Authentication for Cookie based login?

I have a system that is creating claims and adding them to authentication manually on a BlazorServer using AspNet Authentication.
// Maps internal User object into an IdentityUser object
// TODO: Use mapping library
var blazorUser = UserOpenIDToIdentityUser.ConvertUserOpenIDToIdentityUser(user);
var principal = await _signinManager.CreateUserPrincipalAsync(blazorUser);
var identity = new ClaimsIdentity(
principal.Claims,
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme
);
principal = new System.Security.Claims.ClaimsPrincipal(identity);
_signinManager.Context.User = principal;
_hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal)));
This works but if the user refreshes it is gone, as signalR is server side so can't write the cookie to a users browser. One solution I read was to use javascript on the page to save the cookie, but I would much rather just use the new LocalStorage for Asp.Net-Core(As I know the older Blazored had an option for this also). Now this is where it gets fuzzy for me. I need to have something like
services.AddAuthentication(options =>
{
options.DefaultScheme = "MyScheme";
}).AddCookie("MyScheme", options =>
{
options.Cookie.Name = "CookieName";
});
that uses LocalStorage/sessionStorage.
How would I specify which one I want? If I can't have authentication/cookies write to that do I just load the user in from it if there is no current user(I don't want to/wouldn't be the best to use javascript)?

Can I use the Authorize attribute filtering by Roles and Claims when using ASP.NET Identity and JWT authentication from external providers?

So I’m using firebase for authentication on my WebAPI app. It gets a JWT token and then authenticates it and then puts the claims in the HttpContext.User.
My only problem is I just want to use Firebase Authentication for authentication, not for authorization. I want to use ASP.NET Identity for this.
so right now, when someone connects to my server, I’ll check if they don’t have an account, if not I will create one for them.
var name = _contextAccessor.HttpContext.User.Claims.First(c => c.Type == "user_id").Value;
ApplicationUser user = await _userManager.FindByNameAsync(name);
if (user == null)
{
user = new ApplicationUser(name);
await _userManager.CreateAsync(user);
}
So this works, and now there is a record in the ASPNetUsers against the user and later on, I can give it Claims and Roles against whatever business rules I’d like.
However, my question is, previously, when I’ve used ASP.NET Identity I’ve been able to leverage all of the built-in features like the Authorize attribute to do my authorization.
So if both authentication and authorization is done using ASP.NET Identity, I can write
[Authorize(Roles = "Administrator")]
Obviously, that won’t work with external authentication because HttpContext.User is the Firebase Authenticated user, not the corresponding ASP.NET Identity user that has the Administrator role.
Is there a way to customize the existing Authorize attribute to configure it to somehow convert my firebase token into an ASP.NET Identity so that it would recognize the roles and claims it has or if I wanted to do all this through middleware, am I going to need to write my own authorize attribute?
Ok so I've finally figured it out. It seems there's a couple of events you can intercept during the Jwt authentication process. In particular, there is an OnTokenValidated event.
services.AddJwtBearer(options =>
{
...
options.Events = new JwtBearerEvents
{
OnTokenValidated = async ctx =>
{
// 1. grabs the user id from firebase
var name = ctx.Principal.Claims.First(c => c.Type == "user_id").Value;
// Get userManager out of DI
var _userManager = ctx.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
// 2. retrieves the roles that the user has
ApplicationUser user = await _userManager.FindByNameAsync(name);
var userRoles = await _userManager.GetRolesAsync(user);
//3. adds the role as a new claim
ClaimsIdentity identity = ctx.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
foreach (var role in userRoles)
{
identity.AddClaim(new System.Security.Claims.Claim(ClaimTypes.Role, role));
}
}
}
};
});
So what the code above is saying
once the token is authenticated, take the userId from the
external provider
Go into ASP.NET Identity and find the user and
the roles for that user [remember in my original question, I insert a user
into ASP.NET Identity table when they first log in, I'm just grabbing that user]
Insert the roles of that user back into the ClaimsIdentity
The result is that when the Authorise attribute runs, it'll include the AspNET Identity Roles of the user in the check and I can do something like below and it'll check the role.
[Authorize(Roles = "Administrator")]
Yes you can use the authorize attribute with some modification,
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme,Roles = "Administrator")]
But if you want to make it a default scheme, then refer to this answer
Here is a summary, Add it to he Startup class,
services.AddAuthentication(cfg =>
{
cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
I think this should make your Authorization work:
You have to make ClaimsPrincipal the base class of your ApplicationUser class.
(ClaimsPrincipal implements the IPrincipal interface)
Then inside your ApplicationUser class implement/override the IsInRole method that comes with the ClaimsPrincipal (base) class. IsInRole official documentation. The role argument of the method will contain the value of the Roles property on the AuthorizeAtribute that you specify.
Set the HttpContext.User using your instance of type ApplicationUser. (explanation: Setting the property User on Httpcontext allows you to specify (=customize) the user of the current request within asp.net request processing)

Populating User.Identity in ASP.NET MVC 5

I'm writing a simple chat application using ASP.NET MVC 5 and SignalR. The application doesn't require any complicated authentication logic. User simply enters their login and enters the system (if there was no such user in the db before, it's created).
My intent was to use Session to hold the logged in user and their information (id from the database and login/username) and write a global filter to check if user is authenticated on each request. I've got some problems with SignalR though. It's not possible to access the Session from the SignalR Hub, while I need it to find out the login of the user who sent the message.
As fas as I found out, it's possible to work with the User.Identity using SignalR's Context. However, in my case Uder.Identity is completely empty. Presumably because I've created the app as 'no authentication' and the mechanism that User.Identity uses to get user data is not aware of my manipulation with session.
The question is, is it possible to elegantly intergate User.Identity into my application and make it aware of the Session? Creating ASP.NET MVC project with individual user accounts creates a mess with stuff like
public AccountController() :
this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
}
and that's what I don't want to have in my application by any means, since I want to write it as clean as possible and not use any solutions I am not familiar with. I also don't need any external login providers, cookies, etc.
I was thinking about implementing some in-memory storage on my own. However, I would still have to clean this store up at some point of time. I though of cleaning it up when the Session_End event is fired. However, this event will only be fired if there is data in Session which I don't want to have since it would be quite awkward to have standalone in-memory storage and rely on Session events to clean it up and, moreover, to set some data in Session just to make sure Session_End will fire.
Here's the solution I came up with. It's still not as clear as I would like it to be and it uses cookies, so any additions are welcome.
First of all, I had to install Microsoft.AspNet.Identity.Owin package and all its dependencies.
Then I registered my auth as follows:
private void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
This method is then called in Configuration method of Startup.cs file.
In order to work with the authentication, an instance of IAuthenticationManager is required. I inject it into my controller and use Ninject to resolve the dependency
kernel.Bind<IAuthenticationManager>().ToMethod(_ => HttpContext.Current.GetOwinContext().Authentication).InRequestScope();
Here's the Login method of Account controller which user is redirected to when auth is required (thanks to LoginPath in ConfigureAuth method):
[HttpPost]
public ActionResult Login(LoginViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = authenticationService.AuthenticateUser(model.Login);
IdentitySignIn(user.Id, user.Login);
return RedirectToAction("Index", "Home");
}
AuthenticationService is my own class which communicates with the database and performs the login to create or return a user.
IdentitySignIn is declared as follows:
private void IdentitySignIn(int userId, string userLogin)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.PrimarySid, userId.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userLogin));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties()
{
ExpiresUtc = DateTime.UtcNow.AddDays(200),
IsPersistent = true
}, identity);
}
This method creates a cookie with appropriate info. There is one thing, though. When I check the cookie expiration date, it's not the current date plus 200 days, which is kinda awkward.
SignOut method is quite simple:
public void IdentitySignout()
{
authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
}
So, User.Identity is now accessible in the SignalR hub with the Identity.Name property.
To do: it would be also nice to get access to the Id property via something like User.Identity.Id. As far as I know, it requires implementing custom Principal.
I am also still thinking of implementing some sort of session of my own using cookies to store the session id on client side, though it will definitely take more time than using Identity.
Addition:
in order to get user id, one might use the extension method of IdentityExtensions:
(Inside the Hub)
Context.User.Identity.GetUserId()
In order for this to work, the Claim with the value of user's id should have the type ClaimTypes.NameIdentifier.
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userLogin));
Update 2:
Here are some additional links on the subject that greatly helped me. I do not include links to MS guides since they are quite easy to find.
http://leastprivilege.com/2015/07/21/the-state-of-security-in-asp-net-5-and-mvc-6-claims-authentication/
http://weblog.west-wind.com/posts/2015/Apr/29/Adding-minimal-OWIN-Identity-Authentication-to-an-Existing-ASPNET-MVC-Application

Invalidate ASP.NET Identity 2.0 login without removing the application cookie

Background
The application I'm working on is running on several different domains, all sharing the same database and IIS process. The user may switch between these domains by clicking a link, and should remain logged in when doing so. To accomplish this, when the switch domain link is clicked I create an entry in the database which contains the current value of the Identity 2 application cookie (named .AspNet.ApplicationCookie by default). The user is then redirected to the new domain, where the value of the cookie is pulled from the database and set on that domain.
This technique is working, but the problem is that logging out on one domain doesn't log the user out on the other domains because the cookie only gets cleared from the domain that the user happens to be on when he logs out.
Question
Is there a way to make the application cookie invalid upon logging out, so that when it's read on the domains where it still exists (presumably when attempting to authorize the request) it will be ignored and removed from that domain, requiring the user to log in again?
I've tried uncommenting and setting up the OnValidateIdentity callback within the CookieAuthentication configuration. It sounds like this might be related to what I want to do but doesn't seem to do anything on its own.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/"),
CookieName = ApplicationCookieKey, // a string constant
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, User, Guid>(
validateInterval: TimeSpan.FromSeconds(1),
getUserIdCallback: ((identity) => { return identity.GetUserId(); } ),
regenerateIdentityCallback: async (manager, user) => await manager.GenerateUserIdentityAsync(user))
},
});
GenerateUserIdentityAsync method:
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(User user)
{
// Note the authenticationType must match the one
// defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
I've tried uncommenting and setting up the OnValidateIdentity callback
within the CookieAuthentication configuration. It sounds like this
might be related to what I want to do but doesn't seem to do anything
on its own.
This is only triggered when the SecurityStamp gets updated. Otherwise it does nothing. You can manually trigger it by calling userManager.UpdateSecurityStamp()

Persisting claims across requests

var user = UserManager.Find(...);
ClaimsIdentity identity = UserManager.CreateIdentity(
user, DefaultAuthenticationTypes.ApplicationCookie );
var claim1 = new Claim(
ClaimType = ClaimTypes.Country, ClaimValue = "Arctica", UserId = user.Id );
identity.AddClaim(claim1);
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = true }, identity );
var claim2 = new Claim(
ClaimType = ClaimTypes.Country, ClaimValue = "Antartica", UserId = user.Id );
identity.AddClaim(claim2);
Both claim1 and claim2 are persisted across requests only for the time ClaimsIdentity user is logged in. In other words, when user logs out by calling SignOut(), the two claims are also removed and as such the next time this user logs in, it is no longer a member of these two claims ( I assume the two claims don't exist anymore )
The fact that claim2 is persisted across requests ( even though authentication cookie was already created when claim2 was added to the user ) suggests that claims don't get persisted across requests via authentication cookie, but via some other means.
So how are claims persisted across requests?
EDIT:
1) As far as I can tell, claims of type IdentityUserClaim are never persisted in a cookie?
var user = UserManager.Find(...);
/* claim1 won't get persisted in a cookie */
var claim1 = new IdentityUserClaim
{ ClaimType = ClaimTypes.Country, ClaimValue = "Arctica", UserId = user.Id };
user.Claims.Add(claim1);
ClaimsIdentity identity = UserManager.CreateIdentity(
user, DefaultAuthenticationTypes.ApplicationCookie );
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = true }, identity );
If my assumption is correct, is the reason why IdentityUserClaim instances aren't persisted in a cookie because it is assumed that these claims should be stored in a DB and as such could in subsequent requests be retrieved from a DB, while claims of type Claim usually aren't stored in a DB and hence why they need to be persisted in a cookie?
2)
If you'd like to have a deeper look how it all works, check out the
source code of Katana Project
I thought Asp.net Identity 2 was not part of the Katana project ( namely, I've seen people asking when will Microsoft release the source code for Asp.Net Identity, even though Katana source code is already available )?!
thank you
Good question. Even made me do a little experiment.
This line:
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = true }, identity );
Does not set a cookie. Only sets Identity object for the later callback.
Cookie is only set when the control is passed to middleware and some OWIN internal method called Response.OnSendingHeaders.
So your code is just adding claim2 on the identity object that is stored in memory for later user. In theory you can even set claim1 after you have done the AuthenticationManager.SignIn. And it will be persisted in the cookie anyway.
If you try to add a cliam like this in a controller:
public ActionResult AddNonPersistedClaim()
{
var identity = (ClaimsIdentity)ClaimsPrincipal.Current.Identity;
identity.AddClaim(new Claim("Hello", "World"));
return RedirectToAction("SomeAction");
}
This claim won't be set in the cookie and you will not see it in the next request.
If you'd like to have a deeper look how it all works, check out the source code of Katana Project, look on Microsoft.Owin.Security and Microsoft.Owin.Security.Cookies projects. Along with AuthenticationManager in Microsoft.Owin.Net45 project.
Update
To answer your Edit 1 - IdentityUserClaim is indeed persisted into the database and this is the way you can assign persisted claims to the user. You add these on the user through UserManager
await userManager.AddClaimAsync(userId, new Claim("ClaimType", "ClaimValue"));
This creates records in your database table that represents IdentityUserClaim. When next time user is logged in, these claims are read from the database and added to the identity and are available on ClaimsIdentity.Current via property .Claims or by method .HasClaim().
IdentityUserClaim does not do anything else - just way to serialise Claim object into the database. You don't usually access these directly, unless you want to go "bare knuckles" and write to that table yourself, outside of UserManager.
To put it another way - Identity does not set the cookie. OWIN creates the cookie. Have a look on this piece of code:
public async Task SignInAsync(IAuthenticationManager authenticationManager, ApplicationUser applicationUser, bool isPersistent)
{
authenticationManager.SignOut(
DefaultAuthenticationTypes.ExternalCookie,
DefaultAuthenticationTypes.ApplicationCookie,
DefaultAuthenticationTypes.TwoFactorCookie,
DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie,
DefaultAuthenticationTypes.ExternalBearer);
var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);
identity.AddClaim(new Claim(ClaimTypes.Email, applicationUser.Email));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Here Authentication manager is part of OWIN. Identity is part of System.Security.Claims. All that belongs to Identity project is CreateIdentityAsync method - that is basically converts user from the database into ClaimsIdentity with all the persisted roles and claims.
To answer your Edit 2: You are correct, AspNet Identity is not part of Katana project, but Identity uses OWIN (part of Katana) for cookie handling and authorisation. Identity project mostly deals with user/roles/claims persistence and user management, like locking-out, user creation, sending emails with password resetting, 2FA, etc.
What was a surprise for me is that ClaimsPrincipal along with ClaimsIdentity and Claim are part of .Net framework that is available outside of OWIN or Identity. These are used not only in Asp.Net, but in Windows applications. Good thing that .Net now has open-source and you can browse through all these - gives you a better understanding how it all works together. Also if you are doing unit-testing, it is invaluable to know the internals, so you can stub-out all the functionality without using mocks.
If you are using AD authentication and asp core 2.1 or 2.2, there is an option OpenIdConnectOptions when we configure services named ClaimActions, with the help of ClaimActions you can write class [CustomClaimsFactory] which inherits ClaimActions also override its Run method which actually will set the persistent claims, please find code below :
/* startup.cs */ services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ClaimActions.Add(new CustomClaimsFactory(
"userName",
"xxxxx#outlook.com"
));
}
/*CustomClaimsFactory run method*/ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
{
identity.AddClaim(new Claim(_ClaimType, _ValueType, issuer));
}

Categories

Resources