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">
<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">
<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>

View File

@ -40,7 +40,7 @@
<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>
<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>
}

View File

@ -35,7 +35,7 @@
<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>
<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>
}

View File

@ -62,13 +62,19 @@
<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>
<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 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>
<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 class="row mb-1 align-items-center">
@ -104,6 +110,8 @@
private string _hostUsername = string.Empty;
private string _hostPassword = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty;
private bool _register = true;
@ -112,6 +120,7 @@
protected override async Task OnInitializedAsync()
{
_togglepassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault))
{
@ -218,4 +227,18 @@
_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
{
_togglepassword = Localizer["ShowPassword"];
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{
@ -188,8 +188,7 @@
if (!twofactor)
{
bool setCookie = (PageState.Runtime == Oqtane.Shared.Runtime.WebAssembly);
user = await UserService.LoginUserAsync(user, setCookie, false);
user = await UserService.LoginUserAsync(user);
}
else
{
@ -200,23 +199,14 @@
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
{
// server-side Blazor needs to post to the Login page so that the cookies are set correctly
// post back 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 };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
else
{
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
}
else
{
if (user.TwoFactorRequired)
if (PageState.Site.Settings["LoginOptions:TwoFactor"] == "required" || user.TwoFactorRequired)
{
twofactor = true;
validated = false;
@ -308,12 +298,12 @@
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = Localizer["HidePassword"];
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = Localizer["ShowPassword"];
_togglepassword = SharedLocalizer["ShowPassword"];
}
}

View File

@ -27,13 +27,19 @@
<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>
<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 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>
<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 class="row mb-1 align-items-center">
@ -66,12 +72,19 @@ else
private ElementReference form;
private bool validated = false;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnParametersSet()
{
_togglepassword = SharedLocalizer["ShowPassword"];
}
private async Task Register()
{
validated = true;
@ -134,4 +147,18 @@ else
{
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">
<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">
<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 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>
<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>
@ -36,12 +42,16 @@
private bool validated = false;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override async Task OnInitializedAsync()
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{
_username = PageState.QueryString["name"];
@ -110,4 +120,18 @@
{
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");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = Localizer["Show"];
_togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
@ -656,12 +656,12 @@
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = Localizer["Hide"];
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = Localizer["Show"];
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -156,7 +156,7 @@ else
<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>
<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>
}
@ -307,7 +307,7 @@ else
user.SiteId = PageState.Site.SiteId;
user.Username = _hostusername;
user.Password = _hostpassword;
user = await UserService.LoginUserAsync(user, false, false);
user = await UserService.LoginUserAsync(user);
if (user.IsAuthenticated)
{
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>
<div class="col-sm-9">
<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>
</div>
</div>
@ -41,7 +41,10 @@ else
<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>
<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>
@if (allowtwofactor)
@ -246,10 +249,11 @@ else
{
try
{
_togglepassword = Localizer["ShowPassword"];
_togglepassword = SharedLocalizer["ShowPassword"];
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)
@ -455,12 +459,12 @@ else
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = Localizer["HidePassword"];
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_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>
<div class="col-sm-9">
<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>
</div>
</div>
@ -30,7 +30,10 @@
<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>
<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 class="row mb-1 align-items-center">
@ -107,7 +110,7 @@
{
try
{
_togglepassword = Localizer["ShowPassword"];
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>();
}
@ -204,12 +207,12 @@
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = Localizer["HidePassword"];
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_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>
<div class="col-sm-9">
<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>
</div>
</div>
@ -39,7 +39,10 @@ else
<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>
<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 class="row mb-1 align-items-center">
@ -162,10 +165,9 @@ else
{
try
{
// OnParametersSetAsync is called when the edit modal is closed - in which case there is no id parameter
if (PageState.QueryString.ContainsKey("id"))
{
_togglepassword = Localizer["ShowPassword"];
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
userid = Int32.Parse(PageState.QueryString["id"]);
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
@ -279,12 +281,12 @@ else
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = Localizer["HidePassword"];
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = Localizer["ShowPassword"];
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -94,28 +94,28 @@ else
</div>
</div>
}
<div class="row mb-1 align-items-center">
<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))
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<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">
<select id="cookietype" class="form-select" @bind="@_cookietype">
<option value="domain">@Localizer["Domain"]</option>
<option value="site">@Localizer["Site"]</option>
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</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>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<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>
@ -274,15 +274,18 @@ else
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<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">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
}
<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>
<div class="col-sm-9">
@ -338,6 +341,7 @@ else
</div>
</div>
</Section>
}
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
@ -353,7 +357,7 @@ else
private string _allowregistration;
private string _allowsitelogin;
private string _twofactor;
private string _cookietype;
private string _cookiename;
private string _minimumlength;
private string _uniquecharacters;
@ -378,6 +382,7 @@ else
private string _scopes;
private string _pkce;
private string _redirecturl;
private string _identifierclaimtype;
private string _emailclaimtype;
private string _domainfilter;
private string _createusers;
@ -401,8 +406,11 @@ else
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_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");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
@ -423,19 +431,20 @@ else
_userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", "");
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = Localizer["Show"];
_toggleclientsecret = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_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");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
_togglesecret = Localizer["Show"];
_togglesecret = SharedLocalizer["ShowPassword"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_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)
@ -507,8 +516,11 @@ else
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
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: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:RequiredUniqueChars", _uniquecharacters, true);
@ -531,6 +543,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, 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:DomainFilter", _domainfilter, 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:Audience", _audience, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true);
}
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync();
@ -561,13 +575,20 @@ else
private void ProviderTypeChanged(ChangeEventArgs e)
{
_providertype = (string)e.Value;
if (string.IsNullOrEmpty(_providername))
{
if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
_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
{
_scopes = "";
_identifierclaimtype = "sub";
_emailclaimtype = "email";
}
}
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged();
@ -583,12 +604,12 @@ else
if (_clientsecrettype == "password")
{
_clientsecrettype = "text";
_toggleclientsecret = Localizer["Hide"];
_toggleclientsecret = SharedLocalizer["HidePassword"];
}
else
{
_clientsecrettype = "password";
_toggleclientsecret = Localizer["Show"];
_toggleclientsecret = SharedLocalizer["ShowPassword"];
}
}
@ -597,12 +618,12 @@ else
if (_secrettype == "password")
{
_secrettype = "text";
_togglesecret = Localizer["Hide"];
_togglesecret = SharedLocalizer["HidePassword"];
}
else
{
_secrettype = "password";
_togglesecret = Localizer["Show"];
_togglesecret = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -189,12 +189,6 @@
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</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">
<value>Use</value>
</data>
@ -225,4 +219,10 @@
<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>
</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>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -168,12 +168,6 @@
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</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">
<value>Password</value>
</data>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -183,12 +183,6 @@
<data name="Profile.Heading" xml:space="preserve">
<value>Profile</value>
</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">
<value>Password</value>
</data>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -247,10 +247,10 @@
<value>Domain Filter:</value>
</data>
<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 name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim Type:</value>
<value>Email Claim:</value>
</data>
<data name="ExternalLoginSettings.Heading" xml:space="preserve">
<value>External Login Settings</value>
@ -318,11 +318,11 @@
<data name="UserSettings.Heading" xml:space="preserve">
<value>User Settings</value>
</data>
<data name="CookieType.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>
<data name="CookieName.HelpText" xml:space="preserve">
<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 name="CookieType.Text" xml:space="preserve">
<value>Login Cookie Type:</value>
<data name="CookieName.Text" xml:space="preserve">
<value>Cookie Name:</value>
</data>
<data name="CreateToken" xml:space="preserve">
<value>Create Token</value>
@ -355,16 +355,19 @@
<value>Token Settings</value>
</data>
<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 name="TwoFactor.Text" xml:space="preserve">
<value>Allow Two Factor?</value>
<value>Two Factor?</value>
</data>
<data name="Hide" xml:space="preserve">
<value>Hide</value>
<data name="Disabled" xml:space="preserve">
<value>Disabled</value>
</data>
<data name="Show" xml:space="preserve">
<value>Show</value>
<data name="Optional" xml:space="preserve">
<value>Optional</value>
</data>
<data name="Required" xml:space="preserve">
<value>Required</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Created On</value>
@ -375,4 +378,10 @@
<data name="LastLoginOn" xml:space="preserve">
<value>Last Login</value>
</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>

View File

@ -321,4 +321,10 @@
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
</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.
/// </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="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>
Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent);
Task<User> LoginUserAsync(User user);
/// <summary>
/// Logout a <see cref="User"/>

View File

@ -22,10 +22,15 @@ namespace Oqtane.Services
private HttpClient GetHttpClient()
{
var httpClient = _httpClientFactory.CreateClient("Remote");
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && _siteState != null && !string.IsNullOrEmpty(_siteState.AuthorizationToken))
return GetHttpClient(_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;
}

View File

@ -39,9 +39,9 @@ namespace Oqtane.Services
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)

View File

@ -1,10 +1,8 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Providers;
using Oqtane.Security;
using Oqtane.Services;
using Oqtane.Shared;
@ -33,28 +31,19 @@ namespace Oqtane.Themes.Controls
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);
PageState.User = null;
// check if anonymous user can access page
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;
}
if (PageState.Runtime == Shared.Runtime.Server)
{
// server-side Blazor needs to redirect to the Logout page
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/logout/") + "?returnurl=" + WebUtility.UrlEncode(url), true);
}
else
{
// client-side Blazor
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(url, true));
}
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}
}
}

View File

@ -84,20 +84,30 @@
var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction;
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
if (querystring.ContainsKey("reload") || (!route.AbsolutePath.Substring(1).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
// 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") || (!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;
}
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"))
{
refresh = UI.Refresh.Site;
}
}
if (PageState != null)
{

View File

@ -52,6 +52,7 @@ namespace Oqtane.Controllers
}
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);
}
sqlquery.Results = results;

View File

@ -316,7 +316,7 @@ namespace Oqtane.Controllers
// POST api/<controller>/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 };
@ -357,10 +357,6 @@ namespace Oqtane.Controllers
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(loginUser);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
}
else
{

View File

@ -16,9 +16,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OAuth;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Net;
using System.Text.Json.Nodes;
namespace Oqtane.Extensions
{
@ -29,15 +29,7 @@ namespace Oqtane.Extensions
// site cookie authentication options
builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("LoginOptions:CookieType", "domain") == "domain")
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
}
else
{
// use unique cookie name for site
options.Cookie.Name = ".AspNetCore.Identity.Application" + alias.SiteKey;
}
options.Cookie.Name = sitesettings.GetValue("LoginOptions:CookieName", ".AspNetCore.Identity.Application");
});
// site OpenId Connect options
@ -105,6 +97,7 @@ namespace Oqtane.Extensions
// oauth2 events
options.Events.OnCreatingTicket = OnCreatingTicket;
options.Events.OnTicketReceived = OnTicketReceived;
options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure;
}
@ -117,10 +110,13 @@ namespace Oqtane.Extensions
{
// OAuth 2.0
var email = "";
var id = "";
if (context.Options.UserInformationEndpoint != "")
{
try
{
// call user information endpoint
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
@ -129,17 +125,34 @@ namespace Oqtane.Extensions
response.EnsureSuccessStatusCode();
var output = await response.Content.ReadAsStringAsync();
// get email address using Regex on the raw output (could be json or html)
var regex = new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase);
foreach (Match match in regex.Matches(output))
// parse json output
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
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;
}
}
}
if (string.IsNullOrEmpty(id))
{
id = email;
}
}
catch (Exception ex)
{
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
@ -147,29 +160,51 @@ namespace Oqtane.Extensions
}
}
// login user
var status = await LoginUser(email, context.HttpContext, context.Principal);
// validate user
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)
{
// redirect to login page and pass status
var alias = context.HttpContext.GetAlias();
context.Response.Redirect($"{alias.Path}/login?status={status}&returnurl={context.Properties.RedirectUri}", true);
};
context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={status}&returnurl={context.Properties.GetParameter<string>("redirecturl")}"), true);
context.HandleResponse();
}
return Task.CompletedTask;
}
private static async Task OnTokenValidated(TokenValidatedContext context)
{
// 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 email = context.Principal.FindFirstValue(emailClaimType);
// login user
var status = await LoginUser(email, context.HttpContext, context.Principal);
if (status != ExternalLoginStatus.Success)
// validate user
var identity = await ValidateUser(email, id, context.HttpContext);
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
var alias = context.HttpContext.GetAlias();
context.Response.Redirect($"{alias.Path}/login?status={status}&returnurl={context.Properties.RedirectUri}", true);
context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={identity.Label}&returnurl={context.Properties.RedirectUri}"), true);
context.HandleResponse();
}
}
@ -179,8 +214,7 @@ namespace Oqtane.Extensions
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");
// redirect to login page
var alias = context.HttpContext.GetAlias();
context.Response.Redirect($"{alias.Path}/login?returnurl={context.Properties.RedirectUri}", true);
context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={ExternalLoginStatus.AccessDenied}&returnurl={context.Properties.RedirectUri}"), true);
context.HandleResponse();
return Task.CompletedTask;
}
@ -190,16 +224,16 @@ namespace Oqtane.Extensions
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message);
// redirect to login page
var alias = context.HttpContext.GetAlias();
context.Response.Redirect($"{alias.Path}/login", true);
context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={ExternalLoginStatus.RemoteFailure}"), true);
context.HandleResponse();
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 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", "")))
{
@ -209,11 +243,6 @@ namespace Oqtane.Extensions
var alias = httpContext.GetAlias();
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
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;
bool duplicates = false;
@ -231,7 +260,7 @@ namespace Oqtane.Extensions
{
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);
}
else
@ -265,25 +294,25 @@ namespace Oqtane.Extensions
_notifications.AddNotification(notification);
// 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);
}
else
{
status = ExternalLoginStatus.UserNotCreated;
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
}
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());
}
}
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);
}
}
@ -294,14 +323,14 @@ namespace Oqtane.Extensions
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login != null)
{
if (login.ProviderKey == providerKey)
if (login.ProviderKey == id)
{
user = _users.GetUser(identityuser.UserName);
}
else
{
// 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);
}
}
@ -311,23 +340,22 @@ namespace Oqtane.Extensions
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(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}. ";
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);
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);
}
}
// add claims to principal
// manage user
if (user != null)
{
var principal = (ClaimsIdentity)claimsPrincipal.Identity;
UserSecurity.ResetClaimsIdentity(principal);
var identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
principal.AddClaims(identity.Claims);
// create claims identity
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
identity.Label = ExternalLoginStatus.Success;
// update user
user.LastLoginOn = DateTime.UtcNow;
@ -338,25 +366,17 @@ namespace Oqtane.Extensions
}
else // email invalid
{
status = ExternalLoginStatus.InvalidEmail;
identity.Label = ExternalLoginStatus.InvalidEmail;
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);
}
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.");
}
}
}
return status;
return identity;
}
private static bool EmailValid(string email, string domainfilter)

View File

@ -14,5 +14,14 @@ namespace Oqtane.Extensions
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
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
// populate principal
var principal = (ClaimsIdentity)context.User.Identity;
UserSecurity.ResetClaimsIdentity(principal);
principal.AddClaims(identity.Claims);
// set claims identity
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
context.User = new ClaimsPrincipal(claimsidentity);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
}
else

View File

@ -1,11 +1,14 @@
using System.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Extensions;
namespace Oqtane.Pages
{
[AllowAnonymous]
[IgnoreAntiforgeryToken]
public class ExternalModel : PageModel
{
public IActionResult OnGetAsync(string returnurl)
@ -16,7 +19,7 @@ namespace Oqtane.Pages
var providertype = HttpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
if (providertype != "")
{
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl });
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
}
else
{
@ -24,5 +27,22 @@ namespace Oqtane.Pages
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)
{
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
if (!User.Identity.IsAuthenticated && !string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
bool validuser = false;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username);

View File

@ -1,11 +1,8 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Extensions;
using Oqtane.Shared;
namespace Oqtane.Pages
@ -13,7 +10,7 @@ namespace Oqtane.Pages
[Authorize]
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnGetAsync(string returnurl)
public async Task<IActionResult> OnPostAsync(string returnurl)
{
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.Description = (!string.IsNullOrEmpty(moduledef.Description)) ? moduledef.Description : moduledefinition.Description;
moduledefinition.Categories = (!string.IsNullOrEmpty(moduledef.Categories)) ? moduledef.Categories : moduledefinition.Categories;
// manage versioning
if (string.IsNullOrEmpty(moduledefinition.ReleaseVersions))
// manage releaseversions in cases where it was not provided or is lower than the module version
if (string.IsNullOrEmpty(moduledefinition.ReleaseVersions) || Version.Parse(moduledefinition.Version).CompareTo(Version.Parse(moduledefinition.ReleaseVersions.Split(',').Last())) > 0)
{
moduledefinition.ReleaseVersions = moduledefinition.Version;
}

View File

@ -130,7 +130,7 @@ namespace Oqtane
services.AddMvc(options =>
{
//options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
})
.AddNewtonsoftJson()
.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;
}
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";
}
}