Hi,
I have a custom AuthorizationController where I am using the password grant type to authenticate the user and adding some custom claims. This works fine and returns the JWT with additional properties.
However I've noticed when using a refresh token to get a new access token all these claims disappear, so the question is do I have to roll my own endpoint for refresh tokens to be able to set these claims again? If so are there any examples that may help.
Thanks
AuthorizationController
public class AuthorizeController : Controller
{
public AuthorizeController(
CustomOpenIddictManager userManager)
{
_userManager = userManager;
}
private CustomOpenIddictManager _userManager { get; set; }
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIdConnectRequest();
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
var identity = await _userManager.CreateIdentityAsync(user, request.GetScopes());
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetResources(request.GetResources());
ticket.SetScopes(request.GetScopes());
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
This is the "expected" behavior, as custom claims are discarded when using a refresh token (because internally, OpenIddict calls CreateIdentityAsync to generate a fresh new identity and ignores the old claims).
That said, now that you're responsible of creating authentication tickets, this behavior - inherited from the period where OpenIddict magically handled everything for you - doesn't make much sense. Not sure what's the best approach yet, but I'll work on it.
Ok much appreciated thanks
When I initially ask token with scope: offline_access profile email roles, I get the role claims with the initial access_token. However, when doing a refresh_token request, it succeeds but the role claims are gone. Are role claims considered custom claims? Also tried adding scope to refresh_token request, but Openiddict was very angry if I specified roles to scope.
Fixed as part of https://github.com/openiddict/openiddict-core/pull/237 (the claims are now reused and flowed as-is).
Thanks for the quick reply!
Just run the tests with 0455. Following the Mvc.Server sample, the results are not quite.

Postman request. After decoding the first part of token, I had this out of access_token:
{ "jti": "318fb6eb-89f3-446c-93bf-f74c08dc394b", "usage": "access_token", "scope": [ "openid", "email", "profile", "offline_access", "roles" ], "sub": "65189c03-4833-43a9-b66f-8c9909e19d2e", "nbf": 1476811664, "exp": 1476898064, "iat": 1476811664, "iss": "http://localhost:5000/" }
Similarly, id_token showed this:
{ "unique_name": "root@######.com", "sub": "16602217-d6be-48b8-aa44-145ef1263272", "jti": "5da1ac64-5108-44c5-8097-9bf7f1e708a5", "usage": "id_token", "at_hash": "798U9iH2juicd-xBY7NgnA", "nbf": 1476813848, "exp": 1476815048, "iat": 1476813848, "iss": "http://localhost:5000/" }
To me it seems, that no role claims were added to access_token nor id_token. Am I missing some? Test setup is copy-pasted from mvc server sample.
Any chance you could share your token endpoint action?
Sure, no prob. It's supposed to be 1:1 copy-paste from sampel. What am I missing?
`[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task
{
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, IDEALUser user)
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
var scopes = new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess
}.Union(request.GetScopes());
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
// Always include the user identifier in the
// access token and the identity token.
if (claim.Type == ClaimTypes.NameIdentifier)
{
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Include the name claim, but only if the "profile" scope was requested.
else if (claim.Type == ClaimTypes.Name && scopes.Contains(OpenIdConnectConstants.Scopes.Profile))
{
claim.SetDestinations(OpenIdConnectConstants.Destinations.IdentityToken);
}
// Include the role claims, but only if the "roles" scope was requested.
else if (claim.Type == ClaimTypes.Role && scopes.Contains(OpenIddictConstants.Scopes.Roles))
{
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// The other claims won't be added to the access
// and identity tokens and will be kept private.
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
principal, new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetScopes(scopes);
return ticket;
}`
Note that Union with scopes was intentional - without it the if-clause catching the possible Roles-scope never hit the target.
Ah yeah, nice catch: that's because I forgot to add OpenIddictConstants.Scopes.Roles to the "trusted" scopes. Using Intersect should work just fine.
I'll update the samples, thanks for the tip :smile:
Happy to help. Keep up the good work!
For reasons I can't explain, the CodeFlow sample in the openiddict-samples repo already has the right scopes list, but not the sandbox sample used in this repo... most likely a brain bug :trollface:
Most helpful comment
This is the "expected" behavior, as custom claims are discarded when using a refresh token (because internally, OpenIddict calls
CreateIdentityAsyncto generate a fresh new identity and ignores the old claims).That said, now that you're responsible of creating authentication tickets, this behavior - inherited from the period where OpenIddict magically handled everything for you - doesn't make much sense. Not sure what's the best approach yet, but I'll work on it.