Reputation: 1965
I have developed an authentication mechanism in Asp.Net Web Api 2 with the feature for granting refresh tokens, based on the tutorial on Taiseer's blog.
Here is my question. Assume the following scenario: A user logs in using password and get a refresh token and an access token. The access token in fact includes what roles he is in (hence his authorities within the app). In the mean time the system admin will change this person's roles, so once his access token expires and he wants to use the refresh token to obtain a new access token, his new access token must include the newly updated roles for him.
In my "RefreshTokenProvider" class, I am using the following code in "GrantResourceOwnerCredentials" method to get the user roles from the database and add them to the claims:
var roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(new ApplicationDbContext()));
var y = roleManager.Roles.ToList();
var id = new ClaimsIdentity(context.Options.AuthenticationType);
id.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
id.AddClaim(new Claim("sub", context.UserName));
var roles2 = UserRoleManagerProvider.RoleManager().Roles.ToList();
foreach (IdentityRole i in roles2)
{
if (roleIds.Contains(i.Id))
id.AddClaim(new Claim(ClaimTypes.Role, i.Name));
}
This piece works fine (even though I believe there should be a nicer way to do it?!)
But the part that is not working properly is in the "GrantRefreshToken" method, where we need to update roles in order to reflect them in the new access token:
var newId = new ClaimsIdentity(context.Ticket.Identity);
// *** Add shit here....
var userId = context.Ticket.Properties.Dictionary["userId"];
IdentityUser user = UserRoleManagerProvider.UserManager().FindById(userId);
foreach (Claim c in newId.Claims)
{
if (c.Type == ClaimTypes.Role) newId.RemoveClaim(c);
}
if (user.Roles.Count > 0)
{
var roleIds = new List<string>();
var roles2 = UserRoleManagerProvider.RoleManager().Roles.ToList();
foreach (IdentityUserRole ir in user.Roles)
{
roleIds.Add(ir.RoleId);
}
foreach (IdentityRole r in roles2)
{
if (roleIds.Contains(r.Id))
newId.AddClaim(new Claim(ClaimTypes.Role, r.Name));
}
}
Again, if there is a nicer way to do it I'd appreciate you guy's help! But mainly, my problem is that the part for removing the Roles that are not in effect anymore, does not work. Do you by any chance know what is wrong with that piece?!
FYI, in the above code the "UserRoleManagerProvider" is a simple static class I have created which is like this:
public static class UserRoleManagerProvider
{
public static RoleManager<IdentityRole> RoleManager()
{
var roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(new ApplicationDbContext()));
return roleManager;
}
public static UserManager<IdentityUser> UserManager()
{
var userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(new ApplicationDbContext()));
return userManager;
}
}
Upvotes: 7
Views: 3528
Reputation:
It is difficult to answer this question, and since there is a lot that needs to be included, I've tried to seperate some issues.
Claims
There are two ways to add claims to the ClaimsIdentity.
Please note the difference! While in all cases it is called AddClaim, the first variant adds the claims to the store, while the second variant adds the claims directly to the ClaimsIdentity.
So how are persisted claims added to the ClaimsIdentity? This is done automatically!
As a side note, you can extend the IdentityUser with properties, but you can also add user claims to the store. In both cases the claim will be added to the ClaimsIdentity. The extended property has to be added in ApplicationUser.GenerateUserIdentityAsync:
public class ApplicationUser : IdentityUser
{
public string DisplayName { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
userIdentity.AddClaim(new Claim("DisplayName", DisplayName));
return userIdentity;
}
}
Flow
Before issuing a new access_token the server must validate the user. There may be reasons why the server cannot issue a new access_token. Also the changed configuration has to be taken into account. There are two providers for this setup. The access_token provider and the refresh_token provider.
When a clients makes a request to the token endpoint (grant_type = *), AccessTokenProvider.ValidateClientAuthentication is executed first. If you are using client_credentials then you can do something here. But for the current flow we assume context.Validated();
The provider supports various flows. You can read about it here: https://msdn.microsoft.com/en-us/library/microsoft.owin.security.oauth.oauthauthorizationserverprovider(v=vs.113).aspx
The provider is built as opt-in. If you do not override the certain methods, then access is denied.
Access Token
To obtain an access token, credentials have to be sent. For this example I will assume 'grant_type = password'. In AccessTokenProvider.GrantResourceOwnerCredentials the credentials are checked, the ClaimsIdentity is setup and a token is issued.
In order to add a refresh_token to the ticket we need to override AccessTokenProvider.GrantRefreshToken. Here you have two options: reject the token. Because the refresh_token was revoked or for another reason why the user isn't allowed to use the refresh token anymore. Or setup a new ClaimsIdentity to genereate a new access_token for the ticket.
class AccessTokenProvider : OAuthAuthorizationServerProvider
{
public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
// Reject token: context.Rejected(); Or:
// chance to change authentication ticket for refresh token requests
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var appUser = await userManager.FindByNameAsync(context.Ticket.Identity.Name);
var oAuthIdentity = await appUser.GenerateUserIdentityAsync(userManager);
var newTicket = new AuthenticationTicket(oAuthIdentity, context.Ticket.Properties);
context.Validated(newTicket);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var appUser = await userManager.FindAsync(context.UserName, context.Password);
if (appUser == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
var propertyDictionary = new Dictionary<string, string> { { "userName", appUser.UserName } };
var properties = new AuthenticationProperties(propertyDictionary);
var oAuthIdentity = await appUser.GenerateUserIdentityAsync(userManager);
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
// Token is validated.
context.Validated(ticket);
}
}
If the context has a validated ticket, the RefreshTokenProvider is called. In the Create method you can set the expiration time and choose to add the refresh token to the ticket. Do not issue new tokens while the current one isn't expired yet. Otherwise the user may never have to login again!
You can always add the refresh_token if it is somehow persisted. Or you can add a new refresh_token on login only. The user is identified so the 'old' refresh_token doesn't matter anymore since it will expire before the new refresh_token does. If you want to use one active refesh_token only, then you'll have to persist it.
class RefreshTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
var form = context.Request.ReadFormAsync().Result;
var grantType = form.GetValues("grant_type");
// do not issue a new refresh_token if a refresh_token was used.
if (grantType[0] != "refresh_token")
{
// 35 days.
int expire = 35 * 24 * 60 * 60;
context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
// Add the refresh_token to the ticket.
context.SetToken(context.SerializeTicket());
}
base.Create(context);
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
base.Receive(context);
}
}
This is just a simple implementation of the refresh_token flow and not complete nor tested. It is just to give you some ideas on implementing the refresh_token flow. As you can see it isn't hard to add claims to the ClaimsIdentity. I didn't add code where persisted claims are maintained. All what matters is that the persisted claims are automatically added!
Please notice that I reset the ClaimsIdentity (new ticket) on refreshing the access_token using the refresh_token. This will create a new ClaimsIdentity with the current state of claims.
I will end with one final remark. I was talking about roles being claims. You may expect that User.IsInRole checks the AspNetUserRoles table. But it doesn't. As roles are claims it checks the claims collection for available roles.
Upvotes: 5