I have created a C# class library to implement ASP.NET Identity for several web applications that use the same user schema. I am adding custom claims to users' identities in the GenerateIdentity
method in the User
class:
private ClaimsIdentity GenerateIdentity(UserManager userManager)
{
var identity = userManager.CreateIdentity(this, DefaultAuthenticationTypes.ApplicationCookie);
identity.AddClaims(
new[]
{
new Claim(nameof(UserId), UserId.ToString()),
new Claim(ClaimTypes.Role, UserType.ToString()),
new Claim(nameof(FullName), FullName),
new Claim(nameof(FirmName), FirmName),
new Claim(nameof(HasDebit), HasDebit.ToString()),
new Claim(nameof(ShouldShowAssistants), ShouldShowAssistants.ToString()),
new Claim(nameof(IsSuspended), IsSuspended.ToString()),
new Claim(nameof(FirmId), FirmId.ToString()),
});
if (IsSelfAdmin)
{
identity.AddClaim(new Claim(ClaimTypes.Role, SelfAdminRole));
}
return identity;
}
public Task<ClaimsIdentity> GenerateIdentityAsync(UserManager userManager)
{
return Task.FromResult(GenerateIdentity(userManager));
}
The claims are indeed present on the identity object before the method returns during the login process. However, after the user logs in and the request is redirected, all of the custom claims are gone. Here's my login code in one of the web applications (an older VB.NET WebForms app):
Dim signInManager = Context.GetOwinContext().GetUserManager(Of SignInManager)
Dim result = signInManager.PasswordSignIn(UserID_TB.Text, PW_TB.Text, RememberMe_CB.Checked, False)
If result = SignInStatus.Failure
SetMessage("Failed")
Return
End If
Response.Redirect("profile.aspx")
The sign in is a success, but on the profile.aspx page, the User IPrincipal property only has the default 4 claims.
Potentially relevant code from the application's startup:
app.UseCookieAuthentication(New CookieAuthenticationOptions With {
.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
.Provider = New CookieAuthenticationProvider With {
.OnValidateIdentity = SecurityStampValidator.OnValidateIdentity (Of UserManager, User)(
TimeSpan.FromSeconds(0),
Function(manager, user) user.GenerateIdentityAsync(manager))
}
})
The validation interval is 0 as per a specification for this application.
Please let me know if any other code would be useful to see.
I have found that everything works as intended after a second redirect. When login redirects to profile, the claims are missing, but if profile redirects again, the claims are there. I'm really confused as to why this is.
The issue actually arose from me calling UserManager.CreateIdentity
instead of User.GenerateIdentity
. My SecurityStamp.OnValidateIdentity
used the correct one, but not my sign in code. This should have been very obvious since User.GenerateIdentity
contains a call to UserManager.CreateIdentity
before adding all the custom claims, but it flew over my head for some time.
My sign in code is now this:
Dim userManager = Context.GetOwinContext().GetUserManager(Of UserManager)
Dim authenticatingUser = userManager.Find(UserID_TB.Text, PW_TB.Text)
If authenticatingUser Is Nothing
SetMessage("Failed")
Return
End If
userManager.UpdateSecurityStamp(authenticatingUser.Id)
Dim userIdentity = authenticatingUser.GenerateIdentityAsync(userManager).Result
Dim authenticationManager = HttpContext.Current.GetOwinContext().Authentication
authenticationManager.SignIn(
New AuthenticationProperties With {
.IsPersistent = RememberMe_CB.Checked
},
userIdentity)
Response.Redirect("profile.aspx", False)
I also wrote this to handle refreshing user claims:
Public Shared Sub RefreshSignIn()
Dim authenticationManager = HttpContext.Current.GetOwinContext().Authentication
authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie)
Dim userManager = HttpContext.Current.GetOwinContext().GetUserManager(Of UserManager)
Dim user = userManager.FindById(HttpContext.Current.User.Identity.GetUserId())
Dim userIdentity = user.GenerateIdentityAsync(userManager).Result
authenticationManager.SignIn(userIdentity)
End Sub
I solved the problem by adding an additional redirect after the login. In the case of the example application I provided, we always redirect to profile, so I changed profile's Page_Load event handler to this:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not User.IsAuthenticated()
' The user actually isn't authenticated; send them to login
Response.Redirect("login.aspx")
End If
If User.GetSomeClaimThatShouldBeHere() Is Nothing
' The second redirect that somehow makes this work
Response.Redirect("profile.aspx")
End If
' Do the rest of the page load that depends on the user claims
End Sub
If we didn't already use a catch-all page after the login, we could make a dummy page that handles the redirect logic like this:
Public Class LoginRedirect
Inherits Page
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not User.IsAuthenticated()
' The user actually isn't authenticated; send them to login
Response.Redirect("login.aspx")
End If
If User.GetSomeClaimThatShouldBeHere() Is Nothing
' The second redirect that somehow makes this work
Response.Redirect("loginredirect.aspx")
End If
If Not String.IsNullOrWhiteSpace(Request.QueryString("returnUrl"))
' Redirect to a return URL you've been passing around
Response.Redirect(Request.QueryString("returnUrl"))
End If
' Do some other logic to determine where the user (with all their claims available) needs to go
End Sub
End Class
I'm not going to mark this at the accepted answer for a while in case someone comes up with a better solution than to do a seemingly superfluous redirect.