diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index 2b977e3f6313..531b45b6f11e 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -19,21 +19,32 @@ public static void Register(ProjectCommandLineApplication app) Resources.ClearCommand_ForceOption_Description, CommandOptionType.NoValue); + var appsettingsFileOption = cmd.Option( + "--appsettings-file", + Resources.CreateCommand_appsettingsFileOption_Description, + CommandOptionType.SingleValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => { - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), forceOption.HasValue()); + if (!DevJwtCliHelpers.GetProjectAndSecretsId(cmd.ProjectOption.Value(), cmd.Reporter, out var project, out var userSecretsId)) + { + return 1; + } + + if (!DevJwtCliHelpers.GetAppSettingsFile(project, appsettingsFileOption.Value(), cmd.Reporter, out var appsettingsFile)) + { + return 1; + } + + return Execute(cmd.Reporter, project, userSecretsId, forceOption.HasValue(), appsettingsFile); }); }); } - private static int Execute(IReporter reporter, string projectPath, bool force) + private static int Execute(IReporter reporter, string project, string userSecretsId, bool force, string appsettingsFile) { - if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) - { - return 1; - } var jwtStore = new JwtStore(userSecretsId); var count = jwtStore.Jwts.Count; @@ -54,7 +65,7 @@ private static int Execute(IReporter reporter, string projectPath, bool force) } } - var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), appsettingsFile); foreach (var jwt in jwtStore.Jwts) { JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Value.Scheme); diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index ef3a3b833655..58f3969bab76 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -77,24 +77,29 @@ public static void Register(ProjectCommandLineApplication app, Program program) Resources.CreateCommand_ValidForOption_Description, CommandOptionType.SingleValue); + var appsettingsFileOption = cmd.Option( + "--appsettings-file", + Resources.CreateCommand_appsettingsFileOption_Description, + CommandOptionType.SingleValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => { - var (options, isValid, optionsString) = ValidateArguments( - cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); + var (options, isValid, optionsString, appsettingsFile) = ValidateArguments( + cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption, appsettingsFileOption); if (!isValid) { return 1; } - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, cmd.OutputOption.Value(), program); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, cmd.OutputOption.Value(), appsettingsFile, program); }); }); } - private static (JwtCreatorOptions, bool, string) ValidateArguments( + private static (JwtCreatorOptions, bool, string, string) ValidateArguments( IReporter reporter, CommandOption projectOption, CommandOption schemeNameOption, @@ -106,7 +111,8 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( CommandOption validForOption, CommandOption rolesOption, CommandOption scopesOption, - CommandOption claimsOption) + CommandOption claimsOption, + CommandOption appsettingsFileOption) { var isValid = true; var finder = new MsBuildProjectFinder(Directory.GetCurrentDirectory()); @@ -121,6 +127,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( return ( null, isValid, + string.Empty, string.Empty ); } @@ -209,10 +216,19 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(", ", claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}"; } + var appsettingsFile = DevJwtCliHelpers.DefaultAppSettingsFile; + if (appsettingsFileOption.HasValue()) + { + isValid = DevJwtCliHelpers.GetAppSettingsFile(project, appsettingsFileOption.Value(), reporter, out appsettingsFile); + + optionsString += appsettingsFileOption.HasValue() ? $"{Resources.JwtPrint_appsettingsFile}: {appsettingsFile}{Environment.NewLine}" : string.Empty; + } + return ( new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims), isValid, - optionsString); + optionsString, + appsettingsFile); static bool ParseDate(string datetime, out DateTime parsedDateTime) => DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime); @@ -224,6 +240,7 @@ private static int Execute( JwtCreatorOptions options, string optionsString, string outputFormat, + string appsettingsFile, Program program) { if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) @@ -244,7 +261,7 @@ private static int Execute( jwtStore.Jwts.Add(jwtToken.Id, jwt); jwtStore.Save(); - var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), appsettingsFile); var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Audiences, options.Issuer); settingsToWrite.Save(appsettingsFilePath); diff --git a/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs index 0c3a69eaf40e..9260a70b1219 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/RemoveCommand.cs @@ -15,6 +15,12 @@ public static void Register(ProjectCommandLineApplication app) cmd.Description = Resources.RemoveCommand_Description; var idArgument = cmd.Argument("[id]", Resources.RemoveCommand_IdArgument_Description); + + var appsettingsFileOption = cmd.Option( + "--appsettings-file", + Resources.CreateCommand_appsettingsFileOption_Description, + CommandOptionType.SingleValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => @@ -24,17 +30,24 @@ public static void Register(ProjectCommandLineApplication app) cmd.ShowHelp(); return 0; } - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value); + + if (!DevJwtCliHelpers.GetProjectAndSecretsId(cmd.ProjectOption.Value(), cmd.Reporter, out var project, out var userSecretsId)) + { + return 1; + } + + if (!DevJwtCliHelpers.GetAppSettingsFile(project, appsettingsFileOption.Value(), cmd.Reporter, out var appsettingsFile)) + { + return 1; + } + + return Execute(cmd.Reporter, project, userSecretsId, idArgument.Value, appsettingsFile); }); }); } - private static int Execute(IReporter reporter, string projectPath, string id) + private static int Execute(IReporter reporter, string project, string userSecretsId, string id, string appsettingsFile) { - if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) - { - return 1; - } var jwtStore = new JwtStore(userSecretsId); if (!jwtStore.Jwts.TryGetValue(id, out var jwt)) @@ -43,7 +56,7 @@ private static int Execute(IReporter reporter, string projectPath, string id) return 1; } - var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), appsettingsFile); JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Scheme); jwtStore.Jwts.Remove(id); jwtStore.Save(); diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 2d12b67f55a4..38735fb5231f 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal static class DevJwtCliHelpers { + public const string DefaultAppSettingsFile = "appsettings.Development.json"; + public static string GetOrSetUserSecretsId(string projectFilePath) { var resolver = new ProjectIdResolver(NullReporter.Singleton, projectFilePath); @@ -42,6 +44,27 @@ public static bool GetProjectAndSecretsId(string projectPath, IReporter reporter return true; } + public static bool GetAppSettingsFile(string projectPath, string appsettingsFileOption, IReporter reporter, out string appsettingsFile) + { + appsettingsFile = DevJwtCliHelpers.DefaultAppSettingsFile; + if (appsettingsFileOption is not null) + { + appsettingsFile = appsettingsFileOption; + if (!appsettingsFile.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + reporter.Error(Resources.RemoveCommand_InvalidAppsettingsFile_Error); + return false; + } + else if (!File.Exists(Path.Combine(Path.GetDirectoryName(projectPath), appsettingsFile))) + { + reporter.Error(Resources.FormatRemoveCommand_AppsettingsFileNotFound_Error(Path.Combine(Path.GetDirectoryName(projectPath), appsettingsFile))); + return false; + } + } + + return true; + } + public static List GetAudienceCandidatesFromLaunchSettings(string project) { if (string.IsNullOrEmpty(project)) @@ -213,4 +236,5 @@ public static bool IsNullOrEmpty(this IEnumerable enumerable) { return enumerable == null || !enumerable.Any(); } + } diff --git a/src/Tools/dotnet-user-jwts/src/Resources.resx b/src/Tools/dotnet-user-jwts/src/Resources.resx index 094a066418d9..9f4ff3c041ac 100644 --- a/src/Tools/dotnet-user-jwts/src/Resources.resx +++ b/src/Tools/dotnet-user-jwts/src/Resources.resx @@ -150,6 +150,12 @@ The UTC date & time the JWT should expire in the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'. Defaults to 3 months after the --not-before date. Do not use this option in conjunction with the --valid-for option. + + Invalid Appsettings file extension. Ensure file extension is .json. + + + Could not find Appsettings file in '{0}'. Check the filename and that the file exists. + Malformed claims supplied. Ensure each claim is in the format "name=value". @@ -189,6 +195,9 @@ The period the JWT should expire after. Specify using a number followed by a duration type like 'd' for days, 'h' for hours, 'm' for minutes, and 's' for seconds, e.g. '365d'. Do not use this option in conjunction with the --expires-on option. + + The appSettings configuration file to add the test scheme to. + Audience(s) @@ -225,6 +234,9 @@ Scopes + + Appsettings File + Token @@ -312,6 +324,12 @@ No JWT with ID '{0}' found. + + The specified appsettings file is invalid. Please provide a valid JSON file. + + + Could not find Appsettings file '{0}'. Check the filename and that the file exists. + The issuer associated with the signing key to be reset or displayed. Defaults to 'dotnet-user-jwts'. diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs index 946530df7c6a..b812a017255c 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs @@ -81,6 +81,10 @@ public string CreateProject(bool hasSecret = true) Path.Combine(projectPath.FullName, "appsettings.Development.json"), "{}"); + File.WriteAllText( + Path.Combine(projectPath.FullName, "appsettings.Local.json"), + "{}"); + if (hasSecret) { _disposables.Push(() => diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index 71cc2cdb7d10..c8ad90b61a8e 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -79,6 +79,23 @@ public void Create_CanModifyExistingScheme() Assert.Equal("new-issuer", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); } + [Fact] + public void Create_CanModifyExistingSchemeInGivenAppSettings() + { + var project = Path.Combine(fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Local.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json" }); + Assert.Contains("New JWT saved", _console.GetOutput()); + + var appSettings = JsonSerializer.Deserialize(File.ReadAllText(appsettings)); + Assert.Equal("dotnet-user-jwts", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); + app.Run(["create", "--project", project, "--issuer", "new-issuer", "--appsettings-file", "appsettings.Local.json"]); + appSettings = JsonSerializer.Deserialize(File.ReadAllText(appsettings)); + Assert.Equal("new-issuer", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); + } + [Fact] public void Print_ReturnsNothingForMissingToken() { @@ -154,6 +171,24 @@ public void Remove_RemovesGeneratedToken() Assert.Contains("Scheme2", appsettingsContent); } + [Fact] + public void Remove_RemovesGeneratedTokenInGivenAppsettings() + { + var project = Path.Combine(fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Local.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json" }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json", "--scheme", "Scheme2" }); + + app.Run(new[] { "remove", id, "--project", project, "--appsettings-file", "appsettings.Local.json" }); + var appsettingsContent = File.ReadAllText(appsettings); + Assert.DoesNotContain(DevJwtsDefaults.Scheme, appsettingsContent); + Assert.Contains("Scheme2", appsettingsContent); + } + [Fact] public void Clear_RemovesGeneratedTokens() { @@ -172,6 +207,24 @@ public void Clear_RemovesGeneratedTokens() Assert.DoesNotContain("Scheme2", appsettingsContent); } + [Fact] + public void Clear_RemovesGeneratedTokensInGivenAppsettings() + { + var project = Path.Combine(fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Local.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json" }); + app.Run(new[] { "create", "--project", project, "--appsettings-file", "appsettings.Local.json", "--scheme", "Scheme2" }); + + Assert.Contains("New JWT saved", _console.GetOutput()); + + app.Run(new[] { "clear", "--project", project, "--appsettings-file", "appsettings.Local.json", "--force" }); + var appsettingsContent = File.ReadAllText(appsettings); + Assert.DoesNotContain(DevJwtsDefaults.Scheme, appsettingsContent); + Assert.DoesNotContain("Scheme2", appsettingsContent); + } + [Fact] public void Key_CanResetSigningKey() { @@ -625,6 +678,19 @@ public void Create_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.DoesNotContain(Resources.CreateCommand_NoAudience_Error, _console.GetOutput()); } + [Fact] + public void Create_CanHandleAppsettingsOption_WithNoFile() + { + var projectPath = fixture.CreateProject(); + Directory.SetCurrentDirectory(projectPath); + var expectedAppsettingsPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.DoesNotExist.json"); + + var app = new Program(_console); + app.Run(["create", "--appsettings-file", "appsettings.DoesNotExist.json"]); + + Assert.Contains($"Could not find Appsettings file '{expectedAppsettingsPath}'. Check the filename and that the file exists.", _console.GetOutput()); + } + [Fact] public void Delete_CanHandleNoProjectOptionProvided_WithNoProjects() { @@ -637,6 +703,19 @@ public void Delete_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); } + [Fact] + public void Delete_CanHandleAppsettingsOption_WithNoFile() + { + var projectPath = fixture.CreateProject(); + Directory.SetCurrentDirectory(projectPath); + var expectedAppsettingsPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.DoesNotExist.json"); + + var app = new Program(_console); + app.Run(["remove", "some-id", "--appsettings-file", "appsettings.DoesNotExist.json"]); + + Assert.Contains($"Could not find Appsettings file '{expectedAppsettingsPath}'. Check the filename and that the file exists.", _console.GetOutput()); + } + [Fact] public void Clear_CanHandleNoProjectOptionProvided_WithNoProjects() { @@ -649,6 +728,19 @@ public void Clear_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); } + [Fact] + public void Clear_CanHandleAppsettingsOption_WithNoFile() + { + var projectPath = fixture.CreateProject(); + Directory.SetCurrentDirectory(projectPath); + var expectedAppsettingsPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.DoesNotExist.json"); + + var app = new Program(_console); + app.Run(["clear", "--appsettings-file", "appsettings.DoesNotExist.json"]); + + Assert.Contains($"Could not find Appsettings file '{expectedAppsettingsPath}'. Check the filename and that the file exists.", _console.GetOutput()); + } + [Fact] public void List_CanHandleNoProjectOptionProvided_WithNoProjects() { @@ -704,6 +796,21 @@ public void Create_CanHandleRelativePathAsOption() Assert.Contains("New JWT saved", _console.GetOutput()); } + [Fact] + public void Create_CanHandleRelativePathAsOptionForAppsettingsOption() + { + var projectPath = fixture.CreateProject(); + var tempPath = Path.GetTempPath(); + var targetPath = Path.GetRelativePath(tempPath, projectPath); + Directory.SetCurrentDirectory(tempPath); + + var app = new Program(_console); + app.Run(new[] { "create", "--project", targetPath, "--appsettings-file", "appsettings.Local.json" }); + + Assert.DoesNotContain($"Could not find Appsettings file '{projectPath}'. Check the filename and that the file exists.", _console.GetOutput()); + Assert.Contains("New JWT saved", _console.GetOutput()); + } + [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] public void Create_CreatesFileWithUserOnlyUnixFileMode()