MVC Not holding on to authentication - c#

I have two apps I'm working on. Both of them need to use both Windows authentication and anonymous access. so to do this, I edited the web.config to get rid of the authorization tag (with "deny users="?"") and only tagged a few actions with my custom authorization attribute. the trouble is, the server is "forgetting" me. so for instance, on the first app, one user reports that she has to attempt to access the control panel every other time she wants to edit. On the second one, I click login, I'm logged in, and then I click any other link (especially "save") and I'm logged out.
here's one of my custom authorization attributes:
public class AccountsAuthorizeITAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if(httpContext.User.Identity.IsAuthenticated == false)
{
return false;
}
if(httpContext.User.IsInRole("CT-IT"))
{
return true;
}
return false;
}
}
and to log in, I just have this in my _layout:
#Html.ActionLink("Login", "Login", "Login", new { returnURL = HttpContext.Current.Request.RawUrl }, null)
with this login controller:
public class LoginController : Controller
{
[AccountsAuthorizeIT]
public ActionResult Login(string returnURL)
{
return Redirect(returnURL);
}
}
What could cause this? Shouldn't my authentication be stored in the session variable, saved for (roughly) as long as the browser window is open? Do I need to tell the server to remember my data?

Shouldn't my authentication be stored in the session variable, saved
for (roughly) as long as the browser window is open? Do I need to tell
the server to remember my data?
I personally like to store them in Principle object as Claim using OWIN Cookie Middleware.
Here is the sample code. roleNames could be user's assigned Active Directory Group.
public void SignIn(User user, IList<string> roleNames)
{
IList<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
};
foreach (string roleName in roleNames)
{
claims.Add(new Claim(ClaimTypes.Role, roleName));
}
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthenticationType);
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignIn(identity);
}
Startup.cs
Then you register OWIN Cookie Middleware at start up.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ApplicationCookie",
LoginPath = new PathString("/Account/Login")
});
}
}
If you store them in Principle object, you won't even need custom attribute AccountsAuthorizeITAttribute.

Related

ClaimsIdentity not working properly in WebApi

I want to authorize user in WebApi using ClaimsIdentity. In my AccountController which inherits ApiController class I have my two methods to test user authentication. One is a proper method used to receive user's data from other app based on his AD name and authenticates him saving his data as a Claim. The other one is a test method which I call after the previous one to check if the user is authenticated and has claims set.
Unfortunately the login method doesn't seem to set his Identity correctly even though the cookie is generated. The second method than works as if the user wasn't even authenticated and doesn't have any claims.
I have tried some various combination of creating his Identity but nothing seems to work.
Maybe you can see what I am missing.
AccountController.cs
[HttpGet]
[Route("account/login/{userActDirName}/{realmId}")]
public async Task<IHttpActionResult> Login(string userActDirName, long realmId)
{
//getting user data
var user = await UserManager.FindAsync(userActDirName, "1");
if (user == null)
{
user = new ApplicationUser() { UserName = userActDirName };
IdentityResult result = await UserManager.CreateAsync(user, "1");
if (!result.Succeeded)
{
...
}
user = await UserManager.FindAsync(userActDirName, "1");
}
Authentication.SignOut();
ClaimsIdentity cookieIdentity = UserManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
cookieIdentity.AddClaim(new Claim(ClaimTypes.Name, userActDirName));
cookieIdentity.AddClaim(new Claim("User", JsonConvert.SerializeObject(userData)));
Authentication.SignIn(new AuthenticationProperties() { IsPersistent = false }, cookieIdentity);
}
private ApplicationUserManager _userManager;
private IAuthenticationManager Authentication
{
get { return HttpContext.Current.GetOwinContext().Authentication; }
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
IdentityConfig.cs
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = false
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = -1,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = false,
RequireUppercase = false,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
Startup.cs
[assembly: OwinStartup(typeof(Api.Startup))]
namespace Api
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
Since you used the string "User" while creating claim for complete user object as JSON, using the following code :
cookieIdentity.AddClaim(new Claim("User", JsonConvert.SerializeObject(userData)));
Therefore when checking if the user is authenticated or not, use the follwoing code to check if the above mentioned claim exists or not. It will also give you full JSON that you stored while adding "User" Claim.
Remember the type casting below is very important
also use the following namespace
using System.Security.Claims;
before using the following code
var user = "";
var claims =
((ClaimsIdentity)filterContext.RequestContext.Principal.Identity).Claims;
foreach (var c in claims)
{
if (c.Type == "User")
user = c.Value;
}
I have used this code in a custom "AuthorizationFilterAttribute". Therefore I have
filterContext object
you can get
RequestContext object
easily in any WebAPI-Method e.g.
this.RequestContext.Principal.Identity
therefore,
var claims =
((ClaimsIdentity)this.RequestContext.Principal.Identity).Claims;
will work in any web api controller.

.NET Core Antiforgery.GetAndStoreTokens for the wrong user after HttpContext.SignIn is called

I've been using the Antiforgery Cookie for .NET Core (https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery) and I'm always returning a new View after the login:
// - HomeController.cs
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Login()
{
ClaimsIdentity identity = new ClaimsIdentity("myAuthType");
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("myScheme", principal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(10),
AllowRefresh = true,
IsPersistent = true
});
return View();
}
The code is done so that each call/view generates a new X-XSRF-Cookie and sends to the browser, who sends the value back for some Api calls.
The code for that is:
// - Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(o => o.HeaderName = CrossSiteAntiForgery.Header);
services.AddMvc(o => {
o.Filters.AddService(typeof(CrossSiteAntiForgery));
});
}
// - CrossSiteAntiForgery.cs
public class CrossSiteAntiForgery : ResultFilterAttribute
{
public const string Cookie = "XSRF-TOKEN";
public const string Header = "X-XSRF-TOKEN";
private IAntiforgery _antiforgery;
public CrossSiteAntiForgery(IAntiforgery antiforgery)
{
_antiforgery = antiforgery;
}
public override void OnResultExecuting(ResultExecutingContext context)
{
AntiforgeryTokenSet tokens = _antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(Cookie, tokens.RequestToken, new CookieOptions() { HttpOnly = false });
}
}
But I wanted to login via Ajax just because. So I kept everything and changed just the Login:
[HttpPost("Api/User/Login"), ValidateAntiForgeryToken, Produces("application/json")]
public async Task<IActionResult> Login()
{
var userFromDB = DB.GetUser(1);
ClaimsIdentity identity = new ClaimsIdentity("myAuthType");
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("myScheme", principal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(10),
AllowRefresh = true,
IsPersistent = true
});
return Json(userFromDB);
}
However now I get errors (Bad Request) because the XSRF tokens don't match. After some (a lot actually) digging, I think I found the issue, the browser does receive the Set-Cookie header properly from the ajax and sets the new cookie value, but this value is invalid because it seems whenever the _antiforgery.GetAndStoreTokens(HttpContext) is called, it uses the current User to generate the token somehow.
When I return a new View, the Context is updated with the new (logged in) User, the token is generated and sent to the browser and everything works perfectly.
When I use ajax and call HttpContext.SignInAsync(), the current User hasn't been updated yet and so the generated token will be invalid for the logged in User.
// - Login()
var user = HttpContext.User;
await HttpContext.SignInAsync("Login", principal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(10),
AllowRefresh = true,
IsPersistent = true
});
var loggedInUser = HttpContext.User;
bool truth = user.Equals(loggedInUser); // - true
// - meaning anything that relies on the new logged in User is invalid from here on.
The error message in the .NET console is The provided antiforgery token was meant for a different claims-based user. I tried the suggestion here and it seems hacky, it works only for the second request onwards, meaning users would need to see a first "Bad Request" for every User change (Login and Logout).
How can I get/update the User after it has been logged in so the XSRF token generation works? Or, if there's a better solution, how can I solve this?
Edit: I'm working around it for now like this, in case anyone has the same issue:
public async Task<IActionResult> Login()
{
// - Login code omitted for brevity.
return RedirectToAction("LoginUnelegantWorkaround");
}
public IActionResult LoginUnelegantWorkaround()
{
var model = DB.FetchModelIWasGoingToReturnBefore();
return Json(model);
// - token is properly generated now since User has been updated, everything works
}
// - Same thing needed for Logout

Get user claims before any page loads on external ADFS login

What I'm trying to do is accessing user claims which returns from ADFS login. ADFS returns username and with that username I have to run a query to another DB to get user information and store it. I don't really know where to do that and what the best practice is. I can access user claims in the view controller like:
public ActionResult Index()
{
var ctx = Request.GetOwinContext();
ClaimsPrincipal user = ctx.Authentication.User;
IEnumerable<Claim> claims = user.Claims;
return View();
}
But what I need to do is as I said access claims like in global.asax.cs or startup.cs to store user information before the application runs.
This is my Startup.Auth.cs file:
public partial class Startup
{
private static string realm = ConfigurationManager.AppSettings["ida:Wtrealm"];
private static string adfsMetadata = ConfigurationManager.AppSettings["ida:ADFSMetadata"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata
});
}
}
We add an event handler to the WsFederationAuthenticationOptions value in our startup file.
This happens immediately after the security token has been validated.
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
{
MetadataAddress = MetadataAddress,
Wtrealm = Wtrealm,
Wreply = CallbackPath,
Notifications = new WsFederationAuthenticationNotifications()
{
SecurityTokenValidated = (ctx) =>
{
ClaimsIdentity identity = ctx.AuthenticationTicket.Identity;
DoSomethingWithLoggedInUser(identity);
}
}
};

ASP.NET Web Api with Role based authorization

I am using Web API 2 with OWIN token based authentication. Only thing that is not working is authorization based on roles.
In my implementation of AuthorizationServerProvider.GrantResourceOwnerCredentials this is how i assign roles:
identity.AddClaim(client.ApplicationType == ApplicationTypes.WebClient
? new Claim(ClaimTypes.Role, "user")
: new Claim(ClaimTypes.Role, "admin"));
But in the Controller using [Authenticate(Roles="user")] just returns a Authorization denied message to the client. I checked the variables and this is whats inside
So the role seems to be there, but user.Claims is empty and IsInRole("user") also returns negative.
I found several questions here on stackoverflow and logic-wise i don't see what i missed. Only thing that comes to my mind is overwriting the Authorize Command but this is kind of needless as role based authorization seems to be integrated already...
EDIT: This is what my workig method looks like:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin") ?? "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
Client client;
using (var repo = new AuthRepository())
{
client = repo.FindClient(context.ClientId);
if (client.ApplicationType != ApplicationTypes.Service)
{
var user = await repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect." + context.UserName);
return;
}
}
}
Don't add the role claim directly, use the UserManager instead:
UserManagerInstance.AddToRole(userId, "admin");
That way the role will be persisted (to the AspNetUserRoles or whatever you have configured) so it will be there for the later requests. This doesn't happen if you add the claim directly, since you're adding it to an "instance" of your user identity that will die with the current request.
TO ANSWER YOUR FURTHER REQUIREMENTS:
If you want the claims codified on the ticket then you have to do this after adding the claims the way you're doing (in GrantResourceOwnerCredentials):
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "userId", "blah,blah" },
{ "role", "admin" }
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
This way you don't have to "persist" these kind of users
Of course you would have to override the TokenEndpoint method of the OAuthAuthorizationServerProvider in order to retrive those data on later request/responses.
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
Probably solved it somehow, but for me it works if I put it like this:
[Authorize(Roles = "user")]
[Route("")]
[HttpGet]
public async Task<IHttpActionResult> GetUserSpecificServers() { ... }

Identity Role-based Authorization is not working

I have a custom implementation of the ASP.NET Identity base, using Dapper instead of Entity Framework largely from the tutorial here: http://blog.markjohnson.io/exorcising-entity-framework-from-asp-net-identity/.
Everything is fine with signing users in and out with my AuthenticationManager. However, as soon as I redirect anywhere after logging the user in, the httpcontext is basically null and the user is no longer authenticated. If I use the [Authorize] attribute as well, then the user is automatically declared as Unauthorized, throwing a 401 error.
Here are parts of my AccountController:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(Login login, string redundant)
{
var master = new MasterModel();
if (ModelState.IsValid && (!string.IsNullOrEmpty(login.Email) && !string.IsNullOrEmpty(login.PasswordHash)))
{
var user = await Models.User.FetchUserByEmail(login.Email);
if (user != null)
{
await SignInAsync(user, true);
master.User = user; // User is now signed in - No problem
return RedirectToAction("Overview", "Account", master);
}
}
TempData["Message"] = "Your username or password was not recognised. Please try again.";
return View(master);
}
[HttpGet]
//[Authorize(Roles = "admin,subscriber")] // 403 when uncommented
public ActionResult Overview(MasterModel master = null)
{
// master is just a blank new MasterModel ??
if (!HttpContext.User.Identity.IsAuthenticated)
{
// User is always null/blank identity
TempData["Message"] = "Please log in to view this content";
return RedirectToAction("Login", "Account", master);
}
var userName = string.IsNullOrEmpty(HttpContext.User.Identity.Name)
? TempData["UserName"].ToString()
: HttpContext.User.Identity.Name;
var user = Models.User.FetchUserByEmail(userName).Result;
if (master == null) master = new MasterModel();
master.User = user;
return View(master);
}
My UserStore implements the following interfaces:
public class UserStore : IUserStore<User>, IUserPasswordStore<User>, IUserSecurityStampStore<User>, IQueryableUserStore<User>, IUserRoleStore<User>
My RoleStore just implements IQueryableRoleStore<Role>
User and Role simply implement IUser and IRole respectively
What am I missing?
Update1:
Here's part of the AuthenticatonManager:
public IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private async Task SignInAsync(User user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Thanks to #WiktorZychla for pointing out the answer.
Turns out I was missing a fundamental step of adding the cookie authentication to IAppBuilder.
Here's how the OwinStartup.cs now looks for reference:
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
[assembly: OwinStartup(typeof(appNamespace.OwinStartup))]
namespace appNamespace
{
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
}
}
Hopefully this will save someone else from tearing their hair out!

Categories

Resources