itfoxtex saml mvccore, attribute replace NameID - asp.net-core

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
});
}

Related

Can't access Claim by it's key after login with ASP.NET Core Identity in .NET 6 Project

I have a Blazor project in .NET 6. I am using ASP.NET Core Identity for login. After login, I have set some Claims with a custom Key. Here is the code:
Login Code:
var createdUser = await _userManager.FindByNameAsync(Input.UserName.Trim());
var LoginResult = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (LoginResult.Succeeded)
{
var userRoles = await _userManager.GetRolesAsync(createdUser);
var claims = new List<Claim>()
{
new Claim("UserId", createdUser?.Id),
new Claim("Username", createdUser?.UserName),
new Claim("OrganizationId", createdUser?.OrganizationId)
};
if(userRoles!= null && userRoles.Count > 0)
{
foreach (var role in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
}
var appIdentity = new ClaimsIdentity(claims);
var claimPrincpal = new ClaimsPrincipal(appIdentity);
Thread.CurrentPrincipal = claimPrincpal;
User.AddIdentity(appIdentity);
return LocalRedirect(returnUrl);
}
After Login I have a UserInfoService class and there I have some code to get the Claim value. Here is the code below:
public class UserInfoService : IUserInfoService
{
private readonly AuthenticationStateProvider _authProvider;
private readonly UserManager<ApplicationUser> _userManager;
public UserInfoService(
AuthenticationStateProvider authProvider,
UserManager<ApplicationUser> userManager)
{
_authProvider = authProvider;
_userManager = userManager;
}
public async Task<string> GetUserName()
{
var auth = await _authProvider.GetAuthenticationStateAsync();
var user = auth.User;
if (!user.Identity.IsAuthenticated)
{
return null;
}
else
{
var claimsIdentity = user.Identity as ClaimsIdentity;
var userName = claimsIdentity.FindFirst("Username")?.Value;
if (string.IsNullOrWhiteSpace(userName))
return null;
return userName;
}
}
}
And In my Program.cs file I have the below settings for identity:
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
But whenever I call GetUserName method to get the username from Claim, it returns null. I tried to debug the code and during debugging whenever the breakpoint comes upon this line (var claimsIdentity = user.Identity as ClaimsIdentity;), I hover the mouse and have the below information into the Claims Property, which I don't understand how to access. I don't even see the Claims Key (UserId, Username) which I set during the login.
Can anyone help to find the answer to how can I access Claims Key and Value?
var appIdentity = new ClaimsIdentity(claims);
var claimPrincpal = new ClaimsPrincipal(appIdentity);
Thread.CurrentPrincipal = claimPrincpal;
User.AddIdentity(appIdentity);
By using the above code, the new claim will be added to the HttpContext.User, it will store data while processing a single request. The collection's contents are discarded after a request is processed.
So, I suggest you can store the Claims in the AspNetUserClaims table, you could use the UserManager.AddClaimAsync() method to add the specified claim to the user, and then use the SignInManager or ApplicationDbContext to get tye user's claims. Check the following sample code (Login post method):
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
//find current user.
var user = await _userManager.FindByEmailAsync(Input.Email);
//based on user information to query the user and role policy table. Here I set the user role directly.
var userrole = "User";
if (user.UserName.Contains("aa"))
{
userrole = "Admin";
}
//get the current user claims principal
var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
//get the current user's claims.
var claimresult = claimsPrincipal.Claims.ToList();
//it it doesn't contains the Role claims, add a role claims
if (!claimresult.Any(c => c.Type == ClaimTypes.Role))
{
//add claims to current user.
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Role, userrole));
}
//refresh the Login
await _signInManager.RefreshSignInAsync(user);
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
Based on the answer from the Lik Now I have added the below code in my login page and it's working fine.
var claims = new List<Claim>()
{
new Claim("UserId", createdUser?.Id),
new Claim("UserName", createdUser?.UserName),
new Claim("OrganizationId", createdUser?.OrganizationId),
new Claim("OrganizationName", OrganizationName)
};
var userRoles = await _userManager.GetRolesAsync(createdUser);
if (userRoles != null && userRoles.Count > 0)
{
foreach (var role in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
}
var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(createdUser);
var claimResult = claimsPrincipal.Claims.ToList();
if (claims != null && claims.Count > 0)
{
await _userManager.AddClaimsAsync(createdUser, claims.ToList());
}
await _signInManager.RefreshSignInAsync(createdUser);

Custom claim not refreshed

Custom claims set is not updating. I have two API and an MVC project API1 validates the user and set claims, API2 is supposed to consume the custom claims, MVC is the UI.
The below code is from API1 that runs successfully. The claims are added. I can see the same in API2, (but it's now visible anywhere else including the MVC. maybe I don't know how to access it).
But the main problem is not that, the main problem is, the claims are not updated when it's changed the next time through API1. So, even if another user login in the claim still shows the first users' claims.
public async Task<AuthenticationModel> GetTokenAsync(LoginRequestModel model)
{
//...
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
JwtSecurityToken jwtSecurityToken = await CreateJwtToken(user, model.Database);
authenticationModel.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
}
private async Task<JwtSecurityToken> CreateJwtToken(ApplicationUser user, string myClaimValue)
{
var userClaims = await _userManager.GetClaimsAsync(user);
var roles = await _userManager.GetRolesAsync(user);
var roleClaims = new List<Claim>();
for (int i = 0; i < roles.Count; i++)
{
roleClaims.Add(new Claim("roles", roles[i]));
}
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim("uid", user.Id),
new Claim("MyCustomClaim", myClaimValue)
}
.Union(userClaims)
.Union(roleClaims);
var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwt.Issuer,
audience: _jwt.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwt.DurationInMinutes),
signingCredentials: signingCredentials);
return jwtSecurityToken;
}
This data is used in the following code. It works for the first time, but when the values are changed in API1, the same is not reflected in the claims. The claim always shows the first value assigned to it. I tried getting details of the claim in the MVC for debug and initialization, but everywhere else the claims are empty.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
var identity = _httpContextAccessor.HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
var myclaim= identity.FindFirst("MyCustomClaim").Value;
}
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
}

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)

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.

Basic Authentication Middleware with OWIN and ASP.NET WEB API

I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}