Custom claims with Jwt Bearer Authentication - asp.net-core

I have an application that uses JwtBearerAuthentication. I am trying to add my application claims to the User(ClaimsPrincipal) at the beginning of each request. I managed to do that using ClaimsTransformationOptions:
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new ClaimsTransformer<TUser, TRole>()
});
and in my TransformAsync:
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var services = context.Context.RequestServices;
var userManager = services.GetRequiredService<UserManager<TUser>>();
var roleManager = services.GetRequiredService<RoleManager<TRole>>();
var userId = 1; // Get the UserId from my store, let say its 1 for now
if (userId != 0)
{
var user = await userManager.FindByIdAsync(userId);
var claimsPrincipal = await new UserClaimsPrincipalFactory<TUser, TRole>(userManager, roleManager, _optionsAccessor)
.CreateAsync(user);
context.Principal.AddIdentities(claimsPrincipal.Identities);
}
return context.Principal;
}
So far so good and the claims are being loaded from the database and added to the context.Principal. My problem is once I reach the controller, the identities are being overwritten !!

So I solved this problem by putting the app.UseClaimsTransformation after app.UseJwtBearerAuthentication which made sure that whenever JWT is going to amend the ClaimsPrincipal the ClaimsTransformation will be called afterwards to add my own claims.

Related

Why when requesting a token using the grant password and authorization code, I do not get the same claims?

I have a Identity Server client set up to be able to use the password and authorization code grants, I am able to use both, but when reviewing the tokens they do not contain the same claims, is this how its suppose to work or I am missing some configuration?
If this is how it works (different claims in each grant) when using the password grant should I use the Profile service to add the other claims?
You need to implement an IResourceOwnerPasswordValidator, and return the list of claims you need. The default implementation only sends the sub claim.
See the example implementation for ASP.NET Core Identity in IdentityServer repo.
Then modify it to send additional claims. Or use ProfileService to populate it:
public virtual async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var user = await _userManager.FindByNameAsync(context.UserName);
if (user != null)
{
var result = await _signInManager.CheckPasswordSignInAsync(user, context.Password, true);
if (result.Succeeded)
{
var sub = await _userManager.GetUserIdAsync(user);
_logger.LogInformation("Credentials validated for username: {username}", context.UserName);
// return additional claims
var claims = await _userManager.GetClaimsAsync(user);
context.Result = new GrantValidationResult(sub, AuthenticationMethods.Password, claims);
return;
}
// ... see the link above for a full implementation
}
You can also create a new ClaimsPrincipal to populate the results. See the GrantValidationResult constructor overloads for other options.

ASP.NET Core OpenIdConnect and admin consent on the same callback path

I have implemented an OpenIdConnect with Azure. The code is approximately like this:
var options = new OpenIdConnectOptions
{
SignInScheme = PersistentSchemeName,
CallbackPath = "/oauth2office2",
ClientId = pubConf.ApplicationId,
Authority = $"https://login.microsoftonline.com/{configuration.TenantId}"
};
It works perfectly.
But I also need admin consent and I don't want my users to add two CallbackPaths into my app.
So I crafted admin consent url manually.
And added a redirect so it won't conflict with a OpenId middleware:
app.UseRewriter(new RewriteOptions().Add(context =>
{
var request = context.HttpContext.Request;
if (request.Path.StartsWithSegments("/oauth2office2") && request.Method == HttpMethods.Get)
{
request.Path = "/oauth2office";
}
}));
Now i have a controller at /oauth2office that does some extra stuff for me (actually gets tenant id).
Question - is there a way I can achieve it with OpenIdConnect middleware? While still being on the same callback path.
Because adding two paths is an extra i want to avoid.
I'm not even sure I can make OpenIdConnect work with admin consent actually.
One option is to add two AddOpenIDConnect(...) instances with different schema names and different callback endpoints?
You can only have one endpoint per authentication handler.
Also, do be aware that the callback request to the openidconnect handler is done using HTTP POST, like
POST /signin-oidc HTTP/1.1
In your code you are looking for a GET
if (request.Path.StartsWithSegments("/oauth2office2") && request.Method == HttpMethods.Get)
This can be done with a single OpenIdConnect handler by overriding the events RedirectToIdentityProvider and MessageReceived.
public override async Task RedirectToIdentityProvider(RedirectContext context)
{
if (!context.Properties.Items.TryGetValue("AzureTenantId", out var azureTenantId))
azureTenantId = "organizations";
if (context.Properties.Items.TryGetValue("AdminConsent", out var adminConsent) && adminConsent == "true")
{
if (context.Properties.Items.TryGetValue("AdminConsentScope", out var scope))
context.ProtocolMessage.Scope = scope;
context.ProtocolMessage.IssuerAddress =
$"https://login.microsoftonline.com/{azureTenantId}/v2.0/adminconsent";
}
await base.RedirectToIdentityProvider(context);
}
public override async Task MessageReceived(MessageReceivedContext context)
{
// Handle admin consent endpoint response.
if (context.Properties.Items.TryGetValue("AdminConsent", out var adminConsent) && adminConsent == "true")
{
if (!context.ProtocolMessage.Parameters.ContainsKey("admin_consent"))
throw new InvalidOperationException("Expected admin_consent parameter");
var redirectUri = context.Properties.RedirectUri;
var parameters = context.ProtocolMessage.Parameters.ToQueryString();
redirectUri += redirectUri.IndexOf('?') == -1
? "?" + parameters
: "&" + parameters;
context.Response.Redirect(redirectUri);
context.HandleResponse();
return;
}
await base.MessageReceived(context);
}
Then when you need to do admin consent, craft a challenge with the correct properties:
public IActionResult Register()
{
var redirectUrl = Url.Action("RegisterResponse");
var properties = new OpenIdConnectChallengeProperties
{
RedirectUri = redirectUrl,
Items =
{
{ "AdminConsent", "true" },
{ "AdminConsentScope", "https://graph.microsoft.com/.default" }
}
};
return Challenge(properties, "AzureAd");
}
public IActionResult RegisterResponse(
bool admin_consent,
string tenant,
string scope)
{
_logger.LogInformation("Admin Consent for tenant {tenant}: {admin_consent} {scope}", tenant, admin_consent,
scope);
return Ok();
}

how to include the role to the JWT token returning?

What I have in my mind when navigating through the application, I want to save the token to the localhost along with role name and I will check if the users have access to a certain link. Is that how it works? with Authgard in Angular 8?. Can you give me some insight of navigating an application with the role from Identity(which is built in from ASP.net core 3.1).
login
// POST api/auth/login
[HttpPost("login")]
public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
//return null;
return BadRequest(Error.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
var jwt = await Tokens.GenerateJwt(identity, _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
return new OkObjectResult(jwt);
}
Generate Token Method
public static async Task<string> GenerateJwt(ClaimsIdentity identity, IJwtFactory jwtFactory, string userName, JwtIssuerOptions jwtOptions, JsonSerializerSettings serializerSettings)
{
var response = new
{
id = identity.Claims.Single(c => c.Type == "id").Value,
//probably here I want to send the role too!!
auth_token = await jwtFactory.GenerateEncodedToken(userName, identity),
expires_in = (int)jwtOptions.ValidFor.TotalSeconds
};
return JsonConvert.SerializeObject(response, serializerSettings);
}
}
You need to add claims information when generating your JWT.
Here`s an example
And another one:
1 part(how to implement JWT), 2 part(about claims here)

itfoxtex saml mvccore, attribute replace NameID

I cannot figure out how to get an attribute from the the saml response in place of the NameID value.
My IDP team is returning the value I need in an attribute rather than in NameID(which they wont budge on).
Thanks for any help!
I am running MVC Core. I have everything setup and running for NameID from the example 'TestWebAppCore' for ITfoxtec.Identity.Saml2.
I am trying to get this value in place of NameID for the session username:
saml:AttributeStatement>
<saml:Attribute Name="valueName"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
>
<saml:AttributeValue>IDValue</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success) {
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(),
saml2AuthnResponse);
try {
await saml2AuthnResponse.CreateSession(HttpContext,
claimsTransform: (claimsPrincipal) =>
ClaimsTransform.Transform(claimsPrincipal));
}
catch (Exception ex) {
log.writeLog(ex.Message.ToString());
}
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl)
? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}
It is probably not possible to logout without the NameID but you can login without.
In .NET the NameID is translated into the ClaimTypes.NameIdentifier claim. The users claims is handled in the ClaimsTransform.CreateClaimsPrincipal method.
You can either translate the incoming custom claim "valueName" to a ClaimTypes.NameIdentifier claim:
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
claims.AddRange(GetSaml2LogoutClaims(incomingPrincipal));
claims.Add(new Claim(ClaimTypes.NameIdentifier, GetClaimValue(incomingPrincipal, "valueName")));
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}
Or change the identity claim in the ClaimsIdentity to the incoming custom claim "valueName":
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
// All claims
claims.AddRange(incomingPrincipal.Claims);
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, "valueName", ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}

OnAuthenticationCompleted in ASP.NET 5 RC

I have an ASP.NET 5 beta 8 application which integrates with Azure Active Directory using OpenIdConnect. I tried updating the application to RC1 and changed the openid nuget package to "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-rc1-final". Everything seems to be correct, but the OnAuthenticationComplete method which I used to add roles to my ClaimsIdentity is no longer in the Events object, and I can't find the alternative. How can I add claims to my Identity with the new version?
UPDATE: Changing to OnAuthenticationValidated still not works for me. I must be doing something wrong in my event's code:
OnAuthenticationValidated = async notification =>
{
var claimsIdentity = notification.AuthenticationTicket.Principal.Identity as ClaimsIdentity;
var userRepository=new UserRepository(Configuration);
var userId = claimsIdentity?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var user = await userRepository.FindAsync(userId);
if (user == null)
{
await userRepository.AddAsync(new UserDto
{
UserId = userId,
Username = claimsIdentity?.FindFirst(ClaimTypes.Name)?.Value,
DisplayName = claimsIdentity?.FindFirst(ClaimTypes.Name)?.Value
});
}
claimsIdentity?.AddClaim(new Claim(ClaimTypes.Role, "super"));
}
Also, the code I use to do the login is:
[HttpGet]
public IActionResult Login()
{
if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated)
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
return RedirectToAction("Index", "Home");
}
The AuthenticationCompleted event has been renamed to AuthenticationValidated.
You can find more information on this ticket: https://github.com/aspnet/Security/pull/442.