Merge pull request #2060 from sbwalker/dev
Added support for per site options and OpenID Connect
This commit is contained in:
		| @ -18,14 +18,19 @@ | ||||
| 		@if (!twofactor) | ||||
| 		{ | ||||
| 			<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate> | ||||
| 				<div class="container Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))"> | ||||
| 				<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))"> | ||||
| 					@if (PageState.Site.Settings.ContainsKey("OpenIdConnectOptions:Provider") && !string.IsNullOrEmpty(PageState.Site.Settings["OpenIdConnectOptions:Provider"])) | ||||
| 					{ | ||||
| 						<button type="button" class="btn btn-primary" @onclick="ExternalLogin">Use @PageState.Site.Settings["OpenIdConnectOptions:Provider"]</button>						 | ||||
| 						<hr /> | ||||
| 					} | ||||
| 					<div class="form-group"> | ||||
| 						<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label> | ||||
| 						<input id="username" type="text" @ref="username" class="form-control input" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required /> | ||||
| 						<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required /> | ||||
| 					</div> | ||||
| 					<div class="form-group mt-2"> | ||||
| 						<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label> | ||||
| 						<div class="input-group password"> | ||||
| 						<div class="input-group"> | ||||
| 							<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required /> | ||||
| 							<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> | ||||
| 						</div> | ||||
| @ -49,7 +54,7 @@ | ||||
| 				<div class="container Oqtane-Modules-Admin-Login"> | ||||
| 					<div class="form-group"> | ||||
| 						<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label> | ||||
| 						<input id="code" class="form-control input" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required /> | ||||
| 						<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required /> | ||||
| 					</div> | ||||
| 					<br /> | ||||
| 					<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button> | ||||
| @ -245,7 +250,11 @@ | ||||
| 			_passwordtype = "password"; | ||||
| 			_togglepassword = Localizer["ShowPassword"]; | ||||
| 		} | ||||
| 		//StateHasChanged(); | ||||
| 	} | ||||
|  | ||||
| 	private void ExternalLogin() | ||||
| 	{ | ||||
|         NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/oidc?returnurl=" + _returnUrl), true); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ | ||||
| @inject IUserService UserService | ||||
| @inject ISettingService SettingService | ||||
| @inject ISiteService SiteService | ||||
| @inject ISystemService SystemService | ||||
| @inject IStringLocalizer<Index> Localizer | ||||
| @inject IStringLocalizer<SharedResources> SharedLocalizer | ||||
|  | ||||
| @ -65,74 +64,97 @@ else | ||||
| 						</select> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) | ||||
| 				{ | ||||
| 					<br /> | ||||
| 					<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> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<input id="minimumlength" class="form-control" @bind="@_minimumlength" required /> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required /> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<select id="requiredigit" class="form-select" @bind="@_requiredigit" required> | ||||
| 									<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 									<option value="false">@SharedLocalizer["No"]</option> | ||||
| 								</select> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<select id="requireupper" class="form-select" @bind="@_requireupper" required> | ||||
| 									<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 									<option value="false">@SharedLocalizer["No"]</option> | ||||
| 								</select> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<select id="requirelower" class="form-select" @bind="@_requirelower" required> | ||||
| 									<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 									<option value="false">@SharedLocalizer["No"]</option> | ||||
| 								</select> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required> | ||||
| 									<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 									<option value="false">@SharedLocalizer["No"]</option> | ||||
| 								</select> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 					</Section> | ||||
| 					<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings"> | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<input id="maximum" class="form-control" @bind="@_maximumfailures" required /> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 						<div class="row mb-1 align-items-center"> | ||||
| 							<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label> | ||||
| 							<div class="col-sm-9"> | ||||
| 								<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required /> | ||||
| 							</div> | ||||
| 						</div>					 | ||||
| 					</Section> | ||||
| 				} | ||||
| 				<br /> | ||||
| 				<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> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="minimumlength" class="form-control" @bind="@_minimumlength" required /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<select id="requiredigit" class="form-select" @bind="@_requiredigit" required> | ||||
| 								<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 								<option value="false">@SharedLocalizer["No"]</option> | ||||
| 							</select> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<select id="requireupper" class="form-select" @bind="@_requireupper" required> | ||||
| 								<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 								<option value="false">@SharedLocalizer["No"]</option> | ||||
| 							</select> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<select id="requirelower" class="form-select" @bind="@_requirelower" required> | ||||
| 								<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 								<option value="false">@SharedLocalizer["No"]</option> | ||||
| 							</select> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required> | ||||
| 								<option value="true">@SharedLocalizer["Yes"]</option> | ||||
| 								<option value="false">@SharedLocalizer["No"]</option> | ||||
| 							</select> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 				</Section> | ||||
| 				<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings"> | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="maximum" class="form-control" @bind="@_maximumfailures" required /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 				</Section> | ||||
| 				<Section Name="OpenIDConnect" Heading="OpenID Connect Settings" ResourceKey="OpenIDConnectSettings"> | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="provider" HelpText="The OpenID Connect Provider Name" ResourceKey="Provider">Provider:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="provider" class="form-control" @bind="@_provider" /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="authority" HelpText="The OpenID Connect Authority" ResourceKey="Authority">Authority:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="authority" class="form-control" @bind="@_authority" /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="clientid" HelpText="The OpenID Connect Client ID" ResourceKey="ClientID">Client ID:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="clientid" class="form-control" @bind="@_clientid" /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 					<div class="row mb-1 align-items-center"> | ||||
| 						<Label Class="col-sm-3" For="clientsecret" HelpText="The OpenID Connect Client Secret" ResourceKey="ClientSecret">Client Secret:</Label> | ||||
| 						<div class="col-sm-9"> | ||||
| 							<input id="clientsecret" class="form-control" @bind="@_clientsecret" /> | ||||
| 						</div> | ||||
| 					</div>					 | ||||
| 				</Section> | ||||
| 			</div> | ||||
| 			<br /> | ||||
| 			<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button> | ||||
| @ -146,14 +168,18 @@ else | ||||
| 	private string _search; | ||||
|  | ||||
| 	private string _allowregistration; | ||||
| 	private string _minimumlength = "6"; | ||||
| 	private string _uniquecharacters = "1"; | ||||
| 	private string _requiredigit = "true"; | ||||
| 	private string _requireupper = "true"; | ||||
| 	private string _requirelower = "true"; | ||||
| 	private string _requirepunctuation = "true"; | ||||
| 	private string _maximumfailures = "5"; | ||||
| 	private string _lockoutduration = "5"; | ||||
| 	private string _minimumlength; | ||||
| 	private string _uniquecharacters; | ||||
| 	private string _requiredigit; | ||||
| 	private string _requireupper; | ||||
| 	private string _requirelower; | ||||
| 	private string _requirepunctuation; | ||||
| 	private string _maximumfailures; | ||||
| 	private string _lockoutduration; | ||||
| 	private string _provider; | ||||
| 	private string _authority; | ||||
| 	private string _clientid; | ||||
| 	private string _clientsecret; | ||||
|  | ||||
| 	public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; | ||||
|  | ||||
| @ -164,21 +190,19 @@ else | ||||
| 		userroles = Search(_search); | ||||
|  | ||||
| 		_allowregistration = PageState.Site.AllowRegistration.ToString(); | ||||
| 		if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) | ||||
| 		{ | ||||
| 			Dictionary<string, object> systeminfo = await SystemService.GetSystemInfoAsync(); | ||||
| 			if (systeminfo != null) | ||||
| 			{ | ||||
| 				_minimumlength = systeminfo["Password:RequiredLength"].ToString(); | ||||
| 				_uniquecharacters = systeminfo["Password:RequiredUniqueChars"].ToString(); | ||||
| 				_requiredigit = systeminfo["Password:RequireDigit"].ToString(); | ||||
| 				_requireupper = systeminfo["Password:RequireUppercase"].ToString(); | ||||
| 				_requirelower = systeminfo["Password:RequireLowercase"].ToString(); | ||||
| 				_requirepunctuation = systeminfo["Password:RequireNonAlphanumeric"].ToString(); | ||||
| 				_maximumfailures = systeminfo["Lockout:MaxFailedAccessAttempts"].ToString(); | ||||
| 				_lockoutduration = TimeSpan.Parse(systeminfo["Lockout:DefaultLockoutTimeSpan"].ToString()).TotalMinutes.ToString(); | ||||
| 			} | ||||
| 		} | ||||
| 		var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); | ||||
| 		_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6"); | ||||
| 		_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1"); | ||||
| 		_requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true"); | ||||
| 		_requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true"); | ||||
| 		_requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true"); | ||||
| 		_requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true"); | ||||
| 		_maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5"); | ||||
| 		_lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString(); | ||||
| 		_provider = SettingService.GetSetting(settings, "OpenIdConnectOptions:Provider", ""); | ||||
| 		_authority = SettingService.GetSetting(settings, "OpenIdConnectOptions:Authority", ""); | ||||
| 		_clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", ""); | ||||
| 		_clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); | ||||
| 	} | ||||
|  | ||||
| 	private List<UserRole> Search(string search) | ||||
| @ -248,24 +272,22 @@ else | ||||
| 			site.AllowRegistration = bool.Parse(_allowregistration); | ||||
| 			await SiteService.UpdateSiteAsync(site); | ||||
|  | ||||
| 			if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) | ||||
| 			{ | ||||
| 				var settings = new Dictionary<string, object>(); | ||||
| 				settings.Add("Password:RequiredLength", _minimumlength); | ||||
| 				settings.Add("Password:RequiredUniqueChars", _uniquecharacters); | ||||
| 				settings.Add("Password:RequireDigit", _requiredigit); | ||||
| 				settings.Add("Password:RequireUppercase", _requireupper); | ||||
| 				settings.Add("Password:RequireLowercase", _requirelower); | ||||
| 				settings.Add("Password:RequireNonAlphanumeric", _requirepunctuation); | ||||
| 				settings.Add("Lockout:MaxFailedAccessAttempts", _maximumfailures); | ||||
| 				settings.Add("Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString()); | ||||
| 				await SystemService.UpdateSystemInfoAsync(settings); | ||||
| 				AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);				 | ||||
| 			} | ||||
| 			var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true); | ||||
| 			settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true); | ||||
| 			settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Provider", _provider, false); | ||||
| 			settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Authority", _authority, true); | ||||
| 			settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientId", _clientid, true); | ||||
| 			settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientSecret", _clientsecret, true); | ||||
| 			await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); | ||||
|  | ||||
| 			AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);				 | ||||
| 		} | ||||
| 		catch (Exception ex) | ||||
| 		{ | ||||
|  | ||||
| @ -38,7 +38,7 @@ namespace Oqtane.Themes.Controls | ||||
|             PageState.User = null; | ||||
|             bool authorizedtoviewpage = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, PageState.Page.Permissions); | ||||
|  | ||||
|             if (PageState.Runtime == Oqtane.Shared.Runtime.Server) | ||||
|             if (PageState.Runtime == Shared.Runtime.Server) | ||||
|             { | ||||
|                 // server-side Blazor needs to post to the Logout page | ||||
|                 var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = !authorizedtoviewpage ? PageState.Alias.Path : PageState.Alias.Path + "/" + PageState.Page.Path }; | ||||
|  | ||||
| @ -51,14 +51,6 @@ namespace Oqtane.Controllers | ||||
|                     systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); | ||||
|                     systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true")); | ||||
|                     systeminfo.Add("PackageService", _configManager.GetSetting("PackageService", "true")); | ||||
|                     systeminfo.Add("Password:RequiredLength", _configManager.GetSetting("Password:RequiredLength", "6")); | ||||
|                     systeminfo.Add("Password:RequiredUniqueChars", _configManager.GetSetting("Password:RequiredUniqueChars", "1")); | ||||
|                     systeminfo.Add("Password:RequireDigit", _configManager.GetSetting("Password:RequireDigit", "true")); | ||||
|                     systeminfo.Add("Password:RequireUppercase", _configManager.GetSetting("Password:RequireUppercase", "true")); | ||||
|                     systeminfo.Add("Password:RequireLowercase", _configManager.GetSetting("Password:RequireLowercase", "true")); | ||||
|                     systeminfo.Add("Password:RequireNonAlphanumeric", _configManager.GetSetting("Password:RequireNonAlphanumeric", "true")); | ||||
|                     systeminfo.Add("Lockout:MaxFailedAccessAttempts", _configManager.GetSetting("Lockout:MaxFailedAccessAttempts", "5")); | ||||
|                     systeminfo.Add("Lockout:DefaultLockoutTimeSpan", _configManager.GetSetting("Lockout:DefaultLockoutTimeSpan", "00:05:00")); | ||||
|                     break; | ||||
|             } | ||||
|  | ||||
|  | ||||
							
								
								
									
										18
									
								
								Oqtane.Server/Extensions/HttpContextExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Oqtane.Server/Extensions/HttpContextExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Oqtane.Models; | ||||
| using Oqtane.Shared; | ||||
|  | ||||
| namespace Oqtane.Extensions | ||||
| { | ||||
|     public static class HttpContextExtensions | ||||
|     { | ||||
|         public static Alias GetAlias(this HttpContext context) | ||||
|         { | ||||
|             if (context != null && context.Items.ContainsKey(Constants.HttpContextAliasKey)) | ||||
|             { | ||||
|                 return context.Items[Constants.HttpContextAliasKey] as Alias; | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using Oqtane.Infrastructure; | ||||
| using Oqtane.Models; | ||||
| using Oqtane.Modules; | ||||
| using Oqtane.Repository; | ||||
| using Oqtane.Security; | ||||
| @ -58,6 +59,12 @@ namespace Microsoft.Extensions.DependencyInjection | ||||
|             return services; | ||||
|         } | ||||
|  | ||||
|         public static OqtaneSiteOptionsBuilder<T> AddOqtaneSiteOptions<T>(this IServiceCollection services) | ||||
|             where T : class, IAlias, new() | ||||
|         { | ||||
|             return new OqtaneSiteOptionsBuilder<T>(services); | ||||
|         } | ||||
|  | ||||
|         internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) | ||||
|         { | ||||
|             services.AddSingleton<IInstallationManager, InstallationManager>(); | ||||
| @ -71,6 +78,8 @@ namespace Microsoft.Extensions.DependencyInjection | ||||
|         internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) | ||||
|         { | ||||
|             services.AddTransient<ITenantManager, TenantManager>(); | ||||
|             services.AddTransient<IAliasAccessor, AliasAccessor>(); | ||||
|  | ||||
|             services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>(); | ||||
|             services.AddTransient<IThemeRepository, ThemeRepository>(); | ||||
|             services.AddTransient<IUserPermissions, UserPermissions>(); | ||||
| @ -124,6 +133,11 @@ namespace Microsoft.Extensions.DependencyInjection | ||||
|                     context.Response.StatusCode = (int)HttpStatusCode.Forbidden; | ||||
|                     return Task.CompletedTask; | ||||
|                 }; | ||||
|                 options.Events.OnRedirectToLogout = context => | ||||
|                 { | ||||
|                     context.Response.StatusCode = (int)HttpStatusCode.Forbidden; | ||||
|                     return Task.CompletedTask; | ||||
|                 }; | ||||
|                 options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync; | ||||
|             }); | ||||
|  | ||||
| @ -314,7 +328,7 @@ namespace Microsoft.Extensions.DependencyInjection | ||||
|  | ||||
|                         try | ||||
|                         { | ||||
|                             Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyFile.FullName))); | ||||
|                             Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyFile.FullName))); | ||||
|                             Debug.WriteLine($"Oqtane Info: Loaded Assembly {assemblyName}"); | ||||
|                         } | ||||
|                         catch (Exception ex) | ||||
| @ -333,9 +347,9 @@ namespace Microsoft.Extensions.DependencyInjection | ||||
|         private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name) | ||||
|         { | ||||
|             var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll"; | ||||
|             if (File.Exists(assemblyPath)) | ||||
|             if (System.IO.File.Exists(assemblyPath)) | ||||
|             { | ||||
|                 return context.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyPath))); | ||||
|                 return context.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyPath))); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|  | ||||
| @ -0,0 +1,233 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||||
| using Microsoft.AspNetCore.Authentication.OpenIdConnect; | ||||
| using Oqtane.Infrastructure; | ||||
| using Oqtane.Models; | ||||
| using Oqtane.Shared; | ||||
| using Microsoft.IdentityModel.Protocols.OpenIdConnect; | ||||
| using System.Security.Claims; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Identity; | ||||
| using Oqtane.Repository; | ||||
| using System.IO; | ||||
| using System.Collections.Generic; | ||||
| using Oqtane.Security; | ||||
|  | ||||
| namespace Oqtane.Extensions | ||||
| { | ||||
|     public static class OqtaneSiteAuthenticationBuilderExtensions | ||||
|     { | ||||
|         public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthentication<TAlias>( | ||||
|             this OqtaneSiteOptionsBuilder<TAlias> builder) | ||||
|             where TAlias : class, IAlias, new() | ||||
|         { | ||||
|             builder.WithSiteAuthenticationCore(); | ||||
|             builder.WithSiteAuthenticationOptions(); | ||||
|  | ||||
|             return builder; | ||||
|         } | ||||
|  | ||||
|         public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationCore<TAlias>( | ||||
|             this OqtaneSiteOptionsBuilder<TAlias> builder) | ||||
|             where TAlias : class, IAlias, new() | ||||
|         { | ||||
|             builder.Services.DecorateService<IAuthenticationService, SiteAuthenticationService<TAlias>>(); | ||||
|             builder.Services.Replace(ServiceDescriptor.Singleton<IAuthenticationSchemeProvider, SiteAuthenticationSchemeProvider>()); | ||||
|  | ||||
|             return builder; | ||||
|         } | ||||
|  | ||||
|         public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationOptions<TAlias>( | ||||
|             this OqtaneSiteOptionsBuilder<TAlias> builder) | ||||
|             where TAlias : class, IAlias, new() | ||||
|         { | ||||
|             // site OpenIdConnect options | ||||
|             builder.AddSiteOptions<OpenIdConnectOptions>((options, alias) => | ||||
|             { | ||||
|                 if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority")) | ||||
|                 { | ||||
|                     options.Authority = alias.SiteSettings["OpenIdConnectOptions:Authority"]; | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientId")) | ||||
|                 { | ||||
|                     options.ClientId = alias.SiteSettings["OpenIdConnectOptions:ClientId"]; | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientSecret")) | ||||
|                 { | ||||
|                     options.ClientSecret = alias.SiteSettings["OpenIdConnectOptions:ClientSecret"]; | ||||
|                 } | ||||
|  | ||||
|                 // default options | ||||
|                 options.SignInScheme = Constants.AuthenticationScheme; // identity cookie | ||||
|                 options.RequireHttpsMetadata = true; | ||||
|                 options.UsePkce = true; | ||||
|                 options.Scope.Add("openid"); // core claims | ||||
|                 options.Scope.Add("profile");  // name claims | ||||
|                 options.Scope.Add("email"); // email claim | ||||
|                 //options.Scope.Add("offline_access"); // get refresh token | ||||
|                 options.SaveTokens = true; | ||||
|                 options.GetClaimsFromUserInfoEndpoint = true; | ||||
|                 options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc"; | ||||
|                 options.ResponseType = OpenIdConnectResponseType.Code; | ||||
|                 options.Events.OnTokenValidated = OnTokenValidated; | ||||
|             }); | ||||
|  | ||||
|             // site ChallengeScheme options  | ||||
|             builder.AddSiteOptions<AuthenticationOptions>((options, alias) => | ||||
|             { | ||||
|                 if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority") && !string.IsNullOrEmpty(alias.SiteSettings["OpenIdConnectOptions:Authority"])) | ||||
|                 { | ||||
|                     options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return builder; | ||||
|         } | ||||
|  | ||||
|         private static async Task OnTokenValidated(TokenValidatedContext context) | ||||
|         { | ||||
|             var email = context.Principal.Identity.Name; | ||||
|             if (email != null) | ||||
|             { | ||||
|                 var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>(); | ||||
|                 var _users = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>(); | ||||
|                 var _userRoles = context.HttpContext.RequestServices.GetRequiredService<IUserRoleRepository>(); | ||||
|                 User user = null; | ||||
|  | ||||
|                 var identityuser = await _identityUserManager.FindByEmailAsync(email); | ||||
|                 if (identityuser == null) | ||||
|                 { | ||||
|                     identityuser = new IdentityUser(); | ||||
|                     identityuser.UserName = email; | ||||
|                     identityuser.Email = email; | ||||
|                     identityuser.EmailConfirmed = true; | ||||
|                     var result = await _identityUserManager.CreateAsync(identityuser, Guid.NewGuid().ToString("N") + "-Xx!"); | ||||
|                     if (result.Succeeded) | ||||
|                     { | ||||
|                         user = new User(); | ||||
|                         user.SiteId = context.HttpContext.GetAlias().SiteId; | ||||
|                         user.Username = email; | ||||
|                         user.DisplayName = email; | ||||
|                         user.Email = email; | ||||
|                         user.LastLoginOn = null; | ||||
|                         user.LastIPAddress = ""; | ||||
|                         user = _users.AddUser(user); | ||||
|  | ||||
|                         // add folder for user | ||||
|                         var _folders = context.HttpContext.RequestServices.GetRequiredService<IFolderRepository>(); | ||||
|                         Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString())); | ||||
|                         if (folder != null) | ||||
|                         { | ||||
|                             _folders.AddFolder(new Folder | ||||
|                             { | ||||
|                                 SiteId = folder.SiteId, | ||||
|                                 ParentId = folder.FolderId, | ||||
|                                 Name = "My Folder", | ||||
|                                 Type = FolderTypes.Private, | ||||
|                                 Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), Path.DirectorySeparatorChar.ToString()), | ||||
|                                 Order = 1, | ||||
|                                 ImageSizes = "", | ||||
|                                 Capacity = Constants.UserFolderCapacity, | ||||
|                                 IsSystem = true, | ||||
|                                 Permissions = new List<Permission> | ||||
|                             { | ||||
|                                 new Permission(PermissionNames.Browse, user.UserId, true), | ||||
|                                 new Permission(PermissionNames.View, RoleNames.Everyone, true), | ||||
|                                 new Permission(PermissionNames.Edit, user.UserId, true) | ||||
|                             }.EncodePermissions() | ||||
|                             }); | ||||
|                         } | ||||
|  | ||||
|                         // add auto assigned roles to user for site | ||||
|                         var _roles = context.HttpContext.RequestServices.GetRequiredService<IRoleRepository>(); | ||||
|                         List<Role> roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList(); | ||||
|                         foreach (Role role in roles) | ||||
|                         { | ||||
|                             UserRole userrole = new UserRole(); | ||||
|                             userrole.UserId = user.UserId; | ||||
|                             userrole.RoleId = role.RoleId; | ||||
|                             userrole.EffectiveDate = null; | ||||
|                             userrole.ExpiryDate = null; | ||||
|                             _userRoles.AddUserRole(userrole); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     email = identityuser.UserName; | ||||
|                 } | ||||
|  | ||||
|                 // add claims to principal | ||||
|                 user = _users.GetUser(email); | ||||
|                 if (user != null) | ||||
|                 { | ||||
|                     List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, context.HttpContext.GetAlias().SiteId).ToList(); | ||||
|                     var identity = UserSecurity.CreateClaimsIdentity(context.HttpContext.GetAlias(), user, userroles); | ||||
|  | ||||
|                     var principalIdentity = (ClaimsIdentity)context.Principal.Identity; | ||||
|                     foreach (var claim in identity.Claims) | ||||
|                     { | ||||
|                         if (!principalIdentity.Claims.Contains(claim)) | ||||
|                         { | ||||
|                             principalIdentity.AddClaim(claim); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); | ||||
|                 _logger.Log(LogLevel.Information, "OqtaneSiteAuthenticationBuilderExtensions", Enums.LogFunction.Security, "OpenId Connect Server Did Not Return An Email For User"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public static bool DecorateService<TService, TImpl>(this IServiceCollection services, params object[] parameters) | ||||
|         { | ||||
|             var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); | ||||
|             if (existingService == null) | ||||
|                 return false; | ||||
|  | ||||
|             var newService = new ServiceDescriptor(existingService.ServiceType, | ||||
|                 sp => | ||||
|                 { | ||||
|                     TService inner = (TService)ActivatorUtilities.CreateInstance(sp, existingService.ImplementationType!); | ||||
|  | ||||
|                     var parameters2 = new object[parameters.Length + 1]; | ||||
|                     Array.Copy(parameters, 0, parameters2, 1, parameters.Length); | ||||
|                     parameters2[0] = inner; | ||||
|  | ||||
|                     return ActivatorUtilities.CreateInstance<TImpl>(sp, parameters2)!; | ||||
|                 }, | ||||
|                 existingService.Lifetime); | ||||
|  | ||||
|             if (existingService.ImplementationInstance != null) | ||||
|             { | ||||
|                 newService = new ServiceDescriptor(existingService.ServiceType, | ||||
|                     sp => | ||||
|                     { | ||||
|                         TService inner = (TService)existingService.ImplementationInstance; | ||||
|                         return ActivatorUtilities.CreateInstance<TImpl>(sp, inner, parameters)!; | ||||
|                     }, | ||||
|                     existingService.Lifetime); | ||||
|             } | ||||
|             else if (existingService.ImplementationFactory != null) | ||||
|             { | ||||
|                 newService = new ServiceDescriptor(existingService.ServiceType, | ||||
|                     sp => | ||||
|                     { | ||||
|                         TService inner = (TService)existingService.ImplementationFactory(sp); | ||||
|                         return ActivatorUtilities.CreateInstance<TImpl>(sp, inner, parameters)!; | ||||
|                     }, | ||||
|                     existingService.Lifetime); | ||||
|             } | ||||
|  | ||||
|             services.Remove(existingService); | ||||
|             services.Add(newService); | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,57 @@ | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Oqtane.Models; | ||||
| using Microsoft.AspNetCore.Identity; | ||||
| using System; | ||||
|  | ||||
| namespace Oqtane.Extensions | ||||
| { | ||||
|     public static class OqtaneSiteIdentityBuilderExtensions | ||||
|     { | ||||
|         public static OqtaneSiteOptionsBuilder<TAlias> WithSiteIdentity<TAlias>( | ||||
|             this OqtaneSiteOptionsBuilder<TAlias> builder) | ||||
|             where TAlias : class, IAlias, new() | ||||
|         { | ||||
|             // site identity options | ||||
|             builder.AddSiteOptions<IdentityOptions>((options, alias) => | ||||
|             { | ||||
|                 // password options | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredLength")) | ||||
|                 { | ||||
|                     options.Password.RequiredLength = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredLength"]); | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredUniqueChars")) | ||||
|                 { | ||||
|                     options.Password.RequiredUniqueChars = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredUniqueChars"]); | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireDigit")) | ||||
|                 { | ||||
|                     options.Password.RequireDigit = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireDigit"]); | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireUppercase")) | ||||
|                 { | ||||
|                     options.Password.RequireUppercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireUppercase"]); | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireLowercase")) | ||||
|                 { | ||||
|                     options.Password.RequireLowercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireLowercase"]); | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireNonAlphanumeric")) | ||||
|                 { | ||||
|                     options.Password.RequireNonAlphanumeric = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireNonAlphanumeric"]); | ||||
|                 } | ||||
|  | ||||
|                 // lockout options | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:MaxFailedAccessAttempts")) | ||||
|                 { | ||||
|                     options.Lockout.MaxFailedAccessAttempts = int.Parse(alias.SiteSettings["IdentityOptions:Password:MaxFailedAccessAttempts"]); | ||||
|                 } | ||||
|                 if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:DefaultLockoutTimeSpan")) | ||||
|                 { | ||||
|                     options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(alias.SiteSettings["IdentityOptions:Password:DefaultLockoutTimeSpan"]); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return builder; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										39
									
								
								Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| using System; | ||||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Oqtane.Infrastructure; | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Microsoft.Extensions.DependencyInjection | ||||
| { | ||||
|     public partial class OqtaneSiteOptionsBuilder<TSiteOptions> where TSiteOptions : class, IAlias, new() | ||||
|     { | ||||
|         public IServiceCollection Services { get; set; } | ||||
|  | ||||
|         public OqtaneSiteOptionsBuilder(IServiceCollection services) | ||||
|         { | ||||
|             Services = services; | ||||
|         } | ||||
|  | ||||
|         public OqtaneSiteOptionsBuilder<TSiteOptions> AddSiteOptions<TOptions>( | ||||
|             Action<TOptions, TSiteOptions> siteOptions) where TOptions : class, new() | ||||
|         { | ||||
|             Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, SiteOptionsCache<TOptions, TSiteOptions>>(); | ||||
|             Services.AddSingleton<ISiteOptions<TOptions, TSiteOptions>, SiteOptions<TOptions, TSiteOptions>> | ||||
|                 (sp => new SiteOptions<TOptions, TSiteOptions>(siteOptions)); | ||||
|             Services.TryAddTransient<IOptionsFactory<TOptions>, SiteOptionsFactory<TOptions, TSiteOptions>>(); | ||||
|             Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp)); | ||||
|             Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp)); | ||||
|  | ||||
|             return this; | ||||
|         } | ||||
|  | ||||
|         private static SiteOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp) | ||||
|             where TOptions : class, new() | ||||
|         { | ||||
|             var cache = ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsCache<TOptions, TSiteOptions>)); | ||||
|             return (SiteOptionsManager<TOptions>)ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsManager<TOptions>), new[] { cache }); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @ -1,7 +1,5 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Microsoft.AspNetCore.StaticFiles; | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Extensions | ||||
| { | ||||
|  | ||||
							
								
								
									
										18
									
								
								Oqtane.Server/Infrastructure/AliasAccessor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Oqtane.Server/Infrastructure/AliasAccessor.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Oqtane.Extensions; | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public class AliasAccessor : IAliasAccessor | ||||
|     { | ||||
|         private readonly IHttpContextAccessor _httpContextAccessor; | ||||
|  | ||||
|         public AliasAccessor(IHttpContextAccessor httpContextAccessor) | ||||
|         { | ||||
|             _httpContextAccessor = httpContextAccessor; | ||||
|         } | ||||
|  | ||||
|         public Alias Alias => _httpContextAccessor.HttpContext.GetAlias(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public interface IAliasAccessor  | ||||
|     { | ||||
|         Alias Alias { get; } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,112 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.Extensions.Options; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     internal class SiteAuthenticationSchemeProvider : IAuthenticationSchemeProvider | ||||
|     { | ||||
|         public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options) | ||||
|             : this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal)) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes) | ||||
|         { | ||||
|             _optionsProvider = options; | ||||
|  | ||||
|             _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); | ||||
|             _requestHandlers = new List<AuthenticationScheme>(); | ||||
|  | ||||
|             foreach (var builder in _optionsProvider.Value.Schemes) | ||||
|             { | ||||
|                 var scheme = builder.Build(); | ||||
|                 AddScheme(scheme); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private readonly IOptions<AuthenticationOptions> _optionsProvider; | ||||
|         private readonly object _lock = new object(); | ||||
|  | ||||
|         private readonly IDictionary<string, AuthenticationScheme> _schemes; | ||||
|         private readonly List<AuthenticationScheme> _requestHandlers; | ||||
|  | ||||
|         private Task<AuthenticationScheme> GetDefaultSchemeAsync() | ||||
|             => _optionsProvider.Value.DefaultScheme != null | ||||
|             ? GetSchemeAsync(_optionsProvider.Value.DefaultScheme) | ||||
|             : Task.FromResult<AuthenticationScheme>(null); | ||||
|  | ||||
|         public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() | ||||
|             => _optionsProvider.Value.DefaultAuthenticateScheme != null | ||||
|             ? GetSchemeAsync(_optionsProvider.Value.DefaultAuthenticateScheme) | ||||
|             : GetDefaultSchemeAsync(); | ||||
|  | ||||
|         public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() | ||||
|             => _optionsProvider.Value.DefaultChallengeScheme != null | ||||
|             ? GetSchemeAsync(_optionsProvider.Value.DefaultChallengeScheme) | ||||
|             : GetDefaultSchemeAsync(); | ||||
|  | ||||
|         public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() | ||||
|             => _optionsProvider.Value.DefaultForbidScheme != null | ||||
|             ? GetSchemeAsync(_optionsProvider.Value.DefaultForbidScheme) | ||||
|             : GetDefaultChallengeSchemeAsync(); | ||||
|  | ||||
|         public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() | ||||
|             => _optionsProvider.Value.DefaultSignInScheme != null | ||||
|             ? GetSchemeAsync(_optionsProvider.Value.DefaultSignInScheme) | ||||
|             : GetDefaultSchemeAsync(); | ||||
|  | ||||
|         public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() | ||||
|             => _optionsProvider.Value.DefaultSignOutScheme != null | ||||
|             ? GetSchemeAsync(_optionsProvider.Value.DefaultSignOutScheme) | ||||
|             : GetDefaultSignInSchemeAsync(); | ||||
|  | ||||
|         public virtual Task<AuthenticationScheme> GetSchemeAsync(string name) | ||||
|             => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); | ||||
|  | ||||
|         public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync() | ||||
|             => Task.FromResult<IEnumerable<AuthenticationScheme>>(_requestHandlers); | ||||
|  | ||||
|         public virtual void AddScheme(AuthenticationScheme scheme) | ||||
|         { | ||||
|             if (_schemes.ContainsKey(scheme.Name)) | ||||
|             { | ||||
|                 throw new InvalidOperationException("Scheme already exists: " + scheme.Name); | ||||
|             } | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_schemes.ContainsKey(scheme.Name)) | ||||
|                 { | ||||
|                     throw new InvalidOperationException("Scheme already exists: " + scheme.Name); | ||||
|                 } | ||||
|                 if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) | ||||
|                 { | ||||
|                     _requestHandlers.Add(scheme); | ||||
|                 } | ||||
|                 _schemes[scheme.Name] = scheme; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public virtual void RemoveScheme(string name) | ||||
|         { | ||||
|             if (!_schemes.ContainsKey(name)) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (_schemes.ContainsKey(name)) | ||||
|                 { | ||||
|                     var scheme = _schemes[name]; | ||||
|                     _requestHandlers.Remove(scheme); | ||||
|                     _schemes.Remove(name); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync() | ||||
|             => Task.FromResult<IEnumerable<AuthenticationScheme>>(_schemes.Values); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,62 @@ | ||||
| using System.Security.Claims; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Oqtane.Extensions; | ||||
| using Oqtane.Models; | ||||
| using Oqtane.Shared; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     internal class SiteAuthenticationService<TAlias> : IAuthenticationService | ||||
|         where TAlias : class, IAlias, new() | ||||
|     { | ||||
|         private readonly IAuthenticationService _inner; | ||||
|  | ||||
|         public SiteAuthenticationService(IAuthenticationService inner) | ||||
|         { | ||||
|             _inner = inner ?? throw new System.ArgumentNullException(nameof(inner)); | ||||
|         } | ||||
|  | ||||
|         private static void AddTenantIdentifierToProperties(HttpContext context, ref AuthenticationProperties properties) | ||||
|         { | ||||
|             // add site identifier to the authentication properties so on the callback we can use it to set context | ||||
|             var alias = context.GetAlias(); | ||||
|             if (alias != null) | ||||
|             { | ||||
|                 properties ??= new AuthenticationProperties(); | ||||
|                 if (!properties.Items.Keys.Contains(Constants.SiteToken)) | ||||
|                 { | ||||
|                     properties.Items.Add(Constants.SiteToken, alias.SiteKey); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme) | ||||
|             => _inner.AuthenticateAsync(context, scheme); | ||||
|  | ||||
|         public async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) | ||||
|         { | ||||
|             AddTenantIdentifierToProperties(context, ref properties); | ||||
|             await _inner.ChallengeAsync(context, scheme, properties); | ||||
|         } | ||||
|  | ||||
|         public async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) | ||||
|         { | ||||
|             AddTenantIdentifierToProperties(context, ref properties); | ||||
|             await _inner.ForbidAsync(context, scheme, properties); | ||||
|         } | ||||
|  | ||||
|         public async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) | ||||
|         { | ||||
|             AddTenantIdentifierToProperties(context, ref properties); | ||||
|             await _inner.SignInAsync(context, scheme, principal, properties); | ||||
|         } | ||||
|  | ||||
|         public async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) | ||||
|         { | ||||
|             AddTenantIdentifierToProperties(context, ref properties); | ||||
|             await _inner.SignOutAsync(context, scheme, properties); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,8 @@ | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Oqtane.Repository; | ||||
| using Oqtane.Shared; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
| @ -18,19 +21,30 @@ namespace Oqtane.Infrastructure | ||||
|             var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; | ||||
|             if (config.IsInstalled()) | ||||
|             { | ||||
|                 // get alias | ||||
|                 // get alias (note that this also sets SiteState.Alias) | ||||
|                 var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager; | ||||
|                 var alias = tenantManager.GetAlias(); | ||||
|  | ||||
|                 // rewrite path by removing alias path prefix from api and pages requests | ||||
|                 if (alias != null && !string.IsNullOrEmpty(alias.Path)) | ||||
|                 if (alias != null) | ||||
|                 { | ||||
|                     string path = context.Request.Path.ToString(); | ||||
|                     if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/"))) | ||||
|                     // get site settings and store alias in HttpContext | ||||
|                     var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository; | ||||
|                     alias.SiteSettings = settingRepository.GetSettings(EntityNames.Site) | ||||
|                         .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); | ||||
|                     context.Items.Add(Constants.HttpContextAliasKey, alias); | ||||
|  | ||||
|                     // rewrite path by removing alias path prefix from api and pages requests (for consistent routing) | ||||
|                     if (!string.IsNullOrEmpty(alias.Path)) | ||||
|                     { | ||||
|                         context.Request.Path = path.Replace("/" + alias.Path, ""); | ||||
|                         string path = context.Request.Path.ToString(); | ||||
|                         if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/"))) | ||||
|                         { | ||||
|                             context.Request.Path = path.Replace("/" + alias.Path, ""); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|  | ||||
|             // continue processing | ||||
|  | ||||
							
								
								
									
										12
									
								
								Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
|  | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public interface ISiteOptions<TOptions, TAlias> | ||||
|         where TOptions : class, new() | ||||
|         where TAlias : class, IAlias, new() | ||||
|     { | ||||
|         void Configure(TOptions options, TAlias siteOptions); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptions.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| using System; | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public class SiteOptions<TOptions, TAlias> : ISiteOptions<TOptions, TAlias> | ||||
|         where TOptions : class, new() | ||||
|         where TAlias : class, IAlias, new() | ||||
|     { | ||||
|         private readonly Action<TOptions, TAlias> configureOptions; | ||||
|  | ||||
|         public SiteOptions(Action<TOptions, TAlias> configureOptions) | ||||
|         { | ||||
|             this.configureOptions = configureOptions; | ||||
|         } | ||||
|  | ||||
|         public void Configure(TOptions options, TAlias siteOptions) | ||||
|         { | ||||
|             configureOptions(options, siteOptions); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										70
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public class SiteOptionsCache<TOptions, TAlias> : IOptionsMonitorCache<TOptions> | ||||
|         where TOptions : class | ||||
|         where TAlias : class, IAlias, new() | ||||
|     { | ||||
|         private readonly IAliasAccessor _aliasAccessor; | ||||
|         private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>(); | ||||
|  | ||||
|         public SiteOptionsCache(IAliasAccessor aliasAccessor) | ||||
|         { | ||||
|             _aliasAccessor = aliasAccessor; | ||||
|         } | ||||
|  | ||||
|         public void Clear() | ||||
|         { | ||||
|             var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>()); | ||||
|             cache.Clear(); | ||||
|         } | ||||
|  | ||||
|         public void Clear(Alias alias) | ||||
|         { | ||||
|             var cache = map.GetOrAdd(alias.SiteKey, new OptionsCache<TOptions>()); | ||||
|  | ||||
|             cache.Clear(); | ||||
|         } | ||||
|  | ||||
|         public void ClearAll() | ||||
|         { | ||||
|             foreach (var cache in map.Values) | ||||
|             { | ||||
|                 cache.Clear(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public TOptions GetOrAdd(string name, Func<TOptions> createOptions) | ||||
|         { | ||||
|             name = name ?? Options.DefaultName; | ||||
|             var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>()); | ||||
|  | ||||
|             return cache.GetOrAdd(name, createOptions); | ||||
|         } | ||||
|  | ||||
|         public bool TryAdd(string name, TOptions options) | ||||
|         { | ||||
|             name = name ?? Options.DefaultName; | ||||
|             var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>()); | ||||
|  | ||||
|             return cache.TryAdd(name, options); | ||||
|         } | ||||
|  | ||||
|         public bool TryRemove(string name) | ||||
|         { | ||||
|             name = name ?? Options.DefaultName; | ||||
|             var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>()); | ||||
|  | ||||
|             return cache.TryRemove(name); | ||||
|         } | ||||
|  | ||||
|         private string GetKey() | ||||
|         { | ||||
|             return _aliasAccessor?.Alias?.SiteKey ?? ""; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										77
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public class SiteOptionsFactory<TOptions, TAlias> : IOptionsFactory<TOptions> | ||||
|         where TOptions : class, new() | ||||
|         where TAlias : class, IAlias, new() | ||||
|     { | ||||
|         private readonly IConfigureOptions<TOptions>[] _configureOptions; | ||||
|         private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions; | ||||
|         private readonly IValidateOptions<TOptions>[] _validations; | ||||
|         private readonly ISiteOptions<TOptions, TAlias>[] _siteOptions; | ||||
|         private readonly IAliasAccessor _aliasAccessor; | ||||
|  | ||||
|         public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<IValidateOptions<TOptions>> validations, IEnumerable<ISiteOptions<TOptions, TAlias>> siteOptions, IAliasAccessor aliasAccessor) | ||||
|         { | ||||
|             _configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray(); | ||||
|             _postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray(); | ||||
|             _validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray(); | ||||
|             _siteOptions = siteOptions as ISiteOptions<TOptions, TAlias>[] ?? new List<ISiteOptions<TOptions, TAlias>>(siteOptions).ToArray(); | ||||
|             _aliasAccessor = aliasAccessor; | ||||
|          } | ||||
|  | ||||
|         public TOptions Create(string name) | ||||
|         { | ||||
|             // default options | ||||
|             var options = new TOptions(); | ||||
|             foreach (var setup in _configureOptions) | ||||
|             { | ||||
|                 if (setup is IConfigureNamedOptions<TOptions> namedSetup) | ||||
|                 { | ||||
|                     namedSetup.Configure(name, options); | ||||
|                 } | ||||
|                 else if (name == Options.DefaultName) | ||||
|                 { | ||||
|                     setup.Configure(options); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // override with site specific options | ||||
|             if (_aliasAccessor?.Alias != null) | ||||
|             { | ||||
|                 foreach (var siteOption in _siteOptions) | ||||
|                 { | ||||
|                     siteOption.Configure(options, _aliasAccessor.Alias as TAlias); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // post configuration | ||||
|             foreach (var post in _postConfigureOptions) | ||||
|             { | ||||
|                 post.PostConfigure(name, options); | ||||
|             } | ||||
|  | ||||
|             //if (_validations.Length > 0) | ||||
|             //{ | ||||
|             //    var failures = new List<string>(); | ||||
|             //    foreach (IValidateOptions<TOptions> validate in _validations) | ||||
|             //    { | ||||
|             //        ValidateOptionsResult result = validate.Validate(name, options); | ||||
|             //        if (result != null && result.Failed) | ||||
|             //        { | ||||
|             //            failures.AddRange(result.Failures); | ||||
|             //        } | ||||
|             //    } | ||||
|             //    if (failures.Count > 0) | ||||
|             //    { | ||||
|             //        throw new OptionsValidationException(name, typeof(TOptions), failures); | ||||
|             //    } | ||||
|             //} | ||||
|  | ||||
|             return options; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| using Microsoft.Extensions.Options; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public class SiteOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new() | ||||
|     { | ||||
|         private readonly IOptionsFactory<TOptions> _factory; | ||||
|         private readonly IOptionsMonitorCache<TOptions> _cache; // private cache | ||||
|  | ||||
|         public SiteOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache) | ||||
|         { | ||||
|             _factory = factory; | ||||
|             _cache = cache; | ||||
|         } | ||||
|  | ||||
|         public TOptions Value | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 return Get(Options.DefaultName); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public virtual TOptions Get(string name) | ||||
|         { | ||||
|             name = name ?? Options.DefaultName; | ||||
|             return _cache.GetOrAdd(name, () => _factory.Create(name)); | ||||
|         } | ||||
|  | ||||
|         public void Reset() | ||||
|         { | ||||
|             _cache.Clear(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -26,7 +26,7 @@ namespace Oqtane.Infrastructure | ||||
|         { | ||||
|             Alias alias = null; | ||||
|  | ||||
|             if (_siteState != null && _siteState.Alias != null) | ||||
|             if (_siteState != null && _siteState.Alias != null && _siteState.Alias.AliasId != -1) | ||||
|             { | ||||
|                 alias = _siteState.Alias; | ||||
|             } | ||||
|  | ||||
| @ -32,6 +32,7 @@ | ||||
|     <EmbeddedResource Include="Scripts\MigrateTenant.sql" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.3" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" /> | ||||
|  | ||||
							
								
								
									
										3
									
								
								Oqtane.Server/Pages/OIDC.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Oqtane.Server/Pages/OIDC.cshtml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| @page "/pages/oidc" | ||||
| @namespace  Oqtane.Pages | ||||
| @model Oqtane.Pages.OIDCModel | ||||
							
								
								
									
										15
									
								
								Oqtane.Server/Pages/OIDC.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Oqtane.Server/Pages/OIDC.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Authentication.OpenIdConnect; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | ||||
|  | ||||
| namespace Oqtane.Pages | ||||
| { | ||||
|     public class OIDCModel : PageModel | ||||
|     { | ||||
|         public IActionResult OnGetAsync(string returnurl) | ||||
|         { | ||||
|             return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = !string.IsNullOrEmpty(returnurl) ? returnurl : "/" }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -37,17 +37,20 @@ namespace Oqtane.Security | ||||
|                             var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository; | ||||
|                             var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; | ||||
|  | ||||
|                             User user = userRepository.GetUser(context.Principal.Identity.Name); | ||||
|                             if (user != null) | ||||
|                             if (context.Principal.Identity.Name != null) | ||||
|                             { | ||||
|                                 List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList(); | ||||
|                                 var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles); | ||||
|                                 context.ReplacePrincipal(new ClaimsPrincipal(identity)); | ||||
|                                 context.ShouldRenew = true; | ||||
|                             } | ||||
|                             else | ||||
|                             { | ||||
|                                 context.RejectPrincipal(); | ||||
|                                 User user = userRepository.GetUser(context.Principal.Identity.Name); | ||||
|                                 if (user != null) | ||||
|                                 { | ||||
|                                     List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList(); | ||||
|                                     var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles); | ||||
|                                     context.ReplacePrincipal(new ClaimsPrincipal(identity)); | ||||
|                                     context.ShouldRenew = true; | ||||
|                                 } | ||||
|                                 else | ||||
|                                 { | ||||
|                                     context.RejectPrincipal(); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
| @ -16,6 +16,9 @@ using Oqtane.Repository; | ||||
| using Oqtane.Security; | ||||
| using Oqtane.Shared; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Microsoft.IdentityModel.Protocols.OpenIdConnect; | ||||
| using Microsoft.AspNetCore.Authentication.OpenIdConnect; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Oqtane | ||||
| { | ||||
| @ -72,37 +75,12 @@ namespace Oqtane | ||||
|             // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) | ||||
|             services.TryAddHttpClientWithAuthenticationCookie(); | ||||
|  | ||||
|             // register custom authorization policies | ||||
|             services.AddOqtaneAuthorizationPolicies(); | ||||
|  | ||||
|             // register scoped core services | ||||
|             services.AddScoped<IAuthorizationHandler, PermissionHandler>() | ||||
|                 .AddOqtaneScopedServices(); | ||||
|  | ||||
|             services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||||
|  | ||||
|             services.AddIdentityCore<IdentityUser>(options => { }) | ||||
|                 .AddEntityFrameworkStores<TenantDBContext>() | ||||
|                 .AddSignInManager() | ||||
|                 .AddDefaultTokenProviders() | ||||
|                 .AddClaimsPrincipalFactory<ClaimsPrincipalFactory<IdentityUser>>(); // role claims | ||||
|  | ||||
|             services.ConfigureOqtaneIdentityOptions(Configuration); | ||||
|  | ||||
|             services.AddAuthentication(Constants.AuthenticationScheme) | ||||
|                 .AddCookie(Constants.AuthenticationScheme); | ||||
|  | ||||
|             services.ConfigureOqtaneCookieOptions(); | ||||
|  | ||||
|             services.AddAntiforgery(options => | ||||
|             { | ||||
|                 options.HeaderName = Constants.AntiForgeryTokenHeaderName; | ||||
|                 options.Cookie.HttpOnly = false; | ||||
|                 options.Cookie.Name = Constants.AntiForgeryTokenCookieName; | ||||
|                 options.Cookie.SameSite = SameSiteMode.Strict; | ||||
|                 options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; | ||||
|             }); | ||||
|  | ||||
|             // register singleton scoped core services | ||||
|             services.AddSingleton(Configuration) | ||||
|                 .AddOqtaneSingletonServices(); | ||||
| @ -117,10 +95,43 @@ namespace Oqtane | ||||
|             services.AddOqtane(_supportedCultures); | ||||
|             services.AddOqtaneDbContext(); | ||||
|  | ||||
|             services.AddAntiforgery(options => | ||||
|             { | ||||
|                 options.HeaderName = Constants.AntiForgeryTokenHeaderName; | ||||
|                 options.Cookie.Name = Constants.AntiForgeryTokenCookieName; | ||||
|                 options.Cookie.SameSite = SameSiteMode.Strict; | ||||
|                 options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; | ||||
|                 //options.Cookie.HttpOnly = false; | ||||
|             }); | ||||
|  | ||||
|             services.AddIdentityCore<IdentityUser>(options => { }) | ||||
|                 .AddEntityFrameworkStores<TenantDBContext>() | ||||
|                 .AddSignInManager() | ||||
|                 .AddDefaultTokenProviders() | ||||
|                 .AddClaimsPrincipalFactory<ClaimsPrincipalFactory<IdentityUser>>(); // role claims | ||||
|  | ||||
|             services.ConfigureOqtaneIdentityOptions(Configuration); | ||||
|  | ||||
|             services.AddAuthentication(options => | ||||
|                 { | ||||
|                     options.DefaultAuthenticateScheme = Constants.AuthenticationScheme; | ||||
|                     options.DefaultChallengeScheme = Constants.AuthenticationScheme; | ||||
|                 }) | ||||
|                 .AddCookie(Constants.AuthenticationScheme) | ||||
|                 .AddOpenIdConnect(); | ||||
|  | ||||
|             services.ConfigureOqtaneCookieOptions(); | ||||
|  | ||||
|             services.AddOqtaneSiteOptions<Alias>() | ||||
|                 .WithSiteIdentity() | ||||
|                 .WithSiteAuthentication(); | ||||
|  | ||||
|             services.AddOqtaneAuthorizationPolicies(); | ||||
|  | ||||
|             services.AddMvc() | ||||
|                 .AddNewtonsoftJson() | ||||
|                 .AddOqtaneApplicationParts() // register any Controllers from custom modules | ||||
|                 .ConfigureOqtaneMvc(); // any additional configuration from IStart classes. | ||||
|                 .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes | ||||
|  | ||||
|             services.AddSwaggerGen(options => | ||||
|             { | ||||
|  | ||||
| @ -1,9 +1,5 @@ | ||||
| /* Login Module Custom Styles */ | ||||
|  | ||||
| .Oqtane-Modules-Admin-Login .input { | ||||
| .Oqtane-Modules-Admin-Login { | ||||
|     width: 200px; | ||||
| } | ||||
|  | ||||
| .Oqtane-Modules-Admin-Login .password { | ||||
|     width: 270px; | ||||
| } | ||||
							
								
								
									
										21
									
								
								Oqtane.Shared/Interfaces/IAlias.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Oqtane.Shared/Interfaces/IAlias.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Oqtane.Models | ||||
| { | ||||
|      public interface IAlias | ||||
|     { | ||||
|         int AliasId { get; set; } | ||||
|         string Name { get; set; } | ||||
|         int TenantId { get; set; } | ||||
|         int SiteId { get; set; } | ||||
|         bool IsDefault { get; set; } | ||||
|         string CreatedBy { get; set; } | ||||
|         DateTime CreatedOn { get; set; } | ||||
|         string ModifiedBy { get; set; } | ||||
|         DateTime ModifiedOn { get; set; } | ||||
|         string Path { get; } | ||||
|         string SiteKey { get; }  | ||||
|         Dictionary<string, string> SiteSettings { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,5 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
|  | ||||
| namespace Oqtane.Models | ||||
| @ -6,7 +7,7 @@ namespace Oqtane.Models | ||||
|     /// <summary> | ||||
|     /// An Alias maps a url like `oqtane.my` or `oqtane.my/products` to a <see cref="Oqtane.Models.Site"/> and <see cref="Oqtane.Models.Tenant"/> | ||||
|     /// </summary> | ||||
|     public class Alias : IAuditable | ||||
|     public class Alias : IAlias, IAuditable | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// The primary ID for internal use. It's also used in API calls to identify the site.  | ||||
| @ -68,5 +69,22 @@ namespace Oqtane.Models | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Unique key used for identifying a site within a runtime process (ie. cache, etc...) | ||||
|         /// </summary> | ||||
|         [NotMapped] | ||||
|         public string SiteKey | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 return TenantId.ToString() + ":" + SiteId.ToString(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Site-specific settings | ||||
|         /// </summary> | ||||
|         [NotMapped] | ||||
|         public Dictionary<string, string> SiteSettings { get; set; } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -85,5 +85,8 @@ namespace Oqtane.Shared { | ||||
|         public static readonly string AntiForgeryTokenCookieName = "X-XSRF-TOKEN-COOKIE"; | ||||
|  | ||||
|         public static readonly string DefaultVisitorFilter = "bot,crawler,slurp,spider,(none),??"; | ||||
|  | ||||
|         public static readonly string HttpContextAliasKey = "SiteState.Alias"; | ||||
|         public static readonly string SiteToken = "{SiteToken}"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,7 @@ using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Shared | ||||
| { | ||||
|     // this class is used for passing state between components and services, or controllers and repositories | ||||
|     // this class is used for passing state between components and services as well as controllers and repositories | ||||
|     public class SiteState | ||||
|     { | ||||
|         public Alias Alias { get; set; } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Shaun Walker
					Shaun Walker