added support for Forgot Username and Use Login Link

This commit is contained in:
sbwalker
2025-12-14 15:13:53 -05:00
parent 6b883b3f94
commit ec2afd5f03
11 changed files with 500 additions and 124 deletions

View File

@ -296,12 +296,24 @@ namespace Oqtane.Controllers
// POST api/<controller>/forgot
[HttpPost("forgot")]
public async Task Forgot([FromBody] User user)
public async Task<User> Forgot([FromBody] User user)
{
if (ModelState.IsValid)
{
await _userManager.ForgotPassword(user);
return await _userManager.ForgotPassword(user);
}
return null;
}
// POST api/<controller>/forgotusername
[HttpPost("forgotusername")]
public async Task<User> ForgotUsername([FromBody] User user)
{
if (ModelState.IsValid)
{
return await _userManager.ForgotUsername(user);
}
return null;
}
// POST api/<controller>/reset
@ -559,5 +571,16 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// POST api/<controller>/loginlink
[HttpPost("loginlink")]
public async Task<User> SendLoginLink([FromBody] User user)
{
if (ModelState.IsValid)
{
return await _userManager.SendLoginLink(user);
}
return null;
}
}
}

View File

@ -4,10 +4,8 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using Oqtane.Enums;
@ -30,7 +28,8 @@ namespace Oqtane.Managers
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
Task LogoutUserEverywhere(User user);
Task<User> VerifyEmail(User user, string token);
Task ForgotPassword(User user);
Task<User> ForgotPassword(User user);
Task<User> ForgotUsername(User user);
Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token);
Task<UserValidateResult> ValidateUser(string username, string email, string password);
@ -42,6 +41,7 @@ namespace Oqtane.Managers
Task<List<UserLogin>> GetLogins(int userId, int siteId);
Task<User> AddLogin(User user, string token, string type, string key, string name);
Task DeleteLogin(int userId, string provider, string key);
Task<User> SendLoginLink(User user);
}
public class UserManager : IUserManager
@ -519,14 +519,16 @@ namespace Oqtane.Managers
}
return user;
}
public async Task ForgotPassword(User user)
public async Task<User> ForgotPassword(User user)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var alias = _tenantManager.GetAlias();
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
var alias = _tenantManager.GetAlias();
user = GetUser(user.Username, alias.SiteId);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotPasswordEmailSubject"];
@ -537,11 +539,51 @@ namespace Oqtane.Managers
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
return null;
}
}
public async Task<User> ForgotUsername(User user)
{
try
{
IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email);
if (identityuser != null)
{
var alias = _tenantManager.GetAlias();
user = GetUser(identityuser.UserName, alias.SiteId);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username;
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotUsernameEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["ForgotUsernameEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Forgot Username Notification Sent For {Email}", user.Email);
return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email);
return null;
}
}
catch
{
// email may not be unique
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email);
return null;
}
}
@ -588,6 +630,7 @@ namespace Oqtane.Managers
}
return user;
}
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
var validateResult = new UserValidateResult { Succeeded = true };
@ -914,5 +957,49 @@ namespace Oqtane.Managers
}
}
}
public async Task<User> SendLoginLink(User user)
{
try
{
IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email);
if (identityuser != null)
{
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
var alias = _tenantManager.GetAlias();
user = GetUser(identityuser.UserName, alias.SiteId);
user.TwoFactorCode = token;
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
_users.UpdateUser(user);
string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["LoginLinkEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["LoginLinkEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email);
return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email);
return null;
}
}
catch
{
// email may not be unique
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email);
return null;
}
}
}
}

View File

@ -0,0 +1,3 @@
@page "/pages/loginlink"
@namespace Oqtane.Pages
@model Oqtane.Pages.LoginLinkModel

View File

@ -0,0 +1,69 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Enums;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Shared;
namespace Oqtane.Pages
{
[AllowAnonymous]
public class LoginLinkModel : PageModel
{
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly IUserManager _userManager;
private readonly ILogManager _logger;
public LoginLinkModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager, ILogManager logger)
{
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> OnGetAsync(string name, string token)
{
var returnurl = "/login";
if (bool.Parse(HttpContext.GetSiteSettings().GetValue("LoginOptions:LoginLink", "false")) &&
!User.Identity.IsAuthenticated && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token))
{
var validuser = false;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name);
if (identityuser != null)
{
var user = _userManager.GetUser(identityuser.UserName, HttpContext.GetAlias().SiteId);
if (user != null && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{
await _identitySignInManager.SignInAsync(identityuser, false);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name);
validuser = true;
returnurl = "/";
}
}
if (!validuser)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name);
returnurl += $"?status={ExternalLoginStatus.LoginLinkFailed}";
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name);
returnurl = "/";
}
return LocalRedirect(Url.Content("~" + returnurl));
}
}
}

View File

@ -121,7 +121,19 @@
<value>Dear [UserDisplayName]&lt;br&gt;&lt;br&gt;You recently requested to reset your password. Please use the link below to complete the process: &lt;b&gt;&lt;a href="[URL]"&gt;&lt;br&gt;&lt;br&gt;Click here to Reset Password&lt;/a&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;Please note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site.&lt;br&gt;&lt;br&gt;If you did not request to reset your password you can safely ignore this message.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>
</data>
<data name="ForgotPasswordEmailSubject" xml:space="preserve">
<value>Password Reset Notification Sent For [SiteName]</value>
<value>Password Reset Notification For [SiteName]</value>
</data>
<data name="ForgotUsernameEmailBody" xml:space="preserve">
<value>Dear [UserDisplayName]&lt;br&gt;&lt;br&gt;You recently requested a username reminder. Please use the link below to complete the process: &lt;b&gt;&lt;a href="[URL]"&gt;&lt;br&gt;&lt;br&gt;Click here to Login&lt;/a&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;If you did not request a username reminder you can safely ignore this message.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>
</data>
<data name="ForgotUsernameEmailSubject" xml:space="preserve">
<value>Forgotten Username Reminder For [SiteName]</value>
</data>
<data name="LoginLinkEmailBody" xml:space="preserve">
<value>Dear [UserDisplayName]&lt;br&gt;&lt;br&gt;You recently requested a login link. Please use the link below to complete the process: &lt;b&gt;&lt;a href="[URL]"&gt;&lt;br&gt;&lt;br&gt;Click here to Login&lt;/a&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;Please note that the link is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate another login link request on the site.&lt;br&gt;&lt;br&gt;If you did not request a login link you can safely ignore this message.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>
</data>
<data name="LoginLinkEmailSubject" xml:space="preserve">
<value>Login Link Notification For [SiteName]</value>
</data>
<data name="NoVerificationEmailBody" xml:space="preserve">
<value>Dear [UserDisplayName],&lt;br&gt;&lt;br&gt;A user account has been successfully created for you with the username &lt;b&gt;[Username]&lt;/b&gt;. Please &lt;b&gt;&lt;a href="[URL]"&gt;click here to login&lt;/a&gt;&lt;/b&gt;. If you do not know your password, use the forgot password option on the login page to reset your account.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>