Entity Framework MFA with angular project - c#

I encounter the issue with MFA when I copy the MFA feature from my ASP.NET Core MVC project to a Angular project.
Here is how I generate the QR code for MFA:
[HttpGet]
public async Task<IActionResult> GetEnableTwoFactorCode()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new EnableTwoFactorDto();
await LoadSharedKeyAndQrCodeUriAsync(user, model);
return Ok(model);
}
This is when the user log in
[HttpPost("login")]
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
{
if (loginDto.Email != null)
{
var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName.ToLower() == loginDto.Email.ToLower());
if (user == null)
return Unauthorized("Invalid username");
var result = await _signInManager.CheckPasswordSignInAsync(user, loginDto.Password, false);
if (result.Succeeded)
{
return new UserDto
{
Email = user.Email,
FirstName = user.FirstName,
Token = await _tokenService.CreateToken(user),
};
}
if (result.RequiresTwoFactor)
{
return new UserDto
{
Email = user.Email,
FirstName = user.FirstName,
RequiresTwoFactor = true
};
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return Unauthorized("Locked out");
}
else
{
return Unauthorized();
}
}
else
{
return BadRequest("Empty email");
}
}
This is how I validate the MFA code when the user log in; the TwoFactorAuthenticatorSignInAsync will always return false even though I have enabled the MFA and my email address is confirmed.
[HttpPost]
public async Task<ActionResult<UserDto>> Authorize(LoginWithTwoFactorDto loginWithTwoFactorDto)
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var authenticatorCode = loginWithTwoFactorDto.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, loginWithTwoFactorDto.RememberMe, loginWithTwoFactorDto.RememberMachine);
if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
return new UserDto
{
Email = user.Email,
FirstName = user.FirstName,
Token = await _tokenService.CreateToken(user),
};
}
else if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return Unauthorized("Locked out");
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return Unauthorized("Invalid authenticator code.");
}
}
I have the same code in the MVC project and it works perfectly but not when I use Angular as front end and use the API call for logging in and activating the MFA.
I don't know if I did a bad practice here or the system build in MFA is only work in a MVC project ?

Related

ASP.NET Core External Identity Provider Login problem

I've tried to implement Google Login in my API, but the table "AspNetUserLogins" in SQL won't get populated.
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) {
return RedirectToAction(nameof(Login));
}
var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (signInResult.Succeeded)
{
return RedirectToLocal(returnUrl);
}
if (signInResult.IsLockedOut)
{
return RedirectToAction(nameof(ForgotPassword));
}
else
{
ViewData["ReturnUrl"] = returnUrl;
ViewData["Provider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginModel { Email = email });
}
}
On the "var signInResult", I get "failed" and I can't access the If statement. Can you please help me?
I've tried many different solutions, but none of them worked.
After testing, I found that Google and other three-party logins cannot be used like Asp.Net Core Identity, and can be used directly after modification.
When using Google provide.
we need add below code in controller
[HttpPost("google-login")]
public IActionResult GoogleLogin()
{
var properties = new AuthenticationProperties { RedirectUri = "/api/authentication/google-login-callback" };
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[HttpGet("google-login-callback")]
public async Task<IActionResult> GoogleLoginCallback()
{
var result = await HttpContext.AuthenticateAsync(GoogleDefaults.AuthenticationScheme);
if (!result.Succeeded)
{
return BadRequest("Failed to authenticate with Google.");
}
var user = new
{
Id = result.Principal.FindFirst(ClaimTypes.NameIdentifier).Value,
Email = result.Principal.FindFirst(ClaimTypes.Email).Value,
Name = result.Principal.FindFirst(ClaimTypes.Name).Value
};
// TODO: Create or update user account in your database.
return Ok(new { User = user });
}
2. And I replace the form tag in Login.cshtml like below.
3. Test Result
In Asp.Net Core Identity, we can change the code to api endpoint directly like below.
public async Task<IActionResult> LoginApi(string? returnUrl, InputModel? Input)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return Redirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Ok("Invalid login attempt.");
}
}
// If we got this far, something failed, redisplay form
return Ok("Invalid model.");
}

Azure AD authentication to existing ASP.NET Core Identity application

I currently have and application that is using Identity to authorize users. I need to change it to use Azure AD to login. After being authenticated through azure I need to use the information of the logged in user that we have in the identity database. After the user is authenticated I get a
NullReferenceException: Object reference not set to an instance of an object.
and fails at this point:
ApplicationUser user = await manager.FindByNameAsync(context.Principal.Identity.Name);
```
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication().AddOpenIdConnect(c =>
{
c.Authority = "https://login.microsoftonline.com/common";
c.ClientId = "<insert-registered-guid>";
c.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
c.Events.OnTokenValidated = async context =>
{
UserManager<ApplicationUser> manager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
SignInManager<ApplicationUser> signIn = context.HttpContext.RequestServices.GetService<SignInManager<ApplicationUser>>();
ApplicationUser user = await manager.FindByNameAsync(context.Principal.Identity.Name);
if (user != null)
{
await signIn.SignInAsync(user, false);
}
};
});
}
// HomeController.cs
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
public class HomeController : Controller
{
[AllowAnonymous]
public IActionResult LoginWithAzure()
{
string redirectUrl = Url.Content("~/");
return Challenge(new AuthenticationProperties { RedirectUri = redirectUrl }, OpenIdConnectDefaults.AuthenticationScheme);
}
}
```
UPDATE:
I was able to get past the error because I was missing
services.AddIdentity
Now the issue is that it gets stuck in a loop inside the OnTokenValidated.
UserManager<ApplicationUser> manager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
SignInManager<ApplicationUser> signIn = context.HttpContext.RequestServices.GetService<SignInManager<ApplicationUser>>();
ApplicationUser user = await manager.FindByNameAsync(context.Principal.Identity.Name);
if (user != null)
{
await signIn.SignInAsync(user, false);
}
after the if statement it goes back to the manager line.
The above solution was not working so I changed it.
Startup.cs was changed to the following:
// Add Azure AD authentication
services.AddAuthentication(defaultScheme: AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
AccountController.cs was changed to this:
[AllowAnonymous]
[HttpGet]
public ChallengeResult InternalSignIn(string returnUrl = "/")
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback));
var properties = signInManager.ConfigureExternalAuthenticationProperties(AzureADDefaults.AuthenticationScheme, redirectUrl);
return new ChallengeResult(AzureADDefaults.AuthenticationScheme, properties);
}
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
var info = await signInManager.GetExternalLoginInfoAsync();
if (info is null)
{
return BadRequest();
}
var signInResult = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: false);
var email = info.Principal.FindFirstValue(ClaimTypes.Name);
var user = await userManager.FindByEmailAsync(email);
IdentityResult result;
if (user != null)
{
var logins = await userManager.GetLoginsAsync(user);
if (!logins.Any())
{
result = await userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
return View();
}
}
await signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction(nameof(HomeController.Index),"Home");
}
return StatusCode(500, "Internal server error");
}

Integrate authorization(eg: Facebook) claims, authentication tokens with internal user account

In my application, the user would login into the application and then authorize facebook(by clicking a button in a secure area) to allow the application to get facebook posts etc.
Here's the code that I have written to save the claims and authentication tokens and connect those to the internal user account.
Am I following the right approach? If not, would it be easier to directly insert records into the AspNetUserClaims, AspNetUserTokens tables for claims, authentication tokens, using Entity Framework core.
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Settings", new { ReturnUrl = returnUrl });
}
// Get the information about the user from the external login provider
var identityUser = await UserManager.GetUserAsync(User);
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
if (externalLoginInfo == null)
{
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Settings", new { ReturnUrl = returnUrl });
}
var applicationUser = new ApplicationUser { UserName = identityUser.Email, Email = identityUser.Email , SecurityStamp= Guid.NewGuid().ToString()};
var identityResult = await this.UserManager.AddLoginAsync(applicationUser, externalLoginInfo);
if (identityResult.Succeeded)
{
if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
{
await this.UserManager.AddClaimAsync(applicationUser, externalLoginInfo.Principal.FindFirst(ClaimTypes.GivenName));
}
identityResult = await SignInManager.UpdateExternalAuthenticationTokensAsync(externalLoginInfo);
var authenticationProperties = new AuthenticationProperties();
authenticationProperties.StoreTokens(externalLoginInfo.AuthenticationTokens);
//authenticationProperties.IsPersistent = true;
#endregion
return LocalRedirect(returnUrl);
}
foreach (var error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
LoginProvider = externalLoginInfo.LoginProvider;
ReturnUrl = returnUrl;
return Page();
}
#endregion
}

System.ObjectDisposedException only occuers some times others it work fine

In my aproch to keep track of users actions in a web app, I am stuck in this error that occuers only when a wrong password is enter and I canĀ“t discover how to solve it.
The code breaks after the statement _context.UsersActivity.Add(this); and only when user enter a wrong password and this append only after I move the method "SaveUserActivity" from the controller to the class, before i wrote it in the controller and work fine.
Any help will be deligth.
Code from my Controller:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
await SaveUserActivity(model);
return RedirectToLocal("~/Users/Index");
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
{
if (await IfUserExist(model))
PasswordFailure(model);
ModelState.AddModelError(string.Empty, "Log In Failure.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
private async Task SaveUserActivity(LoginViewModel model, [CallerMemberName] string callerName = "")
{
string Action = null;
var RegActivity = new UserActivity();
switch (callerName)
{
case "Login":
Action = "Log In";
break;
case "IfUserExist":
Action = "Log In attempt from no exist user.";
break;
case "PasswordFailure":
Action = "Log In attempt with wrong password.";
break;
}
await RegActivity.SaveUserActivity(_context, _userManager, model.UserName, Action);
}
private async Task<bool> IfUserExist(LoginViewModel model)
{
var LoginAttemp = await _userManager.FindByNameAsync(model.UserName);
if (LoginAttemp == null)
{
ViewData["Page"] = new PageSettings("AccountControllerLoginPostInvalidUser");
await SaveUserActivity(model);
return false;
}
return true;
}
private async void PasswordFailure(LoginViewModel model)
{
ViewData["Page"] = new PageSettings("AccountControllerLoginPostInvalidPass");
await SaveUserActivity(model);
}
The code from my class that saves the users activity is this one:
public async Task SaveUserActivity(SiteDbContext _context, UserManager<SiteUser> _userManager, string _UserName, string _Action)
{
var user = await _userManager.FindByNameAsync(_UserName);
var status = (user == null || _Action.Contains("password"));
var roles = (status) ? null : await _userManager.GetRolesAsync(user);
UserName = _UserName;
Roles = (status) ? null : roles;
TimeStamp = DateTime.Now;
Action = _Action;
Role = (status) ? null : TransformListOfRolesToString();
_context.UsersActivity.Add(this);
_context.SaveChanges();
}

SignInManager.SignIn with non-existing user

I just created a ASP.MVC project and want to provide temporary login (let's call it guest login). This should not require a registration and I want to avoid the creation of a database entry for this user.
I extended the default Login-action of the AccountController by:
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
return View(model);
if (model.GuestLogin) {
var user = UserManager.FindByEmail(model.Email);
if (user != null) {
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
var appUser = new ApplicationUser() { Email = model.Email, UserName = model.Email, Id = Guid.NewGuid().ToString() };
//var creationResult = UserManager.Create(appUser);
SignInManager.SignIn(appUser, false, model.RememberMe);
return RedirectToLocal(returnUrl);
}
//common login stuff ...
}
The SignIn-call fails with:System.InvalidOperationException: UserId not found.
Is it possible to archive this somehow, or not?
Thanks in advance
Okay,
the devil lies in the ClaimsIdentityFactory, which tries to add some user entity related claims (e.g. user role or security stamp claims) during sign in process.
By preventing this, sign in with non-existing user seems to be no problem anymore.
This is my claims factory class:
public class MyClaimsFactory : ClaimsIdentityFactory<MyUser, string>
{
public override async Task<ClaimsIdentity> CreateAsync(UserManager<MyUser, string> manager, MyUser user, string authenticationType)
{
if (manager == null)
throw new ArgumentNullException("manager");
if (user == null)
throw new ArgumentNullException("user");
ClaimsIdentity claimsIdentity = new ClaimsIdentity(authenticationType, this.UserNameClaimType, this.RoleClaimType);
claimsIdentity.AddClaim(new Claim(this.UserIdClaimType, this.ConvertIdToString(user.Id), "http://www.w3.org/2001/XMLSchema#string"));
claimsIdentity.AddClaim(new Claim(this.UserNameClaimType, user.UserName, "http://www.w3.org/2001/XMLSchema#string"));
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"));
if (!user.IsGuest)
{
if (manager.SupportsUserSecurityStamp)
claimsIdentity.AddClaim(new Claim(this.SecurityStampClaimType, await manager.GetSecurityStampAsync(user.Id).WithCurrentCulture<string>()));
if (manager.SupportsUserRole)
{
IList<string> list = await manager.GetRolesAsync(user.Id).WithCurrentCulture<IList<string>>();
foreach (string current in list)
{
claimsIdentity.AddClaim(new Claim(this.RoleClaimType, current, "http://www.w3.org/2001/XMLSchema#string"));
}
}
if (manager.SupportsUserClaim)
claimsIdentity.AddClaims(await manager.GetClaimsAsync(user.Id).WithCurrentCulture<IList<Claim>>());
}
return claimsIdentity;
}
}

Categories