From cdd683ae8fb686d7b84a4565ef92c2f11bf02da3 Mon Sep 17 00:00:00 2001 From: Qstick Date: Mon, 11 Jul 2022 22:12:57 -0500 Subject: [PATCH] New: Rework and Require Authentication Co-Authored-By: Mark McDowall (cherry picked from commit 8911386ed0fcaa5ed0a894e511a81ecc87e58d49) --- frontend/src/Components/Page/Page.js | 7 + frontend/src/Components/Page/PageConnector.js | 6 +- .../FirstRun/AuthenticationRequiredModal.js | 34 ++++ .../AuthenticationRequiredModalContent.css | 5 + ...uthenticationRequiredModalContent.css.d.ts | 7 + .../AuthenticationRequiredModalContent.js | 164 ++++++++++++++++++ ...enticationRequiredModalContentConnector.js | 86 +++++++++ .../src/Settings/General/SecuritySettings.js | 58 ++++++- .../Config/HostConfigResource.cs | 2 + .../AuthenticationBuilderExtensions.cs | 6 + ...leDenyAnonymousAuthorizationRequirement.cs | 8 + .../Authentication/UiAuthorizationHandler.cs | 45 +++++ .../UiAuthorizationPolicyProvider.cs | 3 +- .../AutomationTest.cs | 2 +- .../AuthenticationRequiredType.cs | 8 + .../Authentication/AuthenticationType.cs | 5 +- .../Configuration/ConfigFileProvider.cs | 3 + src/NzbDrone.Host/Startup.cs | 2 + src/NzbDrone.Test.Common/NzbDroneRunner.cs | 8 +- 19 files changed, 444 insertions(+), 15 deletions(-) create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModal.js create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContent.css create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContent.js create mode 100644 frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js create mode 100644 src/Lidarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs create mode 100644 src/Lidarr.Http/Authentication/UiAuthorizationHandler.cs create mode 100644 src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js index eac6d709f..4b24a8231 100644 --- a/frontend/src/Components/Page/Page.js +++ b/frontend/src/Components/Page/Page.js @@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; import ColorImpairedContext from 'App/ColorImpairedContext'; import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; import SignalRConnector from 'Components/SignalRConnector'; +import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import PageHeader from './Header/PageHeader'; import PageSidebar from './Sidebar/PageSidebar'; @@ -75,6 +76,7 @@ class Page extends Component { isSmallScreen, isSidebarVisible, enableColorImpairedMode, + authenticationEnabled, onSidebarToggle, onSidebarVisibleChange } = this.props; @@ -108,6 +110,10 @@ class Page extends Component { isOpen={this.state.isConnectionLostModalOpen} onModalClose={this.onConnectionLostModalClose} /> + + ); @@ -123,6 +129,7 @@ Page.propTypes = { isUpdated: PropTypes.bool.isRequired, isDisconnected: PropTypes.bool.isRequired, enableColorImpairedMode: PropTypes.bool.isRequired, + authenticationEnabled: PropTypes.bool.isRequired, onResize: PropTypes.func.isRequired, onSidebarToggle: PropTypes.func.isRequired, onSidebarVisibleChange: PropTypes.func.isRequired diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index b13695f17..070be9b42 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -10,6 +10,7 @@ import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityPr import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchTags } from 'Store/Actions/tagActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import ErrorPage from './ErrorPage'; import LoadingPage from './LoadingPage'; import Page from './Page'; @@ -132,18 +133,21 @@ function createMapStateToProps() { selectErrors, selectAppProps, createDimensionsSelector(), + createSystemStatusSelector(), ( enableColorImpairedMode, isPopulated, errors, app, - dimensions + dimensions, + systemStatus ) => { return { ...app, ...errors, isPopulated, isSmallScreen: dimensions.isSmallScreen, + authenticationEnabled: systemStatus.authentication !== 'none', enableColorImpairedMode }; } diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.js b/frontend/src/FirstRun/AuthenticationRequiredModal.js new file mode 100644 index 000000000..caa855cb7 --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModal.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector'; + +function onModalClose() { + // No-op +} + +function AuthenticationRequiredModal(props) { + const { + isOpen + } = props; + + return ( + + + + ); +} + +AuthenticationRequiredModal.propTypes = { + isOpen: PropTypes.bool.isRequired +}; + +export default AuthenticationRequiredModal; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.css b/frontend/src/FirstRun/AuthenticationRequiredModalContent.css new file mode 100644 index 000000000..bbc6704e6 --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.css @@ -0,0 +1,5 @@ +.authRequiredAlert { + composes: alert from '~Components/Alert.css'; + + margin-bottom: 20px; +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts b/frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts new file mode 100644 index 000000000..9454d5428 --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'authRequiredAlert': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js new file mode 100644 index 000000000..985186fde --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js @@ -0,0 +1,164 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; +import Alert from 'Components/Alert'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings'; +import styles from './AuthenticationRequiredModalContent.css'; + +function onModalClose() { + // No-op +} + +function AuthenticationRequiredModalContent(props) { + const { + isPopulated, + error, + isSaving, + settings, + onInputChange, + onSavePress, + dispatchFetchStatus + } = props; + + const { + authenticationMethod, + authenticationRequired, + username, + password + } = settings; + + const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; + + const didMount = useRef(false); + + useEffect(() => { + if (!isSaving && didMount.current) { + dispatchFetchStatus(); + } + + didMount.current = true; + }, [isSaving, dispatchFetchStatus]); + + return ( + + + Authentication Required + + + + + {authenticationRequiredWarning} + + + { + isPopulated && !error ? +
+ + Authentication + + + + + { + authenticationEnabled ? + + Authentication Required + + + : + null + } + + { + authenticationEnabled ? + + Username + + + : + null + } + + { + authenticationEnabled ? + + Password + + + : + null + } +
: + null + } + + { + !isPopulated && !error ? : null + } +
+ + + + Save + + +
+ ); +} + +AuthenticationRequiredModalContent.propTypes = { + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + settings: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + dispatchFetchStatus: PropTypes.func.isRequired +}; + +export default AuthenticationRequiredModalContent; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js b/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js new file mode 100644 index 000000000..6653a9d34 --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; + +const SECTION = 'general'; + +function createMapStateToProps() { + return createSelector( + createSettingsSectionSelector(SECTION), + (sectionSettings) => { + return { + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + dispatchClearPendingChanges: clearPendingChanges, + dispatchSetGeneralSettingsValue: setGeneralSettingsValue, + dispatchSaveGeneralSettings: saveGeneralSettings, + dispatchFetchGeneralSettings: fetchGeneralSettings, + dispatchFetchStatus: fetchStatus +}; + +class AuthenticationRequiredModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchGeneralSettings(); + } + + componentWillUnmount() { + this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.dispatchSetGeneralSettingsValue({ name, value }); + }; + + onSavePress = () => { + this.props.dispatchSaveGeneralSettings(); + }; + + // + // Render + + render() { + const { + dispatchClearPendingChanges, + dispatchFetchGeneralSettings, + dispatchSetGeneralSettingsValue, + dispatchSaveGeneralSettings, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AuthenticationRequiredModalContentConnector.propTypes = { + dispatchClearPendingChanges: PropTypes.func.isRequired, + dispatchFetchGeneralSettings: PropTypes.func.isRequired, + dispatchSetGeneralSettingsValue: PropTypes.func.isRequired, + dispatchSaveGeneralSettings: PropTypes.func.isRequired, + dispatchFetchStatus: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector); diff --git a/frontend/src/Settings/General/SecuritySettings.js b/frontend/src/Settings/General/SecuritySettings.js index bb20a9305..2495419ef 100644 --- a/frontend/src/Settings/General/SecuritySettings.js +++ b/frontend/src/Settings/General/SecuritySettings.js @@ -11,10 +11,33 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import { icons, inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +export const authenticationRequiredWarning = 'To prevent remote access without authentication, Radarr now requires authentication to be enabled. You can optionally disable authentication from local addresses.'; + const authenticationMethodOptions = [ - { key: 'none', value: 'None' }, - { key: 'basic', value: 'Basic (Browser Popup)' }, - { key: 'forms', value: 'Forms (Login Page)' } + { + key: 'none', + get value() { + return translate('None'); + }, + isDisabled: true + }, + { + key: 'basic', + get value() { + return translate('AuthBasic'); + } + }, + { + key: 'forms', + get value() { + return translate('AuthForm'); + } + } +]; + +export const authenticationRequiredOptions = [ + { key: 'enabled', value: 'Enabled' }, + { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' } ]; const certificateValidationOptions = [ @@ -68,6 +91,7 @@ class SecuritySettings extends Component { const { authenticationMethod, + authenticationRequired, username, password, apiKey, @@ -88,13 +112,31 @@ class SecuritySettings extends Component { name="authenticationMethod" values={authenticationMethodOptions} helpText={translate('AuthenticationMethodHelpText')} + helpTextWarning={authenticationRequiredWarning} onChange={onInputChange} {...authenticationMethod} /> { - authenticationEnabled && + authenticationEnabled ? + + Authentication Required + + + : + null + } + + { + authenticationEnabled ? {translate('Username')} @@ -106,11 +148,12 @@ class SecuritySettings extends Component { onChange={onInputChange} {...username} /> - + : + null } { - authenticationEnabled && + authenticationEnabled ? {translate('Password')} @@ -122,7 +165,8 @@ class SecuritySettings extends Component { onChange={onInputChange} {...password} /> - + : + null } diff --git a/src/Lidarr.Api.V1/Config/HostConfigResource.cs b/src/Lidarr.Api.V1/Config/HostConfigResource.cs index cf0178018..abdf26c39 100644 --- a/src/Lidarr.Api.V1/Config/HostConfigResource.cs +++ b/src/Lidarr.Api.V1/Config/HostConfigResource.cs @@ -15,6 +15,7 @@ namespace Lidarr.Api.V1.Config public bool EnableSsl { get; set; } public bool LaunchBrowser { get; set; } public AuthenticationType AuthenticationMethod { get; set; } + public AuthenticationRequiredType AuthenticationRequired { get; set; } public bool AnalyticsEnabled { get; set; } public string Username { get; set; } public string Password { get; set; } @@ -57,6 +58,7 @@ namespace Lidarr.Api.V1.Config EnableSsl = model.EnableSsl, LaunchBrowser = model.LaunchBrowser, AuthenticationMethod = model.AuthenticationMethod, + AuthenticationRequired = model.AuthenticationRequired, AnalyticsEnabled = model.AnalyticsEnabled, // Username diff --git a/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs index 30f8a97d9..d336a38c3 100644 --- a/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Lidarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -22,10 +22,16 @@ namespace Lidarr.Http.Authentication return authenticationBuilder.AddScheme(name, options => { }); } + public static AuthenticationBuilder AddExternal(this AuthenticationBuilder authenticationBuilder, string name) + { + return authenticationBuilder.AddScheme(name, options => { }); + } + public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) { return services.AddAuthentication() .AddNone(AuthenticationType.None.ToString()) + .AddExternal(AuthenticationType.External.ToString()) .AddBasic(AuthenticationType.Basic.ToString()) .AddCookie(AuthenticationType.Forms.ToString(), options => { diff --git a/src/Lidarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs b/src/Lidarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs new file mode 100644 index 000000000..3ad4edcba --- /dev/null +++ b/src/Lidarr.Http/Authentication/BypassableDenyAnonymousAuthorizationRequirement.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace NzbDrone.Http.Authentication +{ + public class BypassableDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement + { + } +} diff --git a/src/Lidarr.Http/Authentication/UiAuthorizationHandler.cs b/src/Lidarr.Http/Authentication/UiAuthorizationHandler.cs new file mode 100644 index 000000000..a8d212659 --- /dev/null +++ b/src/Lidarr.Http/Authentication/UiAuthorizationHandler.cs @@ -0,0 +1,45 @@ +using System.Net; +using System.Threading.Tasks; +using Lidarr.Http.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Http.Authentication +{ + public class UiAuthorizationHandler : AuthorizationHandler, IAuthorizationRequirement, IHandle + { + private readonly IConfigFileProvider _configService; + private static AuthenticationRequiredType _authenticationRequired; + + public UiAuthorizationHandler(IConfigFileProvider configService) + { + _configService = configService; + _authenticationRequired = configService.AuthenticationRequired; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BypassableDenyAnonymousAuthorizationRequirement requirement) + { + if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses) + { + if (context.Resource is HttpContext httpContext && + IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) && + ipAddress.IsLocalAddress()) + { + context.Succeed(requirement); + } + } + + return Task.CompletedTask; + } + + public void Handle(ConfigSavedEvent message) + { + _authenticationRequired = _configService.AuthenticationRequired; + } + } +} diff --git a/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs b/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs index a5295a99f..50f1c3ada 100644 --- a/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs +++ b/src/Lidarr.Http/Authentication/UiAuthorizationPolicyProvider.cs @@ -29,7 +29,8 @@ namespace NzbDrone.Http.Authentication if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase)) { var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString()) - .RequireAuthenticatedUser(); + .AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement()); + return Task.FromResult(policy.Build()); } diff --git a/src/NzbDrone.Automation.Test/AutomationTest.cs b/src/NzbDrone.Automation.Test/AutomationTest.cs index 4626f6ba8..bcf777431 100644 --- a/src/NzbDrone.Automation.Test/AutomationTest.cs +++ b/src/NzbDrone.Automation.Test/AutomationTest.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null); _runner.KillAll(); - _runner.Start(); + _runner.Start(true); driver.Url = "http://localhost:8686"; diff --git a/src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs b/src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs new file mode 100644 index 000000000..dc3c2c770 --- /dev/null +++ b/src/NzbDrone.Core/Authentication/AuthenticationRequiredType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Authentication +{ + public enum AuthenticationRequiredType + { + Enabled = 0, + DisabledForLocalAddresses = 1 + } +} diff --git a/src/NzbDrone.Core/Authentication/AuthenticationType.cs b/src/NzbDrone.Core/Authentication/AuthenticationType.cs index 9f21b07a7..ca408774b 100644 --- a/src/NzbDrone.Core/Authentication/AuthenticationType.cs +++ b/src/NzbDrone.Core/Authentication/AuthenticationType.cs @@ -1,9 +1,10 @@ -namespace NzbDrone.Core.Authentication +namespace NzbDrone.Core.Authentication { public enum AuthenticationType { None = 0, Basic = 1, - Forms = 2 + Forms = 2, + External = 3 } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index f1ac61012..2eec12d00 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration bool EnableSsl { get; } bool LaunchBrowser { get; } AuthenticationType AuthenticationMethod { get; } + AuthenticationRequiredType AuthenticationRequired { get; } bool AnalyticsEnabled { get; } string LogLevel { get; } string ConsoleLogLevel { get; } @@ -189,6 +190,8 @@ namespace NzbDrone.Core.Configuration } } + public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); public string Branch => GetValue("Branch", "master").ToLowerInvariant(); diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index 2f0e13602..9b82a692e 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -172,6 +172,8 @@ namespace NzbDrone.Host .PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"])); services.AddSingleton(); + services.AddSingleton(); + services.AddAuthorization(options => { options.AddPolicy("SignalR", policy => diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 73afd6285..a956dff33 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -38,12 +38,12 @@ namespace NzbDrone.Test.Common Port = port; } - public void Start() + public void Start(bool enableAuth = false) { AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID()); Directory.CreateDirectory(AppData); - GenerateConfigFile(); + GenerateConfigFile(enableAuth); string lidarrConsoleExe; if (OsInfo.IsWindows) @@ -177,7 +177,7 @@ namespace NzbDrone.Test.Common } } - private void GenerateConfigFile() + private void GenerateConfigFile(bool enableAuth) { var configFile = Path.Combine(AppData, "config.xml"); @@ -190,6 +190,8 @@ namespace NzbDrone.Test.Common new XElement(nameof(ConfigFileProvider.ApiKey), apiKey), new XElement(nameof(ConfigFileProvider.LogLevel), "trace"), new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false), + new XElement(nameof(ConfigFileProvider.AuthenticationMethod), enableAuth ? "Forms" : "None"), + new XElement(nameof(ConfigFileProvider.AuthenticationRequired), "DisabledForLocalAddresses"), new XElement(nameof(ConfigFileProvider.Port), Port))); var data = xDoc.ToString();