yavsc/src/Yavsc/Controllers/Accounting/AccountController.cs

1157 lines
48 KiB
C#

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

using System.Web;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Yavsc.Models;
using Yavsc.ViewModels.Account;
using Yavsc.Helpers;
using Yavsc.Abstract.Manage;
using Yavsc.Interface;
using IdentityServer8.Test;
using IdentityServer8.Services;
using IdentityServer8.Stores;
using Microsoft.AspNetCore.Authentication;
using Yavsc.Models.Access;
using IdentityServer8.Models;
using Yavsc.Extensions;
using IdentityServer8.Events;
using IdentityServer8.Extensions;
using IdentityServer8;
using IdentityModel;
using System.Security.Cryptography;
using System.Text.Unicode;
using System.Text;
using Yavsc.Server.Helpers;
using System.Reflection;
namespace Yavsc.Controllers
{
public class AccountController : Controller
{
private const string nextPageTokenKey = "nextPageTokenKey";
private const int defaultLen = 10;
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ITrueEmailSender _emailSender;
// private readonly ISmsSender _smsSender;
private readonly ILogger _logger;
readonly SiteSettings _siteSettings;
readonly TwilioSettings _twilioSettings;
readonly IStringLocalizer _localizer;
// TwilioSettings _twilioSettings;
readonly ApplicationDbContext _dbContext;
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IEventService _events;
private readonly RoleManager<IdentityRole> _roleManager;
public AccountController(
RoleManager<IdentityRole> roleManager,
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IAuthenticationSchemeProvider schemeProvider,
IEventService events,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ITrueEmailSender emailSender,
IOptions<SiteSettings> siteSettings,
ILoggerFactory loggerFactory, IOptions<TwilioSettings> twilioSettings,
IStringLocalizerFactory localizerFactory,
ApplicationDbContext dbContext)
{
_interaction = interaction;
_clientStore = clientStore;
_schemeProvider = schemeProvider;
_events = events;
_roleManager = roleManager;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_siteSettings = siteSettings.Value;
_twilioSettings = twilioSettings.Value;
_logger = loggerFactory.CreateLogger<AccountController>();
_dbContext = dbContext;
_localizer = localizerFactory.Create(typeof(AccountController));
}
/// <summary>
/// Entry point into the login workflow
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (vm.IsExternalLoginOnly)
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
}
return View(vm);
}
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
var user = await _userManager.FindByNameAsync(model.Username);
if (user!=null) {
var signin = await _signInManager.CheckPasswordSignInAsync(user, model.Password, true);
// validate username/password against in-memory store
if (signin.Succeeded)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
await HttpContext.SignInAsync(user, _roleManager, model.RememberLogin,_dbContext);
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.Client.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
/// <summary>
/// Show logout page
/// </summary>
[HttpGet][Authorize]
public async Task<IActionResult> Logout(string logoutId)
{
if (string.IsNullOrWhiteSpace(logoutId))
{
if (User.Identity.IsAuthenticated)
{
logoutId = User.GetUserId();
}
}
// build a model so the logout page knows what to display
var vm = await BuildLogoutViewModelAsync(logoutId);
if (vm.ShowLogoutPrompt == false)
{
// if the request for logout was properly authenticated from IdentityServer, then
// we don't need to show the prompt and can just log the user out directly.
return await Logout(vm);
}
return View(vm);
}
/// <summary>
/// Handle logout page postback
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
[HttpGet]
public IActionResult AccessDenied()
{
return View();
}
/*****************************************/
/* helper APIs for the AccountController */
/*****************************************/
private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
{
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
{
var local = context.IdP == IdentityServer8.IdentityServerConstants.LocalIdentityProvider;
// this is meant to short circuit the UI and only trigger the one external IdP
var vm = new LoginViewModel
{
EnableLocalLogin = local,
ReturnUrl = returnUrl,
Username = context?.LoginHint,
};
if (!local)
{
vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } };
}
return vm;
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null)
.Select(x => new ExternalProvider
{
DisplayName = x.DisplayName ?? x.Name,
AuthenticationScheme = x.Name
}).ToList();
var allowLocal = true;
if (context?.Client.ClientId != null)
{
var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.ClientId);
if (client != null)
{
allowLocal = client.EnableLocalLogin;
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
{
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
}
}
}
return new LoginViewModel
{
AllowRememberLogin = AccountOptions.AllowRememberLogin,
EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
ReturnUrl = returnUrl,
Username = context?.LoginHint,
ExternalProviders = providers.ToArray()
};
}
private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
{
var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
vm.Username = model.Username;
vm.RememberLogin = model.RememberLogin;
return vm;
}
private async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt };
if (User?.Identity.IsAuthenticated != true)
{
// if the user is not authenticated, then just show logged out page
vm.ShowLogoutPrompt = false;
return vm;
}
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// it's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
return vm;
}
// show the logout prompt. this prevents attacks where the user
// is automatically signed out by another malicious web page.
return vm;
}
private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
if (User?.Identity.IsAuthenticated == true)
{
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServer8.IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
{
if (vm.LogoutId == null)
{
// if there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// before we signout and redirect away to the external IdP for signout
vm.LogoutId = await _interaction.CreateLogoutContextAsync();
}
vm.ExternalAuthenticationScheme = idp;
}
}
}
return vm;
}
[Authorize("AdministratorOnly")]
public IActionResult Index()
{
IViewComponentHelper h;
return View();
}
[Authorize("AdministratorOnly")]
[Route("Account/UserList/{pageNum?}/{len?}")]
public async Task<IActionResult> UserList(int pageNum=0, int pageLen = defaultLen)
{
var users = _dbContext.Users.OrderBy(u=>u.UserName);
var shown = pageNum * pageLen;
var toShow = users.Skip(shown).Take(pageLen);
ViewBag.page = pageNum;
ViewBag.hasNext = users.Count() > (toShow.Count() + shown);
ViewBag.nextpage = pageNum+1;
ViewBag.pageLen = pageLen;
return View(toShow.ToArray());
}
string GeneratePageToken() {
return System.Guid.NewGuid().ToString();
}
[AllowAnonymous]
[HttpGet(Constants.LoginPath)]
public ActionResult SignIn(string returnUrl = null)
{
// Note: the "returnUrl" parameter corresponds to the endpoint the user agent
// will be redirected to after a successful authentication and not
// the redirect_uri of the requesting client application against the third
// party identity provider.
return View(new SignInModel
{
ReturnUrl = returnUrl ?? "/",
});
/*
Note: When using an external login provider, redirect the query :
var properties = _signInManager.ConfigureExternalAuthenticationProperties(OpenIdConnectDefaults.AuthenticationScheme, returnUrl);
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, properties);
*/
}
[AllowAnonymous]
public ActionResult AccessDenied(string requestUrl = null)
{
ViewBag.UserIsSignedIn = User.Identity.IsAuthenticated;
if (string.IsNullOrWhiteSpace(requestUrl))
if (string.IsNullOrWhiteSpace(Request.Headers["Referer"]))
requestUrl = "/";
else requestUrl = Request.Headers["Referer"];
return View("AccessDenied", requestUrl);
}
[AllowAnonymous]
[HttpPost(Constants.LoginPath)]
public async Task<IActionResult> SignIn(SignInModel model)
{
if (Request.Method == "POST") // "hGbkk9B94NAae#aG"
{
if (model.Provider ==null || model.Provider == "LOCAL")
{
if (ModelState.IsValid)
{
var user = _dbContext.Users.Include(u=>u.Membership).FirstOrDefault(
u=>u.Email == model.EMail);
if (user != null)
{
if (!await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError(string.Empty,
"You must have a confirmed email to log in.");
return this.ViewOk(model);
}
}
else {
ModelState.AddModelError(string.Empty,
"No such user.");
return this.ViewOk(model);
}
// 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.EMail, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
// FIXME should be done at granting resources
await _userManager.AddClaimsAsync(user, user.Membership.Select(
m => new Claim(YavscClaimTypes.CircleMembership, m.CircleId.ToString())
));
return Redirect(model.ReturnUrl ?? "/");
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { returnUrl = model.ReturnUrl, rememberMe = model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning(2, "User account locked out.");
return this.ViewOk("Lockout");
}
else
{
ModelState.AddModelError(string.Empty, $"Invalid login attempt. ({model.EMail}, {model.Password})");
return this.ViewOk(model);
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError(string.Empty, "Unexpected behavior: something failed ... you could try again, or contact me ...");
}
else
{
// Note: the "provider" parameter corresponds to the external
// authentication provider choosen by the user agent.
if (string.IsNullOrEmpty(model.Provider))
{
_logger.LogWarning("Provider not specified");
return BadRequest();
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
// Note: the "returnUrl" parameter corresponds to the endpoint the user agent
// will be redirected to after a successful authentication and not
// the redirect_uri of the requesting client application.
if (string.IsNullOrEmpty(model.ReturnUrl))
{
_logger.LogWarning("ReturnUrl not specified");
return BadRequest();
}
// Note: this still is not the redirect uri given to the third party provider, at building the challenge.
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { model.ReturnUrl }, protocol:"https", host: Config.Authority);
var properties = _signInManager.ConfigureExternalAuthenticationProperties(model.Provider, redirectUrl);
// var properties = new AuthenticationProperties{RedirectUri=ReturnUrl};
return new ChallengeResult(model.Provider, properties);
}
}
return View(model);
}
//
// GET: /Account/Register
[AllowAnonymous]
[HttpGet]
public IActionResult Register()
{
return View();
}
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.UserName, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation(3, "User created a new account with password.");
await _emailSender.SendEmailAsync(Config.SiteSetup.Owner.Name, Config.SiteSetup.Owner.EMail,
$"[{_siteSettings.Title}] Inscription avec mot de passe: {user.UserName} ", $"{user.Id}/{user.UserName}/{user.Email}");
// TODO user.DiskQuota = Startup.SiteSetup.UserFiles.Quota;
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code }, protocol: "https", host: Config.Authority);
await _emailSender.SendEmailAsync(model.UserName, model.Email, _localizer["ConfirmYourAccountTitle"],
string.Format(_localizer["ConfirmYourAccountBody"], _siteSettings.Title, callbackUrl, _siteSettings.Slogan, _siteSettings.Audience));
// No, wait for more than a login pass submission:
// do not await _signInManager.SignInAsync(user, isPersistent: false);
this.NotifyInfo(
"E-mail confirmation",
_localizer["EmailSentForConfirm"]
);
return View("AccountCreated");
}
else {
_logger.LogError("Error registering from a valid model.");
foreach (var error in result.Errors)
{
_logger.LogError($"{error.Code} {error.Description}");
}
AddErrors(result);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
[Authorize, HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> SendConfirationEmail()
{
var user = await _userManager.FindByIdAsync(User.GetUserId());
var model = await SendEMailForConfirmAsync(user);
return View(model);
}
[Authorize("AdministratorOnly")]
public async Task<IActionResult> AdminSendConfirationEmail(string id)
{
var user = await _userManager.FindByIdAsync(id);
var model = await SendEMailForConfirmAsync(user);
return View(model);
}
private async Task<EmailSentViewModel> SendEMailForConfirmAsync(ApplicationUser user)
{
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, code }, protocol: "https", host: Config.Authority);
var res = await _emailSender.SendEmailAsync(user.UserName, user.Email,
this._localizer["ConfirmYourAccountTitle"],
string.Format(this._localizer["ConfirmYourAccountBody"],
_siteSettings.Title, callbackUrl, _siteSettings.Slogan,
_siteSettings.Audience));
return new EmailSentViewModel { EMail = user.Email, Sent = true, MessageId = res };
}
private async Task<EmailSentViewModel> SendEMailFactorAsync(ApplicationUser user, string provider)
{
var code = await _userManager.GenerateTwoFactorTokenAsync(user, provider);
var callbackUrl = Url.Action("VerifyCode", "Account",
new { userId = user.Id, code, provider }, protocol: "https", host: Config.Authority);
var res = await _emailSender.SendEmailAsync(user.UserName, user.Email,
this._localizer["AccountEmailFactorTitle"],
string.Format(this._localizer["AccountEmailFactorBody"],
_siteSettings.Title, callbackUrl, _siteSettings.Slogan,
_siteSettings.Audience, code));
return new EmailSentViewModel { EMail = user.Email, Sent = true, MessageId = res };;
}
//
// POST: /Account/LogOff
[HttpPost(Constants.LogoutPath)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LogOff(string returnUrl = null)
{
await _signInManager.SignOutAsync();
HttpContext.Session.Clear();
_logger.LogInformation(4, "User logged out.");
if (returnUrl == null) return RedirectToAction(nameof(HomeController.Index), "Home");
return Redirect(returnUrl);
}
//
// GET: /Account/ExternalLoginCallback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
_logger.LogWarning("No external provider info found.");
return Redirect("~/signin"); // RedirectToAction(nameof(OAuthController.SignIn));
}
// Sign in the user with this external login provider if the user already has a login.
throw new NotImplementedException();
// info.ProviderDisplayName = info.ExternalPrincipal.Claims.First(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
_logger.LogInformation(5, $"User logged in with {info.LoginProvider} provider, as {info.ProviderDisplayName} ({info.ProviderKey}).");
var ninfo = _dbContext.UserLogins.First(l => l.ProviderKey == info.ProviderKey && l.LoginProvider == info.LoginProvider);
ninfo.ProviderDisplayName = info.ProviderDisplayName;
_dbContext.Entry(ninfo).State = EntityState.Modified;
_dbContext.SaveChanges(User.GetUserId());
return Redirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe= true });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
ViewData["jsonres"] = JsonConvert.SerializeObject(result);
// 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.AuthenticationProperties.GetParameter<string>(ClaimTypes.Email);
var name = info.AuthenticationProperties.GetParameter<string>(ClaimTypes.Name);
var avatar = info.AuthenticationProperties.GetParameter<string>("urn:google:profile");
/* var phone = info.ExternalPrincipal.FindFirstValue(ClaimTypes.HomePhone);
var mobile = info.ExternalPrincipal.FindFirstValue(ClaimTypes.MobilePhone);
var postalcode = info.ExternalPrincipal.FindFirstValue(ClaimTypes.PostalCode);
var locality = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Locality);
var country = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Country);
foreach (var claim in info.ExternalPrincipal.Claims)
_logger.LogWarning("# {0} Claim: {1} {2}", info.LoginProvider, claim.Type, claim.Value);
*/
var access_token = info.AuthenticationProperties.GetParameter<string>("access_token");
var token_type = info.AuthenticationProperties.GetParameter<string>("token_type");
var expires_in = info.AuthenticationProperties.GetParameter<string>("expires_in");
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel
{
Email = email,
Name = name
});
}
}
//
// POST: /Account/ExternalLoginConfirmation
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
{
if (User.IsSignedIn())
{
return RedirectToAction(nameof(ManageController.Index), "Manage");
}
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Name, Email = model.Email };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
info.ProviderDisplayName = info.Principal.Claims.First(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
await _emailSender.SendEmailAsync(Config.SiteSetup.Owner.Name, Config.SiteSetup.Owner.EMail,
$"[{_siteSettings.Title}] Inscription via {info.LoginProvider}: {user.UserName} ", $"{user.Id}/{user.UserName}/{user.Email}");
_logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);
return Redirect(returnUrl);
}
}
AddErrors(result);
}
ViewData["ReturnUrl"] = returnUrl;
return View(model);
}
// GET: /Account/ConfirmEmail
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return View("Error");
}
IdentityResult result=null;
try {
result = await _userManager.ConfirmEmailAsync(user, code);
_dbContext.SaveChanges(userId);
}
catch (Exception ex)
{
_logger.LogError(ex.StackTrace);
_logger.LogError(ex.Message);
}
return View(result.Succeeded ? "EmailConfirmed" : "Error");
}
// GET: /Account/ConfirmTwoFactorToken
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmTwoFactorToken(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return View("Error");
}
bool result=false;
try {
result = await _userManager.VerifyTwoFactorTokenAsync(user, Constants.DefaultFactor, code);
_dbContext.SaveChanges(userId);
}
catch (Exception ex)
{
_logger.LogError(ex.StackTrace);
_logger.LogError(ex.Message);
}
return View(result ? "EmailConfirmed" : "Error");
}
//
// GET: /Account/ForgotPassword
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword()
{
if (User.Identity.IsAuthenticated)
ViewBag.UserEmail = ( await _dbContext.Users.SingleAsync(
u => u.Id == User.GetUserId()
) ).Email;
return View();
}
//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
ApplicationUser user;
// Username should not contain any '@'
if (model.LoginOrEmail.Contains('@'))
{
user = await _userManager.FindByEmailAsync(model.LoginOrEmail);
}
else
{
user = await _dbContext.Users.FirstOrDefaultAsync(u => u.UserName == model.LoginOrEmail);
}
// Don't reveal that the user does not exist or is not confirmed
if (user == null)
{
_logger.LogWarning($"ForgotPassword: Email or User name {model.LoginOrEmail} not found");
return View("ForgotPasswordConfirmation");
}
// We cannot require the email to be confimed,
// or a lot of non confirmed email never be able to finalyze
// registration.
if (!await _userManager.IsEmailConfirmedAsync(user))
{
_logger.LogWarning($"ForgotPassword: Email {model.LoginOrEmail} not confirmed");
// don't break this recovery process here ...
// or else e-mail won't ever be validated, since user lost his password.
// don't return View("ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var f = this.HttpContext.Features;
var callbackUrl = _siteSettings.ExternalUrl + "/Account/ResetPassword/" +
HttpUtility.UrlEncode(user.Id) + "/" + HttpUtility.UrlEncode(code);
var sent = await _emailSender.SendEmailAsync(user.UserName, user.Email, _localizer["Reset Password"],
_localizer["Please reset your password by "] + " <a href=\"" +
callbackUrl + "\" >following this link</a>");
return View("ForgotPasswordConfirmation", sent);
}
// If we got this far, something failed, redisplay form
return View(model);
}
//
// GET: /Account/ForgotPasswordConfirmation
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPasswordConfirmation()
{
return View();
}
// GET: /Account/ResetPassword
[HttpGet("/Account/ResetPassword/{id}/{code}")]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(string id, string code)
{
var user = await _userManager.FindByIdAsync(id);
if (user==null) return new BadRequestResult();
if (!await _userManager.VerifyUserTokenAsync(user,
_userManager.Options.Tokens.PasswordResetTokenProvider,
"ResetPassword", code.Replace("%2f","/")))
{
return BadRequest("code");
}
// We just serve the form to reset here.
return View(new ResetPasswordViewModel
{
Id = id,
Code = code,
Email = user.Email
});
}
// POST: /Account/ResetPassword
[HttpPost("/Account/ResetPassword/{id}/{code}")]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword([FromRoute] string id,
[FromRoute] string code,
ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account");
}
if (user.Id != id) return BadRequest("userid");
var result = await _userManager.ResetPasswordAsync(user,
code.Replace("%2f","/"), model.Password);
if (result.Succeeded)
{
// when ok, the e-mail become validated.
if (!user.EmailConfirmed) {
user.EmailConfirmed=true;
await _dbContext.SaveChangesAsync(nameof(ResetPassword));
}
_logger.LogInformation($"Password reset for {user.UserName}:{model.Password}");
return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account");
}
_logger.LogInformation($"Password reset failed for {user.UserName}:{model.Password}");
AddErrors(result);
return View();
}
//
// GET: /Account/ResetPasswordConfirmation
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPasswordConfirmation()
{
return View();
}
//
// GET: /Account/SendCode
[HttpGet, AllowAnonymous]
public async Task<ActionResult> SendCode(string returnUrl = null, bool rememberMe = true)
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
return View("Error", new Exception("No Two factor authentication user"));
}
var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user);
var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe });
}
//
// POST: /Account/SendCode
[HttpPost]
[ValidateAntiForgeryToken, AllowAnonymous]
public async Task<IActionResult> SendCode(SendCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View();
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
return View("Error", new Exception("user is null"));
}
// Generate the token and send it
if (model.SelectedProvider == Constants.MobileAppFactor)
{
return View("Error", new Exception("No mobile app service was activated"));
}
else
if (model.SelectedProvider == Constants.SMSFactor)
{
return View("Error", new Exception("No SMS service was activated"));
// await _smsSender.SendSmsAsync(_twilioSettings, await _userManager.GetPhoneNumberAsync(user), message);
}
else // if (model.SelectedProvider == Constants.EMailFactor || model.SelectedProvider == "Default" )
{
var sent = await this.SendEMailFactorAsync(user, model.SelectedProvider);
}
return View("VerifyCode", new VerifyCodeViewModel { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
}
//
// GET: /Account/VerifyCode
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> VerifyCode(string code, string provider, bool rememberMe=true, string returnUrl = null)
{
// Require that the user has already logged in via username/password or external login
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
return View("Error", new Exception("user is null"));
}
// it may be a GET response from some email url, or the web response to second factor requirement
return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe, Code = code });
}
//
// POST: /Account/VerifyCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> VerifyCode(VerifyCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// The following code protects for brute force attacks against the two factor codes.
// If a user enters incorrect codes for a specified amount of time then the user account
// will be locked out for a specified amount of time.
_logger.LogWarning("Signin with code: {0} {1}", model.Provider, model.Code);
var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberMe);
if (result.Succeeded)
{
ViewData["StatusMessage"] = "Your code was verified";
_logger.LogInformation($"Signed in. returning to {model.ReturnUrl}");
if (model.ReturnUrl!=null)
return Redirect(model.ReturnUrl);
else RedirectToAction("Index","Home");
}
if (result.IsLockedOut)
{
_logger.LogWarning(7, "User account locked out.");
return View("Lockout");
}
else
{
ModelState.AddModelError("Code", "Code invalide ");
return View(model);
}
}
[HttpGet, Authorize]
public IActionResult Delete()
{
return View(new UnregisterViewModel { UserId = User.GetUserId() });
}
[HttpGet, Authorize("AdministratorOnly")]
public IActionResult AdminDelete(string id, string? returnUrl=null)
{
return View(new UnregisterViewModel { UserId = id, ReturnUrl = returnUrl });
}
[HttpPost, Authorize("AdministratorOnly")]
public async Task<IActionResult> AdminDelete(UnregisterViewModel model)
{
var result = await DeleteUser(model.UserId);
if (!result.Succeeded)
{
AddErrors(result);
return new BadRequestObjectResult(ModelState);
}
return View();
}
[HttpPost, Authorize]
public async Task<IActionResult> Delete(UnregisterViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await DeleteUser(model.UserId);
if (!result.Succeeded)
{
AddErrors(result);
return new BadRequestObjectResult(ModelState);
}
await _signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}
async Task<IdentityResult> DeleteUser(string userId)
{
ApplicationUser user = await _userManager.FindByIdAsync(userId);
_dbContext.DeviceDeclaration.RemoveRange( _dbContext.DeviceDeclaration.Where(g => g.DeviceOwnerId == userId ));
return await _userManager.DeleteAsync(user);
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, _localizer[error.Code]);
}
}
}
}