user identity improvements

This commit is contained in:
sbwalker 2023-11-29 10:42:23 -05:00
parent c8ac4ec1e8
commit 3c33614115
16 changed files with 353 additions and 184 deletions

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Admin.Register @namespace Oqtane.Modules.Admin.Register
@using System.Net
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IUserService UserService @inject IUserService UserService
@ -88,9 +89,9 @@ else
} }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
private async Task Register() private async Task Register()
{ {
@ -120,7 +121,14 @@ else
if (user != null) if (user != null)
{ {
await logger.LogInformation("User Created {Username} {Email}", _username, _email); await logger.LogInformation("User Created {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info); if (PageState.QueryString.ContainsKey("returnurl"))
{
NavigationManager.NavigateTo(WebUtility.UrlDecode(PageState.QueryString["returnurl"]));
}
else // legacy behavior
{
AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info);
}
} }
else else
{ {

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Admin.UserProfile @namespace Oqtane.Modules.Admin.UserProfile
@using System.Net
@using System.Text.RegularExpressions; @using System.Text.RegularExpressions;
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@ -337,6 +338,11 @@
email = PageState.User.Email; email = PageState.User.Email;
displayname = PageState.User.DisplayName; displayname = PageState.User.DisplayName;
if (string.IsNullOrEmpty(email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
// get user folder // get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null) if (folder != null)
@ -427,8 +433,15 @@
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved"); await logger.LogInformation("User Profile Saved");
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success); if (PageState.QueryString.ContainsKey("returnurl"))
StateHasChanged(); {
NavigationManager.NavigateTo(WebUtility.UrlDecode(PageState.QueryString["returnurl"]));
}
else // legacy behavior
{
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
}
} }
else else
{ {

View File

@ -195,7 +195,7 @@ else
@if (_providertype != "") @if (_providertype != "")
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="The external login provider name which will be displayed on the login page" ResourceKey="ProviderName">Provider Name:</Label> <Label Class="col-sm-3" For="providername" HelpText="Specify a friendly name for the external login provider which will be displayed on the Login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" /> <input id="providername" class="form-control" @bind="@_providername" />
</div> </div>
@ -300,41 +300,50 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reviewclaims" HelpText="This option should only be used for testing. It allows the full list of Claims returned by the Provider to be recorded in the Event Log. Please note that external login is restricted when this option is enabled." ResourceKey="ReviewClaims">Review Claims?</Label> <Label Class="col-sm-3" For="reviewclaims" HelpText="This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled." ResourceKey="ReviewClaims">Review Claims?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="reviewclaims" class="form-select" @bind="@_reviewclaims" required> <div class="input-group">
<option value="true">@SharedLocalizer["Yes"]</option> <select id="reviewclaims" class="form-select" @bind="@_reviewclaims" required>
<option value="false">@SharedLocalizer["No"]</option> <option value="true">@SharedLocalizer["Yes"]</option>
</select> <option value="false">@SharedLocalizer["No"]</option>
</select>
@if (_reviewclaims == "true")
{
<a href="@_externalloginurl" target="_blank" class="btn btn-secondary">@SharedLocalizer["Test"]</a>
}
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="The name of the unique user identifier claim provided by the provider" ResourceKey="IdentifierClaimType">Identifier Claim:</Label> <Label Class="col-sm-3" For="identifierclaimtype" HelpText="Specify the type name of the unique user identifier claim provided by the provider. The default value is 'sub'." ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" /> <input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The name of the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim:</Label> <Label Class="col-sm-3" For="nameclaimtype" HelpText="Optionally specify the type name of the user's name claim provided by the provider. The typical value is 'name'." ResourceKey="NameClaimType">Name Claim:</Label>
<div class="col-sm-9">
<input id="nameclaimtype" class="form-control" @bind="@_nameclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="Optionally specify the type name of the email address claim provided by the provider. The typical value is 'email'," ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" /> <input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div> </div>
</div> </div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect) <div class="row mb-1 align-items-center">
{ <Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label>
<div class="row mb-1 align-items-center"> <div class="col-sm-9">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label> <input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
</div> </div>
<div class="row mb-1 align-items-center"> </div>
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label> <div class="row mb-1 align-items-center">
<div class="col-sm-9"> <Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" /> <div class="col-sm-9">
</div> <input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</div> </div>
} </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label> <Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -443,7 +452,9 @@ else
private string _pkce; private string _pkce;
private string _redirecturl; private string _redirecturl;
private string _reviewclaims; private string _reviewclaims;
private string _externalloginurl;
private string _identifierclaimtype; private string _identifierclaimtype;
private string _nameclaimtype;
private string _emailclaimtype; private string _emailclaimtype;
private string _roleclaimtype; private string _roleclaimtype;
private string _profileclaimtypes; private string _profileclaimtypes;
@ -505,7 +516,9 @@ else
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false"); _reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false");
_externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external");
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub"); _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email"); _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", ""); _roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
@ -598,7 +611,8 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);

View File

@ -204,8 +204,8 @@
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve"> <data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value> <value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data> </data>
<data name="ExternalLoginStatus.InvalidEmail" xml:space="preserve"> <data name="ExternalLoginStatus.MissingClaims" xml:space="preserve">
<value>The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions.</value> <value>The External Login Provider Did Not Provide All Of The Required Information. Please Contact Your Administrator For Further Instructions.</value>
</data> </data>
<data name="ExternalLoginStatus.ProviderKeyMismatch" xml:space="preserve"> <data name="ExternalLoginStatus.ProviderKeyMismatch" xml:space="preserve">
<value>An Error Occurred Verifying Your External Login. Please Contact Your Administrator For Further Instructions.</value> <value>An Error Occurred Verifying Your External Login. Please Contact Your Administrator For Further Instructions.</value>

View File

@ -147,6 +147,9 @@
<data name="Message.User.NoLogIn" xml:space="preserve"> <data name="Message.User.NoLogIn" xml:space="preserve">
<value>Current User Is Not Logged In</value> <value>Current User Is Not Logged In</value>
</data> </data>
<data name="Message.User.NoEmail" xml:space="preserve">
<value>You Must Provide An Email Address For Your User Account</value>
</data>
<data name="Error.Profile.Load" xml:space="preserve"> <data name="Error.Profile.Load" xml:space="preserve">
<value>Error Loading User Profile</value> <value>Error Loading User Profile</value>
</data> </data>

View File

@ -247,7 +247,7 @@
<value>Domain Filter:</value> <value>Domain Filter:</value>
</data> </data>
<data name="EmailClaimType.HelpText" xml:space="preserve"> <data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The name of the email address claim provided by the identity provider</value> <value>Optionally specify the type name of the email address claim provided by the identity provider. The typical value is 'email'.</value>
</data> </data>
<data name="EmailClaimType.Text" xml:space="preserve"> <data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim:</value> <value>Email Claim:</value>
@ -274,7 +274,7 @@
<value>Use PKCE?</value> <value>Use PKCE?</value>
</data> </data>
<data name="ProviderName.HelpText" xml:space="preserve"> <data name="ProviderName.HelpText" xml:space="preserve">
<value>The external login provider name which will be displayed on the login page</value> <value>Specify a friendly name for the external login provider which will be displayed on the Login page</value>
</data> </data>
<data name="ProviderName.Text" xml:space="preserve"> <data name="ProviderName.Text" xml:space="preserve">
<value>Provider Name:</value> <value>Provider Name:</value>
@ -373,7 +373,7 @@
<value>Last Login</value> <value>Last Login</value>
</data> </data>
<data name="IdentifierClaimType.HelpText" xml:space="preserve"> <data name="IdentifierClaimType.HelpText" xml:space="preserve">
<value>The name of the unique user identifier claim provided by the identity provider</value> <value>Specify the type name of the unique user identifier claim provided by the identity provider. The default value is 'sub'.</value>
</data> </data>
<data name="IdentifierClaimType.Text" xml:space="preserve"> <data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value> <value>Identifier Claim:</value>
@ -385,13 +385,13 @@
<value>Parameters:</value> <value>Parameters:</value>
</data> </data>
<data name="RoleClaimType.HelpText" xml:space="preserve"> <data name="RoleClaimType.HelpText" xml:space="preserve">
<value>Optionally provide the name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value> <value>Optionally provide the type name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value>
</data> </data>
<data name="RoleClaimType.Text" xml:space="preserve"> <data name="RoleClaimType.Text" xml:space="preserve">
<value>Role Claim:</value> <value>Role Claim:</value>
</data> </data>
<data name="ProfileClaimTypes.HelpText" xml:space="preserve"> <data name="ProfileClaimTypes.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of user profile claims provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value> <value>Optionally provide a comma delimited list of user profile claim type names provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value>
</data> </data>
<data name="ProfileClaimTypes.Text" xml:space="preserve"> <data name="ProfileClaimTypes.Text" xml:space="preserve">
<value>User Profile Claims:</value> <value>User Profile Claims:</value>
@ -460,6 +460,12 @@
<value>Review Claims?</value> <value>Review Claims?</value>
</data> </data>
<data name="ReviewClaims.HelpText" xml:space="preserve"> <data name="ReviewClaims.HelpText" xml:space="preserve">
<value>This option should only be used for testing. It allows the full list of Claims returned by the Provider to be recorded in the Event Log. Please note that external login is restricted when this option is enabled.</value> <value>This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled.</value>
</data>
<data name="NameClaimType.HelpText" xml:space="preserve">
<value>Optionally specify the type name of the user's name claim provided by the identity provider. The typical value is 'name'.</value>
</data>
<data name="NameClaimType.Text" xml:space="preserve">
<value>Name Claim:</value>
</data> </data>
</root> </root>

View File

@ -435,4 +435,7 @@
<data name="Uninstall" xml:space="preserve"> <data name="Uninstall" xml:space="preserve">
<value>Uninstall</value> <value>Uninstall</value>
</data> </data>
<data name="Test" xml:space="preserve">
<value>Test</value>
</data>
</root> </root>

View File

@ -26,8 +26,7 @@ namespace Oqtane.Themes.Controls
var allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false; var allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
var allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); var allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
Route route = new Route(PageState.Uri.AbsoluteUri, PageState.Alias.Path); var returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
var returnurl = WebUtility.UrlEncode(route.PathAndQuery);
if (allowexternallogin && !allowsitelogin) if (allowexternallogin && !allowsitelogin)
{ {
@ -39,7 +38,6 @@ namespace Oqtane.Themes.Controls
// local login // local login
NavigationManager.NavigateTo(NavigateUrl("login", "?returnurl=" + returnurl)); NavigationManager.NavigateTo(NavigateUrl("login", "?returnurl=" + returnurl));
} }
} }
protected async Task LogoutUser() protected async Task LogoutUser()

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Themes.Controls @namespace Oqtane.Themes.Controls
@using System.Net
@inherits ThemeControlBase @inherits ThemeControlBase
@inject IStringLocalizer<UserProfile> Localizer @inject IStringLocalizer<UserProfile> Localizer
@ -26,14 +27,21 @@
[Parameter] [Parameter]
public bool ShowRegister { get; set; } public bool ShowRegister { get; set; }
private string _returnurl = "";
protected override void OnParametersSet()
{
_returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
}
private void RegisterUser() private void RegisterUser()
{ {
NavigationManager.NavigateTo(NavigateUrl("register")); NavigationManager.NavigateTo(NavigateUrl("register", "returnurl=" + _returnurl));
} }
private void UpdateProfile() private void UpdateProfile()
{ {
NavigationManager.NavigateTo(NavigateUrl("profile")); NavigationManager.NavigateTo(NavigateUrl("profile", "returnurl=" + _returnurl));
} }
} }

View File

@ -1,4 +1,5 @@
@namespace Oqtane.UI @namespace Oqtane.UI
@using System.Net
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject SiteState SiteState @inject SiteState SiteState
@ -87,6 +88,13 @@
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
// force user to provide email address (email may be missing if using external login)
if (PageState.User != null && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
if (!firstRender) if (!firstRender)
{ {
if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script")) if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script"))

View File

@ -176,12 +176,13 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{ {
user.EmailConfirmed = User.IsInRole(RoleNames.Admin);
user = await _userManager.UpdateUser(user); user = await _userManager.UpdateUser(user);
} }
else else
{ {
user.Password = ""; // remove sensitive information user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Post Attempt {User}", user); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Put Attempt {User}", user);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
user = null; user = null;
} }

View File

@ -150,15 +150,16 @@ namespace Oqtane.Extensions
private static async Task OnCreatingTicket(OAuthCreatingTicketContext context) private static async Task OnCreatingTicket(OAuthCreatingTicketContext context)
{ {
// OAuth 2.0 // OAuth 2.0
var email = "";
var id = "";
var claims = ""; var claims = "";
var id = "";
var name = "";
var email = "";
if (context.Options.UserInformationEndpoint != "") if (context.Options.UserInformationEndpoint != "")
{ {
try try
{ {
// call user information endpoint // call user information endpoint using access token
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
@ -167,32 +168,57 @@ namespace Oqtane.Extensions
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
claims = await response.Content.ReadAsStringAsync(); claims = await response.Content.ReadAsStringAsync();
// parse json output // get claim types
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""); var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
if (!claims.StartsWith("[") && !claims.EndsWith("]"))
// some user endpoints can return multiple objects (ie. GitHub) so convert single object to array (if necessary)
var jsonclaims = claims;
if (!jsonclaims.StartsWith("[") && !jsonclaims.EndsWith("]"))
{ {
claims = "[" + claims + "]"; // convert to json array jsonclaims = "[" + jsonclaims + "]";
} }
JsonNode items = JsonNode.Parse(claims)!;
// parse claim values
JsonNode items = JsonNode.Parse(jsonclaims)!;
foreach (var item in items.AsArray()) foreach (var item in items.AsArray())
{ {
if (item[emailClaimType] != null) // id claim is required
if (!string.IsNullOrEmpty(idClaimType) && item[idClaimType] != null)
{ {
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) id = item[idClaimType].ToString();
// name claim is optional
if (!string.IsNullOrEmpty(nameClaimType))
{ {
email = item[emailClaimType].ToString().ToLower(); if (item[nameClaimType] != null)
if (item[idClaimType] != null)
{ {
id = item[idClaimType].ToString(); name = item[nameClaimType].ToString();
}
else
{
id = ""; // name claim was specified but was not provided
}
}
// email claim is optional
if (!string.IsNullOrEmpty(emailClaimType))
{
if (item[emailClaimType] != null && EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
email = item[emailClaimType].ToString().ToLower();
}
else
{
id = ""; // email claim was specified but was not provided or is invalid
} }
break;
} }
} }
} if (!string.IsNullOrEmpty(id))
if (string.IsNullOrEmpty(id)) {
{ break;
id = email; }
} }
} }
catch (Exception ex) catch (Exception ex)
@ -203,7 +229,7 @@ namespace Oqtane.Extensions
} }
// validate user // validate user
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal); var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
identity.AddClaim(new Claim("access_token", context.AccessToken)); identity.AddClaim(new Claim("access_token", context.AccessToken));
@ -231,28 +257,53 @@ namespace Oqtane.Extensions
private static async Task OnTokenValidated(TokenValidatedContext context) private static async Task OnTokenValidated(TokenValidatedContext context)
{ {
// OpenID Connect // OpenID Connect
var claims = "";
var id = "";
var name = "";
var email = "";
// serialize claims
foreach (var claim in context.Principal.Claims)
{
claims += "\"" + claim.Type + "\":\"" + claim.Value + "\",";
}
claims = "{" + claims.Substring(0, claims.Length - 1) + "}";
// get claim types
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""); var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var id = context.Principal.FindFirstValue(idClaimType); var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType);
var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray()); // parse claim values
id = context.Principal.FindFirstValue(idClaimType); // required
if (!string.IsNullOrEmpty(nameClaimType))
{
if (context.Principal.FindFirstValue(nameClaimType) != null)
{
name = context.Principal.FindFirstValue(nameClaimType);
}
else
{
id = ""; // name claim was specified but was not provided
}
}
if (!string.IsNullOrEmpty(emailClaimType))
{
if (context.Principal.FindFirstValue(emailClaimType) != null && EmailValid(context.Principal.FindFirstValue(emailClaimType), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
email = context.Principal.FindFirstValue(emailClaimType);
}
else
{
id = ""; // email claim was specified but was not provided or is invalid
}
}
// validate user // validate user
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal); var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
// external roles // include access token
if (!string.IsNullOrEmpty(context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
foreach (var claim in context.Principal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
}
}
}
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData)); identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
context.Principal = new ClaimsPrincipal(identity); context.Principal = new ClaimsPrincipal(identity);
} }
@ -284,13 +335,13 @@ namespace Oqtane.Extensions
return Task.CompletedTask; return Task.CompletedTask;
} }
private static async Task<ClaimsIdentity> ValidateUser(string email, string id, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal) private static async Task<ClaimsIdentity> ValidateUser(string id, string name, string email, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
{ {
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme); ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
// use identity.Label as a temporary location to store validation status information // use identity.Label as a temporary location to store validation status information
// review claims option (for testing) // review claims feature (for testing - external login is disabled)
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:ReviewClaims", "false"))) if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:ReviewClaims", "false")))
{ {
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "Provider Returned The Following Claims: {Claims}", claims); _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "Provider Returned The Following Claims: {Claims}", claims);
@ -316,136 +367,158 @@ namespace Oqtane.Extensions
} }
else else
{ {
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) bool duplicates = false;
if (!string.IsNullOrEmpty(email))
{ {
bool duplicates = false;
try try
{ {
identityuser = await _identityUserManager.FindByEmailAsync(email); identityuser = await _identityUserManager.FindByEmailAsync(email);
} }
catch catch // FindByEmailAsync will throw an error if the email matches multiple user accounts
{ {
// FindByEmailAsync will throw an error if the email matches multiple user accounts
duplicates = true; duplicates = true;
} }
if (identityuser == null) }
if (identityuser == null)
{
if (duplicates)
{ {
if (duplicates) identity.Label = ExternalLoginStatus.DuplicateEmail;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
}
else
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{ {
identity.Label = ExternalLoginStatus.DuplicateEmail; // user identifiers
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email); var username = "";
} var emailaddress = "";
else var displayname = "";
{ bool emailconfirmed = false;
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
identityuser = new IdentityUser();
identityuser.UserName = email;
identityuser.Email = email;
identityuser.EmailConfirmed = true;
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss", CultureInfo.InvariantCulture));
if (result.Succeeded)
{
user = new User
{
SiteId = alias.SiteId,
Username = email,
DisplayName = email,
Email = email,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
if (user != null) if (!string.IsNullOrEmpty(email)) // email claim provided
{
username = email;
emailaddress = email;
displayname = (!string.IsNullOrEmpty(name)) ? name : email;
emailconfirmed = true;
}
else if (!string.IsNullOrEmpty(name)) // name claim provided
{
username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss");
emailaddress = ""; // unknown - will need to be requested from user later
displayname = name;
}
else // neither email nor name provided
{
username = Guid.NewGuid().ToString("N");
emailaddress = ""; // unknown - will need to be requested from user later
displayname = username;
}
identityuser = new IdentityUser();
identityuser.UserName = username;
identityuser.Email = emailaddress;
identityuser.EmailConfirmed = emailconfirmed;
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
var password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
var result = await _identityUserManager.CreateAsync(identityuser, password);
if (result.Succeeded)
{
user = new User
{
SiteId = alias.SiteId,
Username = username,
DisplayName = displayname,
Email = emailaddress,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
if (user != null)
{
if (!string.IsNullOrEmpty(email))
{ {
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>(); var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name; string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!"; string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body); var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
} }
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
} }
else else
{ {
identity.Label = ExternalLoginStatus.UserNotCreated; identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString()); _logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
} }
} }
else else
{ {
identity.Label = ExternalLoginStatus.UserDoesNotExist; identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email); _logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
}
}
}
else
{
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
{
// external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// external login using existing user account - link automatically
user = _users.GetUser(identityuser.UserName);
user.SiteId = alias.SiteId;
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
} }
} }
else else
{ {
// provider keys do not match identity.Label = ExternalLoginStatus.UserDoesNotExist;
identity.Label = ExternalLoginStatus.ProviderKeyMismatch; _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
} }
} }
} }
else // email invalid else
{ {
identity.Label = ExternalLoginStatus.InvalidEmail; var logins = await _identityUserManager.GetLoginsAsync(identityuser);
if (!string.IsNullOrEmpty(email)) var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email); if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
{
// external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// external login using existing user account - link automatically
user = _users.GetUser(identityuser.UserName);
user.SiteId = alias.SiteId;
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
}
} }
else else
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims); // provider keys do not match
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
} }
} }
} }
@ -463,6 +536,25 @@ namespace Oqtane.Extensions
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user); _users.UpdateUser(user);
// external roles
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role))
{
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
}
}
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""));
}
}
// user profile claims // user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{ {
@ -501,7 +593,7 @@ namespace Oqtane.Extensions
} }
else else
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. The Valid Claims Are {Claims}.", mapping.Split(":")[0], claims); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]);
} }
} }
else else
@ -514,9 +606,10 @@ namespace Oqtane.Extensions
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName); _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
} }
} }
else // id invalid else // claims invalid
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims); identity.Label = ExternalLoginStatus.MissingClaims;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
} }
return identity; return identity;

View File

@ -106,7 +106,7 @@ namespace Oqtane.Managers
{ {
if (string.IsNullOrEmpty(user.Password)) if (string.IsNullOrEmpty(user.Password))
{ {
// create random interal password based on random date and punctuation ie. Jan-23-1981+14:43:12! // generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random(); Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60)); var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47); user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
@ -152,7 +152,7 @@ namespace Oqtane.Managers
{ {
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Verification", body); var notification = new Notification(user.SiteId, User, "User Account Verification", body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
} }
@ -205,8 +205,22 @@ namespace Oqtane.Managers
if (user.Email != identityuser.Email) if (user.Email != identityuser.Email)
{ {
await _identityUserManager.SetEmailAsync(identityuser, user.Email); await _identityUserManager.SetEmailAsync(identityuser, user.Email);
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); // if email address changed and user is not administrator, email verification is required for new email address
if (!user.EmailConfirmed)
{
var alias = _tenantManager.GetAlias();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
else
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
}
} }
user = _users.UpdateUser(user); user = _users.UpdateUser(user);
@ -308,7 +322,7 @@ namespace Oqtane.Managers
user = _users.GetUser(identityuser.UserName); user = _users.GetUser(identityuser.UserName);
if (user != null) if (user != null)
{ {
if (identityuser.EmailConfirmed) if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
{ {
user.IsAuthenticated = true; user.IsAuthenticated = true;
user.LastLoginOn = DateTime.UtcNow; user.LastLoginOn = DateTime.UtcNow;
@ -323,7 +337,7 @@ namespace Oqtane.Managers
} }
else else
{ {
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username);
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace Oqtane.Pages
var providertype = HttpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", ""); var providertype = HttpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
if (providertype != "") if (providertype != "")
{ {
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" }); return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
} }
else else
{ {

View File

@ -1,7 +1,7 @@
namespace Oqtane.Shared { namespace Oqtane.Shared {
public class ExternalLoginStatus { public class ExternalLoginStatus {
public const string Success = "Success"; public const string Success = "Success";
public const string InvalidEmail = "InvalidEmail"; public const string MissingClaims = "MissingClaims";
public const string DuplicateEmail = "DuplicateEmail"; public const string DuplicateEmail = "DuplicateEmail";
public const string UserNotCreated = "UserNotCreated"; public const string UserNotCreated = "UserNotCreated";
public const string UserDoesNotExist = "UserDoesNotExist"; public const string UserDoesNotExist = "UserDoesNotExist";