How to Release a change that renames an User Role name - asp.net

We're working on changes to an ASP.NET MVC app.
We're using Owin and OAuth2 to manage User permissions, but are managing the User DB object ourselves.
We have these on App Startup:
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(GetCookieAuthenticationOptions(AuthenticationType))
.UseOpenIdConnectAuthentication(GetOpenIdConnectOptions(AuthenticationType));
And we manually assign Claims to users when they log in Role is an enum:
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, user.Role.ToString()));
If more detail is needed, the auth code is included at the end.
All of this is has been working fine, but we need to rename a role.
The code rename is trivial, and it all works just fine when I log in after the role is renamed. But if I'm already logged in, when the code changes, then my old role Claim string is still in my Auth Cookie, and is no longer recognised by the Auth code.
Becuase I'm already logged in, it doesn't take me to the LogIn page - it just shows me the "Forbidden" error page (As though I'd entered a link to a page I shouldn't have visited)
And because our Auth works by checking whether you have "Role 'x' or any Role greater than 'x'", thus we get Forbidden on every page (because now the user doesn't have any Role and thus fails every Auth test, because their Role isn't recognised as passing any test.
As a result the user has no way to log out.
As a developer, I can wipe my browser cookies and log in from scratch (at which point it works just fine) but a normal user (probably?) won't be able to do that.
My first thought was do somehting like this: http://www.britishdeveloper.co.uk/2010/09/force-client-refresh-browser-cache.html, to all users to log out and get them to log in again, once after the release.
Unfortunately, since EVERY page will fail, I've got nowhere to put that code that will run for the relevant users :(
I could hack around with the Authentication Code so that it knows about the old Roles and grants that Claim permission, but that seem hideous.
Another option would be to modify the Authorisation code so that it logged users out if they don't have any recognised Roles, but that doesn't really feel right either, for some reason I can't put my finger on.
Any suggestions or opinions about the right way to release such a change?
=-=-=-=-=-=-=-=-=-=
Auth code:
private const string AuthenticationType = "FrontEnd" + CookieAuthenticationDefaults.AuthenticationType;
private const string IdTokenClaimName = "id_token";
public void Configuration(IAppBuilder app)
{
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(GetCookieAuthenticationOptions(AuthenticationType))
.UseOpenIdConnectAuthentication(GetOpenIdConnectOptions(AuthenticationType));
}
private static CookieAuthenticationOptions GetCookieAuthenticationOptions(string authenticationType)
{
return new CookieAuthenticationOptions
{
AuthenticationType = authenticationType,
};
}
private OpenIdConnectAuthenticationOptions GetOpenIdConnectOptions(string authenticationType)
{
return new OpenIdConnectAuthenticationOptions
{
Authority = AuthenticationConstants.AuthenticationAuthority,
ClientId = AuthenticationConstants.ClientId,
RedirectUri = AuthenticationConstants.ClientRedirectUrl,
ResponseType = "id_token",
Scope = "openid profile email",
SignInAsAuthenticationType = authenticationType,
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n => Task.Run(() => AuthorizeIfUserExists(n)),
RedirectToIdentityProvider = n => Task.Run(() => SendIdTokenToLogout(n))
}
};
}
private static void SendIdTokenToLogout(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n)
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(IdTokenClaimName).Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
private void AuthorizeIfUserExists(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> authContext)
{
var identity = authContext.AuthenticationTicket.Identity;
var userIdClaim = GetUserIdClaim(identity);
var emailClaim = GetEmailClaim(identity);
var claimsIdentity = new ClaimsIdentity(
identity.AuthenticationType,
ClaimTypes.Name,
ClaimTypes.Role);
claimsIdentity.AddClaim(new Claim(IdTokenClaimName, authContext.ProtocolMessage.IdToken));
claimsIdentity.AddClaim(userIdClaim);
claimsIdentity.AddClaim(emailClaim);
using (var context = new DbDataContext())
{
var user = GetAndInitializeUserIfNecessary(context, userIdClaim.Value, emailClaim.Value);
// We add role and name claims to all successful logins that are also registered in our database.
if (user != null && !user.IsSoftDeleted)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, user.Role.ToString()));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, String.Format("{0} {1}", user.FirstName, user.Surname)));
}
}
authContext.AuthenticationTicket = new AuthenticationTicket(
claimsIdentity,
authContext.AuthenticationTicket.Properties);
}

I could hack around with the Authentication Code so that it knows about the old Roles and grants that Claim permission, but that seem hideous.
That seems best to me.
You have made a change which breaks backwards compatibility for users with active sessions. The usual approach for zero-downtime in that general case is to release code which supports both old and new clients, until you are sure that there are no old clients remaining, then delete the legacy code.

Related

Microsoft Graph SDK - Get drives as authed app (Not user)

Im having some problems retriving data from sharepoint (Disks) for a dotnet core app.
At the moment my app tries to use the app itself, and not the logged in user to retrive disks, but the prefered way would be to use the accesstoken for the logged in user instead.
Maybe authenticating as the app with clientId and secret wont work with drives at all?
The login works fine.
I've set up a dotnet core app with the following startup:
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
})
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
I also have the following services registered:
services.AddTransient<IAuthenticationProvider, GraphAuthenticationProvider>();
services.AddTransient<IGraphServiceClient, GraphServiceClient>();
services.AddTransient<IGraphProvider, MicrosoftGraphProvider>();
where i use the this to authenticate:
public class GraphAuthenticationProvider : IAuthenticationProvider
{
public const string GRAPH_URI = "https://graph.microsoft.com/";
private string _tenantId { get; set; }
private string _clientId { get; set; }
private string _clientSecret { get; set; }
public GraphAuthenticationProvider(IConfiguration configuration)
{
_tenantId = configuration.GetValue<string>("AzureAd:TenantId");
_clientId = configuration.GetValue<string>("AzureAd:ClientId");
_clientSecret = configuration.GetValue<string>("AzureAd:ClientSecret");
}
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{_tenantId}");
ClientCredential creds = new ClientCredential(_clientId, _clientSecret);
//I have tried using acquireTokensAsync with scopes, but there is no such method.
AuthenticationResult authResult = await authContext.AcquireTokenAsync(GRAPH_URI, creds);
request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
}
}
I have given the app plenty of permissions in the API settings in portal, mostly because im unsure what i need, and at the moment im just eager to make it work first, then refactor some.
The app is able to log in, and retrive the following data with the SDK:
var groups = await _graphServiceClient.Groups[appSettings.AzureAd.GroupId].Request().GetAsync();
however: the following does not work:
var groupDrives = await _graphServiceClient.Groups[appSettings.AzureAd.GroupId].Drives
.Request()
.GetAsync();
and i get the following error:
Code: AccessDenied
Message: Either scp or roles claim need to be present in the token.
I also have user login in startup, and the app wont be used without logging in towards azure AD:
Could i use the accessToken for the user instead?
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
options.TokenValidationParameters.ValidateIssuer = false;
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
var roleGroups = new Dictionary<string, string>();
Configuration.Bind("AuthorizationGroups", roleGroups);
var clientApp = ConfidentialClientApplicationBuilder
.Create(Configuration["AzureAD:ClientId"])
.WithTenantId(Configuration["AzureAD:TenantId"])
.WithClientSecret(Configuration["AzureAD:ClientSecret"])
.Build();
var authResult = await clientApp
.AcquireTokenOnBehalfOf(new[] { "User.Read", "Group.Read.All" }, new UserAssertion(ctx.SecurityToken.RawData))
.ExecuteAsync();
var graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", authResult.AccessToken);
}));
//Could i register the graphservice as a singelton with the users accesstoken?
//Fetching drives here with the accessToken from user works.
var graphService = new GraphService(graphClient, Configuration);
var memberGroups = await graphService.CheckMemberGroupsAsync(roleGroups.Keys);
var claims = memberGroups.Select(groupGuid => new Claim(ClaimTypes.Role, roleGroups[groupGuid]));
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
};
});
I would actually like to use the users accesstoken to retrive the drives etc, but im not sure on how to store\reuse the accesstoken. I should probably register the service as a singelton with the users accesstoken as mentioned in the comment?
I followed this guide, and it has the same classes\services i have used:
http://www.keithmsmith.com/get-started-microsoft-graph-api-calls-net-core-3/
I actually thought the option on top here was just a header. It might be easier now.. https://i.imgur.com/yfZWaoe.png
it feels like you are mixing up a whole bunch of concepts here. that example you are using is based on the client credentials flow. you should probably start by reading up on the different types of authentication flows available. https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows
In general when you use the client credential flow, the permissions you need to set are application permissions in the api permissions blade. Delegated permissions are for user login flows.
when you are using delegated permissions like you are above. and you use a flow that gets user tokens, then the access that the application has is based on the access the user has. for example, if you delegate groups.read.all with delegated permissions, then that gives the application access to read all the groups that That specific user has access to. it doesn't give the application access to all groups. if this is what you want, then by all means use the user flow.
You didn't mention if you were writing a web app, or what, but if you are you may want to look carefully at the on-behalf-of flow. here is an example of it. https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/2-WebApp-graph-user/2-1-Call-MSGraph
but again above applies for the permissions, when you get a user token your app will only have access to the items that user has access to. no more. eg user A has access to sharepoint site A, user B has no access to site A, when you use a user token for user B to call graph it will not return results for site A since user B does not have access to it.
You've defined Delegated scopes but are attempting to authenticate using Client Credentials. Delegated scopes are named such because the User is delegating their access to your application.
You need to request Application scopes when authenticating without a User.

asp.net core identity extract and save external login tokens and add claims to local identity

I am a stackoverflow noob so please go easy if I am doing this wrong.
I am using asp.net core with the default core identity template (local accounts).
I have accertained how to add claims to user principal when they login locally like so
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var user = await _userManager.FindByNameAsync(model.Email);
await _userManager.AddClaimAsync(user, new Claim("your-claim", "your-value"));
And I have figured out how to get claims returned from the external login but I cannot figure out how I would add these before the user principal gets created in the ExternalLoginCallback function
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
return View(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
else {
// extract claims from external token here
}
// assume add claims to user here before cookie gets created??
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
I am assuming the the _signInManager.ExternalLoginSignInAsync function works similar to the local login _signInManager.PasswordSignInAsync in the sense that once it is called, the cookie will be created. But I am just not sure.
Essentially what I am hoping to achieve, is understanding of how to add custom claims into the cookie that gets created regardless of how to user logins in (local or external), and how to persist these claims to the database if required.
I am planning on doing some work where if I have a user login using say google auth, I need to save that access_token from google, because I wish to call into the Google APIs later with it. So I need to be able to include this access_token in with the User Principal that gets created, and I would hope the cookie would have a claim on it I could use at the front end as well.
This might be out of scope on this question but I would also like when the google token expires, for some-how it to use the refresh token and go get a new one, or force the user to relogin.
Any help on this would be super appreciated, I have really tried hard to understand this without posting this question to stackoverflow. I have read many articles with lots of useful info, but does not provide the answers this specific question is asking. So Thank you very much in advance.
cheers
When you use await _userManager.AddClaimAsync(user, new Claim("your-claim", "your-value")); that actually updates the Identity's aspnetuserclaims table.
Whenever you sign in (by using _signInManager.PasswordSignIn or _signInManager.ExternalLoginSignInAsync) the claims from that table are read and added to the cookie that on every request becomes the Principal.
So you probably don't want to be calling the AddClaimAsync method from UserManager on every login.
Regarding external login providers, you have access to the claims when you call (in ExternalCallback and ExternalCallbackConfirmation if you are using the default templates) here:
var info = await _signInManager.GetExternalLoginInfoAsync();
The claims are in info.Principal.Claims.
The access token is not included by default. When it is, it will be here (along with the type and expiry date):
var accessToken = info.AuthenticationTokens.Single(f => f.Name == "access_token").Value;
var tokenType = info.AuthenticationTokens.Single(f => f.Name == "token_type").Value;
var expiryDate = info.AuthenticationTokens.Single(f => f.Name == "expires_at").Value;
To have the access token be included in the AuthenticationTokens collection, when you are configuring the GoogleAuthentication middleware set the SaveTokens flag to true:
app.UseGoogleAuthentication(new GoogleOptions{
ClientId = "...",
ClientSecret = "...",
SaveTokens = true
Now, if you want to have control over which claims go in the cookie you have to "take over" the process of creating the claims principal.
This is done for you when you use _signInManager.PasswordSignIn/ExternalLoginSignInAsync.
So, for example, for ExternalLoginSignInAsync replace:
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
With:
var user = await this._userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
var claimsPrincipal = await this._signInManager.CreateUserPrincipalAsync(user);
((ClaimsIdentity)claimsPrincipal.Identity).AddClaim(new Claim("accessToken", info.AuthenticationTokens.Single(t => t.Name == "access_token").Value));
await HttpContext.Authentication.SignInAsync("Identity.Application", claimsPrincipal);
"Identity.Application" is the default cookie name. You can change it in Startup's ConfigureServices method, for example to MainCookie:
services.Configure<IdentityOptions>(options => {
options.Cookies.ApplicationCookie.AuthenticationScheme = "MainCookie";
});
You still need to handle the ExternalCallbackConfirmation action in the AccountController. It will be similar to the example above.

Force another user to refresh their Claims with ASP.NET Identity 2.1.0

I'm using Asp.NET Identity 2.1.0 and I store a list of Accounts that a User has access to, as Claims. The ClaimsIdentity is generated when the User signs in:
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add Claims concerning Account
userIdentity.AddClaim(new Claim("AccountList", SerializedListOfAccounts));
return userIdentity;
}
Let's say that an Administrator revokes User A's access to a specific Account. How can I force User A to regenerate its ClaimsIdentity? Remember that it isn't in the context of User A. And I don't want to wait until the cookie has expired (and a new ClaimsIdentity is automatically generated.
Is it possible? Isn't there a way to tell the server to regard User A's cookie as invalid and force it to regenerate it?
The reason I want this behaviour is to create a custom AuthorizeAttribute that I can put on my controllers that checks the Claims to see if a User has access or not, to avoid an extra round trip to the database.
You can not store their claims on cookie, but apply them on every request to the identity early in the pipeline. You'll have to hack Startup.Auth.cs to do that. I'm doing just that here.
And here is the gist you can work with:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
Provider = GetMyCookieAuthenticationProvider(),
// other configurations
});
// other usual code
}
private static CookieAuthenticationProvider GetMyCookieAuthenticationProvider()
{
var cookieAuthenticationProvider = new CookieAuthenticationProvider();
cookieAuthenticationProvider.OnValidateIdentity = async context =>
{
// execute default cookie validation function
var cookieValidatorFunc = SecurityStampValidator.OnValidateIdentity<UserManager, ApplicationUser>(
TimeSpan.FromMinutes(10),
(manager, user) =>
{
var identity = manager.GenerateUserIdentityAsync(user);
return identity;
});
await cookieValidatorFunc.Invoke(context);
// sanity checks
if (context.Identity == null || !context.Identity.IsAuthenticated)
{
return;
}
// get your claim from your DB or other source
context.Identity.AddClaims(newClaim);
};
return cookieAuthenticationProvider;
}
}
The drawbacks that you need to apply claims on every request and this might be not very performant. But right amount of caching in the right place will help. Also this piece of code is not the easiest place to work, as it is very early in the pipeline and you need to manager yourself DbContext and other dependencies.
The upsides are that the claims are applied right away to every user's request and you get immediate change of permissions without having to re-login.

Complex authentication with existing user database in MVC5

I'm migrating a SaaS app from Classic ASP to .NET MVC5 and will use EF6 Database First. The login form for end users is customisable by each tenant (on their own subdomain but pointing to the same web application). We wish to use the existing database schema and the new authentication & authorization filters.
For example, a user on one tenant may login by entering their first name, surname and a code generated by our system. A user on another tenant may login by entering their email address and a password. Additionally, each tenant has a separate administrator login which uses a username and password. Another tenant may use LDAP authentication against a remote AD server.
Is there a definitive best practice way of doing custom authentication?
Almost every article appears to suggest different ways of accomplishing this: simply setting FormsAuthentication.SetAuthCookie, using a custom OWIN provider, override AuthorizeAttribute, etc.
In Classic ASP, we queried the database to find out the type of login for that tenant, displayed the appropriate fields on the login screen and then on post back, checked the fields match what's in the database and then set the session variables appropriately which were checked on each page request.
Thanks
I find that Identity framework is very flexible in terms of authentication options. Have a look on this bit of authentication code:
var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
This is pretty standard run of the mill authentication part in Identity, you'll find this in every Identity sample on the web. If you look closely it is very flexible - all you need for authentication is ApplicationUser object that framework does not care how you get.
So in theory you can do things like this (pseudocode, I did not try to compile this):
// get user object from the database with whatever conditions you like
// this can be AuthCode which was pre-set on the user object in the db-table
// or some other property
var user = dbContext.Users.Where(u => u.Username == "BillyJoe" && u.Tenant == "ExpensiveClient" && u.AuthCode == "654")
// check user for null
// check if the password is correct - don't have to do that if you are doing
// super-custom auth.
var isCorrectPassword = await userManager.CheckPasswordAsync(user, "enteredPassword");
if (isCorrectPassword)
{
// password is correct, time to login
// this creates ClaimsIdentity object from the ApplicationUser object
var identity = await this.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
// now we can set claims on the identity. Claims are stored in cookie and available without
// querying database
identity.AddClaim(new Claim("MyApp:TenantName", "ExpensiveClient"));
identity.AddClaim(new Claim("MyApp:LoginType", "AuthCode"));
identity.AddClaim(new Claim("MyApp:CanViewProducts", "true"));
// this tells OWIN that it can set auth cookie when it is time to send
// a reply back to the client
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Using this authentication, you have set a few claims on the user - they are stored in the cookie and available everywhere via ClaimsPrincipal.Current.Claims. Claims are essentially a collection of key-value pairs of strings and you can store there anything you like.
I usually access claims from the user via extension method:
public static String GetTenantName(this ClaimsPrincipal principal)
{
var tenantClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:TenantName");
if (tenantClaim != null)
{
return tenantClaim.Value;
}
throw new ApplicationException("Tenant name is not set. Can not proceed");
}
public static String CanViewProducts(this ClaimsPrincipal principal)
{
var productClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:CanViewProducts");
if (productClaim == null)
{
return false;
}
return productClaim.Value == "true";
}
So in your controller/view/business layer you can always call to ClaimsPrincipal.Current.GetTenantName() and in this case you'd get "ExpensiveClient" back.
Or if you need to check if a specific feature is enabled for the user, you do
if(ClaimsPrincipal.Current.CanViewProducts())
{
// display products
}
It is up to you how you store your user properties, but as long as you set them as claims on the cookie, they will be available.
Alternatively you can add claims into the database for every user:
await userManager.AddClaimAsync(user.Id, new Claim("MyApp:TenantName", "ExpensiveClient"));
And this will persist the claim into the database. And by default, Identity framework adds this claim to the user when they login without you needing to add it manually.
But beware, you can't set too many claims on a cookie. Cookies have 4K limit set by browsers. And the way Identity cookie encryption works it increases encoded text by about 1.1, so you can have roughly 3.6K of text representing claims. I've run into this issue here
Update
To control access to controllers via claims you can use the following filter on the controller:
public class ClaimsAuthorizeAttribute : AuthorizeAttribute
{
public string Name { get; private set; }
public ClaimsAuthorizeAttribute(string name)
{
Name = name;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
var user = HttpContext.Current.User as ClaimsPrincipal;
if (user.HasClaim(Name, Name))
{
base.OnAuthorization(filterContext);
}
else
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary()
{
{"controller", "errors"},
{"action", "Unauthorised"}
});
}
}
}
and then use this attribute on controllers or separate actions like this:
[ClaimsAuthorize("Creating Something")]
public ActionResult CreateSomething()
{
return View();
}
User will require "Create Something" claim on them to access this action, otherwise they will be redirected to "Unauthenticated" page.
Recently I've played with claims authentication and made a prototype application similar to your requirement. Please have a look on the simple version: https://github.com/trailmax/ClaimsAuthorisation/tree/SimpleClaims where claims are stored individually for each user. Or there is more complex solution where claims belong to a role and when users login, role claims assigned to the user: https://github.com/trailmax/ClaimsAuthorisation/tree/master
There's two components you need. The authentication itself and the strategy each user gets for authentication.
The first is easy and is accomplished with these two lines...
var identity = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{ IsPersistent = isPersistent }, identity);
When a user is Signed In, they get an identity which contains the user's claims on roles and who they are. These are given to the user as a cookie. After this point you just decorate controllers with [Authorize] to make sure only authenticated users can log in. Pretty standard here.
The only complicated part in the problem is the second part; The strategy for how each user gets authenticated set by the admin.
Some pseudocode for how this could work in actions is this...
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(int tenantId)
{
var tenant = DB.GetTenant(tenantId);
return View(tenant);
}
In your view you would output the authentication strategy for the tenant. That may be email and password, a code and email, or whatever your requirements.
When the user enters their info and clicks to login, you then have to determine what strategy they were using, and check to see if their information matches.
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
var tenant = DB.GetTenant(model.tenantId);
//If user info matches what is expected for the tenants strategy
if(AuthenticateUserInfo(tenant, model.UserInputs))
{
//Sign the user in
var identity = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{ IsPersistent = isPersistent }, identity);
}
}
I did a lot of hand-waving in the second part because of the complicated nature of how dynamic it is. Overall you should use the same strategies you used in your legacy application to generate the right inputs and such. Nothing has changed there, only the way you sign in is going to be different.
Using Visual Studio 2013 Update 3 you can create a new Web Application that comes with MVC5, EF6 and Identity already installed. Here is how to select Identity when you create a new Application:
With MVC Template selected, click Change Authentication and the highlighted window will pop up. Individual User Accounts = Identity. Click ok and continue.
Having done that, you have created an application with Identity. You can now customize your login and registration as follows.
You want to look at your AccountController.cs in the Controllers folder. Here you will find the script for Registration and Login.
If you look at the
public async Task<ActionResult> Register(RegisterViewModel model)
function, you'll notice it contains:
IdentityResult result = await UserManager.CreateAsync(new ApplicationUser() { UserName = newUser.UserName }, newUser.Password);
This is where the user gets created. If you want to use Identity, you should save the users username and password. You can use an e-mail as the username if you want. etc.
After doing that, I add the user a specified role (I find the user and then add it to the role):
ApplicationUser userIDN = UserManager.FindByName(newUser.UserName);
result = await UserManager.AddToRoleAsync(userIDN.Id, "Admin");
In my scenario, I have created an additional extended table where I hold their address, phone number, etc. In that table, you can hold any additional login information. You can add these new entries before or after creating the users account in Identity. I would create the extended information and then create the Identity account just to be sure.
IMPORTANT: For any scenarios where a user is logging in with something that is not a username or e-mail address that isn't saved into via Identity, you will have to do a custom solution.
Example: User types in their first name, surname and the code. You could do two things: Save the first name and surname into the username field of identity and the code into the password and verify the login that way
OR
you would check your custom table for those properties and make sure they match, if and when they do you could call this little beauty:
await SignInAsync(new ApplicationUser() { UserName = model.UserName }, isPersistent: false);
Once you call that SignInAsync function, you can go ahead and direct them to your protected page.
NOTE: I'm creating the ApplicationUser on the function call but if you use it more than once it would be ideal for you to declare the ApplicationUser as follows:
ApplicationUser user = new ApplicationUser() { UserName = model.UserName };
NOTE #2: If you don't want to user Async methods, those functions all have non-async versions of them.
Note #3: At the very top of any page using UserManagement, it is being declared. Make sure if you are creating your own controller that wasn't generated by Visual Studio to use Identity, you include the UserManagement declaration script at the top inside of the class:
namespace NameOfProject.Controllers
{
[Authorize]
public class AccountController : Controller
{
public AccountController() : this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()))) { }
public AccountController(UserManager<ApplicationUser> userManager) { UserManager = userManager; }
public UserManager<ApplicationUser> UserManager { get; private set; }
Please let me know if you have any questions and I hope this helps.

Server side claims caching with Owin Authentication

I have an application that used to use FormsAuthentication, and a while ago I switched it to use the IdentityModel from WindowsIdentityFramework so that I could benefit from claims based authentication, but it was rather ugly to use and implement. So now I'm looking at OwinAuthentication.
I'm looking at OwinAuthentication and the Asp.Net Identity framework. But the Asp.Net Identity framework's only implementation at the moment uses EntityModel and I'm using nHibernate. So for now I'm looking to try bypassing Asp.Net Identity and just use the Owin Authentication directly. I was finally able to get a working login using the tips from "How do I ignore the Identity Framework magic and just use the OWIN auth middleware to get the claims I seek?", but now my cookie holding the claims is rather large. When I used the IdentityModel I was able to use a server side caching mechanism that cached the claims on the server and the cookie just held a simple token for the cached information. Is there a similar feature in OwinAuthentication, or would I have to implement it myself?
I expect I'm going to be in one of these boats...
The cookie stays as 3KB, oh well it's a little large.
Enable a feature similar to IdentityModel's SessionCaching in Owin that I don't know about.
Write my own implementation to cache the information causing the cookie to bloat and see if I can hook it up when I configure Owin at application startup.
I'm doing this all wrong and there's an approach I've not thought of or I'm misusing something in Owin.
public class OwinConfiguration
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true,
CookieName = "Application",
ExpireTimeSpan = TimeSpan.FromMinutes(30),
LoginPath = "/Login",
LogoutPath = "/Logout",
ReturnUrlParameter="ReturnUrl",
SlidingExpiration = true,
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = async context =>
{
//handle custom caching here??
}
}
//CookieName = CookieAuthenticationDefaults.CookiePrefix + ExternalAuthentication.ExternalCookieName,
//ExpireTimeSpan = TimeSpan.FromMinutes(5),
});
}
}
UPDATE
I was able to get the desired effect using the information Hongye provided and I came up with the below logic...
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = async context =>
{
var userId = context.Identity.GetUserId(); //Just a simple extension method to get the ID using identity.FindFirst(x => x.Type == ClaimTypes.NameIdentifier) and account for possible NULLs
if (userId == null) return;
var cacheKey = "MyApplication_Claim_Roles_" + userId.ToString();
var cachedClaims = System.Web.HttpContext.Current.Cache[cacheKey] as IEnumerable<Claim>;
if (cachedClaims == null)
{
var securityService = DependencyResolver.Current.GetService<ISecurityService>(); //My own service to get the user's roles from the database
cachedClaims = securityService.GetRoles(context.Identity.Name).Select(role => new Claim(ClaimTypes.Role, role.RoleName));
System.Web.HttpContext.Current.Cache[cacheKey] = cachedClaims;
}
context.Identity.AddClaims(cachedClaims);
}
}
OWIN cookie authentication middleware doesn't support session caching like feature yet. #2 is not an options.
#3 is the right way to go. As Prabu suggested, you should do following in your code:
OnResponseSignIn:
Save context.Identity in cache with a unique key(GUID)
Create a new ClaimsIdentity embedded with the unique key
Replace context.Identity with the new identity
OnValidateIdentity:
Get the unique key claim from context.Identity
Get the cached identity by the unique key
Call context.ReplaceIdentity with the cached identity
I was going to suggest you to gzip the cookie, but I found that OWIN already did that in its TicketSerializer. Not an option for you.
Provider = new CookieAuthenticationProvider()
{
OnResponseSignIn = async context =>
{
// This is the last chance before the ClaimsIdentity get serialized into a cookie.
// You can modify the ClaimsIdentity here and create the mapping here.
// This event is invoked one time on sign in.
},
OnValidateIdentity = async context =>
{
// This method gets invoked for every request after the cookie is converted
// into a ClaimsIdentity. Here you can look up your claims from the mapping table.
}
}
You can implement IAuthenticationSessionStore to store cookies into database.
Here's example for storing cookie in redis.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
SessionStore = new RedisSessionStore(new TicketDataFormat(dataProtector)),
LoginPath = new PathString("/Auth/LogOn"),
LogoutPath = new PathString("/Auth/LogOut"),
});
Check out full example at here

Resources