I have an asp.net application that authenticates against a third party via ADFS. Once authentication is successful, the user is redirected to a landing page where claims for the user are pulled from a database. I populate the user's claims using the following code:
IClaimsPrincipal principal = Thread.CurrentPrincipal as IClaimsPrincipal;
IClaimsIdentity claimsIdentity = (IClaimsIdentity)principal.Identity;
foreach (string claim in customClaims)
{
Claim newClaim = new Claim(ClaimTypes.Role, claim);
claimsIdentity.Claims.Add(newClaim);
}
Once I have populated the claims, I save the IClaimsPrincipal to session. Here is where things get odd. After the user is redirected from the landing page to their desired page, the claims are missing. If I query the the claims using the following code...
IClaimsPrincipal principal= Thread.CurrentPrincipal as IClaimsPrincipal;
IClaimsIdentity claimsIdentity = (IClaimsIdentity)principal.Identity;
foreach (Claim claim in claimsIdentity.Claims)
{
Response.Write(claim.ClaimType + ": " + claim.Value + "<br/>");
}
...I don't get back any of the added claims. Instead I only have the original claim given me from ADFS (the username). The odd thing is that if I pull the IClaimsPrincipal out of Session, and query it's collection of claims, I get back all of the claims I added. What is going on here?
You should use the ClaimsAuthenticationManager extensibility point to pull more claims - everything you add there will automatically (and probably correctly) saved to the authentication session.
Try updating the session cookie after adding the claims, example:
var user = HttpContext.User as ClaimsPrincipal;
var claims = new List<Claim>();
claims.Add(new Claim("MyClaimType", "MyClaimValue"));
user.AddIdentity(new ClaimsIdentity(claims));
// Update cookie
var sam = FederatedAuthentication.SessionAuthenticationModule;
if (sam != null)
{
var token = new SessionSecurityToken(user);
sam.WriteSessionTokenToCookie(token);
}
I believe if you manipulate claims outside the "WIF controlled" methods like ClaimsAuthorizationManager then you have to manually update the authentication session cookie or else you lose all the changes.
Related
I am a stackoverflow noob so please go easy if I am doing this wrong.
I am using asp.net core with the default core identity template (local accounts).
I have accertained how to add claims to user principal when they login locally like so
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var user = await _userManager.FindByNameAsync(model.Email);
await _userManager.AddClaimAsync(user, new Claim("your-claim", "your-value"));
And I have figured out how to get claims returned from the external login but I cannot figure out how I would add these before the user principal gets created in the ExternalLoginCallback function
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
return View(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
else {
// extract claims from external token here
}
// assume add claims to user here before cookie gets created??
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
I am assuming the the _signInManager.ExternalLoginSignInAsync function works similar to the local login _signInManager.PasswordSignInAsync in the sense that once it is called, the cookie will be created. But I am just not sure.
Essentially what I am hoping to achieve, is understanding of how to add custom claims into the cookie that gets created regardless of how to user logins in (local or external), and how to persist these claims to the database if required.
I am planning on doing some work where if I have a user login using say google auth, I need to save that access_token from google, because I wish to call into the Google APIs later with it. So I need to be able to include this access_token in with the User Principal that gets created, and I would hope the cookie would have a claim on it I could use at the front end as well.
This might be out of scope on this question but I would also like when the google token expires, for some-how it to use the refresh token and go get a new one, or force the user to relogin.
Any help on this would be super appreciated, I have really tried hard to understand this without posting this question to stackoverflow. I have read many articles with lots of useful info, but does not provide the answers this specific question is asking. So Thank you very much in advance.
cheers
When you use await _userManager.AddClaimAsync(user, new Claim("your-claim", "your-value")); that actually updates the Identity's aspnetuserclaims table.
Whenever you sign in (by using _signInManager.PasswordSignIn or _signInManager.ExternalLoginSignInAsync) the claims from that table are read and added to the cookie that on every request becomes the Principal.
So you probably don't want to be calling the AddClaimAsync method from UserManager on every login.
Regarding external login providers, you have access to the claims when you call (in ExternalCallback and ExternalCallbackConfirmation if you are using the default templates) here:
var info = await _signInManager.GetExternalLoginInfoAsync();
The claims are in info.Principal.Claims.
The access token is not included by default. When it is, it will be here (along with the type and expiry date):
var accessToken = info.AuthenticationTokens.Single(f => f.Name == "access_token").Value;
var tokenType = info.AuthenticationTokens.Single(f => f.Name == "token_type").Value;
var expiryDate = info.AuthenticationTokens.Single(f => f.Name == "expires_at").Value;
To have the access token be included in the AuthenticationTokens collection, when you are configuring the GoogleAuthentication middleware set the SaveTokens flag to true:
app.UseGoogleAuthentication(new GoogleOptions{
ClientId = "...",
ClientSecret = "...",
SaveTokens = true
Now, if you want to have control over which claims go in the cookie you have to "take over" the process of creating the claims principal.
This is done for you when you use _signInManager.PasswordSignIn/ExternalLoginSignInAsync.
So, for example, for ExternalLoginSignInAsync replace:
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
With:
var user = await this._userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
var claimsPrincipal = await this._signInManager.CreateUserPrincipalAsync(user);
((ClaimsIdentity)claimsPrincipal.Identity).AddClaim(new Claim("accessToken", info.AuthenticationTokens.Single(t => t.Name == "access_token").Value));
await HttpContext.Authentication.SignInAsync("Identity.Application", claimsPrincipal);
"Identity.Application" is the default cookie name. You can change it in Startup's ConfigureServices method, for example to MainCookie:
services.Configure<IdentityOptions>(options => {
options.Cookies.ApplicationCookie.AuthenticationScheme = "MainCookie";
});
You still need to handle the ExternalCallbackConfirmation action in the AccountController. It will be similar to the example above.
I'm using Microsoft Identity 3. I can access the claims of the current user.
But I can't figure out how to do the same for another user.
For the current user, inside the controller, I can access the claims collection via:
IEnumerable<Claim> claims = User.Claims;
I can see all the user claims and I can add a claim as follows:
var user = await GetCurrentUserAsync();
await _userManager.AddClaimAsync(user, new Claim("role", "manager"));
But if I do this:
IdentityUser user = await _userManager.FindByIdAsync(userid);
"user" has a "Claims" collection but the count is zero and the collection is empty.
How can I access the claims of other than the current user and be able to add and delete claims?
The solution to what I wanted to do turned out to be very easy -- even though I don't fully understand the "why". I can just up-cast IdentityUser to ApplicationUser in the following and it works:
IdentityUser user = await _userManager.FindByIdAsync(userId);
// The following will show the current claims:
var claims = await _userManager.GetClaimsAsync((ApplicationUser) user);
// The following adds a new claim:
await _userManager.AddClaimAsync((ApplicationUser) user, new Claim("time", "yesterday"));
I am storing custom claims, such as the user's real name, in the ASP.NET Identity cookie to avoid unnecessary database queries on every request. At least that's what I assume this code is doing:
var identity = await user.GenerateUserIdentityAsync(UserManager);
identity.AddClaim(new Claim(ClaimTypes.GivenName, user.FirstName)));
// etc.
AuthenticationManager.SignIn(new AuthenticationProperties {IsPersistent=true}, identity);
This works fine, and I can retrieve these claims with:
private static string GetClaim(string claimType)
{
var identity = (ClaimsPrincipal) Thread.CurrentPrincipal;
var claim = identity.Claims.SingleOrDefault(o => o.Type == claimType);
return claim == null ? null : claim.Value;
}
The identity.Claims property contains the following claims, as expected:
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: ced2d16c-cb6c-4af0-ad5a-09df14dc8207
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: me#example.com
http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider: ASP.NET Identity
AspNet.Identity.SecurityStamp: 284c648c-9cc7-4321-b0ce-8a347cd5bcbf
http://schemas.microsoft.com/ws/2008/06/identity/claims/role: Admin
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname: My Name
The trouble is that after some time (usually several hours), my custom claims seem to disappear - in this example, givenname no longer exists in the enumeration. The user is still authenticated, and all the default claims are still there.
What's going on, and how can I fix this? The only thing I can think of is that the cookie is expiring and being re-issued behind the scenes, but I don't know why (or if) that would happen.
Yes, the issue is most likely the cookie getting expired. Since you didn't add the custom claims to the user's claims in the database, they are lost on refresh since you aren't adding the claim inside the method being called. You can either add the claim via:
userManager.AddClaim(user.Id, new Claim(ClaimTypes.GivenName, user.FirstName));
or you can move this inside of the method that's called when the cookie is regenerated (by default its user.GenerateUserIdentityAsync).
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider {
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
In Net5 you can get old cookie Custom Claim and add to new cookie like this,
builder.Services.Configure<SecurityStampValidatorOptions>(options =>
{
// set time how oftern the cookie is invalidated
options.ValidationInterval = TimeSpan.FromMinutes(10);
options.OnRefreshingPrincipal = context =>
{
// Get my custom claim from old cookie
var guidClaim = context.CurrentPrincipal.FindFirst("GUID");
// Add value to new cookie
if (guidClaim != null)
context.NewPrincipal.Identities.First().AddClaim(guidClaim);
return Task.FromResult(0);
};
});
I'm trying to get the current user in a web forms project using the new Visual Studio 2013 Preview for Web. I'm able to get the username using Page.User, but trying to get the user id is where I'm stuck. It's using this new Identity Model thing they've come out with.
This is what I've got:
//Gets the correct username
string uname = User.Identity.Name;
//Returns a null object
Microsoft.AspNet.Identity.IUser user = IdentityConfig.Users.Find(uname);
//What I hope to call to add a user to a role
IdentityConfig.Roles.AddUserToRole("NPO", user.Id);
If you are using the Default Membership that come's with the ASP.NET WebForms Template you should do something like this to retrieve the user:
if (this.User != null && this.User.Identity.IsAuthenticated)
{
var userName = HttpContext.Current.User.Identity.Name;
}
The new model you are talking about is ClaimsPrincipal. The unique difference is this Claims Based Secury, that is completly compatible with the older versions but more powerfull.
EDIT:
To add an user to some Role programaticaly you should do this, passing the user name and the role name:
if (this.User != null && this.User.Identity.IsAuthenticated)
{
var userName = HttpContext.Current.User.Identity.Name;
System.Web.Security.Roles.AddUserToRole(userName, "Role Name");
}
Using the new Claim Based Security
if (this.User != null && this.User.Identity.IsAuthenticated)
{
var userName = HttpContext.Current.User.Identity.Name;
ClaimsPrincipal cp = (ClaimsPrincipal)HttpContext.Current.User;
GenericIdentity genericIdentity;
ClaimsIdentity claimsIdentity;
Claim claim;
genericIdentity = new GenericIdentity(userName, "Custom Claims Principal");
claimsIdentity = new ClaimsIdentity(genericIdentity);
claim = new Claim(ClaimTypes.Role, "Role Name");
claimsIdentity.AddClaim(claim);
cp.AddIdentity(claimsIdentity);
}
The user id is stored in the ClaimTypes.NameIdentifier claim by default. We provided an extension method as well that lives in Microsoft.AspNet.Identity so you can simply call Page.User.Identity.GetUserId(), instead of fetching the user just to get out the id.
Just a post for people who find this today this is now very easy to accomplish
ApplicationUserManager manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
ApplicationSignInManager signinManager = Context.GetOwinContext().GetUserManager<ApplicationSignInManager>();
ApplicationUser user = signinManager.UserManager.FindByNameAsync("username").Result;
At this point you have access to everything Id, Email, Phone number, Even the Hashed version of the user password.
I have an existing MVC4 app (.NET 4.5) using FormsAuthentication that I'm looking to switch to using SessionAuthenticationModule so that I can get a Claims aware a identity for both an easy of additional data to the identoty and as a first step to eventually migrating to performing authentication via WIF (Windows Identity Foundation) with an STS (Security Token Service) service like ADFS (Active Directory Federation Services), but that's all later down the road.
My question is, what determines the timeout when a user is
authenticated using SessionAuthenticationModule?
I used this page to get my authentication working, and it seems to work fine. Basically my authentication looks like this.
Snippet from my Login action method
var personId = securityService.AuthenticateUser(model.Login, model.Password);
if (!personId.IsEmpty())
{
authenticationService.SignIn(personId, model.RememberMe);
if (Url.IsLocalUrl(model.ReturnUrl))
return Redirect(model.ReturnUrl);
else
return RedirectToAction("Index", "Home");
}
AuthenticationService.SignIn()
public void SignIn(Guid personId, bool createPersistentCookie)
{
var login = securityService.GetLoginByPersonId(personId);
if (String.IsNullOrEmpty(login.Name)) throw new ArgumentException("Value cannot be null or empty.", "userName");
var claims = LoadClaimsForUser(login.Name);
var identity = new ClaimsIdentity(claims, "Forms");
var claimsPrincipal = new ClaimsPrincipal(identity);
var token = new SessionSecurityToken(claimsPrincipal, ".CookieName", DateTime.UtcNow, DateTime.UtcNow.AddMinutes(30)) { IsPersistent = createPersistentCookie };
var sam = FederatedAuthentication.SessionAuthenticationModule;
sam.WriteSessionTokenToCookie(token);
}
AuthenticationService.LoadClaimsForUser()
private IEnumerable<Claim> LoadClaimsForUser(string userName)
{
var person = securityService.GetPersonByLoginName(userName);
if (person == null)
return null;
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, person.PersonId.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userName));
/* .... etc..... */
}
But there is the only concern I had with this is that I want to retain the behavior of sliding expiration so the user is not prompted to re-login when their login expires, but upon working on this problem I noticed that I can't find out what determines how long they stay logged in at all. I've set the session timeout, forms timeout and the validTo parameter on the SessionSecurityToken constructor to 1 minute, but even after that elapses, I'm still able to access the site. The cookie appears in the browser with an expiry date of "Session", which I'm not sure why but even if the cookie is valid for the session shouldn't the token, identity or whatever you want to call it expire after 1 minute and force the user to log back in?
I had similar issues once, here is my question containing my approach to invalidate cookies upon token expiration
How to set the timeout properly when federating with the ADFS 2.0
Adding a bit of different logic gives you sliding expiration
http://brockallen.com/2013/02/17/sliding-sessions-in-wif-with-the-session-authentication-module-sam-and-thinktecture-identitymodel/
web.config - Setting MaxClockSkew
<system.identityModel>
<identityConfiguration>
<securityTokenHandlers>
<securityTokenHandlerConfiguration maximumClockSkew="0">
</securityTokenHandlerConfiguration>
</securityTokenHandlers>
</identityConfiguration>
</system.identityModel>