add passkey and login management to User Management

This commit is contained in:
sbwalker
2025-10-30 11:08:56 -04:00
parent adfd870319
commit ab4bc7e678
7 changed files with 280 additions and 115 deletions

View File

@ -143,15 +143,15 @@ else
if (PageState.QueryString.ContainsKey("key"))
{
user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
user = await UserService.AddLoginAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
await logger.LogInformation(LogFunction.Security, "User Login Linkage Successful For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Linked"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "External Login Linkage Failed For Username {Username}", _username);
await logger.LogError(LogFunction.Security, "User Login Linkage Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
}
_username = "";

View File

@ -654,7 +654,7 @@
{
if (_allowpasskeys)
{
_passkeys = await UserService.GetPasskeysAsync();
_passkeys = await UserService.GetPasskeysAsync(PageState.User.UserId);
}
}
@ -709,7 +709,7 @@
private async Task DeletePasskey(UserPasskey passkey)
{
await UserService.DeletePasskeyAsync(passkey.CredentialId);
await UserService.DeletePasskeyAsync(PageState.User.UserId, passkey.CredentialId);
await GetPasskeys();
StateHasChanged();
}
@ -718,7 +718,7 @@
{
if (!string.IsNullOrEmpty(_passkeyName))
{
await UserService.UpdatePasskeyAsync(new UserPasskey { CredentialId = _passkeyId, Name = _passkeyName });
await UserService.UpdatePasskeyAsync(new UserPasskey { CredentialId = _passkeyId, Name = _passkeyName, UserId = PageState.User.UserId });
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
@ -736,13 +736,13 @@
{
if (_allowexternallogin)
{
_logins = await UserService.GetLoginsAsync();
_logins = await UserService.GetLoginsAsync(PageState.User.UserId);
}
}
private async Task DeleteLogin(UserLogin login)
{
await UserService.DeleteLoginAsync(login.Provider, login.Key);
await UserService.DeleteLoginAsync(PageState.User.UserId, login.Provider, login.Key);
await GetLogins();
StateHasChanged();
}

View File

@ -103,6 +103,53 @@
</div>
</div>
</div>
<br /><br />
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
@if (_passkeys != null && _passkeys.Count > 0)
{
<Pager Items="@_passkeys">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Passkey"]</th>
</Header>
<Row>
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeletePasskey(context))" ResourceKey="DeletePasskey" Class="btn btn-danger" Header="Delete Passkey" Message="@string.Format(Localizer["Confirm.Passkey.Delete", context.Name])" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Login"]</th>
</Header>
<Row>
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeleteLogin(context))" ResourceKey="DeleteLogin" Class="btn btn-danger" Header="Delete Login" Message="@string.Format(Localizer["Confirm.Login.Delete", context.Name])" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />
}
</TabPanel>
<TabPanel Name="Profile" Heading="Profile" ResourceKey="Profile">
<div class="container">
@ -173,24 +220,30 @@
}
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private bool _allowpasskeys = false;
private bool _allowexternallogin = false;
private int _userid;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private string _isdeleted;
private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private string _passwordrequirements;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private List<UserPasskey> _passkeys;
private List<UserLogin> _logins;
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
private string _category = string.Empty;
@ -208,19 +261,8 @@
{
try
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_timezones = TimeZoneService.GetTimeZones();
_allowpasskeys = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false") == "true");
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{
@ -232,13 +274,30 @@
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
_isdeleted = user.IsDeleted.ToString();
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
_settings = user.Settings;
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
await GetPasskeys();
await GetLogins();
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_settings = user.Settings;
_createdby = user.CreatedBy;
_createdon = user.CreatedOn;
_modifiedby = user.ModifiedBy;
@ -358,6 +417,35 @@
}
}
private async Task GetPasskeys()
{
if (_allowpasskeys)
{
_passkeys = await UserService.GetPasskeysAsync(_userid);
}
}
private async Task DeletePasskey(UserPasskey passkey)
{
await UserService.DeletePasskeyAsync(_userid, passkey.CredentialId);
await GetPasskeys();
StateHasChanged();
}
private async Task GetLogins()
{
if (_allowexternallogin)
{
_logins = await UserService.GetLoginsAsync(_userid);
}
}
private async Task DeleteLogin(UserLogin login)
{
await UserService.DeleteLoginAsync(_userid, login.Provider, login.Key);
await GetLogins();
StateHasChanged();
}
private bool ValidateProfiles()
{
foreach (Profile profile in _profiles)

View File

@ -222,4 +222,34 @@
<data name="Security.Heading" xml:space="preserve">
<value>Security</value>
</data>
<data name="Passkeys.Heading" xml:space="preserve">
<value>Passkeys</value>
</data>
<data name="Logins.Heading" xml:space="preserve">
<value>Logins</value>
</data>
<data name="Passkey" xml:space="preserve">
<value>Passkey</value>
</data>
<data name="Login" xml:space="preserve">
<value>Login</value>
</data>
<data name="DeletePasskey.Header" xml:space="preserve">
<value>Delete Passkey</value>
</data>
<data name="DeleteLogin.Header" xml:space="preserve">
<value>Delete Login</value>
</data>
<data name="Confirm.Passkey.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="Confirm.Login.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="Message.Passkeys.None" xml:space="preserve">
<value>You Have Not Created Any Passkeys</value>
</data>
<data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value>
</data>
</root>

View File

@ -147,17 +147,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<string> GetPersonalAccessTokenAsync();
/// <summary>
/// Link an external login with a local user account
/// </summary>
/// <param name="user">The <see cref="User"/> we're verifying</param>
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
/// <param name="type">External Login provider type</param>
/// <param name="key">External Login provider key</param>
/// <param name="name">External Login provider display name</param>
/// <returns></returns>
Task<User> LinkUserAsync(User user, string token, string type, string key, string name);
/// <summary>
/// Get password requirements for site
/// </summary>
@ -177,8 +166,9 @@ namespace Oqtane.Services
/// <summary>
/// Get passkeys for a user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<List<UserPasskey>> GetPasskeysAsync();
Task<List<UserPasskey>> GetPasskeysAsync(int userId);
/// <summary>
/// Update a user passkey
@ -190,23 +180,37 @@ namespace Oqtane.Services
/// <summary>
/// Delete a user passkey
/// </summary>
/// <param name="userId"></param>
/// <param name="credentialId"></param>
/// <returns></returns>
Task DeletePasskeyAsync(byte[] credentialId);
Task DeletePasskeyAsync(int userId, byte[] credentialId);
/// <summary>
/// Get logins for a user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<List<UserLogin>> GetLoginsAsync();
Task<List<UserLogin>> GetLoginsAsync(int userId);
/// <summary>
/// Link an external login with a local user account
/// </summary>
/// <param name="user">The <see cref="User"/> we're verifying</param>
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
/// <param name="type">External Login provider type</param>
/// <param name="key">External Login provider key</param>
/// <param name="name">External Login provider display name</param>
/// <returns></returns>
Task<User> AddLoginAsync(User user, string token, string type, string key, string name);
/// <summary>
/// Delete a user login
/// </summary>
/// <param name="userId"></param>
/// <param name="provider"></param>
/// <param name="key"></param>
/// <returns></returns>
Task DeleteLoginAsync(string provider, string key);
Task DeleteLoginAsync(int userId, string provider, string key);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@ -253,7 +257,7 @@ namespace Oqtane.Services
public async Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent)
{
return await PostJsonAsync<User>($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user);
return await PostJsonAsync<User>($"{Apiurl}/signin?setcookie={setCookie}&persistent={isPersistent}", user);
}
public async Task LogoutUserAsync(User user)
@ -306,11 +310,6 @@ namespace Oqtane.Services
return await GetStringAsync($"{Apiurl}/personalaccesstoken");
}
public async Task<User> LinkUserAsync(User user, string token, string type, string key, string name)
{
return await PostJsonAsync<User>($"{Apiurl}/link?token={token}&type={type}&key={key}&name={name}", user);
}
public async Task<string> GetPasswordRequirementsAsync(int siteId)
{
var requirements = await GetJsonAsync<Dictionary<string, string>>($"{Apiurl}/passwordrequirements/{siteId}");
@ -338,9 +337,9 @@ namespace Oqtane.Services
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
public async Task<List<UserPasskey>> GetPasskeysAsync()
public async Task<List<UserPasskey>> GetPasskeysAsync(int userId)
{
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey");
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}");
}
public async Task<UserPasskey> UpdatePasskeyAsync(UserPasskey passkey)
@ -348,19 +347,24 @@ namespace Oqtane.Services
return await PutJsonAsync<UserPasskey>($"{Apiurl}/passkey", passkey);
}
public async Task DeletePasskeyAsync(byte[] credentialId)
public async Task DeletePasskeyAsync(int userId, byte[] credentialId)
{
await DeleteAsync($"{Apiurl}/passkey?id={Base64Url.EncodeToString(credentialId)}");
await DeleteAsync($"{Apiurl}/passkey?id={userId}&credential={Base64Url.EncodeToString(credentialId)}");
}
public async Task<List<UserLogin>> GetLoginsAsync()
public async Task<List<UserLogin>> GetLoginsAsync(int userId)
{
return await GetJsonAsync<List<UserLogin>>($"{Apiurl}/login");
return await GetJsonAsync<List<UserLogin>>($"{Apiurl}/login?id={userId}");
}
public async Task DeleteLoginAsync(string provider, string key)
public async Task<User> AddLoginAsync(User user, string token, string type, string key, string name)
{
await DeleteAsync($"{Apiurl}/login?provider={provider}&key={key}");
return await PostJsonAsync<User>($"{Apiurl}/login?token={token}&type={type}&key={key}&name={name}", user);
}
public async Task DeleteLoginAsync(int userId, string provider, string key)
{
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@ -240,8 +241,8 @@ namespace Oqtane.Controllers
}
}
// POST api/<controller>/login
[HttpPost("login")]
// POST api/<controller>/signin
[HttpPost("signin")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent)
{
if (ModelState.IsValid)
@ -330,22 +331,6 @@ namespace Oqtane.Controllers
return user;
}
// POST api/<controller>/link
[HttpPost("link")]
public async Task<User> Link([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
user = await _userManager.LinkExternalAccount(user, token, type, key, name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
return user;
}
// GET api/<controller>/validate/x
[HttpGet("validateuser")]
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
@ -466,12 +451,21 @@ namespace Oqtane.Controllers
}
}
// GET: api/<controller>/passkey
// GET: api/<controller>/passkey?id=x
[HttpGet("passkey")]
[Authorize]
public async Task<IEnumerable<UserPasskey>> GetPasskeys()
public async Task<IEnumerable<UserPasskey>> GetPasskeys(int id)
{
return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId, _tenantManager.GetAlias().SiteId);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
return await _userManager.GetPasskeys(id, _tenantManager.GetAlias().SiteId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Get Attempt {UserId} {SiteId}", id, _tenantManager.GetAlias().SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// PUT api/<controller>/passkey
@ -481,10 +475,17 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
// passkey name is prefixed with SiteId for multi-tenancy
passkey.Name = $"{_tenantManager.GetAlias().SiteId}:" + passkey.Name;
passkey.UserId = _userPermissions.GetUser(User).UserId;
await _userManager.UpdatePasskey(passkey);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == passkey.UserId)
{
// passkey name is prefixed with SiteId for multi-tenancy
passkey.Name = $"{_tenantManager.GetAlias().SiteId}:" + passkey.Name;
await _userManager.UpdatePasskey(passkey);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Put Attempt {PassKey}", passkey);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
else
{
@ -493,28 +494,70 @@ namespace Oqtane.Controllers
}
}
// DELETE api/<controller>/passkey?id=x
// DELETE api/<controller>/passkey?id=x&credential=y
[HttpDelete("passkey")]
[Authorize]
public async Task DeletePasskey(string id)
public async Task DeletePasskey(int id, string credential)
{
await _userManager.DeletePasskey(_userPermissions.GetUser(User).UserId, Base64Url.DecodeFromChars(id));
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
await _userManager.DeletePasskey(id, Base64Url.DecodeFromChars(credential));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Delete Attempt {UserId} {Credential}", id, credential);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// GET: api/<controller>/login
// GET: api/<controller>/login?id=x
[HttpGet("login")]
[Authorize]
public async Task<IEnumerable<UserLogin>> GetLogins()
public async Task<IEnumerable<UserLogin>> GetLogins(int id)
{
return await _userManager.GetLogins(_userPermissions.GetUser(User).UserId, _tenantManager.GetAlias().SiteId);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
return await _userManager.GetLogins(id, _tenantManager.GetAlias().SiteId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Login Get Attempt {UserId} {SiteId}", id, _tenantManager.GetAlias().SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// DELETE api/<controller>/login?provider=x&key=y
// PUT api/<controller>/login
[HttpPost("login")]
public async Task<User> AddLogin([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
user = await _userManager.AddLogin(user, token, type, key, name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Login Post Attempt {Username} {Token}", user.Username, token);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
user = null;
}
return user;
}
// DELETE api/<controller>/login?id=x&provider=y&key=z
[HttpDelete("login")]
[Authorize]
public async Task DeleteLogin(string provider, string key)
public async Task DeleteLogin(int id, string provider, string key)
{
await _userManager.DeleteLogin(_userPermissions.GetUser(User).UserId, provider, key);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
await _userManager.DeleteLogin(id, provider, key);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Login Delete Attempt {UserId} {Provider} {Key}", id, provider, key);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@ -33,7 +33,6 @@ namespace Oqtane.Managers
Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
Task<UserValidateResult> ValidateUser(string username, string email, string password);
Task<bool> ValidatePassword(string password);
Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify);
@ -41,6 +40,7 @@ namespace Oqtane.Managers
Task UpdatePasskey(UserPasskey passkey);
Task DeletePasskey(int userId, byte[] credentialId);
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);
}
@ -588,29 +588,6 @@ namespace Oqtane.Managers
}
return user;
}
public async Task<User> LinkExternalAccount(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
var validateResult = new UserValidateResult { Succeeded = true };
@ -902,6 +879,29 @@ namespace Oqtane.Managers
return logins;
}
public async Task<User> AddLogin(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task DeleteLogin(int userId, string provider, string key)
{
var user = _users.GetUser(userId);