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
}
Related
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.");
}
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");
}
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 ?
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;
}
}
This time I'm triying to set and get the whole information about the user at the FrontSide but I don't know whant i'm doing wrong
I have two separated projects the first one is the Webapi Project and I'm using it to SingIn the user giving then a token.
// GET api/Account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
if (!User.Identity.IsAuthenticated)
return new ChallengeResult(provider, this);
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
return InternalServerError();
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
AppJobSeeker user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await UserManager.CreateIdentityAsync(user, CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName, user.Id);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
And the client side is a MVC 5 Project where I have one method to postasyn the authentication and another one to Create the AuthTickect like this...
public async Task<T> AuthenticateAsync<T>(string userName, string password)
{
using (var client = new HttpClient())
{
var result = await client.PostAsync((#"http://localhost:8060/Token"), new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(#"grant_type", #"password"),
new KeyValuePair<string, string>(#"userName", userName),
new KeyValuePair<string, string>(#"password", password)
}));
string json = await result.Content.ReadAsStringAsync();
if (result.IsSuccessStatusCode)
return JsonConvert.DeserializeObject<T>(json);
throw new ApiException(result.StatusCode, json);
}
}
private void CreateTicket(SignInResult result, SignInModel model, string returnUrl)
{
//Let's keep the user authenticated in the MVC webapp.
//By using the AccessToken, we can use User.Identity.Name in the MVC controllers to make API calls.
FormsAuthentication.SetAuthCookie(result.AccessToken, model.RememberMe);
//Create an AuthenticationTicket to generate a cookie used to authenticate against Web API.
//But before we can do that, we need a ClaimsIdentity that can be authenticated in Web API.
Claim[] claims =
{
new Claim(ClaimTypes.Name, result.AccessToken), //Name is the default name claim type, and UserName is the one known also in Web API.
new Claim(ClaimTypes.Email, result.UserName), //If you want to use User.Identity.GetUserId in Web API, you need a NameIdentifier claim.
};
//Generate a new ClaimsIdentity, using the DefaultAuthenticationTypes.ApplicationCookie authenticationType.
//This also matches what we've set up in Web API.
AuthenticationTicket authTicket = new AuthenticationTicket(new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie), new AuthenticationProperties
{
ExpiresUtc = result.Expires,
IsPersistent = model.RememberMe,
IssuedUtc = result.Issued,
RedirectUri = returnUrl,
});
//HttpContext.Response..User = principal;
//And now it's time to generate the cookie data. This is using the same code that is being used by the CookieAuthenticationMiddleware class in OWIN.
byte[] userData = DataSerializers.Ticket.Serialize(authTicket);
//Protect this user data and add the extra properties. These need to be the same as in Web API!
byte[] protectedData = MachineKey.Protect(userData, new[] { "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", DefaultAuthenticationTypes.ApplicationCookie, "v1" });
//base64-encode this data.
string protectedText = TextEncodings.Base64Url.Encode(protectedData);
//And now, we have the cookie.
Response.SetCookie(new HttpCookie("JobSeekerAuth")
{
HttpOnly = true,
Expires = result.Expires.UtcDateTime,
Value = protectedText,
});
}
And my login method looks like
// POST: Account/SignIn
[HttpPost]
public async Task<ActionResult> Login(SignInModel model, string returnUrl)
{
if (!ModelState.IsValid)
return View(model);
try
{
CreateTicket(await WebApiService.Instance.AuthenticateAsync<SignInResult>(model.Email, model.Password), model, returnUrl);
return RedirectToLocal(returnUrl);
//return await WebApiService.Instance.AuthenticateAsync<SignInResult>(model.Email, model.Password) != null ? RedirectToLocal(returnUrl) : RedirectToLocal(returnUrl);
}
catch (ApiException ex)
{
//No 200 OK result, what went wrong?
HandleBadRequest(ex);
if (!ModelState.IsValid)
return View(model);
throw;
}
}
The problem is I want to use the GenericPrincipal at the Razor View two get the userId or the username fro the logged user and when I trying to doing so It gives me nothing more than the token here
#if (HttpContext.Current.User.Identity.IsAuthenticated)
{
<li>#Html.ActionLink("Sign Out", "SignOut", "Account")</li>
}
else
{...
So, I don't know how to get this goal
Best regards!...
My Authentication Method Works Well because once I go to the login method this one gives me my entity SignInResult wich looks like with all its value setted
[JsonProperty("access_token")]
public string AccessToken { get; set; }
//Included to show all the available properties, but unused in this sample
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public uint ExpiresIn { get; set; }
[JsonProperty("userName")]
public string UserName { get; set; }
[JsonProperty(".issued")]
public DateTimeOffset Issued { get; set; }
[JsonProperty(".expires")]
public DateTimeOffset Expires { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
I also tried to set to the Thread.CurrentPrincipal but not success
Best Regards