completed antiforgery implementation, improved external login claim mapping, principal construction, and user experience

This commit is contained in:
Shaun Walker 2022-04-22 17:54:20 -04:00
parent 391713b84d
commit e4c648ee92
38 changed files with 645 additions and 525 deletions

View File

@ -28,7 +28,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label> <Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" /> <input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label> <Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" /> <input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div> </div>
</div> </div>
} }

View File

@ -35,7 +35,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label> <Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" /> <input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div> </div>
</div> </div>
} }

View File

@ -62,13 +62,19 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label> <Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_hostPassword" /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</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="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label> <Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirmPassword" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -104,6 +110,8 @@
private string _hostUsername = string.Empty; private string _hostUsername = string.Empty;
private string _hostPassword = string.Empty; private string _hostPassword = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirmPassword = string.Empty; private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty; private string _hostEmail = string.Empty;
private bool _register = true; private bool _register = true;
@ -112,6 +120,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_togglepassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault)) if (_databases.Exists(item => item.IsDefault))
{ {
@ -218,4 +227,18 @@
_message = Localizer["Message.Require.DbInfo"]; _message = Localizer["Message.Require.DbInfo"];
} }
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -95,7 +95,7 @@
{ {
try try
{ {
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"])) if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{ {
@ -188,8 +188,7 @@
if (!twofactor) if (!twofactor)
{ {
bool setCookie = (PageState.Runtime == Oqtane.Shared.Runtime.WebAssembly); user = await UserService.LoginUserAsync(user);
user = await UserService.LoginUserAsync(user, setCookie, false);
} }
else else
{ {
@ -200,23 +199,14 @@
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server) // post back to the Login page so that the cookies are set correctly
{
// server-side Blazor needs to post to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
else else
{ {
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); if (PageState.Site.Settings["LoginOptions:TwoFactor"] == "required" || user.TwoFactorRequired)
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
}
else
{
if (user.TwoFactorRequired)
{ {
twofactor = true; twofactor = true;
validated = false; validated = false;
@ -308,12 +298,12 @@
if (_passwordtype == "password") if (_passwordtype == "password")
{ {
_passwordtype = "text"; _passwordtype = "text";
_togglepassword = Localizer["HidePassword"]; _togglepassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_passwordtype = "password"; _passwordtype = "password";
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
} }

View File

@ -27,13 +27,19 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_password" autocomplete="new-password" required /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</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="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" autocomplete="new-password" required /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -66,12 +72,19 @@ else
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty; private string _confirm = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnParametersSet()
{
_togglepassword = SharedLocalizer["ShowPassword"];
}
private async Task Register() private async Task Register()
{ {
validated = true; validated = true;
@ -134,4 +147,18 @@ else
{ {
NavigationManager.NavigateTo(NavigateUrl(string.Empty)); NavigationManager.NavigateTo(NavigateUrl(string.Empty));
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -16,13 +16,19 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The new password. It must satisfy complexity rules for the site." ResourceKey="Password">Password: </Label> <Label Class="col-sm-3" For="password" HelpText="The new password. It must satisfy complexity rules for the site." ResourceKey="Password">Password: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_password" required /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</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="confirm" HelpText="Enter the password again. It must exactly match the password entered above." ResourceKey="Confirm">Confirm: </Label> <Label Class="col-sm-3" For="confirm" HelpText="Enter the password again. It must exactly match the password entered above." ResourceKey="Confirm">Confirm: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" required /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -36,12 +42,16 @@
private bool validated = false; private bool validated = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty; private string _confirm = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token")) if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{ {
_username = PageState.QueryString["name"]; _username = PageState.QueryString["name"];
@ -110,4 +120,18 @@
{ {
NavigationManager.NavigateTo(NavigateUrl(string.Empty)); NavigationManager.NavigateTo(NavigateUrl(string.Empty));
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -341,7 +341,7 @@
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = Localizer["Show"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30"); _retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
@ -656,12 +656,12 @@
if (_smtppasswordtype == "password") if (_smtppasswordtype == "password")
{ {
_smtppasswordtype = "text"; _smtppasswordtype = "text";
_togglesmtppassword = Localizer["Hide"]; _togglesmtppassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_smtppasswordtype = "password"; _smtppasswordtype = "password";
_togglesmtppassword = Localizer["Show"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -156,7 +156,7 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="hostPassword" HelpText="Enter the password of an existing host user" ResourceKey="HostPassword">Host Password:</Label> <Label Class="col-sm-3" For="hostPassword" HelpText="Enter the password of an existing host user" ResourceKey="HostPassword">Host Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" required /> <input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" autocomplete="new-password" required />
</div> </div>
</div> </div>
} }
@ -307,7 +307,7 @@ else
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = _hostusername; user.Username = _hostusername;
user.Password = _hostpassword; user.Password = _hostpassword;
user = await UserService.LoginUserAsync(user, false, false); user = await UserService.LoginUserAsync(user);
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
var connectionString = String.Empty; var connectionString = String.Empty;

View File

@ -33,7 +33,7 @@ else
<Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" @bind="@_password" autocomplete="new-password" /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div> </div>
</div> </div>
@ -41,7 +41,10 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
@if (allowtwofactor) @if (allowtwofactor)
@ -246,10 +249,11 @@ else
{ {
try try
{ {
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"])) if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{ {
allowtwofactor = bool.Parse(PageState.Site.Settings["LoginOptions:TwoFactor"]); allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
} }
if (PageState.User != null) if (PageState.User != null)
@ -455,12 +459,12 @@ else
if (_passwordtype == "password") if (_passwordtype == "password")
{ {
_passwordtype = "text"; _passwordtype = "text";
_togglepassword = Localizer["HidePassword"]; _togglepassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_passwordtype = "password"; _passwordtype = "password";
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
} }

View File

@ -22,7 +22,7 @@
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" @bind="@_password" required /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div> </div>
</div> </div>
@ -30,7 +30,10 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@confirm" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -107,7 +110,7 @@
{ {
try try
{ {
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>(); settings = new Dictionary<string, string>();
} }
@ -204,12 +207,12 @@
if (_passwordtype == "password") if (_passwordtype == "password")
{ {
_passwordtype = "text"; _passwordtype = "text";
_togglepassword = Localizer["HidePassword"]; _togglepassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_passwordtype = "password"; _passwordtype = "password";
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -31,7 +31,7 @@ else
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" @bind="@_password" required /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div> </div>
</div> </div>
@ -39,7 +39,10 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@confirm" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -162,10 +165,9 @@ else
{ {
try try
{ {
// OnParametersSetAsync is called when the edit modal is closed - in which case there is no id parameter
if (PageState.QueryString.ContainsKey("id")) if (PageState.QueryString.ContainsKey("id"))
{ {
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
userid = Int32.Parse(PageState.QueryString["id"]); userid = Int32.Parse(PageState.QueryString["id"]);
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
@ -279,12 +281,12 @@ else
if (_passwordtype == "password") if (_passwordtype == "password")
{ {
_passwordtype = "text"; _passwordtype = "text";
_togglepassword = Localizer["HidePassword"]; _togglepassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_passwordtype = "password"; _passwordtype = "password";
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -94,28 +94,28 @@ else
</div> </div>
</div> </div>
} }
<div class="row mb-1 align-items-center"> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want to allow users to use two factor authentication? Note that the Notification Job in Scheduled Jobs needs to be enabled for this option to work properly." ResourceKey="TwoFactor">Allow Two Factor?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(PageState.Alias.Path))
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookietype" HelpText="Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site (this option is only applicable to micro-sites)." ResourceKey="CookieType">Cookie Type:</Label> <Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="cookietype" class="form-select" @bind="@_cookietype"> <select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="domain">@Localizer["Domain"]</option> <option value="false">@Localizer["Disabled"]</option>
<option value="site">@Localizer["Site"]</option> <option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
} }
</Section> </Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings"> <Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label> <Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
@ -274,15 +274,18 @@ else
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly /> <input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div> </div>
</div> </div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<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 type name for the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim Type:</Label> <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>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<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>
<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>
}
<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">
@ -338,6 +341,7 @@ else
</div> </div>
</div> </div>
</Section> </Section>
}
</div> </div>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
@ -353,7 +357,7 @@ else
private string _allowregistration; private string _allowregistration;
private string _allowsitelogin; private string _allowsitelogin;
private string _twofactor; private string _twofactor;
private string _cookietype; private string _cookiename;
private string _minimumlength; private string _minimumlength;
private string _uniquecharacters; private string _uniquecharacters;
@ -378,6 +382,7 @@ else
private string _scopes; private string _scopes;
private string _pkce; private string _pkce;
private string _redirecturl; private string _redirecturl;
private string _identifierclaimtype;
private string _emailclaimtype; private string _emailclaimtype;
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
@ -401,8 +406,11 @@ else
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString(); _allowregistration = PageState.Site.AllowRegistration.ToString();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false"); _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookietype = SettingService.GetSetting(settings, "LoginOptions:CookieType", "domain"); _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6"); _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1"); _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
@ -423,19 +431,20 @@ else
_userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", ""); _userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", "");
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", ""); _clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = Localizer["Show"]; _toggleclientsecret = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_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;
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", ""); _secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
_togglesecret = Localizer["Show"]; _togglesecret = SharedLocalizer["ShowPassword"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name); _issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", ""); _audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); _lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); }
} }
private List<UserRole> Search(string search) private List<UserRole> Search(string search)
@ -507,8 +516,11 @@ else
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieType", _cookietype, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
@ -531,6 +543,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
@ -540,6 +553,7 @@ else
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true); settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true);
}
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync(); await SettingService.ClearSiteSettingsCacheAsync();
@ -561,13 +575,20 @@ else
private void ProviderTypeChanged(ChangeEventArgs e) private void ProviderTypeChanged(ChangeEventArgs e)
{ {
_providertype = (string)e.Value; _providertype = (string)e.Value;
if (string.IsNullOrEmpty(_providername))
{
if (_providertype == AuthenticationProviderTypes.OpenIDConnect) if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{ {
_scopes = "openid,profile,email"; _scopes = "openid,profile,email";
_identifierclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
_emailclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
} }
else else
{ {
_scopes = ""; _scopes = "";
_identifierclaimtype = "sub";
_emailclaimtype = "email";
}
} }
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged(); StateHasChanged();
@ -583,12 +604,12 @@ else
if (_clientsecrettype == "password") if (_clientsecrettype == "password")
{ {
_clientsecrettype = "text"; _clientsecrettype = "text";
_toggleclientsecret = Localizer["Hide"]; _toggleclientsecret = SharedLocalizer["HidePassword"];
} }
else else
{ {
_clientsecrettype = "password"; _clientsecrettype = "password";
_toggleclientsecret = Localizer["Show"]; _toggleclientsecret = SharedLocalizer["ShowPassword"];
} }
} }
@ -597,12 +618,12 @@ else
if (_secrettype == "password") if (_secrettype == "password")
{ {
_secrettype = "text"; _secrettype = "text";
_togglesecret = Localizer["Hide"]; _togglesecret = SharedLocalizer["HidePassword"];
} }
else else
{ {
_secrettype = "password"; _secrettype = "password";
_togglesecret = Localizer["Show"]; _togglesecret = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -189,12 +189,6 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
<data name="Use" xml:space="preserve"> <data name="Use" xml:space="preserve">
<value>Use</value> <value>Use</value>
</data> </data>
@ -225,4 +219,10 @@
<data name="ExternalLoginStatus.VerificationRequired" xml:space="preserve"> <data name="ExternalLoginStatus.VerificationRequired" xml:space="preserve">
<value>In Order To Link Your External Login With Your User Account You Must Verify Your Identity. Please Check Your Email For Further Instructions.</value> <value>In Order To Link Your External Login With Your User Account You Must Verify Your Identity. Please Check Your Email For Further Instructions.</value>
</data> </data>
<data name="ExternalLoginStatus.AccessDenied" xml:space="preserve">
<value>Your External Login Was Denied Access. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.RemoteFailure" xml:space="preserve">
<value>Your External Login Failed. Please Contact Your Administrator For Further Instructions.</value>
</data>
</root> </root>

View File

@ -324,10 +324,4 @@
<data name="Aliases.Heading" xml:space="preserve"> <data name="Aliases.Heading" xml:space="preserve">
<value>Aliases</value> <value>Aliases</value>
</data> </data>
<data name="Hide" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Show" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -219,10 +219,4 @@
<data name="DeleteAllNotifications.Text" xml:space="preserve"> <data name="DeleteAllNotifications.Text" xml:space="preserve">
<value>Delete ALL Notifications</value> <value>Delete ALL Notifications</value>
</data> </data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -168,12 +168,6 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
<data name="Password.Placeholder" xml:space="preserve"> <data name="Password.Placeholder" xml:space="preserve">
<value>Password</value> <value>Password</value>
</data> </data>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -183,12 +183,6 @@
<data name="Profile.Heading" xml:space="preserve"> <data name="Profile.Heading" xml:space="preserve">
<value>Profile</value> <value>Profile</value>
</data> </data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
<data name="Password.Placeholder" xml:space="preserve"> <data name="Password.Placeholder" xml:space="preserve">
<value>Password</value> <value>Password</value>
</data> </data>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -247,10 +247,10 @@
<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 type name for the email address claim provided by the provider</value> <value>The name of the email address claim provided by the provider</value>
</data> </data>
<data name="EmailClaimType.Text" xml:space="preserve"> <data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim Type:</value> <value>Email Claim:</value>
</data> </data>
<data name="ExternalLoginSettings.Heading" xml:space="preserve"> <data name="ExternalLoginSettings.Heading" xml:space="preserve">
<value>External Login Settings</value> <value>External Login Settings</value>
@ -318,11 +318,11 @@
<data name="UserSettings.Heading" xml:space="preserve"> <data name="UserSettings.Heading" xml:space="preserve">
<value>User Settings</value> <value>User Settings</value>
</data> </data>
<data name="CookieType.HelpText" xml:space="preserve"> <data name="CookieName.HelpText" xml:space="preserve">
<value>Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site (this option is only applicable to micro-sites).</value> <value>You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users.</value>
</data> </data>
<data name="CookieType.Text" xml:space="preserve"> <data name="CookieName.Text" xml:space="preserve">
<value>Login Cookie Type:</value> <value>Cookie Name:</value>
</data> </data>
<data name="CreateToken" xml:space="preserve"> <data name="CreateToken" xml:space="preserve">
<value>Create Token</value> <value>Create Token</value>
@ -355,16 +355,19 @@
<value>Token Settings</value> <value>Token Settings</value>
</data> </data>
<data name="TwoFactor.HelpText" xml:space="preserve"> <data name="TwoFactor.HelpText" xml:space="preserve">
<value>Do you want to allow users to use two factor authentication? Note that the Notification Job in Scheduled Jobs needs to be enabled and your SMTP options need to be configured in Site Settings for this option to work properly.</value> <value>Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out.</value>
</data> </data>
<data name="TwoFactor.Text" xml:space="preserve"> <data name="TwoFactor.Text" xml:space="preserve">
<value>Allow Two Factor?</value> <value>Two Factor?</value>
</data> </data>
<data name="Hide" xml:space="preserve"> <data name="Disabled" xml:space="preserve">
<value>Hide</value> <value>Disabled</value>
</data> </data>
<data name="Show" xml:space="preserve"> <data name="Optional" xml:space="preserve">
<value>Show</value> <value>Optional</value>
</data>
<data name="Required" xml:space="preserve">
<value>Required</value>
</data> </data>
<data name="CreatedOn" xml:space="preserve"> <data name="CreatedOn" xml:space="preserve">
<value>Created On</value> <value>Created On</value>
@ -375,4 +378,10 @@
<data name="LastLoginOn" xml:space="preserve"> <data name="LastLoginOn" xml:space="preserve">
<value>Last Login</value> <value>Last Login</value>
</data> </data>
<data name="IdentifierClaimType.HelpText" xml:space="preserve">
<value>The name of the unique user identifier claim provided by the provider</value>
</data>
<data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value>
</data>
</root> </root>

View File

@ -321,4 +321,10 @@
<data name="Settings" xml:space="preserve"> <data name="Settings" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -54,10 +54,8 @@ namespace Oqtane.Services
/// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled. /// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled.
/// </summary> /// </summary>
/// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param> /// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param>
/// <param name="setCookie">Determines if the login should be stored in the cookie.</param>
/// <param name="isPersistent">Determines if the login should be persisted in the cookie for a long time.</param>
/// <returns></returns> /// <returns></returns>
Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent); Task<User> LoginUserAsync(User user);
/// <summary> /// <summary>
/// Logout a <see cref="User"/> /// Logout a <see cref="User"/>

View File

@ -22,10 +22,15 @@ namespace Oqtane.Services
private HttpClient GetHttpClient() private HttpClient GetHttpClient()
{ {
var httpClient = _httpClientFactory.CreateClient("Remote"); return GetHttpClient(_siteState?.AuthorizationToken);
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && _siteState != null && !string.IsNullOrEmpty(_siteState.AuthorizationToken)) }
private HttpClient GetHttpClient(string AuthorizationToken)
{ {
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + _siteState.AuthorizationToken); var httpClient = _httpClientFactory.CreateClient("Remote");
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && !string.IsNullOrEmpty(AuthorizationToken))
{
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + AuthorizationToken);
} }
return httpClient; return httpClient;
} }

View File

@ -39,9 +39,9 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{userId}?siteid={siteId}"); await DeleteAsync($"{Apiurl}/{userId}?siteid={siteId}");
} }
public async Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent) public async Task<User> LoginUserAsync(User user)
{ {
return await PostJsonAsync<User>($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user); return await PostJsonAsync<User>($"{Apiurl}/login", user);
} }
public async Task LogoutUserAsync(User user) public async Task LogoutUserAsync(User user)

View File

@ -1,10 +1,8 @@
using System; using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Providers;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
@ -33,28 +31,19 @@ namespace Oqtane.Themes.Controls
protected async Task LogoutUser() protected async Task LogoutUser()
{ {
await UserService.LogoutUserAsync(PageState.User);
await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User.Username); await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User.Username);
PageState.User = null;
// check if anonymous user can access page
var url = PageState.Alias.Path + "/" + PageState.Page.Path; var url = PageState.Alias.Path + "/" + PageState.Page.Path;
if (!UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, PageState.Page.Permissions)) if (!UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.Permissions))
{ {
url = PageState.Alias.Path; url = PageState.Alias.Path;
} }
if (PageState.Runtime == Shared.Runtime.Server) // post to the Logout page to complete the logout process
{ var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url };
// server-side Blazor needs to redirect to the Logout page var interop = new Interop(jsRuntime);
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/logout/") + "?returnurl=" + WebUtility.UrlEncode(url), true); await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}
else
{
// client-side Blazor
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(url, true));
}
} }
} }
} }

View File

@ -84,20 +84,30 @@
var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction; var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction;
var querystring = ParseQueryString(route.Query); var querystring = ParseQueryString(route.Query);
// reload the client application if there is a forced reload or the user navigated to a site with a different alias // reload the client application from the server if there is a forced reload or the user navigated to a site with a different alias
if (querystring.ContainsKey("reload") || (!route.AbsolutePath.Substring(1).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path))) if (querystring.ContainsKey("reload") || (!NavigationManager.ToBaseRelativePath(_absoluteUri).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
{ {
NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", ""), true); if (querystring["reload"] == "post")
{
// post back so that the cookies are set correctly - required on any change to the principal
var interop = new Interop(JSRuntime);
var fields = new { returnurl = "/" + NavigationManager.ToBaseRelativePath(_absoluteUri) };
string url = Utilities.TenantUrl(SiteState.Alias, "/pages/external/");
await interop.SubmitForm(url, fields);
return; return;
} }
else else
{ {
// the refresh parameter is used to refresh the PageState NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", ""), true);
return;
}
}
// the refresh parameter is used to refresh the client-side PageState
if (querystring.ContainsKey("refresh")) if (querystring.ContainsKey("refresh"))
{ {
refresh = UI.Refresh.Site; refresh = UI.Refresh.Site;
} }
}
if (PageState != null) if (PageState != null)
{ {

View File

@ -52,6 +52,7 @@ namespace Oqtane.Controllers
} }
catch (Exception ex) catch (Exception ex)
{ {
results.Add(new Dictionary<string, string>() { { "Error", ex.Message } });
_logger.Log(LogLevel.Error, this, LogFunction.Other, "Sql Query {Query} Executed on Tenant {TenantId} Resulted In An Error {Error}", sqlquery.Query, sqlquery.TenantId, ex.Message); _logger.Log(LogLevel.Error, this, LogFunction.Other, "Sql Query {Query} Executed on Tenant {TenantId} Resulted In An Error {Error}", sqlquery.Query, sqlquery.TenantId, ex.Message);
} }
sqlquery.Results = results; sqlquery.Results = results;

View File

@ -316,7 +316,7 @@ namespace Oqtane.Controllers
// POST api/<controller>/login // POST api/<controller>/login
[HttpPost("login")] [HttpPost("login")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent) public async Task<User> Login([FromBody] User user)
{ {
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
@ -357,10 +357,6 @@ namespace Oqtane.Controllers
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(loginUser); _users.UpdateUser(loginUser);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
} }
else else
{ {

View File

@ -16,9 +16,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OAuth;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using System.Net; using System.Net;
using System.Text.Json.Nodes;
namespace Oqtane.Extensions namespace Oqtane.Extensions
{ {
@ -29,15 +29,7 @@ namespace Oqtane.Extensions
// site cookie authentication options // site cookie authentication options
builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) => builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) =>
{ {
if (sitesettings.GetValue("LoginOptions:CookieType", "domain") == "domain") options.Cookie.Name = sitesettings.GetValue("LoginOptions:CookieName", ".AspNetCore.Identity.Application");
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
}
else
{
// use unique cookie name for site
options.Cookie.Name = ".AspNetCore.Identity.Application" + alias.SiteKey;
}
}); });
// site OpenId Connect options // site OpenId Connect options
@ -105,6 +97,7 @@ namespace Oqtane.Extensions
// oauth2 events // oauth2 events
options.Events.OnCreatingTicket = OnCreatingTicket; options.Events.OnCreatingTicket = OnCreatingTicket;
options.Events.OnTicketReceived = OnTicketReceived;
options.Events.OnAccessDenied = OnAccessDenied; options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
} }
@ -117,10 +110,13 @@ namespace Oqtane.Extensions
{ {
// OAuth 2.0 // OAuth 2.0
var email = ""; var email = "";
var id = "";
if (context.Options.UserInformationEndpoint != "") if (context.Options.UserInformationEndpoint != "")
{ {
try try
{ {
// call user information endpoint
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));
@ -129,17 +125,34 @@ namespace Oqtane.Extensions
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var output = await response.Content.ReadAsStringAsync(); var output = await response.Content.ReadAsStringAsync();
// get email address using Regex on the raw output (could be json or html) // parse json output
var regex = new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase); var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
foreach (Match match in regex.Matches(output)) var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
if (!output.StartsWith("[") && !output.EndsWith("]"))
{ {
if (EmailValid(match.Value, context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) output = "[" + output + "]"; // convert to json array
}
JsonNode items = JsonNode.Parse(output)!;
foreach(var item in items.AsArray())
{ {
email = match.Value.ToLower(); if (item[emailClaimType] != null)
{
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
email = item[emailClaimType].ToString().ToLower();
if (item[idClaimType] != null)
{
id = item[idClaimType].ToString();
}
break; break;
} }
} }
} }
if (string.IsNullOrEmpty(id))
{
id = email;
}
}
catch (Exception ex) catch (Exception ex)
{ {
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
@ -147,29 +160,51 @@ namespace Oqtane.Extensions
} }
} }
// login user // validate user
var status = await LoginUser(email, context.HttpContext, context.Principal); var identity = await ValidateUser(email, id, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.AccessToken));
context.Principal = new ClaimsPrincipal(identity);
}
// pass properties to OnTicketReceived
context.Properties.SetParameter("status", identity.Label);
context.Properties.SetParameter("redirecturl", context.Properties.RedirectUri);
}
private static Task OnTicketReceived(TicketReceivedContext context)
{
// OAuth 2.0
var status = context.Properties.GetParameter<string>("status");
if (status != ExternalLoginStatus.Success) if (status != ExternalLoginStatus.Success)
{ {
// redirect to login page and pass status // redirect to login page and pass status
var alias = context.HttpContext.GetAlias(); context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={status}&returnurl={context.Properties.GetParameter<string>("redirecturl")}"), true);
context.Response.Redirect($"{alias.Path}/login?status={status}&returnurl={context.Properties.RedirectUri}", true); context.HandleResponse();
}; }
return Task.CompletedTask;
} }
private static async Task OnTokenValidated(TokenValidatedContext context) private static async Task OnTokenValidated(TokenValidatedContext context)
{ {
// OpenID Connect // OpenID Connect
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var id = context.Principal.FindFirstValue(idClaimType);
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType); var email = context.Principal.FindFirstValue(emailClaimType);
// login user // validate user
var status = await LoginUser(email, context.HttpContext, context.Principal); var identity = await ValidateUser(email, id, context.HttpContext);
if (status != ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
context.Principal = new ClaimsPrincipal(identity);
}
else
{ {
// redirect to login page and pass status // redirect to login page and pass status
var alias = context.HttpContext.GetAlias(); context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={identity.Label}&returnurl={context.Properties.RedirectUri}"), true);
context.Response.Redirect($"{alias.Path}/login?status={status}&returnurl={context.Properties.RedirectUri}", true);
context.HandleResponse(); context.HandleResponse();
} }
} }
@ -179,8 +214,7 @@ namespace Oqtane.Extensions
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External Login Access Denied - User May Have Cancelled Their External Login Attempt"); _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External Login Access Denied - User May Have Cancelled Their External Login Attempt");
// redirect to login page // redirect to login page
var alias = context.HttpContext.GetAlias(); context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={ExternalLoginStatus.AccessDenied}&returnurl={context.Properties.RedirectUri}"), true);
context.Response.Redirect($"{alias.Path}/login?returnurl={context.Properties.RedirectUri}", true);
context.HandleResponse(); context.HandleResponse();
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -190,16 +224,16 @@ namespace Oqtane.Extensions
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message);
// redirect to login page // redirect to login page
var alias = context.HttpContext.GetAlias(); context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={ExternalLoginStatus.RemoteFailure}"), true);
context.Response.Redirect($"{alias.Path}/login", true);
context.HandleResponse(); context.HandleResponse();
return Task.CompletedTask; return Task.CompletedTask;
} }
private static async Task<ExternalLoginStatus> LoginUser(string email, HttpContext httpContext, ClaimsPrincipal claimsPrincipal) private static async Task<ClaimsIdentity> ValidateUser(string email, string id, HttpContext httpContext)
{ {
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
var status = ExternalLoginStatus.Success; ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
// use identity.Label as a temporary location to store validation status information
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{ {
@ -209,11 +243,6 @@ namespace Oqtane.Extensions
var alias = httpContext.GetAlias(); var alias = httpContext.GetAlias();
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", ""); var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", ""); var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", "");
var providerKey = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
if (providerKey == null)
{
providerKey = email; // OAuth2 does not pass claims
}
User user = null; User user = null;
bool duplicates = false; bool duplicates = false;
@ -231,7 +260,7 @@ namespace Oqtane.Extensions
{ {
if (duplicates) if (duplicates)
{ {
status = ExternalLoginStatus.DuplicateEmail; identity.Label = ExternalLoginStatus.DuplicateEmail;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
} }
else else
@ -265,25 +294,25 @@ namespace Oqtane.Extensions
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
// add user login // add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, providerKey, "")); await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, id, ""));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user); _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
} }
else else
{ {
status = ExternalLoginStatus.UserNotCreated; identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email); _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
} }
} }
else else
{ {
status = ExternalLoginStatus.UserNotCreated; identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString()); _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
} }
} }
else else
{ {
status = ExternalLoginStatus.UserDoesNotExist; identity.Label = ExternalLoginStatus.UserDoesNotExist;
_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, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
} }
} }
@ -294,14 +323,14 @@ namespace Oqtane.Extensions
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString())); var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login != null) if (login != null)
{ {
if (login.ProviderKey == providerKey) if (login.ProviderKey == id)
{ {
user = _users.GetUser(identityuser.UserName); user = _users.GetUser(identityuser.UserName);
} }
else else
{ {
// provider keys do not match // provider keys do not match
status = ExternalLoginStatus.ProviderKeyMismatch; identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
} }
} }
@ -311,23 +340,22 @@ namespace Oqtane.Extensions
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>(); var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name; string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(providerKey)}"; 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}. "; 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!"; 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); var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
status = ExternalLoginStatus.VerificationRequired; 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); _logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
} }
} }
// add claims to principal // manage user
if (user != null) if (user != null)
{ {
var principal = (ClaimsIdentity)claimsPrincipal.Identity; // create claims identity
UserSecurity.ResetClaimsIdentity(principal); identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
var identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList()); identity.Label = ExternalLoginStatus.Success;
principal.AddClaims(identity.Claims);
// update user // update user
user.LastLoginOn = DateTime.UtcNow; user.LastLoginOn = DateTime.UtcNow;
@ -338,25 +366,17 @@ namespace Oqtane.Extensions
} }
else // email invalid else // email invalid
{ {
status = ExternalLoginStatus.InvalidEmail; identity.Label = ExternalLoginStatus.InvalidEmail;
if (!string.IsNullOrEmpty(email)) if (!string.IsNullOrEmpty(email))
{ {
_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); _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);
} }
else else
{
var emailclaimtype = claimsPrincipal.Claims.FirstOrDefault(item => item.Value.Contains("@") && item.Value.Contains("."));
if (emailclaimtype != null)
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Please Verify If \"{ClaimType}\" Is A Valid Email Claim Type For The Provider And Update Your External Login Settings Accordingly", emailclaimtype.Type);
}
else
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User."); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User.");
} }
} }
} return identity;
return status;
} }
private static bool EmailValid(string email, string domainfilter) private static bool EmailValid(string email, string domainfilter)

View File

@ -14,5 +14,14 @@ namespace Oqtane.Extensions
return list.Any(f => s.StartsWith(f)); return list.Any(f => s.StartsWith(f));
} }
public static string ReplaceMultiple(this string s, string[] oldValues, string newValue)
{
foreach(string value in oldValues)
{
s = s.Replace(value, newValue);
}
return s;
}
} }
} }

View File

@ -45,12 +45,11 @@ namespace Oqtane.Infrastructure
}; };
// jwt already contains the roles - we are reloading to ensure most accurate permissions // jwt already contains the roles - we are reloading to ensure most accurate permissions
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
// populate principal // set claims identity
var principal = (ClaimsIdentity)context.User.Identity; var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
UserSecurity.ResetClaimsIdentity(principal); context.User = new ClaimsPrincipal(claimsidentity);
principal.AddClaims(identity.Claims);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username); logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
} }
else else

View File

@ -1,11 +1,14 @@
using System.Net; using System.Net;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Extensions; using Oqtane.Extensions;
namespace Oqtane.Pages namespace Oqtane.Pages
{ {
[AllowAnonymous]
[IgnoreAntiforgeryToken]
public class ExternalModel : PageModel public class ExternalModel : PageModel
{ {
public IActionResult OnGetAsync(string returnurl) public IActionResult OnGetAsync(string returnurl)
@ -16,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 }); return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
} }
else else
{ {
@ -24,5 +27,22 @@ namespace Oqtane.Pages
return new EmptyResult(); return new EmptyResult();
} }
} }
public IActionResult OnPostAsync(string returnurl)
{
if (returnurl == null)
{
returnurl = "";
}
if (!returnurl.StartsWith("/"))
{
returnurl = "/" + returnurl;
}
// remove reload parameter
returnurl = returnurl.ReplaceMultiple(new string[] { "?reload=post", "&reload=post" }, "");
return LocalRedirect(Url.Content("~" + returnurl));
}
} }
} }

View File

@ -20,7 +20,7 @@ namespace Oqtane.Pages
public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl) public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl)
{ {
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) if (!User.Identity.IsAuthenticated && !string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{ {
bool validuser = false; bool validuser = false;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username);

View File

@ -1,11 +1,8 @@
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Extensions;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Pages namespace Oqtane.Pages
@ -13,7 +10,7 @@ namespace Oqtane.Pages
[Authorize] [Authorize]
public class LogoutModel : PageModel public class LogoutModel : PageModel
{ {
public async Task<IActionResult> OnGetAsync(string returnurl) public async Task<IActionResult> OnPostAsync(string returnurl)
{ {
await HttpContext.SignOutAsync(Constants.AuthenticationScheme); await HttpContext.SignOutAsync(Constants.AuthenticationScheme);

View File

@ -159,8 +159,8 @@ namespace Oqtane.Repository
moduledefinition.Name = (!string.IsNullOrEmpty(moduledef.Name)) ? moduledef.Name : moduledefinition.Name; moduledefinition.Name = (!string.IsNullOrEmpty(moduledef.Name)) ? moduledef.Name : moduledefinition.Name;
moduledefinition.Description = (!string.IsNullOrEmpty(moduledef.Description)) ? moduledef.Description : moduledefinition.Description; moduledefinition.Description = (!string.IsNullOrEmpty(moduledef.Description)) ? moduledef.Description : moduledefinition.Description;
moduledefinition.Categories = (!string.IsNullOrEmpty(moduledef.Categories)) ? moduledef.Categories : moduledefinition.Categories; moduledefinition.Categories = (!string.IsNullOrEmpty(moduledef.Categories)) ? moduledef.Categories : moduledefinition.Categories;
// manage versioning // manage releaseversions in cases where it was not provided or is lower than the module version
if (string.IsNullOrEmpty(moduledefinition.ReleaseVersions)) if (string.IsNullOrEmpty(moduledefinition.ReleaseVersions) || Version.Parse(moduledefinition.Version).CompareTo(Version.Parse(moduledefinition.ReleaseVersions.Split(',').Last())) > 0)
{ {
moduledefinition.ReleaseVersions = moduledefinition.Version; moduledefinition.ReleaseVersions = moduledefinition.Version;
} }

View File

@ -130,7 +130,7 @@ namespace Oqtane
services.AddMvc(options => services.AddMvc(options =>
{ {
//options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
}) })
.AddNewtonsoftJson() .AddNewtonsoftJson()
.AddOqtaneApplicationParts() // register any Controllers from custom modules .AddOqtaneApplicationParts() // register any Controllers from custom modules

View File

@ -1,13 +0,0 @@
namespace Oqtane.Shared
{
public enum ExternalLoginStatus
{
Success,
InvalidEmail,
DuplicateEmail,
UserNotCreated,
UserDoesNotExist,
ProviderKeyMismatch,
VerificationRequired
}
}

View File

@ -152,14 +152,5 @@ namespace Oqtane.Security
} }
return identity; return identity;
} }
public static void ResetClaimsIdentity(ClaimsIdentity identity)
{
var claims = identity.Claims.ToList(); // clone
foreach (var claim in claims)
{
identity.RemoveClaim(claim);
}
}
} }
} }

View File

@ -0,0 +1,13 @@
namespace Oqtane.Shared {
public class ExternalLoginStatus {
public const string Success = "Success";
public const string InvalidEmail = "InvalidEmail";
public const string DuplicateEmail = "DuplicateEmail";
public const string UserNotCreated = "UserNotCreated";
public const string UserDoesNotExist = "UserDoesNotExist";
public const string ProviderKeyMismatch = "ProviderKeyMismatch";
public const string VerificationRequired = "VerificationRequired";
public const string AccessDenied = "AccessDenied";
public const string RemoteFailure = "RemoteFailure";
}
}