Blazor WASM AuthenticationState using AAD - Claims null until Refresh - authentication

New to Blazor and have been doing a hatchet job to get things working how I want.
I am using Blazor WASM with AAD for Authentication created based on this document MS Doc. I implemented the SecureAccountFactory class from the example and call a db where I get the associated user based on the AAD Guid, then add everything into Claims.
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(SecureUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
var userIdentity = (ClaimsIdentity)initialUser.Identity;
var claims = userIdentity.Claims;
var principalId = claims.Where(x => x.Type == "oid").First();
//Get some user info from SQL
var User = await _UserService.Get(principalId.Value);
//Get user Roles from SQL and add to Claims
var UsersInRoles = await _UsersInRoleService.RolesByUserId(principalId.Value);
//Add the ClientId to Claims
userIdentity.AddClaim(new Claim("clientId", User.ClientId.ToString()));
foreach (var userrole in UsersInRoles)
{
userIdentity.AddClaim(new Claim("appRole", userrole.Role.Name));
}
}
return initialUser;
}
I then have a Profile Component that appears on every page as part of the MainLayout which should have some info about the current user, so I made a static class to retrieve this info.
public static class UserHelper
{
public static async Task<CurrentUserClaims> GetCurrentUserClaims(Task<AuthenticationState> authenticationStateTask)
{
AuthenticationState authenticationState;
authenticationState = await authenticationStateTask;
var AuthenticationStateUser = authenticationState.User;
var user = authenticationState.User;
var claims = user.Claims;
var clientClaim = claims.Where(x => x.Type == "clientId").First();
var principalId = claims.Where(x => x.Type == "oid").First();
return new CurrentUserClaims
{
ClientId = Convert.ToInt32(clientClaim.Value),
PrincipalId = Guid.Parse(principalId.Value),
user = user
};
}
}
In my ProfileComponent, I call CascadingParameter and then onParametersSet I query my Static class for the info from the current logged in user
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private string profilePath;
protected override async Task OnParametersSetAsync()
{
CurrentUserClaims UserClaims = await UserHelper.GetCurrentUserClaims(authenticationStateTask);
var principal = UserClaims.PrincipalId;
//... do stuff
}
The above all works, after a Refresh or once I route to any other page. The initial Load, after login on the home page shows that the below line always fails with 'Sequence contains no elements'
var clientClaim = claims.Where(x => x.Type == "clientId").First();
I am using Authorize to protect the pages and I will eventually be using the Roles to determine what to display to the user.
A: Surely there's a better way of doing the above. There are lots and lots of articles on creating a custom Auth which inherits AuthenticationState but every one I've seen adds the Claims manually as a fake user, so I don't see how to access the actual Claims.
B: I'm wondering if just using LocalStorage for the User info might be a simpler way to go but is it considered 'safe' or best practice?
Any pointers to a solution are appreciated.

Related

How to use Microsoft Graph API to get all the groups for Authorization in .net core application?

I am working on .net core project. I am trying to implement authorize using AD groups. My requirement is, I have many groups in the azure ad. If the current user belongs to any of the available groups in azure ad then I want to authorize those users to access apis written in .net core application. I tried as below. I have added below two classes
public class IsMemberOfGroupHandler : AuthorizationHandler<IsMemberOfGroupRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, IsMemberOfGroupRequirement requirement)
{
var groupClaim = context.User.Claims
.FirstOrDefault(claim => claim.Type == "groups" &&
claim.Value.Equals(requirement.GroupId, StringComparison.InvariantCultureIgnoreCase));
if (groupClaim != null)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
public class IsMemberOfGroupRequirement : IAuthorizationRequirement
{
public readonly string GroupId;
public readonly string GroupName;
public IsMemberOfGroupRequirement(string groupName, string groupId)
{
GroupName = groupName;
GroupId = groupId;
}
}
Below is my startup class.
services.AddAuthorization(options =>
{
var adGroupConfig = new List<AdGroupConfig>();
_configuration.Bind("AdGroups", adGroupConfig);
foreach (var adGroup in adGroupConfig)
options.AddPolicy(
adGroup.GroupName,
policy =>
policy.AddRequirements(new IsMemberOfGroupRequirement(adGroup.GroupName, adGroup.GroupId)));
});
Above code checks groups available in configuration file. Now my requirement is use microsoft graph api to get all the available groups. I could not find any way to handle this requirement. Can someone help me with this? Any help would be appreciated. Thanks
Please firstly check this code sample , which use OpenID Connect to sign in users and use MSAL to get the Microsoft Graph API token to retire groups .
If config the your application to receive group claims by editing the manifest :
{
...
"errorUrl": null,
"groupMembershipClaims": "SecurityGroup",
...
}
The object id of the security groups the signed in user is member of is returned in the groups claim of the token.
If a user is member of more groups than the overage limit (150 for SAML tokens, 200 for JWT tokens), then the Microsoft Identity Platform does not emit the groups claim in the token. Instead, it includes an overage claim in the token that indicates to the application to query the Graph API to retrieve the user’s group membership.
{
...
"_claim_names": {
"groups": "src1"
},
{
"_claim_sources": {
"src1": {
"endpoint":"[Graph Url to get this user's group membership from]"
}
}
...
}
So you can follow the process :
Check for the claim _claim_names with one of the values being groups. This indicates overage.
If found, make a call to the endpoint specified in _claim_sources to fetch user’s groups.
If none found, look into the groups claim for user’s groups.
Of course , you can directly call Microsoft Graph API to retire current user's groups without using group claims :
https://learn.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0&tabs=http
You can then authorize based on that groups . For example , if using policy :
services.AddAuthorization(options =>
{
options.AddPolicy("GroupsCheck", policy =>
policy.Requirements.Add(new GroupsCheckRequirement("YourGroupID")));
});
services.AddScoped<IAuthorizationHandler, GroupsCheckHandler>();
GroupsCheckRequirement.cs:
public class GroupsCheckRequirement : IAuthorizationRequirement
{
public string groups;
public GroupsCheckRequirement(string groups)
{
this.groups = groups;
}
}
GroupsCheckHandler.cs :
public class GroupsCheckHandler : AuthorizationHandler<GroupsCheckRequirement>
{
private readonly ITokenAcquisition tokenAcquisition;
private readonly IMSGraphService graphService;
public GroupsCheckHandler(ITokenAcquisition tokenAcquisition, IMSGraphService MSGraphService)
{
this.tokenAcquisition = tokenAcquisition;
this.graphService = MSGraphService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
GroupsCheckRequirement requirement)
{
string accessToken = await tokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(new[] { Constants.ScopeUserRead, Constants.ScopeDirectoryReadAll });
User me = await graphService.GetMeAsync(accessToken);
IList<Group> groups = await graphService.GetMyMemberOfGroupsAsync(accessToken);
var result = false;
foreach (var group in groups)
{
if (requirement.groups.Equals(group.Id))
{
result = true;
}
}
if (result)
{
context.Succeed(requirement);
}
}
}
And then using policy :
[Authorize(Policy = "GroupsCheck")]
You can use this graph api to get all the groups the user is a direct member of.
GET /me/memberOf
In .net-core you can use GraphServiceClient to call graph api. Here is a sample for your reference.
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
// Get back the access token.
var accessToken = "";
if (!String.IsNullOrEmpty(accessToken))
{
// Configure the HTTP bearer Authorization Header
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}
else
{
throw new Exception("Invalid authorization context");
}
return (Task.FromResult(0));
}
));
var groups = graphClient.Me.MemberOf.Request().GetAsync().Result;

Add RememberMe value as Claim in JWT (Identity Server 4)

I'm using IdentityServer 4.
Is it possible to access the value of the RememberMe boolean when issuing claims? (named isPersistent in the Microsoft.AspNetCore.Identity)
My idea is to add a claim reflecting the RememberMe value so that other applications can use the value.
Currently I'm adding my Claims in the implementation of the interface IProfileService.GetProfileDataAsync.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
await Task.Run(() =>
{
try
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
// I'm adding my current claims here, like so:
new Claim("contact_id", user.ContactId.ToString()),
// etc
// I would like to add RememberMe
new Claim("remember_me", ??? )
};
context.IssuedClaims.AddRange(claims);
// ..
Or can the RememberMe value be accessed by some other method?
You can add additional claims during the user's login. There is an overload for SignInAsync which accepts an array of additional claims.
Here is a code snippet.
public async Task<IActionResult> Login(LoginInputModel model)
...
AuthenticationProperties props = null;
Claim keepMeLoggedIn = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
keepMeLoggedIn = new Claim(AccountOptions.KeepLoggedInClaim, true.ToString());
}
await HttpContext.SignInAsync(userId.ToString(), model.Username, props, keepMeLoggedIn);
Please note that to make this solution work it's necessary to insert your claim name to the IdentityClaims table.
Yes , you should add claim to tokens . In standard OIDC specifications, token is the
bond between client and identity provider . The profile service is called whenever IdentityServer needs to return claims about a user to a client applications , and could be used to add your custom claims .
http://docs.identityserver.io/en/latest/reference/profileservice.html

Identity Server 4 User Impersonation

I am struggling to implement a Impersonation feature into the Identity Server 4 Service. I understand that there's a lot of people who are against implementing it the way I want to but I really need the full redirect back to the SSO server in order to generate a new list of claims. The user that is being impersonated will have a completely different set of Rules associated with them as claims, so it must come from the IdSrvr.
I have read through https://github.com/IdentityServer/IdentityServer4/issues/853, and also IdentityServer4 - How to Implement Impersonation
Here's what I've attempted so far, We did this perfectly inside of Identity Server 3 with ACR values and the Pre-Auth even on the UserService.
This controller method I am calling from one of the Clients of my identity server:
public IActionResult Impersonate(string userIdToImpersonate, string redirectUri)
{
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(){RedirectUri = redirectUri, Items = { {"acr_values", $"userIdToImpersonate:{userIdToImpersonate}"}}});
}
Here is my OnRedirectToProvider:
OnRedirectToIdentityProvider = context =>
{
if (context.Properties.Items.ContainsKey("acr_values"))
{
context.ProtocolMessage.AcrValues = context.Properties.Items["acr_values"].ToString();
}
return Task.CompletedTask;
}
This is where i start to get lost, at the moment, I've inherited from the AuthorizeInteractionResponseGenerator class and implemented my own with the override to the ProcessLoginAsync (this is the only thing i could find that was close to the pre-auth event previously)
protected override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
{
if (!request.IsOpenIdRequest) return await base.ProcessLoginAsync(request);
var items = request.GetAcrValues();
if (items.Any(i => i.Contains("userIdToImpersonate")) && request.Subject.IsAuthenticated())
{
//handle impersonation
var userIdToImpersonate = items.FirstOrDefault(m => m.Contains("userIdToImpersonate")).Split(':').LastOrDefault();
request.Subject = await _signInManager.ImpersonateAsync(userIdToImpersonate);
//var userToImpersonate = await _signInManager.UserManager.FindByIdAsync(userIdToImpersonate);
//if (userToImpersonate == null) return await base.ProcessLoginAsync(request);
//var userBeingImpersonated = await _signInManager.UserManager.FindByIdAsync(userIdToImpersonate);
//var currentUserIdentity = await _signInManager.CreateUserPrincipalAsync(userBeingImpersonated);
//var currentClaims = currentUserIdentity.Claims.ToList();
//currentClaims.Add(new Claim(IdentityServiceClaimTypes.ImpersonatedById, request.Subject.IsBeingImpersonated() ? request.Subject.GetClaimValue(IdentityServiceClaimTypes.ImpersonatedById) : _signInManager.UserManager.GetUserId(request.Subject)));
//request.Subject = new ClaimsPrincipal(new ClaimsIdentity(currentClaims));
//return await base.ProcessLoginAsync(request);
return new InteractionResponse();
}
else
{
return await base.ProcessLoginAsync(request);
}
}
As you can see, i've tried a couple different things here, When not using OIDC as a authentication scheme, and my IdServer/Site is the same site, I had a function that impersonation worked with. Which is where _signInManager.ImpersonateAsync(...) is. Here is that Implementation:
public async Task<ClaimsPrincipal> ImpersonateAsync(string userIdToImpersonate)
{
var userBeingImpersonated = await UserManager.FindByIdAsync(userIdToImpersonate);
if (userBeingImpersonated == null) return null;
var currentUserIdentity = await CreateUserPrincipalAsync(userBeingImpersonated);
var currentClaims = currentUserIdentity.Claims.ToList();
currentClaims.Add(new Claim(IdentityServiceClaimTypes.ImpersonatedById, Context.User.IsBeingImpersonated() ? Context.User.GetClaimValue(IdentityServiceClaimTypes.ImpersonatedById) : UserManager.GetUserId(Context.User)));
//sign out current user
await SignOutAsync();
//sign in new one
var newIdentity = new ClaimsPrincipal(new ClaimsIdentity(currentClaims));
await Context.SignInAsync(IdentityConstants.ApplicationScheme, newIdentity);
return Context.User;
}
In an effort to simply 'replace' who was signing in, or at least who the identity server was thinking was signing in, i just replaced Request.Subject with the Impersonation Result. This doesn't actually change anything that I can find, at least not on my client app. If i use the redirect URI of 'https://localhost:44322/signin-oidc' (localhost because i'm running the sites locally), I get a "Correlation failed at signin-oidc redirect" message. If anyone has implemented something like this or done anything similar I would greatly appreciate the help getting this complete.
Suggestions welcome for completely different implementations, this was just my best stab at what worked flawlessly with idsrvr3.

Check if a user has permission to access a controller action (before accessing it)

I have an action, TopSecret(), which has a security policy applied to it:
[Authorize(Policy = "Level2SecurityClearance")]
public IActionResult TopSecret()
I could check the user meets the requirements of the policy by doing this (authorizationService is of type IAuthorizationService)
bool isAuthorised = await authorizationService.AuthorizeAsync(User, "Level2SecurityClearance");
This action may have a different policy applied at some point in the future and I don't want to have to find all the places I generate links to it and update the code. Is it possible to test if a user can access a specific action?
Maybe something like this:
// Not a real method!!!
bool isAuthorised = authorizationService.IsAuthorisedForAction(User, "TopSecret", "SecretController");
You should look into developing Requirements
Here's an example for you using your criteria:
note: I'm assuming you're using Identity3 and your User has claims with the access
In a new class called Level2SecurityClearanceRequirement
public class Level2SecurityClearanceRequirement : AuthorizationHandler<Level2SecurityClearanceRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Level2SecurityClearanceRequirement requirement)
{
if (context.User.HasClaim("TopSecret","yes")
context.Succeed(requirement);
return Task.FromResult(0);
}
}
In your controller method:
public async Task<IActionResult> BlahBlah() {
if (!await _authorizationService.AuthorizeAsync(User, nameof(PolicyName.Level2SecurityClearance), new Level2SecurityClearanceRequirement()))
return new ChallengeResult();
}
note that I'm using nameof() here so that you don't have any magic strings and all your resources are centralized.
In this case I have an enum:
public enum PolicyName {
Level2SecurityClearance
}
in your startup.cs:
in the ConfigureServices method
add the following:
services.AddAuthorization(options =>{
options.AddPolicy(nameof(PolicyName.Level2SecurityClearance), policy => { policy.AddRequirements(new Level2SecurityClearanceRequirement()); });
});
you can then use this requirement whereever you please and the checks are done in the requirement itself
Try this. Tested in ASP.NET Core 1.1
//somewhere in view
#if (await Url.HasAccess(urlActionContext))
{
<p>You have access</p>
}
Extension method
public static async Task<bool> HasAccess(this IUrlHelper urlHelper, UrlActionContext urlActionContext, string httpMethod = "GET" )
{
var httpContext = urlHelper.ActionContext.HttpContext;
var routeValues = new RouteValueDictionary(urlActionContext.Values);
routeValues["action"] = urlActionContext.Action;
routeValues["controller"] = urlActionContext.Controller;
var path = urlHelper.Action(urlActionContext);
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(new HttpRequestFeature()
{
Method = httpMethod,
Path = path,
});
var ctx = new DefaultHttpContext(features);
var routeContext = new RouteContext(ctx);
foreach (var entry in routeValues)
{
routeContext.RouteData.Values.Add(entry.Key, entry.Value);
}
var actionSelector = httpContext.RequestServices.GetRequiredService<IActionSelector>();
var provider = httpContext.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();
var actionDescriptors = actionSelector.SelectCandidates(routeContext);
var actionDescriptor = actionSelector.SelectBestCandidate(routeContext, actionDescriptors);
var authService = httpContext.RequestServices.GetRequiredService<IAuthorizationService>();
//You need to implement your own AuthorizationHandler that
//checks the actionDescriptor. It will be in AuthorizationHandlerContext.Resource.
//In my case, I have custom Authorize attribute applied to the
//controller action and this attribute is available
//in actionDescriptor.FilterDescriptors
var ok = await authService.AuthorizeAsync(httpContext.User, actionDescriptor, "YOUR_POLICY");
return ok;
}

Updating Roles when granting Refresh Token in Web Api 2

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;
}
}
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.
Persist claims in the store (in the database the tables AspNetUserClaims, AspNetRoleClaims). To add claims use UserManager.AddClaim or RoleManager.AddClaim. Roles (AspNetUserRoles) are special, since they are also counted as claims.
Add claims in code. You can add claims from the ApplicationUser class (usefull for extended properties of the IdentityUser) or in the flow.
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.