Identity Framework User Lockdown - c#

I'm trying to lock user login after 3 unsuccessful login attempts for 5 minutes. I have add this 3 lines to App_Start/IdentityConfig.cs public static ApplicationUserManager Create( ... ) method:
manager.MaxFailedAccessAttemptsBeforeLockout = 3;
manager.DefaultAccountLockoutTimeSpan = new TimeSpan(0, 5, 0);
manager.UserLockoutEnabledByDefault = true;
After that I register new user via POST /api/Account/Register (in default scaffolded AccountController). Account is created and LockoutEnabled property is set to true. But if I try to login for via POST /Token few times with wrong password account isn't locked down.
I'm also interested where is implementation of /Token endpoint. Is it in AccountController GET api/Account/ExternalLogin. I have set breakpoint there but execution wasn't stopped there when I tried to login.
What am I missing?

If you are using the default Web API template from Visual Studio, you have to change the behavior of GrantResourceOwnerCredentials method of the ApplicationOAuthProvider class (found inside the Provider folder of your Web API project). Something like this could allow you to track failed login attempts, and stop locked out users from logging in:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var user = await userManager.FindByNameAsync(context.UserName);
if (user == null)
{
context.SetError("invalid_grant", "Wrong username or password."); //user not found
return;
}
if (await userManager.IsLockedOutAsync(user.Id))
{
context.SetError("locked_out", "User is locked out");
return;
}
var check = await userManager.CheckPasswordAsync(user, context.Password);
if (!check)
{
await userManager.AccessFailedAsync(user.Id);
context.SetError("invalid_grant", "Wrong username or password."); //wrong password
return;
}
await userManager.ResetAccessFailedCountAsync(user.Id);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
Be aware that this way you can only lock out users trying to login using the password grant (Resource Owner Credentials). If you also want to disallow locked out user to login using other grants, you have to override the other methods (GrantAuthorizationCode, GrantRefreshToken, etc.), checking if await userManager.IsLockedOutAsync(user.Id) is true in those methods too.

Related

How to implement a new policy in Razor Pages with addAuthorization?

Objective:
Once logged in, be authorized to reroute to Contact page which is authorized with the
Must Belong To HR Department policy.
(Note: Using razor pages)
Expected:
Log in, see the claims in user for HR department, have the policy requirements be read, allow the user access to the contact page.
Actual:
Log in successfully, see claims in user for HR department, policy requirements not read or met or something, denied the user access to the contact page.
Program.cs file:
//Authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("MustBelongToHRDepartment",
policy => policy.RequireClaim("Department", "HR"));
});
////RazorPage Options
builder.Services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/contact", "MustBelongToHRDepartment");
});
Login page:
public async Task<IActionResult> OnPostAsync(User user)
{
var result = await _signInManager.PasswordSignInAsync(user.username,
user.password, user.rememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
//Create the security context
var claims = new List<Claim> {
new Claim(ClaimTypes.Name, "admin"),
new Claim(ClaimTypes.Email, "admin#mywebsite.com"),
new Claim("Department", "HR")
};
var identity = new ClaimsIdentity(claims, "MyCookieAuth");
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("MyCookieAuth", claimsPrincipal);
return RedirectToPage("contact");
}
else
{
return Page();
}
}
Is there anything I'm missing? Format? Silly error?
I have tried docs, YouTube videos, website, asking friends, and searching forums.
I have tried different methods of authorization and such, but nothing seems to work.
This method I am using now should work according to documentation, but I know something is wrong with it.
Resources I have been using:
This is a YouTube video on register and login
docs
docs
var IdeUser = await _userManager.FindByNameAsync(user.username);
await _userManager.AddClaimsAsync(IdeUser, claims);
var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(IdeUser);
await _signInManager.RefreshSignInAsync(IdeUser);
I added this and it worked. I think claims weren't going to userManager.

How to get UserID from ASP.NET Identity when sending Rest Request

I have an API which uses ASP.NET Identity, it is fairly easy for me to get the UserId once the token has been generated as following
HttpContext.Current.User.Identity.GetUserId().ToString())
Now I have an external application which is trying to authenticate using this API but I need the UserId of the user who generated the token
When I send a request to http://myapiURL/token I get the following
access_token
token_type
expires_in
userName
issued
expires
And when I send a request to get API/Account/UserInfo using the generated token I get the following
Email
HasRegistered
LoginProvider
Question How do I get UserId?
I have two options,
A. I modify UserInfoViewModel GetUserInfo() to have UserId in UserInfoViewModel?
B. I create a new method in ApiController such as GetUserId (API/Account/GetUserId) which runs HttpContext.Current.User.Identity.GetUserId().ToString()) and sends back the
UserId
Is there any other way?
Cheers
I believe you want UserId in the response of /Token.
By default Identity does not add UserId in response.
so you need to add it manually in ApplicationOAuthProvider.cs in method GrantResourceOwnerCredentials
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
ticket.Properties.Dictionary.Add("UserId", user.Id);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}

ASN.NET Core 2.0 Facebook authentication ExternalLoginSignInAsync Fails (IsNotAllowed)

I am creating a new application in ASP.NET Core 2.0 with Ddentity. I have enabled the Facebook Authentication according to the docs.
The flow that is running right now is the following:
When a new user authenticates from facebook the applicatiion asks for an email in order to be used for the application
When the user provides the email a user is created in my dbo.AspNetUsers table with the email provided by the user and a new line is created in my dbo.AspNetUserLogins with the reference to the provider, the providerKey and the userId.
At that point I can see that in my database all rows have correct data.
Now when the user logs out and tries to login again with facebook at some point the application checks if the user can login with the providers credentials through the:
// 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, bypassTwoFactor: true);
At that point my result comes as NotAllowed (attribute IsNotAllowed = true) and it asks the user to enter a new email (Like the original flow for a new user). Of course then the email is already registered and the user can't create a new account as he shouldn't have to.
My controller for the ExternalLoginCallback is the default implementation of the project.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// 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, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
Can anyone help me find what am I missing or what should I see in order to get the answer to this? Thanks you in advance.
IsNotAllowed is set to true when you have set either RequireConfirmedEmail or RequireConfirmedPhoneNumber to true on SignInOptions (usually configured as part of AddIdentity) but the email address or phone number has not been confirmed.
If you don't want to require a confirmed email or phone number for external logins, you can set the relevant properties when creating your IdentityUser derived class after its first creation. e.g.:
var identityUser = new IdentityUser
{
// ...
EmailConfirmed = true,
PhoneNumberConfirmed = true
};
Otherwise, if you do want the verification process to occur, you can follow the guide in the ASP.NET Core docs for the email address side of things.
You can see how all this works in the source code. You end up in PreSignInCheck:
if (!await CanSignInAsync(user))
{
return SignInResult.NotAllowed;
}
The CanSignInAsync implementation looks like this (with logging removed):
public virtual async Task<bool> CanSignInAsync(TUser user)
{
if (Options.SignIn.RequireConfirmedEmail &&
!(await UserManager.IsEmailConfirmedAsync(user)))
{
return false;
}
if (Options.SignIn.RequireConfirmedPhoneNumber &&
!(await UserManager.IsPhoneNumberConfirmedAsync(user)))
{
return false;
}
return true;
}

WebApi oauth from latest Visual Studio webapi template not completing processing after successful login

Using the latest Visual Studio WebApi template with Individual accounts enabled I have configured 3 external providers - twitter, facebook & google for oauth authenticaton.
All three are getting to the stage where they authenticate with the 3rd party provider and are redirected to the correct url which causes the following code to execute -
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
// this code is executed
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
After completing this code the user is redirected to the url below and processing stops.
https://localhost:44301/#access_token=Qw5g_ZlGV1
I have watched the video https://channel9.msdn.com/Shows/Web+Camps+TV/Securing-ASPNET-Web-APIs (33 mins 20 secs) which shows that the next step is missing - a redirect to the register page where the user is able to configure & register themselves.
Does anyone know what is missing here?
Thanks

Always receiving 'invalid_client' error when POSTing to /Token endpoint with ASP Identity 2

About a month ago I had a project working perfectly with ASP Identity OAuth. I'd send a POST request to the /Token endpoint with grant_type, username, and password, and all was dandy.
I recently started a new project based off of Visual Studio 2013 RC2's SPA template. It's a bit different than the old template. Authentication is set up to pretty basic defaults,
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
//AuthorizeEndpointPath = new PathString("/Account/Authorize"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
Nothing significant changed from the default template. I can register accounts successfully through a Web API controller method I have implemented;
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (ModelState.IsValid)
{
var user = new TunrUser() { UserName = model.Email, Email = model.Email, DisplayName = model.DisplayName };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
return Created(new Uri("/api/Users/" + user.Id,UriKind.Relative), user.toViewModel());
}
else
{
return BadRequest(result.Errors.First());
}
}
return BadRequest(ModelState);
}
However, no matter what I POST to the /Token endpoint, I always get the same response.
{"error":"invalid_client"}
Normally I pass the following request body
grant_type=password&username=user%40domain.com&password=userpassword
But this results in the same error. This worked in the previous VS2013 SPA template / Identity. What's changed?
Thank you!
You have to Override the ValidateClientAuthentication & GrantResourceOwnerCredentials in the OAuthAuthorizationServerProvider.
See example here:
http://www.tugberkugurlu.com/archive/simple-oauth-server-implementing-a-simple-oauth-server-with-katana-oauth-authorization-server-components-part-1
So it turns out that the new templates don't include a functional implementation of ApplicationOAuthProvider that was present in the older templates.
After watching this build talk, I investigated further and found that a working implementation of ApplicationOAuthProvider is available to check out in this NuGet package! It's very similar to the old implementation.
In addition, you can use the ApplicationOAuthProvider class that comes with the WebApi template when Individual User Accounts is chosen as the Security option. However, you'll have to change a couple other things, which I've listed below. I hope it helps.
The ApplicationOAuthProvider class that comes with the WebApi/Individual User Accounts template contains the following method:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
Copy this to the ApplicationOAuthProvider class in your SPA template project, overwriting the original method. The code user.GenerateUserIdentityAsync method is invalid when copied to the SPA template project because the ApplicationUser class does not allow for the "bearer" authentication type.
Add an overload similar to the following to the ApplicationUser class (find it in the Models\IdentityModels.cs file):
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager , string authenticationType)
{
var userIdentity = await manager.CreateIdentityAsync(this , authenticationType);
// Add custom user claims here
return userIdentity;
}
You should now be able to use /Token endpoint correctly.

Categories