From bf231543789a6bc9b6655e43b037e4ce593876fd Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 3 May 2022 16:42:51 -0700 Subject: [PATCH 01/18] Add dotnet dev-jwts tool --- AspNetCore.sln | 19 ++ eng/Signing.props | 1 + .../CommandLine/ConsoleTable.cs | 81 +++++++ src/Tools/Tools.slnf | 1 + .../src/Commands/ClearCommand.cs | 72 +++++++ .../src/Commands/CreateCommand.cs | 203 +++++++++++++++++ .../src/Commands/DeleteCommand.cs | 57 +++++ .../src/Commands/KeyCommand.cs | 75 +++++++ .../src/Commands/ListCommand.cs | 86 ++++++++ .../src/Commands/PrintCommand.cs | 69 ++++++ .../src/Helpers/DevJwtCliHelpers.cs | 204 ++++++++++++++++++ .../src/Helpers/DevJwtDefaults.cs | 13 ++ src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs | 33 +++ .../dotnet-dev-jwts/src/Helpers/JwtIssuer.cs | 87 ++++++++ .../dotnet-dev-jwts/src/Helpers/JwtStore.cs | 52 +++++ src/Tools/dotnet-dev-jwts/src/Program.cs | 27 +++ .../src/dotnet-dev-jwts.csproj | 25 +++ 17 files changed, 1105 insertions(+) create mode 100644 src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Helpers/JwtIssuer.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/Program.cs create mode 100644 src/Tools/dotnet-dev-jwts/src/dotnet-dev-jwts.csproj diff --git a/AspNetCore.sln b/AspNetCore.sln index 88ed078aec9e..a3a0ba3e2125 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1710,6 +1710,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-jwts", "src\Tools\dotnet-dev-jwts\src\dotnet-dev-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10247,6 +10249,22 @@ Global {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x64.Build.0 = Release|Any CPU {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.ActiveCfg = Release|Any CPU {487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.Build.0 = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|arm64.ActiveCfg = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|arm64.Build.0 = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|x64.ActiveCfg = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|x64.Build.0 = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|x86.ActiveCfg = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Debug|x86.Build.0 = Debug|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|Any CPU.Build.0 = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|arm64.ActiveCfg = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|arm64.Build.0 = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|x64.ActiveCfg = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|x64.Build.0 = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|x86.ActiveCfg = Release|Any CPU + {B34CB502-0286-4939-B25F-45998528A802}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11094,6 +11112,7 @@ Global {51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F} {487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088} {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} + {B34CB502-0286-4939-B25F-45998528A802} = {DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/Signing.props b/eng/Signing.props index 66f8dc2ce805..3cd40a432e83 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -79,6 +79,7 @@ + diff --git a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs new file mode 100644 index 000000000000..2df4b0f5882c --- /dev/null +++ b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.CommandLineUtils; + +internal class ConsoleTable +{ + private readonly List _columns = new(); + private readonly List _rows = new(); + + public void AddColumns(params string[] names) + { + _columns.AddRange(names); + } + + public void AddRows(params object[] values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (!_columns.Any()) + { + throw new Exception("Columns must be set before rows can be added."); + } + + if (_columns.Count != values.Length) + { + throw new Exception( + $"The number columns in the table '{_columns.Count}' does not match the number of columns in the row '{values.Length}'."); + } + + _rows.Add(values); + } + + public void Write() + { + var builder = new StringBuilder(); + + var maxColumnLengths = _columns + .Select((t, i) => _rows.Select(x => x[i]) + .Union(new[] { _columns[i] }) + .Where(x => x != null) + .Select(x => x!.ToString()!.Length).Max()) + .ToList(); + + var formatRow = Enumerable.Range(0, _columns.Count) + .Select(i => " | {" + i + ", " + maxColumnLengths[i] + "}") + .Aggregate((previousRowColumn, nextRowColumn) => previousRowColumn + nextRowColumn) + " |"; + + var maxRowLength = Math.Max(0, _rows.Any() ? _rows.Max(row => string.Format(CultureInfo.InvariantCulture, formatRow, row).Length) : 0); + var columnHeaders = string.Format(CultureInfo.InvariantCulture, formatRow, _columns.ToArray()); + + var maxTotalLength = Math.Max(maxRowLength, columnHeaders.Length); + + var formattedRows = _rows.Select(row => string.Format(CultureInfo.InvariantCulture, formatRow, row)).ToList(); + + var rowDivider = $" {string.Join("", Enumerable.Repeat("-", maxTotalLength - 1))} "; + + builder.AppendLine(rowDivider); + builder.AppendLine(columnHeaders); + + foreach (var formattedRow in formattedRows) + { + builder.AppendLine(rowDivider); + builder.AppendLine(formattedRow); + } + + builder.AppendLine(rowDivider); + + Console.WriteLine(builder.ToString()); + } +} diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 8527dde99dcc..d2dc97d6ea96 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -97,6 +97,7 @@ "src\\Tools\\Microsoft.dotnet-openapi\\src\\Microsoft.dotnet-openapi.csproj", "src\\Tools\\Microsoft.dotnet-openapi\\test\\dotnet-microsoft.openapi.Tests.csproj", "src\\Tools\\dotnet-dev-certs\\src\\dotnet-dev-certs.csproj", + "src\\Tools\\dotnet-dev-jwts\\src\\dotnet-dev-jwts.csproj", "src\\Tools\\dotnet-getdocument\\src\\dotnet-getdocument.csproj", "src\\Tools\\dotnet-sql-cache\\src\\dotnet-sql-cache.csproj", "src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj", diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs new file mode 100644 index 000000000000..dd6545abdf98 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class ClearCommand +{ + public static void Register(CommandLineApplication app) + { + app.Command("clear", cmd => + { + cmd.Description = "Delete all issued JWTs for a project"; + + var projectOption = cmd.Option( + "--project", + "The path of the project to operate on. Defaults to the project in the current directory.", + CommandOptionType.SingleValue); + + var forceOption = cmd.Option( + "--force", + "Don't prompt for confirmation before deleting JWTs", + CommandOptionType.NoValue); + + cmd.HelpOption("-h|--help"); + + cmd.OnExecute(() => + { + return Execute(projectOption.Value(), forceOption.HasValue()); + }); + }); + } + + private static int Execute(string projectPath, bool force) + { + var project = DevJwtCliHelpers.GetProject(projectPath); + if (project == null) + { + Console.WriteLine($"No project found at {projectPath} or current directory."); + return 1; + } + + var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); + var jwtStore = new JwtStore(userSecretsId); + + var count = jwtStore.Jwts.Count; + + if (count == 0) + { + Console.WriteLine($"There are no JWTs to delete from {project}"); + return 0; + } + + if (!force) + { + Console.WriteLine($"Are you sure you want to delete {count} JWT(s) for {project}? \n [Y]es / [N]o"); + if (Console.ReadKey().Key != ConsoleKey.Y) + { + Console.WriteLine("Cancelled, no JWTs were deleted"); + return 0; + } + } + + jwtStore.Jwts.Clear(); + jwtStore.Save(); + + Console.WriteLine($"Deleted {count} token(s) from {project} successfully"); + + return 0; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs new file mode 100644 index 000000000000..45a1a07c320f --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class CreateCommand +{ + private static readonly string[] _dateTimeFormats = new[] { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss" }; + private static readonly string[] _timeSpanFormats = new[] { + @"d\dh\hm\ms\s", @"d\dh\hm\m", @"d\dh\h", @"d\d", + @"h\hm\ms\s", @"h\hm\m", @"h\h", + @"m\ms\s", @"m\m", + @"s\s" + }; + + public static void Register(CommandLineApplication app) + { + app.Command("create", cmd => + { + cmd.Description = "Issue a new JSON Web Token"; + + var projectOption = cmd.Option( + "--project", + "The path of the project to operate on. Defaults to the project in the current directory.", + CommandOptionType.SingleValue); + + var nameOption = cmd.Option( + "--name", + "The name of the user to create the JWT for. Defaults to the current environment user.", + CommandOptionType.SingleValue); + + var audienceOption = cmd.Option( + "--audience", + "The audience to create the JWT for. Defaults to the first HTTPS URL configured in the project's launchSettings.json", + CommandOptionType.SingleValue); + + var issuerOption = cmd.Option( + "--issuer", + "The issuer of the JWT. Defaults to the dotnet-dev-jwt", + CommandOptionType.SingleValue); + + var scopesOption = cmd.Option( + "--scope", + "The issuer of the JWT. Defaults to the dotnet-dev-jwt", + CommandOptionType.MultipleValue); + + var rolesOption = cmd.Option( + "--role", + "A role claim to add to the JWT. Specify once for each role", + CommandOptionType.MultipleValue); + + var claimsOption = cmd.Option( + "--claim", + "Claims to add to the JWT. Specify once for each claim in the format \"name=value\"", + CommandOptionType.MultipleValue); + + var notBeforeOption = cmd.Option( + "--not-before", + @"The UTC date & time the JWT should not be valid before in the format 'yyyy-MM-dd [[HH:mm[[:ss]]]]'. Defaults to the date & time the JWT is created", + CommandOptionType.SingleValue); + + var expiresOnOption = cmd.Option( + "--expires-on", + @"The UTC date & time the JWT should expire in the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'. Defaults to 6 months after the --not-before date. " + + "Do not use this option in conjunction with the --valid-for option.", + CommandOptionType.SingleValue); + + var validForOption = cmd.Option( + "--valid-for", + "The period the JWT should expire after. Specify using a number followed by a period 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.", + CommandOptionType.SingleValue); + + cmd.HelpOption("-h|--help"); + + cmd.OnExecute(() => + { + var (name, audience, issuer, notBefore, expiresOn, roles, scopes, claims, isValid) = ValidateArguments( + projectOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); + + if (!isValid) + { + return 1; + } + + return Execute(projectOption.Value(), name, audience, issuer, scopes, roles, claims, notBefore, expiresOn); + }); + }); + } + + private static (string, string, string, DateTime, DateTime, List, List, Dictionary, bool) ValidateArguments( + CommandOption projectOption, + CommandOption nameOption, + CommandOption audienceOption, + CommandOption issuerOption, + CommandOption notBeforeOption, + CommandOption expiresOnOption, + CommandOption validForOption, + CommandOption rolesOption, + CommandOption scopesOption, + CommandOption claimsOption) + { + var isValid = true; + var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName; + var project = DevJwtCliHelpers.GetProject(projectOption.Value()); + var audience = audienceOption.HasValue() ? audienceOption.Value() : DevJwtCliHelpers.GetApplicationUrl(project); + if (audience is null) + { + Console.WriteLine("Could not determine the project's HTTPS URL. Please specify an audience for the JWT using the --audience option."); + isValid = false; + } + var issuer = issuerOption.HasValue() ? issuerOption.Value() : DevJwtsDefaults.Issuer; + + var notBefore = DateTime.UtcNow; + if (notBeforeOption.HasValue()) + { + if (!ParseDate(notBeforeOption.Value(), out notBefore)) + { + Console.WriteLine(@"The date provided for --not-before could not be parsed. Ensure you use the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'."); + isValid = false; + } + } + + var expiresOn = notBefore.AddMonths(6); + if (notBeforeOption.HasValue()) + { + if (!ParseDate(expiresOnOption.Value(), out expiresOn)) + { + Console.WriteLine(@"The date provided for -expires-on could not be parsed. Ensure you use the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'."); + isValid = false; + } + } + + if (validForOption.HasValue()) + { + if (!TimeSpan.TryParseExact(validForOption.Value(), _timeSpanFormats, CultureInfo.InvariantCulture, out var validForValue)) + { + Console.WriteLine("The period provided for --valid-for could not be parsed. Ensure you use a format like '10d', '24h', etc."); + } + expiresOn = notBefore.Add(validForValue); + } + + var roles = rolesOption.HasValue() ? rolesOption.Values : new List(); + var scopes = scopesOption.HasValue() ? scopesOption.Values : new List(); + + var claims = new Dictionary(); + if (claimsOption.HasValue()) + { + if (!DevJwtCliHelpers.TryParseClaims(claimsOption.Values, out claims)) + { + Console.WriteLine("Malformed claims supplied. Ensure each claim is in the format \"name=value\"."); + isValid = false; + } + } + + return (name, audience, issuer, notBefore, expiresOn, roles, scopes, claims, isValid); + + static bool ParseDate(string datetime, out DateTime parsedDateTime) => + DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime); + } + + private static int Execute( + string projectPath, + string name, + string audience, + string issuer, + List scopes, + List roles, + IDictionary claims, + DateTime notBefore, + DateTime expiresOn) + { + var project = DevJwtCliHelpers.GetProject(projectPath); + if (project == null) + { + Console.WriteLine($"No project found at {projectPath} or current directory."); + return 1; + } + + var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); + var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId); + + var jwtIssuer = new JwtIssuer(issuer, keyMaterial); + var jwtToken = jwtIssuer.Create(name, audience, notBefore, expiresOn, issuedAt: DateTime.UtcNow, scopes: scopes, roles, claims); + + var jwtStore = new JwtStore(userSecretsId); + var jwt = Jwt.Create(jwtToken, jwtIssuer.WriteToken(jwtToken), scopes, roles, claims); + if (claims is { } customClaims) + { + jwt.CustomClaims = customClaims; + } + jwtStore.Jwts.Add(jwtToken.Id, jwt); + jwtStore.Save(); + + Console.WriteLine("New JWT saved!"); + + return 0; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs new file mode 100644 index 000000000000..7ebf8a860256 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class DeleteCommand +{ + public static void Register(CommandLineApplication app) + { + app.Command("delete", cmd => + { + cmd.Description = "Delete a given JWT"; + + var idArgument = cmd.Argument("id", "The ID of the JWT to delete"); + + var projectOption = cmd.Option( + "--project", + "The path of the project to operate on. Defaults to the project in the current directory.", + CommandOptionType.SingleValue); + + cmd.HelpOption("-h|--help"); + + cmd.OnExecute(() => + { + return Execute(projectOption.Value(), idArgument.Value); + }); + }); + } + + private static int Execute(string projectPath, string id) + { + var project = DevJwtCliHelpers.GetProject(projectPath); + if (project == null) + { + Console.WriteLine($"No project found at {projectPath} or current directory."); + return 1; + } + + var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); + var jwtStore = new JwtStore(userSecretsId); + + if (!jwtStore.Jwts.ContainsKey(id)) + { + Console.WriteLine($"[ERROR] No JWT with ID '{id}' found"); + return 1; + } + + jwtStore.Jwts.Remove(id); + jwtStore.Save(); + + Console.WriteLine($"Deleted JWT with ID '{id}'"); + + return 0; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs new file mode 100644 index 000000000000..41fcacab4945 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class KeyCommand +{ + public static void Register(CommandLineApplication app) + { + app.Command("key", cmd => + { + cmd.Description = "Display or reset the signing key used to issue JWTs"; + + var projectOption = cmd.Option( + "--project", + "The path of the project to operate on. Defaults to the project in the current directory.", + CommandOptionType.SingleValue); + + var resetOption = cmd.Option( + "--reset", + "Reset the signing key. This will invalidate all previously issued JWTs for this project.", + CommandOptionType.NoValue); + + cmd.HelpOption("-h|--help"); + + cmd.OnExecute(() => + { + return Execute(projectOption.Value(), resetOption.HasValue()); + }); + }); + } + + private static int Execute(string projectPath, bool reset) + { + var project = DevJwtCliHelpers.GetProject(projectPath); + if (project == null) + { + Console.WriteLine($"No project found at {projectPath} or current directory."); + return 1; + } + + var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); + + if (reset == true) + { + Console.WriteLine("Are you sure you want to reset the JWT signing key? This will invalidate all JWTs previously issued for this project.\n [Y]es / [N]o"); + if (Console.ReadKey().Key == ConsoleKey.Y) + { + var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, reset: true); + Console.WriteLine($"New signing key created: {Convert.ToBase64String(key)}"); + return 0; + } + + Console.WriteLine("Key reset canceled."); + return 0; + } + + var projectConfiguration = new ConfigurationBuilder() + .AddUserSecrets(userSecretsId) + .Build(); + var signingKeyMaterial = projectConfiguration[DevJwtsDefaults.SigningKeyConfigurationKey]; + + if (signingKeyMaterial is null) + { + Console.WriteLine("Signing key for JWTs was not found. One will be created automatically when the first JWT is created, or you can force creation of a key with the --reset option."); + return 0; + } + + Console.WriteLine($"Signing Key: {signingKeyMaterial}"); + return 0; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs new file mode 100644 index 000000000000..e7b9295a78c9 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class ListCommand +{ + public static void Register(CommandLineApplication app) + { + app.Command("list", cmd => + { + cmd.Description = "Lists the JWTs issued for the project"; + + var projectOption = cmd.Option( + "--project", + "The path of the project to operate on. Defaults to the project in the current directory", + CommandOptionType.SingleValue); + + var showTokensOption = cmd.Option( + "--show-tokens", + "Indicates whether JWT base64 strings should be shown", + CommandOptionType.NoValue); + + cmd.HelpOption("-h|--help"); + + cmd.OnExecute(() => + { + return Execute(projectOption.Value(), showTokensOption.HasValue()); + }); + }); + } + + private static int Execute(string projectPath, bool showTokens) + { + var project = DevJwtCliHelpers.GetProject(projectPath); + if (project == null) + { + Console.WriteLine($"No project found at {projectPath} or current directory."); + return 1; + } + var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); + if (userSecretsId == null) + { + Console.WriteLine($"Project does not contain a user secret ID."); + return 1; + } + var jwtStore = new JwtStore(userSecretsId); + + Console.WriteLine($"Project: {project}"); + Console.WriteLine($"User Secrets Id: {userSecretsId}"); + + if (jwtStore.Jwts is { Count: > 0 } jwts) + { + var table = new ConsoleTable(); + table.AddColumns("Id", "Name", "Audience", "Issued", "Expires"); + + if (showTokens) + { + table.AddColumns("Encoded Token"); + } + + foreach (var jwtRow in jwts) + { + var jwt = jwtRow.Value; + if (showTokens) + { + table.AddRows(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"), jwt.Token); + } + else + { + table.AddRows(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O")); + } + } + + table.Write(); + } + else + { + Console.WriteLine("No JWTs created yet!"); + } + + return 0; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs new file mode 100644 index 000000000000..714e5a744134 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IdentityModel.Tokens.Jwt; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class PrintCommand +{ + public static void Register(CommandLineApplication app) + { + app.Command("print", cmd => + { + cmd.Description = "Print the details of a given JWT"; + + var idArgument = cmd.Argument("id", "The ID of the JWT to print"); + + var projectOption = cmd.Option( + "--project", + "The path of the project to operate on. Defaults to the project in the current directory.", + CommandOptionType.SingleValue); + + var showFullOption = cmd.Option( + "--show-full", + "Whether to show the full JWT contents in addition to the compact serialized format", + CommandOptionType.NoValue); + + cmd.HelpOption("-h|--help"); + + cmd.OnExecute(() => + { + return Execute(projectOption.Value(), idArgument.Value, showFullOption.HasValue()); + }); + }); + } + + private static int Execute(string projectPath, string id, bool showFull) + { + var project = DevJwtCliHelpers.GetProject(projectPath); + if (project == null) + { + Console.WriteLine($"No project found at {projectPath} or current directory."); + return 1; + } + + var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); + var jwtStore = new JwtStore(userSecretsId); + + if (!jwtStore.Jwts.ContainsKey(id)) + { + Console.WriteLine($"No token with ID '{id}' found"); + return 1; + } + + Console.WriteLine($"Found JWT with ID '{id}'"); + var jwt = jwtStore.Jwts[id]; + JwtSecurityToken fullToken; + if (showFull) + { + var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId); + var jwtIssuer = new JwtIssuer(DevJwtsDefaults.Issuer, keyMaterial); + fullToken = jwtIssuer.Extract(jwt.Token); + DevJwtCliHelpers.PrintJwt(jwt, fullToken); + } + + return 0; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs new file mode 100644 index 000000000000..cbce63fae9e4 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal static class DevJwtCliHelpers +{ + public static string GetUserSecretsId(string projectFilePath) + { + var projectDocument = XDocument.Load(projectFilePath, LoadOptions.PreserveWhitespace); + var existingUserSecretsId = projectDocument.XPathSelectElements("//UserSecretsId").FirstOrDefault(); + + if (existingUserSecretsId == null) + { + return null; + } + + return existingUserSecretsId.Value; + } + + public static string GetProject(string projectPath = null) + { + if (projectPath is not null) + { + return projectPath; + } + + var csprojFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj"); + if (csprojFiles is [var path]) + { + return path; + } + return null; + } + + public static byte[] GetOrCreateSigningKeyMaterial(string userSecretsId) + { + var projectConfiguration = new ConfigurationBuilder() + .AddUserSecrets(userSecretsId) + .Build(); + + var signingKeyMaterial = projectConfiguration[DevJwtsDefaults.SigningKeyConfigurationKey]; + + var keyMaterial = new byte[DevJwtsDefaults.SigningKeyLength]; + if (signingKeyMaterial is not null && Convert.TryFromBase64String(signingKeyMaterial, keyMaterial, out var bytesWritten) && bytesWritten == DevJwtsDefaults.SigningKeyLength) + { + return keyMaterial; + } + + return CreateSigningKeyMaterial(userSecretsId); + } + + public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = false) + { + // Create signing material and save to user secrets + var newKeyMaterial = System.Security.Cryptography.RandomNumberGenerator.GetBytes(DevJwtsDefaults.SigningKeyLength); + + string secretsFilePath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "UserSecrets", userSecretsId, "secrets.json"); + } + else + { + secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "microsoft", "usersecrets", userSecretsId, "secret.json"); + } + + IDictionary secrets = null; + if (File.Exists(secretsFilePath)) + { + using var secretsFileStream = new FileStream(secretsFilePath, FileMode.Open, FileAccess.Read); + if (secretsFileStream.Length > 0) + { + secrets = JsonSerializer.Deserialize>(secretsFileStream) ?? new Dictionary(); + } + } + + secrets ??= new Dictionary(); + + if (reset && secrets.ContainsKey(DevJwtsDefaults.SigningKeyConfigurationKey)) + { + secrets.Remove(DevJwtsDefaults.SigningKeyConfigurationKey); + } + secrets.Add(DevJwtsDefaults.SigningKeyConfigurationKey, Convert.ToBase64String(newKeyMaterial)); + + using var secretsWriteStream = new FileStream(secretsFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + JsonSerializer.Serialize(secretsWriteStream, secrets); + + return newKeyMaterial; + } + + public static string GetApplicationUrl(string project) + { + ArgumentException.ThrowIfNullOrEmpty(nameof(project)); + + var launchSettingsFilePath = Path.Combine(Path.GetDirectoryName(project)!, "Properties", "launchSettings.json"); + if (File.Exists(launchSettingsFilePath)) + { + using var launchSettingsFileStream = new FileStream(launchSettingsFilePath, FileMode.Open, FileAccess.Read); + if (launchSettingsFileStream.Length > 0) + { + var launchSettingsJson = JsonDocument.Parse(launchSettingsFileStream); + if (launchSettingsJson.RootElement.TryGetProperty("profiles", out var profiles)) + { + var profilesEnumerator = profiles.EnumerateObject(); + foreach (var profile in profilesEnumerator) + { + if (profile.Value.TryGetProperty("commandName", out var commandName)) + { + if (commandName.ValueEquals("Project")) + { + if (profile.Value.TryGetProperty("applicationUrl", out var applicationUrl)) + { + var value = applicationUrl.GetString(); + if (value is { } applicationUrls) + { + var urls = applicationUrls.Split(";"); + var firstHttpsUrl = urls.FirstOrDefault(u => u.StartsWith("https:", StringComparison.OrdinalIgnoreCase)); + if (firstHttpsUrl is { } result) + { + return result; + } + } + } + } + } + } + } + } + } + + return null; + } + + public static void PrintJwt(Jwt jwt, JwtSecurityToken fullToken = null) + { + var table = new ConsoleTable(); + table.AddColumns("Name", "Id", "Audience", "Expires", "Issued", "Scopes", "Roles", "Custom Claims"); + if (fullToken is not null) + { + table.AddColumns("Token Header", "Token Payload"); + } + + if (fullToken is not null) + { + table.AddRows( + jwt.Id, + jwt.Name, + jwt.Audience, + jwt.Expires.ToString("O"), + jwt.Issued.ToString("O"), + jwt.Scopes is not null ? string.Join(", ", jwt.Scopes) : "[none]", + jwt.Roles is not null ? string.Join(", ", jwt.Roles) : "[none]", + jwt.CustomClaims?.Count > 0 ? jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}") : "[none]", + fullToken.Header.SerializeToJson(), + fullToken.Payload.SerializeToJson() + ); + } + else + { + table.AddRows( + jwt.Id, + jwt.Name, + jwt.Audience, + jwt.Expires.ToString("O"), + jwt.Issued.ToString("O"), + jwt.Scopes is not null ? string.Join(", ", jwt.Scopes) : "[none]", + jwt.Roles is not null ? string.Join(", ", jwt.Roles) : "[none]", + jwt.CustomClaims?.Count > 0 ? jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}") : "[none]" + ); + } + + table.Write(); + Console.WriteLine($"Compact Token: {jwt.Token}"); + } + + public static bool TryParseClaims(List input, out Dictionary claims) + { + claims = new Dictionary(); + foreach (var claim in input) + { + var parts = claim.Split('='); + if (parts.Length != 2) + { + return false; + } + + var key = parts[0]; + var value = parts[1]; + + claims.Add(key, value); + } + return true; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs new file mode 100644 index 000000000000..6890e3dde9d2 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal static class DevJwtsDefaults +{ + public static string Issuer => "dotnet-dev-jwt"; + + public static string SigningKeyConfigurationKey => $"{Issuer}:KeyMaterial"; + + public static int SigningKeyLength => 32; +} diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs new file mode 100644 index 000000000000..b0554244ccfd --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IdentityModel.Tokens.Jwt; +using System.Linq; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +public record Jwt(string Id, string Name, string Audience, DateTimeOffset NotBefore, DateTimeOffset Expires, DateTimeOffset Issued, string Token) +{ + public IEnumerable Scopes { get; set; } = new List(); + + public IEnumerable Roles { get; set; } = new List(); + + public IDictionary CustomClaims { get; set; } = new Dictionary(); + + public override string ToString() => Token; + + public static Jwt Create( + JwtSecurityToken token, + string encodedToken, + IEnumerable scopes = null, + IEnumerable roles = null, + IDictionary customClaims = null) + { + return new Jwt(token.Id, token.Subject, token.Audiences.FirstOrDefault(), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken) + { + Scopes = scopes, + Roles = roles, + CustomClaims = customClaims + }; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/JwtIssuer.cs new file mode 100644 index 000000000000..9fa880cf20d6 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/JwtIssuer.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +public class JwtIssuer +{ + private readonly SymmetricSecurityKey _signingKey; + + public JwtIssuer(string issuer, byte[] signingKeyMaterial) + { + Issuer = issuer; + _signingKey = new SymmetricSecurityKey(signingKeyMaterial); + } + + public string Issuer { get; } + + public JwtSecurityToken Create( + string name, + string audience, + DateTime notBefore, + DateTime expires, + DateTime issuedAt, + IEnumerable scopes = null, + IEnumerable roles = null, + IDictionary claims = null) + { + var identity = new GenericIdentity(name); + + identity.AddClaim(new Claim(JwtRegisteredClaimNames.Sub, name)); + + var id = Guid.NewGuid().ToString().GetHashCode().ToString("x", CultureInfo.InvariantCulture); + identity.AddClaim(new Claim(JwtRegisteredClaimNames.Jti, id)); + + if (scopes is { } scopesToAdd) + { + identity.AddClaims(scopesToAdd.Select(s => new Claim("scope", s))); + } + + if (roles is { } rolesToAdd) + { + identity.AddClaims(rolesToAdd.Select(r => new Claim(ClaimTypes.Role, r))); + } + + if (claims is { Count: > 0 } claimsToAdd) + { + identity.AddClaims(claimsToAdd.Select(kvp => new Claim(kvp.Key, kvp.Value))); + } + + var handler = new JwtSecurityTokenHandler(); + var jwtSigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256Signature); + var jwtToken = handler.CreateJwtSecurityToken(Issuer, audience, identity, notBefore, expires, issuedAt, jwtSigningCredentials); + return jwtToken; + } + + public string WriteToken(JwtSecurityToken token) + { + var handler = new JwtSecurityTokenHandler(); + return handler.WriteToken(token); + } + + public JwtSecurityToken Extract(string token) => new JwtSecurityToken(token); + + public bool IsValid(string encodedToken) + { + var handler = new JwtSecurityTokenHandler(); + var tokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = _signingKey, + ValidateAudience = false, + ValidateIssuer = false, + ValidateIssuerSigningKey = true + }; + if (handler.ValidateToken(encodedToken, tokenValidationParameters, out _).Identity?.IsAuthenticated == true) + { + return true; + } + return false; + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs new file mode 100644 index 000000000000..731432cff2b6 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +public class JwtStore +{ + private const string FileName = "dev-jwts.json"; + private readonly string _userSecretsId; + private readonly string _filePath; + private readonly JsonSerializerOptions _jsonSerializerOptions = JsonSerializerOptions.Default; + + public JwtStore(string userSecretsId) + { + _userSecretsId = userSecretsId; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "UserSecrets", _userSecretsId, FileName); + } + else + { + _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "microsoft", "usersecrets", _userSecretsId, FileName); + } + Load(); + } + + public IDictionary Jwts { get; private set; } = new Dictionary(); + + public void Load() + { + if (File.Exists(_filePath)) + { + using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read); + if (fileStream.Length > 0) + { + Jwts = JsonSerializer.Deserialize>(fileStream, _jsonSerializerOptions) ?? new Dictionary(); + } + } + } + + public void Save() + { + if (Jwts is not null) + { + using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write); + JsonSerializer.Serialize(fileStream, Jwts, _jsonSerializerOptions); + } + } +} diff --git a/src/Tools/dotnet-dev-jwts/src/Program.cs b/src/Tools/dotnet-dev-jwts/src/Program.cs new file mode 100644 index 000000000000..36a50f2f4c44 --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/Program.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +using Microsoft.Extensions.CommandLineUtils; + +CommandLineApplication devJwts = new() +{ + Name = "dotnet dev-jwts" +}; + +devJwts.HelpOption("-h|--help"); + +// dotnet dev-jwts list +ListCommand.Register(devJwts); +// dotnet dev-jwts create +CreateCommand.Register(devJwts); +// dotnet dev-jwts print ecd045 +PrintCommand.Register(devJwts); +// dotnet dev-jwts delete ecd045 +DeleteCommand.Register(devJwts); +// dotnet dev-jwts clear +ClearCommand.Register(devJwts); +// dotnet dev-jwts key +KeyCommand.Register(devJwts); + +devJwts.Execute(args); diff --git a/src/Tools/dotnet-dev-jwts/src/dotnet-dev-jwts.csproj b/src/Tools/dotnet-dev-jwts/src/dotnet-dev-jwts.csproj new file mode 100644 index 000000000000..93b34c52c64b --- /dev/null +++ b/src/Tools/dotnet-dev-jwts/src/dotnet-dev-jwts.csproj @@ -0,0 +1,25 @@ + + + $(DefaultNetCoreTargetFramework) + exe + Command line tool to manage JSON Web Tokens in a user application. + false + configuration;authentication;authorization;jwt + Microsoft.AspNetCore.Authentication.JwtBearer.Tools + true + + false + + + + + + + + + + + + + + \ No newline at end of file From 6961df5f27e695e951f4db67ea8127620dd2a982 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 3 May 2022 16:42:51 -0700 Subject: [PATCH 02/18] Add dotnet dev-jwts tool --- AspNetCore.sln | 3 +++ src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs | 8 ++++++++ src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs | 4 ++++ src/Tools/dotnet-dev-jwts/src/Program.cs | 8 ++++++++ 11 files changed, 51 insertions(+) diff --git a/AspNetCore.sln b/AspNetCore.sln index a3a0ba3e2125..ff1a8af86adb 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1710,6 +1710,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-dev-jwts", "dotnet-dev-jwts", "{DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-jwts", "src\Tools\dotnet-dev-jwts\src\dotnet-dev-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" EndProject Global @@ -11112,6 +11114,7 @@ Global {51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F} {487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088} {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} + {DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} {B34CB502-0286-4939-B25F-45998528A802} = {DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs index 2df4b0f5882c..f3828c5d120c 100644 --- a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs +++ b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs @@ -10,7 +10,11 @@ namespace Microsoft.Extensions.CommandLineUtils; +<<<<<<< HEAD internal class ConsoleTable +======= +internal sealed class ConsoleTable +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { private readonly List _columns = new(); private readonly List _rows = new(); diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs index dd6545abdf98..417efc102eed 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs @@ -5,7 +5,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +<<<<<<< HEAD internal class ClearCommand +======= +internal sealed class ClearCommand +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { public static void Register(CommandLineApplication app) { diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs index 45a1a07c320f..055b8857164f 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs @@ -6,7 +6,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +<<<<<<< HEAD internal class CreateCommand +======= +internal sealed class CreateCommand +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { private static readonly string[] _dateTimeFormats = new[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss" }; diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs index 7ebf8a860256..0ad4a08020f0 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs +++ b/src/Tools/dotnet-dev-jwts/src/Commands/DeleteCommand.cs @@ -5,7 +5,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +<<<<<<< HEAD internal class DeleteCommand +======= +internal sealed class DeleteCommand +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { public static void Register(CommandLineApplication app) { diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs index 41fcacab4945..90fbadb9685b 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs +++ b/src/Tools/dotnet-dev-jwts/src/Commands/KeyCommand.cs @@ -6,7 +6,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +<<<<<<< HEAD internal class KeyCommand +======= +internal sealed class KeyCommand +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { public static void Register(CommandLineApplication app) { diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs index e7b9295a78c9..72e34a0fb5d1 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs +++ b/src/Tools/dotnet-dev-jwts/src/Commands/ListCommand.cs @@ -5,7 +5,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +<<<<<<< HEAD internal class ListCommand +======= +internal sealed class ListCommand +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { public static void Register(CommandLineApplication app) { diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs index 714e5a744134..918c51c0524b 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs +++ b/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs @@ -6,7 +6,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +<<<<<<< HEAD internal class PrintCommand +======= +internal sealed class PrintCommand +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { public static void Register(CommandLineApplication app) { diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs index cbce63fae9e4..bf992d4f4d2c 100644 --- a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -71,7 +71,11 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = } else { +<<<<<<< HEAD secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "microsoft", "usersecrets", userSecretsId, "secret.json"); +======= + secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".microsoft", "usersecrets", userSecretsId, "secrets.json"); +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) } IDictionary secrets = null; @@ -144,7 +148,11 @@ public static string GetApplicationUrl(string project) public static void PrintJwt(Jwt jwt, JwtSecurityToken fullToken = null) { var table = new ConsoleTable(); +<<<<<<< HEAD table.AddColumns("Name", "Id", "Audience", "Expires", "Issued", "Scopes", "Roles", "Custom Claims"); +======= + table.AddColumns("Id", "Name", "Audience", "Expires", "Issued", "Scopes", "Roles", "Custom Claims"); +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) if (fullToken is not null) { table.AddColumns("Token Header", "Token Payload"); diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs index 731432cff2b6..f039545575cb 100644 --- a/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs +++ b/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs @@ -22,7 +22,11 @@ public JwtStore(string userSecretsId) } else { +<<<<<<< HEAD _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "microsoft", "usersecrets", _userSecretsId, FileName); +======= + _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".microsoft", "usersecrets", _userSecretsId, FileName); +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) } Load(); } diff --git a/src/Tools/dotnet-dev-jwts/src/Program.cs b/src/Tools/dotnet-dev-jwts/src/Program.cs index 36a50f2f4c44..cb4ce6bcbf75 100644 --- a/src/Tools/dotnet-dev-jwts/src/Program.cs +++ b/src/Tools/dotnet-dev-jwts/src/Program.cs @@ -4,6 +4,10 @@ using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; using Microsoft.Extensions.CommandLineUtils; +<<<<<<< HEAD +======= +#pragma warning disable CA1852 // Seal internal types +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) CommandLineApplication devJwts = new() { Name = "dotnet dev-jwts" @@ -25,3 +29,7 @@ KeyCommand.Register(devJwts); devJwts.Execute(args); +<<<<<<< HEAD +======= +#pragma warning restore CA1852 // Seal internal types +>>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) From 212cf04764dfa3729a05ae85890d1717c7b6bef0 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 May 2022 11:40:10 -0700 Subject: [PATCH 03/18] Address feedback from review --- AspNetCore.sln | 4 +--- eng/Signing.props | 2 +- .../CommandLineUtils/CommandLine/ConsoleTable.cs | 8 ++++---- src/Tools/Tools.slnf | 4 ++-- .../src/Commands/ClearCommand.cs | 2 +- .../src/Commands/CreateCommand.cs | 10 +++++----- .../src/Commands/DeleteCommand.cs | 2 +- .../src/Commands/KeyCommand.cs | 2 +- .../src/Commands/ListCommand.cs | 6 +++--- .../src/Commands/PrintCommand.cs | 2 +- .../src/Helpers/DevJwtCliHelpers.cs | 0 .../src/Helpers/DevJwtDefaults.cs | 2 +- .../src/Helpers/Jwt.cs | 0 .../src/Helpers/JwtIssuer.cs | 0 .../src/Helpers/JwtStore.cs | 0 .../src/Program.cs | 14 +++++++------- .../src/dotnet-dev-jwts.csproj | 0 17 files changed, 28 insertions(+), 30 deletions(-) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Commands/ClearCommand.cs (95%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Commands/CreateCommand.cs (94%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Commands/DeleteCommand.cs (94%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Commands/KeyCommand.cs (96%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Commands/ListCommand.cs (93%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Commands/PrintCommand.cs (96%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Helpers/DevJwtCliHelpers.cs (100%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Helpers/DevJwtDefaults.cs (87%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Helpers/Jwt.cs (100%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Helpers/JwtIssuer.cs (100%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Helpers/JwtStore.cs (100%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/Program.cs (79%) rename src/Tools/{dotnet-dev-jwts => dotnet-user-jwts}/src/dotnet-dev-jwts.csproj (100%) diff --git a/AspNetCore.sln b/AspNetCore.sln index ff1a8af86adb..8ae40d269f9e 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1710,9 +1710,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-dev-jwts", "dotnet-dev-jwts", "{DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-jwts", "src\Tools\dotnet-dev-jwts\src\dotnet-dev-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/eng/Signing.props b/eng/Signing.props index 3cd40a432e83..db20e2e9a45f 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -79,7 +79,7 @@ - + diff --git a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs index f3828c5d120c..5033f6adb026 100644 --- a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs +++ b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs @@ -39,7 +39,7 @@ public void AddRows(params object[] values) if (_columns.Count != values.Length) { throw new Exception( - $"The number columns in the table '{_columns.Count}' does not match the number of columns in the row '{values.Length}'."); + $"The number of columns in the table '{_columns.Count}' does not match the number of columns in the row '{values.Length}'."); } _rows.Add(values); @@ -60,13 +60,13 @@ public void Write() .Select(i => " | {" + i + ", " + maxColumnLengths[i] + "}") .Aggregate((previousRowColumn, nextRowColumn) => previousRowColumn + nextRowColumn) + " |"; - var maxRowLength = Math.Max(0, _rows.Any() ? _rows.Max(row => string.Format(CultureInfo.InvariantCulture, formatRow, row).Length) : 0); + var formattedRows = _rows.Select(row => string.Format(CultureInfo.InvariantCulture, formatRow, row)).ToList(); + + var maxRowLength = Math.Max(0, _rows.Any() ? _rows.Max(row => formattedRows.Count) : 0); var columnHeaders = string.Format(CultureInfo.InvariantCulture, formatRow, _columns.ToArray()); var maxTotalLength = Math.Max(maxRowLength, columnHeaders.Length); - var formattedRows = _rows.Select(row => string.Format(CultureInfo.InvariantCulture, formatRow, row)).ToList(); - var rowDivider = $" {string.Join("", Enumerable.Repeat("-", maxTotalLength - 1))} "; builder.AppendLine(rowDivider); diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index d2dc97d6ea96..12656af958cc 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -97,7 +97,7 @@ "src\\Tools\\Microsoft.dotnet-openapi\\src\\Microsoft.dotnet-openapi.csproj", "src\\Tools\\Microsoft.dotnet-openapi\\test\\dotnet-microsoft.openapi.Tests.csproj", "src\\Tools\\dotnet-dev-certs\\src\\dotnet-dev-certs.csproj", - "src\\Tools\\dotnet-dev-jwts\\src\\dotnet-dev-jwts.csproj", + "src\\Tools\\dotnet-user-jwts\\src\\dotnet-user-jwts.csproj", "src\\Tools\\dotnet-getdocument\\src\\dotnet-getdocument.csproj", "src\\Tools\\dotnet-sql-cache\\src\\dotnet-sql-cache.csproj", "src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj", @@ -105,4 +105,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs similarity index 95% rename from src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs rename to src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index 417efc102eed..c17644ae23e3 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -41,7 +41,7 @@ private static int Execute(string projectPath, bool force) var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at {projectPath} or current directory."); + Console.WriteLine($"No project found at `--project` path or current directory."); return 1; } diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs similarity index 94% rename from src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs rename to src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 055b8857164f..608a6e3a4611 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -44,12 +44,12 @@ public static void Register(CommandLineApplication app) var issuerOption = cmd.Option( "--issuer", - "The issuer of the JWT. Defaults to the dotnet-dev-jwt", + "The issuer of the JWT. Defaults to the dotnet-user-jwts", CommandOptionType.SingleValue); var scopesOption = cmd.Option( "--scope", - "The issuer of the JWT. Defaults to the dotnet-dev-jwt", + "A scope claim to add to the JWT. Specify once for each scope.", CommandOptionType.MultipleValue); var rolesOption = cmd.Option( @@ -130,11 +130,11 @@ private static (string, string, string, DateTime, DateTime, List, List 0 } jwts) { diff --git a/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs similarity index 96% rename from src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs rename to src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs index 918c51c0524b..15f8ed945c86 100644 --- a/src/Tools/dotnet-dev-jwts/src/Commands/PrintCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs @@ -44,7 +44,7 @@ private static int Execute(string projectPath, string id, bool showFull) var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at {projectPath} or current directory."); + Console.WriteLine($"No project found at `--project` path or current directory."); return 1; } diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs similarity index 100% rename from src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtCliHelpers.cs rename to src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs similarity index 87% rename from src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs rename to src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs index 6890e3dde9d2..595d7c510b46 100644 --- a/src/Tools/dotnet-dev-jwts/src/Helpers/DevJwtDefaults.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal static class DevJwtsDefaults { - public static string Issuer => "dotnet-dev-jwt"; + public static string Issuer => "dotnet-user-jwts"; public static string SigningKeyConfigurationKey => $"{Issuer}:KeyMaterial"; diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs similarity index 100% rename from src/Tools/dotnet-dev-jwts/src/Helpers/Jwt.cs rename to src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs similarity index 100% rename from src/Tools/dotnet-dev-jwts/src/Helpers/JwtIssuer.cs rename to src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs diff --git a/src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs similarity index 100% rename from src/Tools/dotnet-dev-jwts/src/Helpers/JwtStore.cs rename to src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs diff --git a/src/Tools/dotnet-dev-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs similarity index 79% rename from src/Tools/dotnet-dev-jwts/src/Program.cs rename to src/Tools/dotnet-user-jwts/src/Program.cs index cb4ce6bcbf75..7efc9f4c5df3 100644 --- a/src/Tools/dotnet-dev-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -10,22 +10,22 @@ >>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) CommandLineApplication devJwts = new() { - Name = "dotnet dev-jwts" + Name = "dotnet user-jwts" }; devJwts.HelpOption("-h|--help"); -// dotnet dev-jwts list +// dotnet user-jwts list ListCommand.Register(devJwts); -// dotnet dev-jwts create +// dotnet user-jwts create CreateCommand.Register(devJwts); -// dotnet dev-jwts print ecd045 +// dotnet user-jwts print ecd045 PrintCommand.Register(devJwts); -// dotnet dev-jwts delete ecd045 +// dotnet user-jwts delete ecd045 DeleteCommand.Register(devJwts); -// dotnet dev-jwts clear +// dotnet user-jwts clear ClearCommand.Register(devJwts); -// dotnet dev-jwts key +// dotnet user-jwts key KeyCommand.Register(devJwts); devJwts.Execute(args); diff --git a/src/Tools/dotnet-dev-jwts/src/dotnet-dev-jwts.csproj b/src/Tools/dotnet-user-jwts/src/dotnet-dev-jwts.csproj similarity index 100% rename from src/Tools/dotnet-dev-jwts/src/dotnet-dev-jwts.csproj rename to src/Tools/dotnet-user-jwts/src/dotnet-dev-jwts.csproj From 3ae682ebcd83b8bb498043691d485570a4db7a2d Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 May 2022 11:55:04 -0700 Subject: [PATCH 04/18] Rename project file --- .../src/{dotnet-dev-jwts.csproj => dotnet-user-jwts.csproj} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Tools/dotnet-user-jwts/src/{dotnet-dev-jwts.csproj => dotnet-user-jwts.csproj} (100%) diff --git a/src/Tools/dotnet-user-jwts/src/dotnet-dev-jwts.csproj b/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj similarity index 100% rename from src/Tools/dotnet-user-jwts/src/dotnet-dev-jwts.csproj rename to src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj From 5f5c0403a662d5faa89f1ab916b0b6e5ba3112eb Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 23 May 2022 18:10:22 -0700 Subject: [PATCH 05/18] Write auth config to app settings --- .../src/Commands/CreateCommand.cs | 44 +++++++------ .../src/Commands/PrintCommand.cs | 5 +- .../src/Helpers/DevJwtCliHelpers.cs | 14 +---- .../JwtAuthenticationSchemeSettings.cs | 62 +++++++++++++++++++ .../src/Helpers/JwtCreatorOptions.cs | 15 +++++ .../dotnet-user-jwts/src/Helpers/JwtIssuer.cs | 28 +++------ 6 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs create mode 100644 src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 608a6e3a4611..c6d40acc82cd 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -32,6 +32,12 @@ public static void Register(CommandLineApplication app) "The path of the project to operate on. Defaults to the project in the current directory.", CommandOptionType.SingleValue); + var schemeNameOption = cmd.Option( + "--scheme", + "The scheme name to use for the generated token. Defaults to 'Bearer'", + CommandOptionType.SingleValue + ); + var nameOption = cmd.Option( "--name", "The name of the user to create the JWT for. Defaults to the current environment user.", @@ -83,21 +89,22 @@ public static void Register(CommandLineApplication app) cmd.OnExecute(() => { - var (name, audience, issuer, notBefore, expiresOn, roles, scopes, claims, isValid) = ValidateArguments( - projectOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); + var (options, isValid) = ValidateArguments( + projectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); if (!isValid) { return 1; } - return Execute(projectOption.Value(), name, audience, issuer, scopes, roles, claims, notBefore, expiresOn); + return Execute(projectOption.Value(), options); }); }); } - private static (string, string, string, DateTime, DateTime, List, List, Dictionary, bool) ValidateArguments( + private static (JwtCreatorOptions, bool) ValidateArguments( CommandOption projectOption, + CommandOption schemeNameOption, CommandOption nameOption, CommandOption audienceOption, CommandOption issuerOption, @@ -109,8 +116,10 @@ private static (string, string, string, DateTime, DateTime, List, List, List, List DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime); @@ -169,14 +178,7 @@ static bool ParseDate(string datetime, out DateTime parsedDateTime) => private static int Execute( string projectPath, - string name, - string audience, - string issuer, - List scopes, - List roles, - IDictionary claims, - DateTime notBefore, - DateTime expiresOn) + JwtCreatorOptions options) { var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) @@ -188,18 +190,22 @@ private static int Execute( var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId); - var jwtIssuer = new JwtIssuer(issuer, keyMaterial); - var jwtToken = jwtIssuer.Create(name, audience, notBefore, expiresOn, issuedAt: DateTime.UtcNow, scopes: scopes, roles, claims); + var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial); + var jwtToken = jwtIssuer.Create(options); var jwtStore = new JwtStore(userSecretsId); - var jwt = Jwt.Create(jwtToken, jwtIssuer.WriteToken(jwtToken), scopes, roles, claims); - if (claims is { } customClaims) + var jwt = Jwt.Create(jwtToken, JwtIssuer.WriteToken(jwtToken), options.Scopes, options.Roles, options.Claims); + if (options.Claims is { } customClaims) { jwt.CustomClaims = customClaims; } jwtStore.Jwts.Add(jwtToken.Id, jwt); jwtStore.Save(); + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Name, options.Audience, options.Issuer); + settingsToWrite.Save(appsettingsFilePath); + Console.WriteLine("New JWT saved!"); return 0; diff --git a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs index 15f8ed945c86..ca2e58930516 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs @@ -60,11 +60,10 @@ private static int Execute(string projectPath, string id, bool showFull) Console.WriteLine($"Found JWT with ID '{id}'"); var jwt = jwtStore.Jwts[id]; JwtSecurityToken fullToken; + if (showFull) { - var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId); - var jwtIssuer = new JwtIssuer(DevJwtsDefaults.Issuer, keyMaterial); - fullToken = jwtIssuer.Extract(jwt.Token); + fullToken = JwtIssuer.Extract(jwt.Token); DevJwtCliHelpers.PrintJwt(jwt, fullToken); } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index bf992d4f4d2c..c92560e73d0c 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -71,11 +71,7 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = } else { -<<<<<<< HEAD - secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "microsoft", "usersecrets", userSecretsId, "secret.json"); -======= secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".microsoft", "usersecrets", userSecretsId, "secrets.json"); ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) } IDictionary secrets = null; @@ -148,11 +144,7 @@ public static string GetApplicationUrl(string project) public static void PrintJwt(Jwt jwt, JwtSecurityToken fullToken = null) { var table = new ConsoleTable(); -<<<<<<< HEAD - table.AddColumns("Name", "Id", "Audience", "Expires", "Issued", "Scopes", "Roles", "Custom Claims"); -======= table.AddColumns("Id", "Name", "Audience", "Expires", "Issued", "Scopes", "Roles", "Custom Claims"); ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) if (fullToken is not null) { table.AddColumns("Token Header", "Token Payload"); @@ -166,9 +158,9 @@ public static void PrintJwt(Jwt jwt, JwtSecurityToken fullToken = null) jwt.Audience, jwt.Expires.ToString("O"), jwt.Issued.ToString("O"), - jwt.Scopes is not null ? string.Join(", ", jwt.Scopes) : "[none]", - jwt.Roles is not null ? string.Join(", ", jwt.Roles) : "[none]", - jwt.CustomClaims?.Count > 0 ? jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}") : "[none]", + jwt.Scopes.Any() ? string.Join(", ", jwt.Scopes) : "[none]", + jwt.Roles.Any() ? string.Join(", ", jwt.Roles) : "[none]", + jwt.CustomClaims?.Count > 0 ? string.Join(", ", jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}")) : "[none]", fullToken.Header.SerializeToJson(), fullToken.Payload.SerializeToJson() ); diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs new file mode 100644 index 000000000000..468671236917 --- /dev/null +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal record JwtAuthenticationSchemeSettings(string SchemeName, string UserName, string Audience, string ClaimsIssuer) +{ + private const string AuthenticationKey = "Authentication"; + private const string SchemesKey = "Schemes"; + + private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + }; + + public void Save(string filePath) + { + using var reader = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var config = JsonSerializer.Deserialize(reader, _jsonSerializerOptions); + reader.Close(); + + var settingsObject = new JsonObject + { + [nameof(UserName)] = UserName, + [nameof(Audience)] = Audience, + [nameof(ClaimsIssuer)] = ClaimsIssuer, + }; + + if (config[AuthenticationKey] is JsonObject authentication) + { + if (authentication[SchemesKey] is JsonObject schemes) + { + // If a scheme with the same name has already been registered, we + // override with the latest token's options + schemes[SchemeName] = settingsObject; + } + else + { + authentication.Add(SchemesKey, new JsonObject + { + [SchemeName] = settingsObject + }); + } + } + else + { + config[AuthenticationKey] = new JsonObject + { + [AuthenticationKey] = new JsonObject + { + [SchemeName] = settingsObject + } + }; + } + + using var writer = new FileStream(filePath, FileMode.Open, FileAccess.Write); + JsonSerializer.Serialize(writer, config, _jsonSerializerOptions); + } +} diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs new file mode 100644 index 000000000000..61a6ddf6ad00 --- /dev/null +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal record JwtCreatorOptions( + string Scheme, + string Name, + string Audience, + string Issuer, + DateTime NotBefore, + DateTime ExpiresOn, + List Roles, + List Scopes, + Dictionary Claims); diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs index 9fa880cf20d6..9fc11cd46493 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -public class JwtIssuer +internal class JwtIssuer { private readonly SymmetricSecurityKey _signingKey; @@ -22,51 +22,43 @@ public JwtIssuer(string issuer, byte[] signingKeyMaterial) public string Issuer { get; } - public JwtSecurityToken Create( - string name, - string audience, - DateTime notBefore, - DateTime expires, - DateTime issuedAt, - IEnumerable scopes = null, - IEnumerable roles = null, - IDictionary claims = null) + public JwtSecurityToken Create(JwtCreatorOptions options) { - var identity = new GenericIdentity(name); + var identity = new GenericIdentity(options.Name); - identity.AddClaim(new Claim(JwtRegisteredClaimNames.Sub, name)); + identity.AddClaim(new Claim(JwtRegisteredClaimNames.Sub, options.Name)); var id = Guid.NewGuid().ToString().GetHashCode().ToString("x", CultureInfo.InvariantCulture); identity.AddClaim(new Claim(JwtRegisteredClaimNames.Jti, id)); - if (scopes is { } scopesToAdd) + if (options.Scopes is { } scopesToAdd) { identity.AddClaims(scopesToAdd.Select(s => new Claim("scope", s))); } - if (roles is { } rolesToAdd) + if (options.Roles is { } rolesToAdd) { identity.AddClaims(rolesToAdd.Select(r => new Claim(ClaimTypes.Role, r))); } - if (claims is { Count: > 0 } claimsToAdd) + if (options.Claims is { Count: > 0 } claimsToAdd) { identity.AddClaims(claimsToAdd.Select(kvp => new Claim(kvp.Key, kvp.Value))); } var handler = new JwtSecurityTokenHandler(); var jwtSigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256Signature); - var jwtToken = handler.CreateJwtSecurityToken(Issuer, audience, identity, notBefore, expires, issuedAt, jwtSigningCredentials); + var jwtToken = handler.CreateJwtSecurityToken(Issuer, options.Audience, identity, options.NotBefore, options.ExpiresOn, issuedAt: DateTime.UtcNow, jwtSigningCredentials); return jwtToken; } - public string WriteToken(JwtSecurityToken token) + public static string WriteToken(JwtSecurityToken token) { var handler = new JwtSecurityTokenHandler(); return handler.WriteToken(token); } - public JwtSecurityToken Extract(string token) => new JwtSecurityToken(token); + public static JwtSecurityToken Extract(string token) => new JwtSecurityToken(token); public bool IsValid(string encodedToken) { From 2675c503d6e0d82d69649c6ed4cfaf6387c929a8 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 24 May 2022 12:50:26 -0700 Subject: [PATCH 06/18] Address more feedback --- AspNetCore.sln | 6 ++- .../CommandLine/ConsoleTable.cs | 15 ++---- .../src/Commands/ClearCommand.cs | 15 ++---- .../src/Commands/CreateCommand.cs | 17 ++----- .../src/Commands/DeleteCommand.cs | 23 ++++----- .../src/Commands/KeyCommand.cs | 39 ++++++++------- .../src/Commands/ListCommand.cs | 19 ++------ .../src/Commands/PrintCommand.cs | 23 ++++----- .../Commands/ProjectCommandLineApplication.cs | 22 +++++++++ .../src/Helpers/DevJwtCliHelpers.cs | 48 ++----------------- .../dotnet-user-jwts/src/Helpers/JwtStore.cs | 20 ++------ src/Tools/dotnet-user-jwts/src/Program.cs | 29 +++++------ 12 files changed, 99 insertions(+), 177 deletions(-) create mode 100644 src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 8ae40d269f9e..26a5a2ab1f25 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1712,6 +1712,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimitin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -11112,8 +11114,8 @@ Global {51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F} {487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088} {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} - {B34CB502-0286-4939-B25F-45998528A802} = {DBADF9EC-1571-42CA-9AC4-3D02F90D5BE8} + {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730} + {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs index 5033f6adb026..a6f85c736be6 100644 --- a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs +++ b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs @@ -10,11 +10,7 @@ namespace Microsoft.Extensions.CommandLineUtils; -<<<<<<< HEAD -internal class ConsoleTable -======= internal sealed class ConsoleTable ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { private readonly List _columns = new(); private readonly List _rows = new(); @@ -24,7 +20,7 @@ public void AddColumns(params string[] names) _columns.AddRange(names); } - public void AddRows(params object[] values) + public void AddRow(params object[] values) { if (values == null) { @@ -51,7 +47,7 @@ public void Write() var maxColumnLengths = _columns .Select((t, i) => _rows.Select(x => x[i]) - .Union(new[] { _columns[i] }) + .Concat(new[] { _columns[i] }) .Where(x => x != null) .Select(x => x!.ToString()!.Length).Max()) .ToList(); @@ -61,13 +57,8 @@ public void Write() .Aggregate((previousRowColumn, nextRowColumn) => previousRowColumn + nextRowColumn) + " |"; var formattedRows = _rows.Select(row => string.Format(CultureInfo.InvariantCulture, formatRow, row)).ToList(); - - var maxRowLength = Math.Max(0, _rows.Any() ? _rows.Max(row => formattedRows.Count) : 0); var columnHeaders = string.Format(CultureInfo.InvariantCulture, formatRow, _columns.ToArray()); - - var maxTotalLength = Math.Max(maxRowLength, columnHeaders.Length); - - var rowDivider = $" {string.Join("", Enumerable.Repeat("-", maxTotalLength - 1))} "; + var rowDivider = $" {new string('-', columnHeaders.Length - 1)} "; builder.AppendLine(rowDivider); builder.AppendLine(columnHeaders); diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index c17644ae23e3..6783983ffe5b 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -5,23 +5,14 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -<<<<<<< HEAD -internal class ClearCommand -======= internal sealed class ClearCommand ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { - public static void Register(CommandLineApplication app) + public static void Register(ProjectCommandLineApplication app) { app.Command("clear", cmd => { cmd.Description = "Delete all issued JWTs for a project"; - var projectOption = cmd.Option( - "--project", - "The path of the project to operate on. Defaults to the project in the current directory.", - CommandOptionType.SingleValue); - var forceOption = cmd.Option( "--force", "Don't prompt for confirmation before deleting JWTs", @@ -31,7 +22,7 @@ public static void Register(CommandLineApplication app) cmd.OnExecute(() => { - return Execute(projectOption.Value(), forceOption.HasValue()); + return Execute(app.ProjectOption.Value(), forceOption.HasValue()); }); }); } @@ -41,7 +32,7 @@ private static int Execute(string projectPath, bool force) var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at `--project` path or current directory."); + Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index c6d40acc82cd..64510d509cc7 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -6,11 +6,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -<<<<<<< HEAD -internal class CreateCommand -======= internal sealed class CreateCommand ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { private static readonly string[] _dateTimeFormats = new[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss" }; @@ -21,17 +17,12 @@ internal sealed class CreateCommand @"s\s" }; - public static void Register(CommandLineApplication app) + public static void Register(ProjectCommandLineApplication app) { app.Command("create", cmd => { cmd.Description = "Issue a new JSON Web Token"; - var projectOption = cmd.Option( - "--project", - "The path of the project to operate on. Defaults to the project in the current directory.", - CommandOptionType.SingleValue); - var schemeNameOption = cmd.Option( "--scheme", "The scheme name to use for the generated token. Defaults to 'Bearer'", @@ -90,14 +81,14 @@ public static void Register(CommandLineApplication app) cmd.OnExecute(() => { var (options, isValid) = ValidateArguments( - projectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); + app.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); if (!isValid) { return 1; } - return Execute(projectOption.Value(), options); + return Execute(app.ProjectOption.Value(), options); }); }); } @@ -183,7 +174,7 @@ private static int Execute( var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at `--project` path or current directory."); + Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs index f8eadf62ac6d..d26323772020 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs @@ -5,30 +5,25 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -<<<<<<< HEAD -internal class DeleteCommand -======= internal sealed class DeleteCommand ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { - public static void Register(CommandLineApplication app) + public static void Register(ProjectCommandLineApplication app) { app.Command("delete", cmd => { cmd.Description = "Delete a given JWT"; - var idArgument = cmd.Argument("id", "The ID of the JWT to delete"); - - var projectOption = cmd.Option( - "--project", - "The path of the project to operate on. Defaults to the project in the current directory.", - CommandOptionType.SingleValue); - + var idArgument = cmd.Argument("[id]", "The ID of the JWT to delete"); cmd.HelpOption("-h|--help"); cmd.OnExecute(() => { - return Execute(projectOption.Value(), idArgument.Value); + if (idArgument.Value is null) + { + cmd.ShowHelp(); + return 0; + } + return Execute(app.ProjectOption.Value(), idArgument.Value); }); }); } @@ -38,7 +33,7 @@ private static int Execute(string projectPath, string id) var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at `--project` path or current directory."); + Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs index ed3cc4734661..ccd0ce26da60 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs @@ -6,43 +6,39 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -<<<<<<< HEAD -internal class KeyCommand -======= internal sealed class KeyCommand ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { - public static void Register(CommandLineApplication app) + public static void Register(ProjectCommandLineApplication app) { app.Command("key", cmd => { cmd.Description = "Display or reset the signing key used to issue JWTs"; - var projectOption = cmd.Option( - "--project", - "The path of the project to operate on. Defaults to the project in the current directory.", - CommandOptionType.SingleValue); - var resetOption = cmd.Option( "--reset", "Reset the signing key. This will invalidate all previously issued JWTs for this project.", CommandOptionType.NoValue); + var forceOption = cmd.Option( + "--force", + "Don't prompt for confirmation before resetting the signing key.", + CommandOptionType.NoValue); + cmd.HelpOption("-h|--help"); cmd.OnExecute(() => { - return Execute(projectOption.Value(), resetOption.HasValue()); + return Execute(app.ProjectOption.Value(), resetOption.HasValue(), forceOption.HasValue()); }); }); } - private static int Execute(string projectPath, bool reset) + private static int Execute(string projectPath, bool reset, bool force) { var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at `--project` path or current directory."); + Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } @@ -50,16 +46,19 @@ private static int Execute(string projectPath, bool reset) if (reset == true) { - Console.WriteLine("Are you sure you want to reset the JWT signing key? This will invalidate all JWTs previously issued for this project.\n [Y]es / [N]o"); - if (Console.ReadKey().Key == ConsoleKey.Y) + if (!force) { - var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, reset: true); - Console.WriteLine($"New signing key created: {Convert.ToBase64String(key)}"); - return 0; + Console.WriteLine("Are you sure you want to reset the JWT signing key? This will invalidate all JWTs previously issued for this project.\n [Y]es / [N]o"); + if (Console.ReadLine().Trim().ToUpperInvariant() != "Y") + { + Console.WriteLine("Key reset canceled."); + return 0; + } } - Console.WriteLine("Key reset canceled."); - return 0; + var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, reset: true); + Console.WriteLine($"New signing key created: {Convert.ToBase64String(key)}"); + return 0; } var projectConfiguration = new ConfigurationBuilder() diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs index eb90da18becf..7a6703d522ff 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs @@ -5,23 +5,14 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -<<<<<<< HEAD -internal class ListCommand -======= internal sealed class ListCommand ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { - public static void Register(CommandLineApplication app) + public static void Register(ProjectCommandLineApplication app) { app.Command("list", cmd => { cmd.Description = "Lists the JWTs issued for the project"; - var projectOption = cmd.Option( - "--project", - "The path of the project to operate on. Defaults to the project in the current directory", - CommandOptionType.SingleValue); - var showTokensOption = cmd.Option( "--show-tokens", "Indicates whether JWT base64 strings should be shown", @@ -31,7 +22,7 @@ public static void Register(CommandLineApplication app) cmd.OnExecute(() => { - return Execute(projectOption.Value(), showTokensOption.HasValue()); + return Execute(app.ProjectOption.Value(), showTokensOption.HasValue()); }); }); } @@ -41,7 +32,7 @@ private static int Execute(string projectPath, bool showTokens) var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at `--project` path or current directory."); + Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); @@ -70,11 +61,11 @@ private static int Execute(string projectPath, bool showTokens) var jwt = jwtRow.Value; if (showTokens) { - table.AddRows(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"), jwt.Token); + table.AddRow(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"), jwt.Token); } else { - table.AddRows(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O")); + table.AddRow(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O")); } } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs index ca2e58930516..3cff02f5e208 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs @@ -5,25 +5,15 @@ using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; - -<<<<<<< HEAD -internal class PrintCommand -======= internal sealed class PrintCommand ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) { - public static void Register(CommandLineApplication app) + public static void Register(ProjectCommandLineApplication app) { app.Command("print", cmd => { cmd.Description = "Print the details of a given JWT"; - var idArgument = cmd.Argument("id", "The ID of the JWT to print"); - - var projectOption = cmd.Option( - "--project", - "The path of the project to operate on. Defaults to the project in the current directory.", - CommandOptionType.SingleValue); + var idArgument = cmd.Argument("[id]", "The ID of the JWT to print"); var showFullOption = cmd.Option( "--show-full", @@ -34,7 +24,12 @@ public static void Register(CommandLineApplication app) cmd.OnExecute(() => { - return Execute(projectOption.Value(), idArgument.Value, showFullOption.HasValue()); + if (idArgument.Value is null) + { + cmd.ShowHelp(); + return 0; + } + return Execute(app.ProjectOption.Value(), idArgument.Value, showFullOption.HasValue()); }); }); } @@ -44,7 +39,7 @@ private static int Execute(string projectPath, string id, bool showFull) var project = DevJwtCliHelpers.GetProject(projectPath); if (project == null) { - Console.WriteLine($"No project found at `--project` path or current directory."); + Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs new file mode 100644 index 000000000000..860f5563655b --- /dev/null +++ b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +internal class ProjectCommandLineApplication : CommandLineApplication +{ + public CommandOption ProjectOption { get; private set; } + + public ProjectCommandLineApplication(bool throwOnUnexpectedArg = true, bool continueAfterUnexpectedArg = false, bool treatUnmatchedOptionsAsArguments = false) + : base(throwOnUnexpectedArg, continueAfterUnexpectedArg, treatUnmatchedOptionsAsArguments) + { + ProjectOption = Option( + "-p|--project", + "The path of the project to operate on. Defaults to the project in the current directory", + CommandOptionType.SingleValue); + + Options.Add(ProjectOption); + } +} diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index c92560e73d0c..12a3d0ae1e20 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -9,6 +9,7 @@ using System.Xml.XPath; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.UserSecrets; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -63,16 +64,7 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = { // Create signing material and save to user secrets var newKeyMaterial = System.Security.Cryptography.RandomNumberGenerator.GetBytes(DevJwtsDefaults.SigningKeyLength); - - string secretsFilePath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "UserSecrets", userSecretsId, "secrets.json"); - } - else - { - secretsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".microsoft", "usersecrets", userSecretsId, "secrets.json"); - } + var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); IDictionary secrets = null; if (File.Exists(secretsFilePath)) @@ -143,43 +135,13 @@ public static string GetApplicationUrl(string project) public static void PrintJwt(Jwt jwt, JwtSecurityToken fullToken = null) { - var table = new ConsoleTable(); - table.AddColumns("Id", "Name", "Audience", "Expires", "Issued", "Scopes", "Roles", "Custom Claims"); - if (fullToken is not null) - { - table.AddColumns("Token Header", "Token Payload"); - } + Console.WriteLine(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true })); if (fullToken is not null) { - table.AddRows( - jwt.Id, - jwt.Name, - jwt.Audience, - jwt.Expires.ToString("O"), - jwt.Issued.ToString("O"), - jwt.Scopes.Any() ? string.Join(", ", jwt.Scopes) : "[none]", - jwt.Roles.Any() ? string.Join(", ", jwt.Roles) : "[none]", - jwt.CustomClaims?.Count > 0 ? string.Join(", ", jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}")) : "[none]", - fullToken.Header.SerializeToJson(), - fullToken.Payload.SerializeToJson() - ); + Console.WriteLine($"Token Header: {fullToken.Header.SerializeToJson()}"); + Console.WriteLine($"Token Payload: {fullToken.Payload.SerializeToJson()}"); } - else - { - table.AddRows( - jwt.Id, - jwt.Name, - jwt.Audience, - jwt.Expires.ToString("O"), - jwt.Issued.ToString("O"), - jwt.Scopes is not null ? string.Join(", ", jwt.Scopes) : "[none]", - jwt.Roles is not null ? string.Join(", ", jwt.Roles) : "[none]", - jwt.CustomClaims?.Count > 0 ? jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}") : "[none]" - ); - } - - table.Write(); Console.WriteLine($"Compact Token: {jwt.Token}"); } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs index f039545575cb..39a55bb77754 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; using System.Text.Json; +using Microsoft.Extensions.Configuration.UserSecrets; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -11,23 +11,11 @@ public class JwtStore private const string FileName = "dev-jwts.json"; private readonly string _userSecretsId; private readonly string _filePath; - private readonly JsonSerializerOptions _jsonSerializerOptions = JsonSerializerOptions.Default; public JwtStore(string userSecretsId) { _userSecretsId = userSecretsId; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "UserSecrets", _userSecretsId, FileName); - } - else - { -<<<<<<< HEAD - _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "microsoft", "usersecrets", _userSecretsId, FileName); -======= - _filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".microsoft", "usersecrets", _userSecretsId, FileName); ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) - } + _filePath = Path.Combine(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(userSecretsId)), FileName); Load(); } @@ -40,7 +28,7 @@ public void Load() using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read); if (fileStream.Length > 0) { - Jwts = JsonSerializer.Deserialize>(fileStream, _jsonSerializerOptions) ?? new Dictionary(); + Jwts = JsonSerializer.Deserialize>(fileStream) ?? new Dictionary(); } } } @@ -50,7 +38,7 @@ public void Save() if (Jwts is not null) { using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write); - JsonSerializer.Serialize(fileStream, Jwts, _jsonSerializerOptions); + JsonSerializer.Serialize(fileStream, Jwts); } } } diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs index 7efc9f4c5df3..23257226b3c0 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -4,32 +4,27 @@ using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; using Microsoft.Extensions.CommandLineUtils; -<<<<<<< HEAD -======= -#pragma warning disable CA1852 // Seal internal types ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) -CommandLineApplication devJwts = new() +ProjectCommandLineApplication userJwts = new() { Name = "dotnet user-jwts" }; -devJwts.HelpOption("-h|--help"); +userJwts.HelpOption("-h|--help"); // dotnet user-jwts list -ListCommand.Register(devJwts); +ListCommand.Register(userJwts); // dotnet user-jwts create -CreateCommand.Register(devJwts); +CreateCommand.Register(userJwts); // dotnet user-jwts print ecd045 -PrintCommand.Register(devJwts); +PrintCommand.Register(userJwts); // dotnet user-jwts delete ecd045 -DeleteCommand.Register(devJwts); +DeleteCommand.Register(userJwts); // dotnet user-jwts clear -ClearCommand.Register(devJwts); +ClearCommand.Register(userJwts); // dotnet user-jwts key -KeyCommand.Register(devJwts); +KeyCommand.Register(userJwts); -devJwts.Execute(args); -<<<<<<< HEAD -======= -#pragma warning restore CA1852 // Seal internal types ->>>>>>> aed8a228a7 (Add dotnet dev-jwts tool) +// Show help information if no subcommand/option was specified. +userJwts.OnExecute(() => userJwts.ShowHelp()); + +userJwts.Execute(args); From c37a2a61708cd15e566edd17d37aaf44fcde990e Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 24 May 2022 13:31:38 -0700 Subject: [PATCH 07/18] :seal: --- .../src/Commands/ProjectCommandLineApplication.cs | 2 +- .../src/Helpers/JwtAuthenticationSchemeSettings.cs | 2 +- src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs | 2 +- src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs | 2 +- src/Tools/dotnet-user-jwts/src/Program.cs | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs index 860f5563655b..635b611fe9e0 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -internal class ProjectCommandLineApplication : CommandLineApplication +internal sealed class ProjectCommandLineApplication : CommandLineApplication { public CommandOption ProjectOption { get; private set; } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs index 468671236917..191669a5316d 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -internal record JwtAuthenticationSchemeSettings(string SchemeName, string UserName, string Audience, string ClaimsIssuer) +internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, string UserName, string Audience, string ClaimsIssuer) { private const string AuthenticationKey = "Authentication"; private const string SchemesKey = "Schemes"; diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs index 61a6ddf6ad00..053f6db6a80b 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -internal record JwtCreatorOptions( +internal sealed record JwtCreatorOptions( string Scheme, string Name, string Audience, diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs index 9fc11cd46493..220a41a0066b 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -internal class JwtIssuer +internal sealed class JwtIssuer { private readonly SymmetricSecurityKey _signingKey; diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs index 23257226b3c0..cc26e5c45da4 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; using Microsoft.Extensions.CommandLineUtils; +#pragma warning disable CA1852 // Seal internal types ProjectCommandLineApplication userJwts = new() { Name = "dotnet user-jwts" @@ -28,3 +29,4 @@ userJwts.OnExecute(() => userJwts.ShowHelp()); userJwts.Execute(args); +#pragma warning restore CA1852 // Seal internal types \ No newline at end of file From 212b42f9e10732efce5d5922ca0e1f61eeee2343 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 24 May 2022 14:35:54 -0700 Subject: [PATCH 08/18] Apply suggestions from code review Co-authored-by: Brennan --- src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs | 8 ++++---- src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index 6783983ffe5b..c05c5ad71759 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -43,16 +43,16 @@ private static int Execute(string projectPath, bool force) if (count == 0) { - Console.WriteLine($"There are no JWTs to delete from {project}"); + Console.WriteLine($"There are no JWTs to delete from {project}."); return 0; } if (!force) { - Console.WriteLine($"Are you sure you want to delete {count} JWT(s) for {project}? \n [Y]es / [N]o"); + Console.WriteLine($"Are you sure you want to delete {count} JWT(s) for {project}?{Environment.NewLine} [Y]es / [N]o"); if (Console.ReadKey().Key != ConsoleKey.Y) { - Console.WriteLine("Cancelled, no JWTs were deleted"); + Console.WriteLine("Canceled, no JWTs were deleted."); return 0; } } @@ -60,7 +60,7 @@ private static int Execute(string projectPath, bool force) jwtStore.Jwts.Clear(); jwtStore.Save(); - Console.WriteLine($"Deleted {count} token(s) from {project} successfully"); + Console.WriteLine($"Deleted {count} token(s) from {project} successfully."); return 0; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 64510d509cc7..ad9188d0e15c 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -134,7 +134,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!ParseDate(expiresOnOption.Value(), out expiresOn)) { - Console.WriteLine(@"The date provided for -expires-on could not be parsed. Ensure you use the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'."); + Console.WriteLine(@"The date provided for --expires-on could not be parsed. Ensure you use the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'."); isValid = false; } } @@ -143,7 +143,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!TimeSpan.TryParseExact(validForOption.Value(), _timeSpanFormats, CultureInfo.InvariantCulture, out var validForValue)) { - Console.WriteLine("The period provided for --valid-for could not be parsed. Ensure you use a format like '10d', '24h', etc."); + Console.WriteLine("The period provided for --valid-for could not be parsed. Ensure you use a format like '10d', '22h', '45s' etc."); } expiresOn = notBefore.Add(validForValue); } From ef45270ff914db1c0990bc12d3562c046fdb9950 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 24 May 2022 21:53:42 -0700 Subject: [PATCH 09/18] Address more feedback --- .../src/Commands/ClearCommand.cs | 2 +- .../src/Commands/CreateCommand.cs | 17 +++++++++-------- .../src/Helpers/DevJwtCliHelpers.cs | 15 +++++---------- .../Helpers/JwtAuthenticationSchemeSettings.cs | 8 ++++---- .../src/Helpers/JwtCreatorOptions.cs | 2 +- .../dotnet-user-jwts/src/Helpers/JwtIssuer.cs | 11 ++++++++++- .../dotnet-user-jwts/src/Helpers/JwtStore.cs | 2 +- 7 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index c05c5ad71759..72a94bfeee55 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -50,7 +50,7 @@ private static int Execute(string projectPath, bool force) if (!force) { Console.WriteLine($"Are you sure you want to delete {count} JWT(s) for {project}?{Environment.NewLine} [Y]es / [N]o"); - if (Console.ReadKey().Key != ConsoleKey.Y) + if (Console.ReadLine().Trim().ToUpperInvariant() != "Y") { Console.WriteLine("Canceled, no JWTs were deleted."); return 0; diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index ad9188d0e15c..7167b71857f4 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Linq; using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal sealed class CreateCommand { private static readonly string[] _dateTimeFormats = new[] { - "yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss" }; + "yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy/MM/dd", "yyyy/MM/dd HH:mm" }; private static readonly string[] _timeSpanFormats = new[] { @"d\dh\hm\ms\s", @"d\dh\hm\m", @"d\dh\h", @"d\d", @"h\hm\ms\s", @"h\hm\m", @"h\h", @@ -36,8 +37,8 @@ public static void Register(ProjectCommandLineApplication app) var audienceOption = cmd.Option( "--audience", - "The audience to create the JWT for. Defaults to the first HTTPS URL configured in the project's launchSettings.json", - CommandOptionType.SingleValue); + "The audiences to create the JWT for. Defaults to the URLs configured in the project's launchSettings.json", + CommandOptionType.MultipleValue); var issuerOption = cmd.Option( "--issuer", @@ -111,7 +112,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( var scheme = schemeNameOption.HasValue() ? schemeNameOption.Value() : "Bearer"; var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName; - var audience = audienceOption.HasValue() ? audienceOption.Value() : DevJwtCliHelpers.GetApplicationUrl(project); + var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project).ToList(); if (audience is null) { Console.WriteLine("Could not determine the project's HTTPS URL. Please specify an audience for the JWT using the --audience option."); @@ -124,17 +125,17 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!ParseDate(notBeforeOption.Value(), out notBefore)) { - Console.WriteLine(@"The date provided for --not-before could not be parsed. Ensure you use the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'."); + Console.WriteLine(@"The date provided for --not-before could not be parsed. Dates must consist of a date and can include an optional timestamp."); isValid = false; } } - var expiresOn = notBefore.AddMonths(6); + var expiresOn = notBefore.AddMonths(3); if (expiresOnOption.HasValue()) { if (!ParseDate(expiresOnOption.Value(), out expiresOn)) { - Console.WriteLine(@"The date provided for --expires-on could not be parsed. Ensure you use the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'."); + Console.WriteLine(@"The date provided for --expires-on could not be parsed. Dates must consist of a date and can include an optional timestamp."); isValid = false; } } @@ -194,7 +195,7 @@ private static int Execute( jwtStore.Save(); var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); - var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Name, options.Audience, options.Issuer); + var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Audiences, options.Issuer); settingsToWrite.Save(appsettingsFilePath); Console.WriteLine("New JWT saved!"); diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 12a3d0ae1e20..04532f22c4e3 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -3,11 +3,9 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; -using System.Runtime.InteropServices; using System.Text.Json; using System.Xml.Linq; using System.Xml.XPath; -using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.UserSecrets; @@ -35,7 +33,9 @@ public static string GetProject(string projectPath = null) return projectPath; } - var csprojFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj"); + var csprojFiles = Directory.EnumerateFileSystemEntries(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly) + .Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase)) + .ToList(); if (csprojFiles is [var path]) { return path; @@ -90,7 +90,7 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = return newKeyMaterial; } - public static string GetApplicationUrl(string project) + public static string[] GetAudienceCandidatesFromLaunchSettings(string project) { ArgumentException.ThrowIfNullOrEmpty(nameof(project)); @@ -115,12 +115,7 @@ public static string GetApplicationUrl(string project) var value = applicationUrl.GetString(); if (value is { } applicationUrls) { - var urls = applicationUrls.Split(";"); - var firstHttpsUrl = urls.FirstOrDefault(u => u.StartsWith("https:", StringComparison.OrdinalIgnoreCase)); - if (firstHttpsUrl is { } result) - { - return result; - } + return applicationUrls.Split(";"); } } } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs index 191669a5316d..775a733fc915 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs @@ -1,12 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, string UserName, string Audience, string ClaimsIssuer) +internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, List Audiences, string ClaimsIssuer) { private const string AuthenticationKey = "Authentication"; private const string SchemesKey = "Schemes"; @@ -24,9 +25,8 @@ public void Save(string filePath) var settingsObject = new JsonObject { - [nameof(UserName)] = UserName, - [nameof(Audience)] = Audience, - [nameof(ClaimsIssuer)] = ClaimsIssuer, + [nameof(Audiences)] = new JsonArray(Audiences.Select(aud => JsonValue.Create(aud)).ToArray()), + [nameof(ClaimsIssuer)] = ClaimsIssuer }; if (config[AuthenticationKey] is JsonObject authentication) diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs index 053f6db6a80b..589f3d5d07f6 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal sealed record JwtCreatorOptions( string Scheme, string Name, - string Audience, + List Audiences, string Issuer, DateTime NotBefore, DateTime ExpiresOn, diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs index 220a41a0066b..c3748935dd09 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs @@ -46,9 +46,18 @@ public JwtSecurityToken Create(JwtCreatorOptions options) identity.AddClaims(claimsToAdd.Select(kvp => new Claim(kvp.Key, kvp.Value))); } + // Although the JwtPayload supports having multiple audiences registered, the + // creator methods and constructors don't provide a way of setting multiple + // audiences. Instead, we have to register an `aud` claim for each audience + // we want to add so that the multiple audiences are populated correctly. + if (options.Audiences is { Count: > 0} audiences) + { + identity.AddClaims(audiences.Select(aud => new Claim(JwtRegisteredClaimNames.Aud, aud))); + } + var handler = new JwtSecurityTokenHandler(); var jwtSigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256Signature); - var jwtToken = handler.CreateJwtSecurityToken(Issuer, options.Audience, identity, options.NotBefore, options.ExpiresOn, issuedAt: DateTime.UtcNow, jwtSigningCredentials); + var jwtToken = handler.CreateJwtSecurityToken(Issuer, audience: null, identity, options.NotBefore, options.ExpiresOn, issuedAt: DateTime.UtcNow, jwtSigningCredentials); return jwtToken; } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs index 39a55bb77754..8bffc9d9c2ce 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; public class JwtStore { - private const string FileName = "dev-jwts.json"; + private const string FileName = "user-jwts.json"; private readonly string _userSecretsId; private readonly string _filePath; From 751c1d70cd89998b66369d7eb3149e6f229e41fe Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 25 May 2022 16:25:33 -0700 Subject: [PATCH 10/18] Add framework support for authentication changes --- AspNetCore.sln | 19 ++++++ .../src/Microsoft.AspNetCore.csproj | 2 + .../src/PublicAPI.Unshipped.txt | 1 + .../WebApplicationAuthenticationBuilder.cs | 45 ++++++++++++ .../src/WebApplicationBuilder.cs | 13 ++++ .../IAuthenticationConfigurationProvider.cs | 25 +++++++ .../src/PublicAPI.Unshipped.txt | 3 + ...thenticationServiceCollectionExtensions.cs | 1 + ...aultAuthenticationConfigurationProvider.cs | 25 +++++++ .../MinimalJwtBearerSample.csproj | 17 +++++ .../samples/MinimalJwtBearerSample/Program.cs | 32 +++++++++ .../Properties/launchSettings.json | 31 +++++++++ .../appsettings.Development.json | 26 +++++++ .../MinimalJwtBearerSample/appsettings.json | 9 +++ .../src/AuthenticationConfigurationOptions.cs | 68 +++++++++++++++++++ .../JwtBearer/src/JwtBearerExtensions.cs | 13 ++++ .../JwtBearer/src/PublicAPI.Unshipped.txt | 1 + .../dotnet-user-jwts/src/Helpers/JwtIssuer.cs | 2 +- 18 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs create mode 100644 src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs create mode 100644 src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs create mode 100644 src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj create mode 100644 src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs create mode 100644 src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json create mode 100644 src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json create mode 100644 src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json create mode 100644 src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 26a5a2ab1f25..694e4f41cdde 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1714,6 +1714,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Too EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10267,6 +10269,22 @@ Global {B34CB502-0286-4939-B25F-45998528A802}.Release|x64.Build.0 = Release|Any CPU {B34CB502-0286-4939-B25F-45998528A802}.Release|x86.ActiveCfg = Release|Any CPU {B34CB502-0286-4939-B25F-45998528A802}.Release|x86.Build.0 = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|arm64.ActiveCfg = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|arm64.Build.0 = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x64.Build.0 = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x86.Build.0 = Debug|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|Any CPU.Build.0 = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|arm64.ActiveCfg = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|arm64.Build.0 = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x64.ActiveCfg = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x64.Build.0 = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x86.ActiveCfg = Release|Any CPU + {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11116,6 +11134,7 @@ Global {1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF} {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730} {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} + {7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj index 2c66a8407db3..b54de7e85b80 100644 --- a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj +++ b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt index 88ff0b5ecc80..6eba7bee0ff8 100644 --- a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt +++ b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable Microsoft.AspNetCore.Builder.WebApplication.Use(System.Func! middleware) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Authentication.get -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! static Microsoft.Extensions.Hosting.GenericHostBuilderExtensions.ConfigureWebHostDefaults(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action! configure, System.Action! configureOptions) -> Microsoft.Extensions.Hosting.IHostBuilder! diff --git a/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs new file mode 100644 index 000000000000..7afd68c7d7af --- /dev/null +++ b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication; + +internal class WebApplicationAuthenticationBuilder : AuthenticationBuilder +{ + public bool IsAuthenticationConfigured { get; private set; } + + public WebApplicationAuthenticationBuilder(IServiceCollection services) : base(services) { } + + public override AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string? displayName, Action configureOptions) + { + RegisterServices(authenticationScheme); + return base.AddPolicyScheme(authenticationScheme, displayName, configureOptions); + } + + public override AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string? displayName, Action? configureOptions) + { + RegisterServices(authenticationScheme); + return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + + public override AuthenticationBuilder AddScheme(string authenticationScheme, string? displayName, Action? configureOptions) + { + RegisterServices(authenticationScheme); + return base.AddScheme(authenticationScheme, displayName, configureOptions); + } + + public override AuthenticationBuilder AddScheme(string authenticationScheme, Action? configureOptions) + { + RegisterServices(authenticationScheme); + return base.AddScheme(authenticationScheme, configureOptions); + } + + private void RegisterServices(string authenticationScheme) + { + IsAuthenticationConfigured = true; + Services.AddAuthentication(authenticationScheme); + Services.AddAuthorization(); + } +} diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index c5eabba6816d..2ce88c53c1c6 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -79,6 +80,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action @@ -113,6 +115,11 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action public ConfigureHostBuilder Host { get; } + /// + /// An for configuration authentication-related properties. + /// + public AuthenticationBuilder Authentication { get; } + /// /// Builds the . /// @@ -166,6 +173,12 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui } } + if (Authentication is WebApplicationAuthenticationBuilder webAuthBuilder && webAuthBuilder.IsAuthenticationConfigured is true) + { + app.UseAuthentication(); + app.UseAuthorization(); + } + // Wire the source pipeline to run in the destination pipeline app.Use(next => { diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs new file mode 100644 index 000000000000..60520835e30c --- /dev/null +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Provides an interface for implmenting a construct that provides +/// access to specific configuration sections. +/// +public interface IAuthenticationConfigurationProvider +{ + /// + /// Gets the root configuration managed by the provider. + /// + IConfigurationRoot Configuration { get; } + + /// + /// Returns the specified object. + /// + /// The path to the section to be returned. + /// The specified object, or null if the requested section does not exist. + IConfiguration GetSection(string name); +} diff --git a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..2337b4a3aa9b 100644 --- a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider +Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider.Configuration.get -> Microsoft.Extensions.Configuration.IConfigurationRoot! +Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider.GetSection(string! name) -> Microsoft.Extensions.Configuration.IConfiguration! diff --git a/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs b/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs index b29001aa37a7..2e34894d99b2 100644 --- a/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs +++ b/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ public static AuthenticationBuilder AddAuthentication(this IServiceCollection se services.AddDataProtection(); services.AddWebEncoders(); services.TryAddSingleton(); + services.TryAddSingleton(); return new AuthenticationBuilder(services); } diff --git a/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs b/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs new file mode 100644 index 000000000000..33adb5dabefb --- /dev/null +++ b/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Authentication; + +internal sealed class DefaultAuthenticationConfigurationProvider : IAuthenticationConfigurationProvider +{ + public IConfigurationRoot Configuration { get; } + + public DefaultAuthenticationConfigurationProvider(IConfiguration configuration) + { + if (configuration is not IConfigurationRoot configurationRoot) + { + throw new ArgumentException("Could not resolve IConfigurationRoot instance."); + } + Configuration = configurationRoot; + } + + public IConfiguration GetSection(string name) + { + return Configuration.GetSection($"Authentication:Schemes:{name}"); + } +} diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj new file mode 100644 index 000000000000..e0ab758b6a83 --- /dev/null +++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj @@ -0,0 +1,17 @@ + + + + $(DefaultNetCoreTargetFramework) + MinimalJwtBearerSample-20151210102827 + enable + enable + + + + + + + + + + diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs new file mode 100644 index 000000000000..ff0c3ecd22cb --- /dev/null +++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; + +var builder = WebApplication.CreateBuilder(args); + +builder.Authentication.AddJwtBearer(); +builder.Authentication.AddJwtBearer("ClaimedDetails"); + +builder.Services.AddAuthorization(options => + options.AddPolicy("is_admin", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("is_admin", "true"); + })); + +var app = builder.Build(); + +app.MapGet("/protected", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}!") + .RequireAuthorization(); + +app.MapGet("/protected-with-claims", (ClaimsPrincipal user) => +{ + return $"Glory be to the admin {user.Identity?.Name}!"; +}) +.RequireAuthorization("is_admin"); + +app.Run(); diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json new file mode 100644 index 000000000000..ea8f54aeb1a4 --- /dev/null +++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56852", + "sslPort": 44385 + } + }, + "profiles": { + "MinimalJwtBearerSample": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "protected", + "applicationUrl": "https://localhost:7259;http://localhost:5259", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "protected", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json new file mode 100644 index 000000000000..9fe2b7a74f48 --- /dev/null +++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Authentication": { + "Schemes": { + "Bearer": { + "Audiences": [ + "https://localhost:7259", + "http://localhost:5259" + ], + "ClaimsIssuer": "dotnet-user-jwts" + }, + "ClaimedDetails": { + "Audiences": [ + "https://localhost:7259", + "http://localhost:5259" + ], + "ClaimsIssuer": "dotnet-user-jwts" + } + } + } +} \ No newline at end of file diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs b/src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs new file mode 100644 index 000000000000..84acb16466f8 --- /dev/null +++ b/src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication; + +internal class AuthenticationConfigurationOptions : IConfigureNamedOptions +{ + private readonly IAuthenticationConfigurationProvider _configurationProvider; + + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance. + public AuthenticationConfigurationOptions(IAuthenticationConfigurationProvider configurationProvider) + { + _configurationProvider = configurationProvider; + } + + /// + public void Configure(string? name, JwtBearerOptions options) + { + if (string.IsNullOrEmpty(name)) + { + return; + } + + var configSection = _configurationProvider.GetSection(name); + + if (configSection is null) + { + return; + } + + var issuer = configSection["ClaimsIssuer"]; + var audiences = configSection.GetSection("Audiences").GetChildren().Select(aud => aud.Value).ToArray(); + options.TokenValidationParameters = new() + { + ValidateIssuer = issuer is not null, + ValidIssuers = new[] { issuer }, + ValidateAudience = audiences.Length > 0, + ValidAudiences = audiences, + ValidateIssuerSigningKey = true, + IssuerSigningKey = GetIssuerSigningKey(_configurationProvider.Configuration, issuer), + }; + } + + private static SecurityKey GetIssuerSigningKey(IConfiguration configuration, string? issuer) + { + var jwtKeyMaterialSecret = configuration[$"{issuer}:KeyMaterial"]; + var jwtKeyMaterial = !string.IsNullOrEmpty(jwtKeyMaterialSecret) + ? Convert.FromBase64String(jwtKeyMaterialSecret) + : System.Security.Cryptography.RandomNumberGenerator.GetBytes(32); + return new SymmetricSecurityKey(jwtKeyMaterial); + } + + /// + public void Configure(JwtBearerOptions options) + { + Configure(Options.DefaultName, options); + } +} diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs index 12022ed0780d..703e0b8e1cb4 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs @@ -24,6 +24,18 @@ public static class JwtBearerExtensions public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder) => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { }); + /// + /// Enables JWT-bearer authentication using a pre-defined scheme. + /// + /// JWT bearer authentication performs authentication by extracting and validating a JWT token from the Authorization request header. + /// + /// + /// The . + /// The authentication scheme. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme) + => builder.AddJwtBearer(authenticationScheme, _ => { }); + /// /// Enables JWT-bearer authentication using the default scheme . /// @@ -62,6 +74,7 @@ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder buil /// A reference to after the operation has completed. public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action configureOptions) { + builder.Services.AddSingleton, AuthenticationConfigurationOptions>(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); return builder.AddScheme(authenticationScheme, displayName, configureOptions); } diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt index 2c729aeee8d2..e66f11d82ec1 100644 --- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string! name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void +static Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, string! authenticationScheme) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs index c3748935dd09..cf086d1d769f 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs @@ -50,7 +50,7 @@ public JwtSecurityToken Create(JwtCreatorOptions options) // creator methods and constructors don't provide a way of setting multiple // audiences. Instead, we have to register an `aud` claim for each audience // we want to add so that the multiple audiences are populated correctly. - if (options.Audiences is { Count: > 0} audiences) + if (options.Audiences is { Count: > 0} audiences) { identity.AddClaims(audiences.Select(aud => new Claim(JwtRegisteredClaimNames.Aud, aud))); } From 656236680928c0a9c33df0b94970652eaddcdd0a Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 27 May 2022 12:36:25 -0700 Subject: [PATCH 11/18] Add tests for user-jwts CLI and react to feedback --- AspNetCore.sln | 19 +++ .../WebApplicationAuthenticationBuilder.cs | 9 +- .../IAuthenticationConfigurationProvider.cs | 9 +- .../src/PublicAPI.Unshipped.txt | 3 +- ...aultAuthenticationConfigurationProvider.cs | 12 +- ...ptions.cs => JwtBearerConfigureOptions.cs} | 20 ++- .../JwtBearer/src/JwtBearerExtensions.cs | 2 +- src/Security/Security.slnf | 5 +- .../CommandLine/ConsoleTable.cs | 9 +- src/Tools/Tools.slnf | 5 +- .../src/Commands/ClearCommand.cs | 26 ++-- .../src/Commands/CreateCommand.cs | 27 ++-- .../src/Commands/DeleteCommand.cs | 18 +-- .../src/Commands/KeyCommand.cs | 21 ++- .../src/Commands/ListCommand.cs | 29 ++-- .../src/Commands/PrintCommand.cs | 17 +- .../Commands/ProjectCommandLineApplication.cs | 14 +- .../src/Helpers/DevJwtCliHelpers.cs | 30 +++- src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs | 5 +- .../JwtAuthenticationSchemeSettings.cs | 20 ++- src/Tools/dotnet-user-jwts/src/Program.cs | 72 +++++---- .../test/UserJwtsTestFixture.cs | 102 ++++++++++++ .../dotnet-user-jwts/test/UserJwtsTests.cs | 146 ++++++++++++++++++ .../test/dotnet-user-jwts.Tests.csproj | 16 ++ 24 files changed, 490 insertions(+), 146 deletions(-) rename src/Security/Authentication/JwtBearer/src/{AuthenticationConfigurationOptions.cs => JwtBearerConfigureOptions.cs} (67%) create mode 100644 src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs create mode 100644 src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs create mode 100644 src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj diff --git a/AspNetCore.sln b/AspNetCore.sln index 694e4f41cdde..02b6e7c51c77 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1716,6 +1716,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet- EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts.Tests", "src\Tools\dotnet-user-jwts\test\dotnet-user-jwts.Tests.csproj", "{89896261-C5DD-4901-BCA7-7A5F718BC008}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10285,6 +10287,22 @@ Global {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x64.Build.0 = Release|Any CPU {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x86.ActiveCfg = Release|Any CPU {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x86.Build.0 = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|arm64.ActiveCfg = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|arm64.Build.0 = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x64.ActiveCfg = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x64.Build.0 = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x86.ActiveCfg = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x86.Build.0 = Debug|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|Any CPU.Build.0 = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|arm64.ActiveCfg = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|arm64.Build.0 = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x64.ActiveCfg = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x64.Build.0 = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.ActiveCfg = Release|Any CPU + {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11135,6 +11153,7 @@ Global {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730} {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} {7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67} + {89896261-C5DD-4901-BCA7-7A5F718BC008} = {AB4B9E75-719C-4589-B852-20FBFD727730} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs index 7afd68c7d7af..f95ca8a05896 100644 --- a/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs @@ -38,8 +38,11 @@ public override AuthenticationBuilder AddPolicyScheme(string authenticationSchem private void RegisterServices(string authenticationScheme) { - IsAuthenticationConfigured = true; - Services.AddAuthentication(authenticationScheme); - Services.AddAuthorization(); + if (!IsAuthenticationConfigured) + { + IsAuthenticationConfigured = true; + Services.AddAuthentication(authenticationScheme); + Services.AddAuthorization(); + } } } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs index 60520835e30c..0665de7acb0b 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs @@ -11,15 +11,10 @@ namespace Microsoft.AspNetCore.Authentication; /// public interface IAuthenticationConfigurationProvider { - /// - /// Gets the root configuration managed by the provider. - /// - IConfigurationRoot Configuration { get; } - /// /// Returns the specified object. /// - /// The path to the section to be returned. + /// The path to the section to be returned. /// The specified object, or null if the requested section does not exist. - IConfiguration GetSection(string name); + IConfiguration GetAuthenticationSchemeConfiguration(string authenticationScheme); } diff --git a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt index 2337b4a3aa9b..efe32d90a931 100644 --- a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider -Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider.Configuration.get -> Microsoft.Extensions.Configuration.IConfigurationRoot! -Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider.GetSection(string! name) -> Microsoft.Extensions.Configuration.IConfiguration! +Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider.GetAuthenticationSchemeConfiguration(string! authenticationScheme) -> Microsoft.Extensions.Configuration.IConfiguration! diff --git a/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs b/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs index 33adb5dabefb..057e1a5ad1d4 100644 --- a/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs +++ b/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs @@ -7,19 +7,15 @@ namespace Microsoft.AspNetCore.Authentication; internal sealed class DefaultAuthenticationConfigurationProvider : IAuthenticationConfigurationProvider { - public IConfigurationRoot Configuration { get; } + private readonly IConfiguration _configuration; public DefaultAuthenticationConfigurationProvider(IConfiguration configuration) { - if (configuration is not IConfigurationRoot configurationRoot) - { - throw new ArgumentException("Could not resolve IConfigurationRoot instance."); - } - Configuration = configurationRoot; + _configuration = configuration; } - public IConfiguration GetSection(string name) + public IConfiguration GetAuthenticationSchemeConfiguration(string authenticationScheme) { - return Configuration.GetSection($"Authentication:Schemes:{name}"); + return _configuration.GetSection($"Authentication:Schemes:{authenticationScheme}"); } } diff --git a/src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs similarity index 67% rename from src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs rename to src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs index 84acb16466f8..5cb7ce6dfbd4 100644 --- a/src/Security/Authentication/JwtBearer/src/AuthenticationConfigurationOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Security.Cryptography; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; @@ -9,18 +10,21 @@ namespace Microsoft.AspNetCore.Authentication; -internal class AuthenticationConfigurationOptions : IConfigureNamedOptions +internal sealed class JwtBearerConfigureOptions : IConfigureNamedOptions { - private readonly IAuthenticationConfigurationProvider _configurationProvider; + private readonly IAuthenticationConfigurationProvider _authenticationConfigurationProvider; + private readonly IConfiguration _configuration; /// - /// Initializes a new given the configuration + /// Initializes a new given the configuration /// provided by the . /// /// An instance. - public AuthenticationConfigurationOptions(IAuthenticationConfigurationProvider configurationProvider) + /// An instance for accessing configuration elements not in the schema. + public JwtBearerConfigureOptions(IAuthenticationConfigurationProvider configurationProvider, IConfiguration configuration) { - _configurationProvider = configurationProvider; + _authenticationConfigurationProvider = configurationProvider; + _configuration = configuration; } /// @@ -31,7 +35,7 @@ public void Configure(string? name, JwtBearerOptions options) return; } - var configSection = _configurationProvider.GetSection(name); + var configSection = _authenticationConfigurationProvider.GetAuthenticationSchemeConfiguration(name); if (configSection is null) { @@ -47,7 +51,7 @@ public void Configure(string? name, JwtBearerOptions options) ValidateAudience = audiences.Length > 0, ValidAudiences = audiences, ValidateIssuerSigningKey = true, - IssuerSigningKey = GetIssuerSigningKey(_configurationProvider.Configuration, issuer), + IssuerSigningKey = GetIssuerSigningKey(_configuration, issuer), }; } @@ -56,7 +60,7 @@ private static SecurityKey GetIssuerSigningKey(IConfiguration configuration, str var jwtKeyMaterialSecret = configuration[$"{issuer}:KeyMaterial"]; var jwtKeyMaterial = !string.IsNullOrEmpty(jwtKeyMaterialSecret) ? Convert.FromBase64String(jwtKeyMaterialSecret) - : System.Security.Cryptography.RandomNumberGenerator.GetBytes(32); + : RandomNumberGenerator.GetBytes(32); return new SymmetricSecurityKey(jwtKeyMaterial); } diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs index 703e0b8e1cb4..f4a7ec2e9ae3 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs @@ -74,7 +74,7 @@ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder buil /// A reference to after the operation has completed. public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action configureOptions) { - builder.Services.AddSingleton, AuthenticationConfigurationOptions>(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerConfigureOptions>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); return builder.AddScheme(authenticationScheme, displayName, configureOptions); } diff --git a/src/Security/Security.slnf b/src/Security/Security.slnf index 0dd86cd84fd2..e8b8685e04b7 100644 --- a/src/Security/Security.slnf +++ b/src/Security/Security.slnf @@ -6,6 +6,7 @@ "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj", "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj", "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", + "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", @@ -15,7 +16,6 @@ "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", - "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", @@ -38,6 +38,7 @@ "src\\Security\\Authentication\\Facebook\\src\\Microsoft.AspNetCore.Authentication.Facebook.csproj", "src\\Security\\Authentication\\Google\\src\\Microsoft.AspNetCore.Authentication.Google.csproj", "src\\Security\\Authentication\\JwtBearer\\samples\\JwtBearerSample\\JwtBearerSample.csproj", + "src\\Security\\Authentication\\JwtBearer\\samples\\MinimalJwtBearerSample\\MinimalJwtBearerSample.csproj", "src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj", "src\\Security\\Authentication\\MicrosoftAccount\\src\\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj", "src\\Security\\Authentication\\Negotiate\\samples\\NegotiateAuthSample\\NegotiateAuthSample.csproj", @@ -69,4 +70,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs index a6f85c736be6..b7773b719df4 100644 --- a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs +++ b/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.Extensions.CommandLineUtils; @@ -14,6 +15,12 @@ internal sealed class ConsoleTable { private readonly List _columns = new(); private readonly List _rows = new(); + private readonly IReporter _reporter; + + public ConsoleTable(IReporter reporter) + { + _reporter = reporter; + } public void AddColumns(params string[] names) { @@ -71,6 +78,6 @@ public void Write() builder.AppendLine(rowDivider); - Console.WriteLine(builder.ToString()); + _reporter.Output(builder.ToString()); } } diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 12656af958cc..e277c5a31543 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -97,12 +97,13 @@ "src\\Tools\\Microsoft.dotnet-openapi\\src\\Microsoft.dotnet-openapi.csproj", "src\\Tools\\Microsoft.dotnet-openapi\\test\\dotnet-microsoft.openapi.Tests.csproj", "src\\Tools\\dotnet-dev-certs\\src\\dotnet-dev-certs.csproj", - "src\\Tools\\dotnet-user-jwts\\src\\dotnet-user-jwts.csproj", "src\\Tools\\dotnet-getdocument\\src\\dotnet-getdocument.csproj", "src\\Tools\\dotnet-sql-cache\\src\\dotnet-sql-cache.csproj", + "src\\Tools\\dotnet-user-jwts\\src\\dotnet-user-jwts.csproj", + "src\\Tools\\dotnet-user-jwts\\test\\dotnet-user-jwts.Tests.csproj", "src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj", "src\\Tools\\dotnet-user-secrets\\test\\dotnet-user-secrets.Tests.csproj", "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs index 72a94bfeee55..e0880812fc8e 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -22,45 +23,46 @@ public static void Register(ProjectCommandLineApplication app) cmd.OnExecute(() => { - return Execute(app.ProjectOption.Value(), forceOption.HasValue()); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), forceOption.HasValue()); }); }); } - private static int Execute(string projectPath, bool force) + private static int Execute(IReporter reporter, string projectPath, bool force) { - var project = DevJwtCliHelpers.GetProject(projectPath); - if (project == null) + if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) { - Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } - - var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); var jwtStore = new JwtStore(userSecretsId); - var count = jwtStore.Jwts.Count; if (count == 0) { - Console.WriteLine($"There are no JWTs to delete from {project}."); + reporter.Output($"There are no JWTs to delete from {project}."); return 0; } if (!force) { - Console.WriteLine($"Are you sure you want to delete {count} JWT(s) for {project}?{Environment.NewLine} [Y]es / [N]o"); + reporter.Output($"Are you sure you want to delete {count} JWT(s) for {project}?{Environment.NewLine} [Y]es / [N]o"); if (Console.ReadLine().Trim().ToUpperInvariant() != "Y") { - Console.WriteLine("Canceled, no JWTs were deleted."); + reporter.Output("Canceled, no JWTs were deleted."); return 0; } } + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + foreach (var jwt in jwtStore.Jwts) + { + JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Value.Scheme); + } + jwtStore.Jwts.Clear(); jwtStore.Save(); - Console.WriteLine($"Deleted {count} token(s) from {project} successfully."); + reporter.Output($"Deleted {count} token(s) from {project} successfully."); return 0; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 7167b71857f4..17ac345c5945 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -82,19 +83,20 @@ public static void Register(ProjectCommandLineApplication app) cmd.OnExecute(() => { var (options, isValid) = ValidateArguments( - app.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); + cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption); if (!isValid) { return 1; } - return Execute(app.ProjectOption.Value(), options); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options); }); }); } private static (JwtCreatorOptions, bool) ValidateArguments( + IReporter reporter, CommandOption projectOption, CommandOption schemeNameOption, CommandOption nameOption, @@ -115,7 +117,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project).ToList(); if (audience is null) { - Console.WriteLine("Could not determine the project's HTTPS URL. Please specify an audience for the JWT using the --audience option."); + reporter.Error("Could not determine the project's HTTPS URL. Please specify an audience for the JWT using the --audience option."); isValid = false; } var issuer = issuerOption.HasValue() ? issuerOption.Value() : DevJwtsDefaults.Issuer; @@ -125,7 +127,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!ParseDate(notBeforeOption.Value(), out notBefore)) { - Console.WriteLine(@"The date provided for --not-before could not be parsed. Dates must consist of a date and can include an optional timestamp."); + reporter.Error(@"The date provided for --not-before could not be parsed. Dates must consist of a date and can include an optional timestamp."); isValid = false; } } @@ -135,7 +137,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!ParseDate(expiresOnOption.Value(), out expiresOn)) { - Console.WriteLine(@"The date provided for --expires-on could not be parsed. Dates must consist of a date and can include an optional timestamp."); + reporter.Error(@"The date provided for --expires-on could not be parsed. Dates must consist of a date and can include an optional timestamp."); isValid = false; } } @@ -144,7 +146,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!TimeSpan.TryParseExact(validForOption.Value(), _timeSpanFormats, CultureInfo.InvariantCulture, out var validForValue)) { - Console.WriteLine("The period provided for --valid-for could not be parsed. Ensure you use a format like '10d', '22h', '45s' etc."); + reporter.Error("The period provided for --valid-for could not be parsed. Ensure you use a format like '10d', '22h', '45s' etc."); } expiresOn = notBefore.Add(validForValue); } @@ -157,7 +159,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments( { if (!DevJwtCliHelpers.TryParseClaims(claimsOption.Values, out claims)) { - Console.WriteLine("Malformed claims supplied. Ensure each claim is in the format \"name=value\"."); + reporter.Error("Malformed claims supplied. Ensure each claim is in the format \"name=value\"."); isValid = false; } } @@ -169,24 +171,21 @@ static bool ParseDate(string datetime, out DateTime parsedDateTime) => } private static int Execute( + IReporter reporter, string projectPath, JwtCreatorOptions options) { - var project = DevJwtCliHelpers.GetProject(projectPath); - if (project == null) + if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) { - Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } - - var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId); var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial); var jwtToken = jwtIssuer.Create(options); var jwtStore = new JwtStore(userSecretsId); - var jwt = Jwt.Create(jwtToken, JwtIssuer.WriteToken(jwtToken), options.Scopes, options.Roles, options.Claims); + var jwt = Jwt.Create(options.Scheme, jwtToken, JwtIssuer.WriteToken(jwtToken), options.Scopes, options.Roles, options.Claims); if (options.Claims is { } customClaims) { jwt.CustomClaims = customClaims; @@ -198,7 +197,7 @@ private static int Execute( var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Audiences, options.Issuer); settingsToWrite.Save(appsettingsFilePath); - Console.WriteLine("New JWT saved!"); + reporter.Output($"New JWT saved with ID '{jwtToken.Id}'."); return 0; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs index d26323772020..83b287b81a2f 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -23,33 +24,32 @@ public static void Register(ProjectCommandLineApplication app) cmd.ShowHelp(); return 0; } - return Execute(app.ProjectOption.Value(), idArgument.Value); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value); }); }); } - private static int Execute(string projectPath, string id) + private static int Execute(IReporter reporter, string projectPath, string id) { - var project = DevJwtCliHelpers.GetProject(projectPath); - if (project == null) + if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) { - Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } - - var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); var jwtStore = new JwtStore(userSecretsId); if (!jwtStore.Jwts.ContainsKey(id)) { - Console.WriteLine($"[ERROR] No JWT with ID '{id}' found"); + reporter.Error($"[ERROR] No JWT with ID '{id}' found"); return 1; } + var jwt = jwtStore.Jwts[id]; + var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Scheme); jwtStore.Jwts.Remove(id); jwtStore.Save(); - Console.WriteLine($"Deleted JWT with ID '{id}'"); + reporter.Output($"Deleted JWT with ID '{id}'"); return 0; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs index ccd0ce26da60..1637d7d7f6ed 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -28,36 +29,32 @@ public static void Register(ProjectCommandLineApplication app) cmd.OnExecute(() => { - return Execute(app.ProjectOption.Value(), resetOption.HasValue(), forceOption.HasValue()); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), resetOption.HasValue(), forceOption.HasValue()); }); }); } - private static int Execute(string projectPath, bool reset, bool force) + private static int Execute(IReporter reporter, string projectPath, bool reset, bool force) { - var project = DevJwtCliHelpers.GetProject(projectPath); - if (project == null) + if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var _, out var userSecretsId)) { - Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } - var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); - if (reset == true) { if (!force) { - Console.WriteLine("Are you sure you want to reset the JWT signing key? This will invalidate all JWTs previously issued for this project.\n [Y]es / [N]o"); + reporter.Output("Are you sure you want to reset the JWT signing key? This will invalidate all JWTs previously issued for this project.\n [Y]es / [N]o"); if (Console.ReadLine().Trim().ToUpperInvariant() != "Y") { - Console.WriteLine("Key reset canceled."); + reporter.Output("Key reset canceled."); return 0; } } var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, reset: true); - Console.WriteLine($"New signing key created: {Convert.ToBase64String(key)}"); + reporter.Output($"New signing key created: {Convert.ToBase64String(key)}"); return 0; } @@ -68,11 +65,11 @@ private static int Execute(string projectPath, bool reset, bool force) if (signingKeyMaterial is null) { - Console.WriteLine("Signing key for JWTs was not found. One will be created automatically when the first JWT is created, or you can force creation of a key with the --reset option."); + reporter.Output("Signing key for JWTs was not found. One will be created automatically when the first JWT is created, or you can force creation of a key with the --reset option."); return 0; } - Console.WriteLine($"Signing Key: {signingKeyMaterial}"); + reporter.Output($"Signing Key: {signingKeyMaterial}"); return 0; } } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs index 7a6703d522ff..8013e298997f 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -22,34 +23,26 @@ public static void Register(ProjectCommandLineApplication app) cmd.OnExecute(() => { - return Execute(app.ProjectOption.Value(), showTokensOption.HasValue()); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), showTokensOption.HasValue()); }); }); } - private static int Execute(string projectPath, bool showTokens) + private static int Execute(IReporter reporter, string projectPath, bool showTokens) { - var project = DevJwtCliHelpers.GetProject(projectPath); - if (project == null) + if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) { - Console.WriteLine($"No project found at `-p|--project` path or current directory."); - return 1; - } - var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); - if (userSecretsId == null) - { - Console.WriteLine($"Project does not contain a user secrets ID."); return 1; } var jwtStore = new JwtStore(userSecretsId); - Console.WriteLine($"Project: {project}"); - Console.WriteLine($"User Secrets ID: {userSecretsId}"); + reporter.Output($"Project: {project}"); + reporter.Output($"User Secrets ID: {userSecretsId}"); if (jwtStore.Jwts is { Count: > 0 } jwts) { - var table = new ConsoleTable(); - table.AddColumns("Id", "Name", "Audience", "Issued", "Expires"); + var table = new ConsoleTable(reporter); + table.AddColumns("Id", "Scheme Name", "Audience", "Issued", "Expires"); if (showTokens) { @@ -61,11 +54,11 @@ private static int Execute(string projectPath, bool showTokens) var jwt = jwtRow.Value; if (showTokens) { - table.AddRow(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"), jwt.Token); + table.AddRow(jwt.Id, jwt.Scheme, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"), jwt.Token); } else { - table.AddRow(jwt.Id, jwt.Name, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O")); + table.AddRow(jwt.Id, jwt.Scheme, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O")); } } @@ -73,7 +66,7 @@ private static int Execute(string projectPath, bool showTokens) } else { - Console.WriteLine("No JWTs created yet!"); + reporter.Output("No JWTs created yet!"); } return 0; diff --git a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs index 3cff02f5e208..3d144b9c5001 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs @@ -3,6 +3,7 @@ using System.IdentityModel.Tokens.Jwt; using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal sealed class PrintCommand @@ -29,37 +30,33 @@ public static void Register(ProjectCommandLineApplication app) cmd.ShowHelp(); return 0; } - return Execute(app.ProjectOption.Value(), idArgument.Value, showFullOption.HasValue()); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value, showFullOption.HasValue()); }); }); } - private static int Execute(string projectPath, string id, bool showFull) + private static int Execute(IReporter reporter, string projectPath, string id, bool showFull) { - var project = DevJwtCliHelpers.GetProject(projectPath); - if (project == null) + if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var _, out var userSecretsId)) { - Console.WriteLine($"No project found at `-p|--project` path or current directory."); return 1; } - - var userSecretsId = DevJwtCliHelpers.GetUserSecretsId(project); var jwtStore = new JwtStore(userSecretsId); if (!jwtStore.Jwts.ContainsKey(id)) { - Console.WriteLine($"No token with ID '{id}' found"); + reporter.Output($"No token with ID '{id}' found"); return 1; } - Console.WriteLine($"Found JWT with ID '{id}'"); + reporter.Output($"Found JWT with ID '{id}'"); var jwt = jwtStore.Jwts[id]; JwtSecurityToken fullToken; if (showFull) { fullToken = JwtIssuer.Extract(jwt.Token); - DevJwtCliHelpers.PrintJwt(jwt, fullToken); + DevJwtCliHelpers.PrintJwt(reporter, jwt, fullToken); } return 0; diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs index 635b611fe9e0..a15391cc99b6 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -9,14 +10,23 @@ internal sealed class ProjectCommandLineApplication : CommandLineApplication { public CommandOption ProjectOption { get; private set; } - public ProjectCommandLineApplication(bool throwOnUnexpectedArg = true, bool continueAfterUnexpectedArg = false, bool treatUnmatchedOptionsAsArguments = false) + public IReporter Reporter { get; private set; } + + public ProjectCommandLineApplication(IReporter reporter, bool throwOnUnexpectedArg = true, bool continueAfterUnexpectedArg = false, bool treatUnmatchedOptionsAsArguments = false) : base(throwOnUnexpectedArg, continueAfterUnexpectedArg, treatUnmatchedOptionsAsArguments) { ProjectOption = Option( "-p|--project", "The path of the project to operate on. Defaults to the project in the current directory", CommandOptionType.SingleValue); + Reporter = reporter; + } - Options.Add(ProjectOption); + public ProjectCommandLineApplication Command(string name, Action configuration) + { + var command = new ProjectCommandLineApplication(Reporter) { Name = name, Parent = this }; + Commands.Add(command); + configuration(command); + return command; } } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 04532f22c4e3..621ec228d76b 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -8,6 +8,7 @@ using System.Xml.XPath; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.UserSecrets; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; @@ -43,6 +44,25 @@ public static string GetProject(string projectPath = null) return null; } + public static bool GetProjectAndSecretsId(string projectPath, IReporter reporter, out string project, out string userSecretsId) + { + project = GetProject(projectPath); + userSecretsId = null; + if (project == null) + { + reporter.Error($"No project found at `-p|--project` path or current directory."); + return false; + } + + userSecretsId = GetUserSecretsId(project); + if (userSecretsId == null) + { + reporter.Error($"Project does not contain a user secrets ID."); + return false; + } + return true; + } + public static byte[] GetOrCreateSigningKeyMaterial(string userSecretsId) { var projectConfiguration = new ConfigurationBuilder() @@ -128,16 +148,16 @@ public static string[] GetAudienceCandidatesFromLaunchSettings(string project) return null; } - public static void PrintJwt(Jwt jwt, JwtSecurityToken fullToken = null) + public static void PrintJwt(IReporter reporter, Jwt jwt, JwtSecurityToken fullToken = null) { - Console.WriteLine(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true })); + reporter.Output(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true })); if (fullToken is not null) { - Console.WriteLine($"Token Header: {fullToken.Header.SerializeToJson()}"); - Console.WriteLine($"Token Payload: {fullToken.Payload.SerializeToJson()}"); + reporter.Output($"Token Header: {fullToken.Header.SerializeToJson()}"); + reporter.Output($"Token Payload: {fullToken.Payload.SerializeToJson()}"); } - Console.WriteLine($"Compact Token: {jwt.Token}"); + reporter.Output($"Compact Token: {jwt.Token}"); } public static bool TryParseClaims(List input, out Dictionary claims) diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs index b0554244ccfd..e78112fa6cda 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -public record Jwt(string Id, string Name, string Audience, DateTimeOffset NotBefore, DateTimeOffset Expires, DateTimeOffset Issued, string Token) +public record Jwt(string Id, string Scheme, string Name, string Audience, DateTimeOffset NotBefore, DateTimeOffset Expires, DateTimeOffset Issued, string Token) { public IEnumerable Scopes { get; set; } = new List(); @@ -17,13 +17,14 @@ public record Jwt(string Id, string Name, string Audience, DateTimeOffset NotBef public override string ToString() => Token; public static Jwt Create( + string scheme, JwtSecurityToken token, string encodedToken, IEnumerable scopes = null, IEnumerable roles = null, IDictionary customClaims = null) { - return new Jwt(token.Id, token.Subject, token.Audiences.FirstOrDefault(), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken) + return new Jwt(token.Id, scheme, token.Subject, token.Audiences.FirstOrDefault(), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken) { Scopes = scopes, Roles = roles, diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs index 775a733fc915..b8108f5294c7 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs @@ -12,7 +12,7 @@ internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, List(reader); + reader.Close(); + + if (config[AuthenticationKey] is JsonObject authentication && + authentication[SchemesKey] is JsonObject schemes) + { + schemes.Remove(name); + } + + using var writer = new FileStream(filePath, FileMode.Create, FileAccess.Write); + JsonSerializer.Serialize(writer, config, _jsonSerializerOptions); + } } diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs index cc26e5c45da4..24d2ea9dafbe 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -1,32 +1,52 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; -#pragma warning disable CA1852 // Seal internal types -ProjectCommandLineApplication userJwts = new() +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; + +public class Program { - Name = "dotnet user-jwts" -}; - -userJwts.HelpOption("-h|--help"); - -// dotnet user-jwts list -ListCommand.Register(userJwts); -// dotnet user-jwts create -CreateCommand.Register(userJwts); -// dotnet user-jwts print ecd045 -PrintCommand.Register(userJwts); -// dotnet user-jwts delete ecd045 -DeleteCommand.Register(userJwts); -// dotnet user-jwts clear -ClearCommand.Register(userJwts); -// dotnet user-jwts key -KeyCommand.Register(userJwts); - -// Show help information if no subcommand/option was specified. -userJwts.OnExecute(() => userJwts.ShowHelp()); - -userJwts.Execute(args); -#pragma warning restore CA1852 // Seal internal types \ No newline at end of file + private readonly IConsole _console; + private readonly IReporter _reporter; + + public Program(IConsole console) + { + _console = console; + _reporter = new ConsoleReporter(console); + } + + public static void Main(string[] args) + { + new Program(PhysicalConsole.Singleton).Run(args); + } + + public void Run(string[] args) + { + ProjectCommandLineApplication userJwts = new(_reporter) + { + Name = "dotnet user-jwts" + }; + + userJwts.HelpOption("-h|--help"); + + // dotnet user-jwts list + ListCommand.Register(userJwts); + // dotnet user-jwts create + CreateCommand.Register(userJwts); + // dotnet user-jwts print ecd045 + PrintCommand.Register(userJwts); + // dotnet user-jwts delete ecd045 + DeleteCommand.Register(userJwts); + // dotnet user-jwts clear + ClearCommand.Register(userJwts); + // dotnet user-jwts key + KeyCommand.Register(userJwts); + + // Show help information if no subcommand/option was specified. + userJwts.OnExecute(() => userJwts.ShowHelp()); + + userJwts.Execute(args); + } +} diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs new file mode 100644 index 000000000000..08e003c8e456 --- /dev/null +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Microsoft.Extensions.Configuration.UserSecrets; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests; + +public class UserJwtsTestFixture : IDisposable +{ + private Stack _disposables = new Stack(); + private string TestSecretsId = Guid.NewGuid().ToString(); + + private const string ProjectTemplate = @" + + Exe + net7.0 + {0} + false + +"; + + private const string LaunchSettingsTemplate = @" +{ + ""profiles"": { + ""HttpApiSampleApp"": { + ""commandName"": ""Project"", + ""dotnetRunMessages"": true, + ""launchBrowser"": true, + ""applicationUrl"": ""https://localhost:5001;http://localhost:5000"", + ""environmentVariables"": { + ""ASPNETCORE_ENVIRONMENT"": ""Development"" + } + } + } +}"; + + public string CreateProject(bool hasSecret = true) + { + var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "userjwtstest", Guid.NewGuid().ToString())); + Directory.CreateDirectory(Path.Combine(projectPath.FullName, "Properties")); + var prop = hasSecret ? $"{TestSecretsId}" : string.Empty; + if (hasSecret) + { + Directory.CreateDirectory(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId))); + } + + File.WriteAllText( + Path.Combine(projectPath.FullName, "TestProject.csproj"), + string.Format(CultureInfo.InvariantCulture, ProjectTemplate, prop)); + + File.WriteAllText(Path.Combine(projectPath.FullName, "Properties", "launchSettings.json"), + LaunchSettingsTemplate); + + File.WriteAllText( + Path.Combine(projectPath.FullName, "appsettings.Development.json"), + "{}"); + + if (hasSecret) + { + _disposables.Push(() => + { + try + { + var secretsDir = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId)); + TryDelete(TestSecretsId); + } + catch { } + }); + } + + _disposables.Push(() => TryDelete(projectPath.FullName)); + + return projectPath.FullName; + } + + private static void TryDelete(string directory) + { + try + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, true); + } + } + catch (Exception) + { + Console.WriteLine("Failed to delete " + directory); + } + } + + public void Dispose() + { + while (_disposables.Count > 0) + { + _disposables.Pop()?.Invoke(); + } + } +} diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs new file mode 100644 index 000000000000..ac0cf3d95723 --- /dev/null +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Tools.Internal; +using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; +using Xunit; +using Xunit.Abstractions; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests; + +public class UserJwtsTests : IClassFixture +{ + private readonly TestConsole _console; + private readonly UserJwtsTestFixture _fixture; + private readonly ITestOutputHelper _testOut; + + public UserJwtsTests(UserJwtsTestFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _testOut = output; + _console = new TestConsole(output); + } + + [Fact] + public void List_NoTokensForNewProject() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "list", "--project", project }); + Assert.Contains("No JWTs created yet!", _console.GetOutput()); + } + + [Fact] + public void List_HandlesNoSecretsInProject() + { + var project = Path.Combine(_fixture.CreateProject(false), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "list", "--project", project }); + Assert.Contains("Project does not contain a user secrets ID.", _console.GetOutput()); + } + + [Fact] + public void Create_WarnsOnNoSecretInproject() + { + var project = Path.Combine(_fixture.CreateProject(false), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + Assert.Contains("Project does not contain a user secrets ID.", _console.GetOutput()); + } + + [Fact] + public void Create_WritesGeneratedTokenToDisk() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + Assert.Contains("New JWT saved", _console.GetOutput()); + Assert.Contains("dotnet-user-jwts", File.ReadAllText(appsettings)); + } + + [Fact] + public void Print_ReturnsNothingForMissingToken() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "print", "invalid-id", "--project", project }); + Assert.Contains("No token with ID 'invalid-id' found", _console.GetOutput()); + } + + [Fact] + public void List_ReturnsIdForGeneratedToken() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--scheme", "MyCustomScheme" }); + Assert.Contains("New JWT saved", _console.GetOutput()); + + app.Run(new[] { "list", "--project", project }); + Assert.Contains("MyCustomScheme", _console.GetOutput()); + } + + [Fact] + public void Delete_RemovesGeneratedToken() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + app.Run(new[] { "create", "--project", project, "--scheme", "Scheme2" }); + + app.Run(new[] { "delete", id, "--project", project }); + var appsettingsContent = File.ReadAllText(appsettings); + Assert.DoesNotContain("Bearer", appsettingsContent); + Assert.Contains("Scheme2", appsettingsContent); + } + + [Fact] + public void Clear_RemovesGeneratedTokens() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + app.Run(new[] { "create", "--project", project, "--scheme", "Scheme2" }); + + Assert.Contains("New JWT saved", _console.GetOutput()); + + app.Run(new[] { "clear", "--project", project, "--force" }); + var appsettingsContent = File.ReadAllText(appsettings); + Assert.DoesNotContain("Bearer", appsettingsContent); + Assert.DoesNotContain("Scheme2", appsettingsContent); + } + + [Fact] + public void Key_CanResetSigningKey() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + app.Run(new[] { "key", "--project", project }); + Assert.Contains("Signing Key:", _console.GetOutput()); + + app.Run(new[] { "key", "--reset", "--force", "--project", project }); + Assert.Contains("New signing key created:", _console.GetOutput()); + } +} diff --git a/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj b/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj new file mode 100644 index 000000000000..84d7ec58c998 --- /dev/null +++ b/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj @@ -0,0 +1,16 @@ + + + + $(DefaultNetCoreTargetFramework) + Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests + + + + + + + + + + + \ No newline at end of file From 672fb64c2765b4efc00f86ff85329d94b098f390 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 27 May 2022 13:19:25 -0700 Subject: [PATCH 12/18] Move ConsoleTable implementation to avoid conflicts in ProjectTemplates --- .../dotnet-user-jwts/src/Helpers}/ConsoleTable.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{Shared/CommandLineUtils/CommandLine => Tools/dotnet-user-jwts/src/Helpers}/ConsoleTable.cs (100%) diff --git a/src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs b/src/Tools/dotnet-user-jwts/src/Helpers/ConsoleTable.cs similarity index 100% rename from src/Shared/CommandLineUtils/CommandLine/ConsoleTable.cs rename to src/Tools/dotnet-user-jwts/src/Helpers/ConsoleTable.cs From bd19796c841678b60350065d440236a1bc452643 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 27 May 2022 16:57:00 -0700 Subject: [PATCH 13/18] Update existing auth tests and fix middleware registration --- ...AzureADAuthenticationBuilderExtensionsTests.cs | 7 +++++++ src/DefaultBuilder/src/WebApplicationBuilder.cs | 15 ++++++++++----- .../Core/src/AuthAppBuilderExtensions.cs | 3 +++ .../test/SharedAuthenticationTests.cs | 9 +++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs b/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs index 8070f11abb66..616e24982958 100644 --- a/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs +++ b/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -21,6 +22,7 @@ public void AddAzureAD_AddsAllAuthenticationHandlers() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -288,6 +290,7 @@ public void AddAzureADBearer_AddsAllAuthenticationHandlers() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -305,6 +308,7 @@ public void AddAzureADBearer_ConfiguresAllOptions() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -340,6 +344,7 @@ public void AddAzureADBearer_CanOverrideJwtBearerOptionsConfiguration() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -373,6 +378,7 @@ public void AddAzureADBearer_RegisteringJwtBearerHasNoImpactOnAzureAAExtensions( // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -473,6 +479,7 @@ public void AddAzureADBearer_SkipsOptionsValidationForNonAzureCookies() { var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); services.AddAuthentication() .AddAzureADBearer(o => { }) diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index 2ce88c53c1c6..e3b581b01564 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -17,9 +17,11 @@ namespace Microsoft.AspNetCore.Builder; public sealed class WebApplicationBuilder { private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder"; + private const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet"; private readonly HostApplicationBuilder _hostApplicationBuilder; private readonly ServiceDescriptor _genericWebHostServiceDescriptor; + private readonly WebApplicationAuthenticationBuilder _webAuthBuilder; private WebApplication? _builtApplication; @@ -80,7 +82,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action @@ -118,7 +120,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action /// An for configuration authentication-related properties. /// - public AuthenticationBuilder Authentication { get; } + public AuthenticationBuilder Authentication => _webAuthBuilder; /// /// Builds the . @@ -173,10 +175,13 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui } } - if (Authentication is WebApplicationAuthenticationBuilder webAuthBuilder && webAuthBuilder.IsAuthenticationConfigured is true) + if (_webAuthBuilder.IsAuthenticationConfigured) { - app.UseAuthentication(); - app.UseAuthorization(); + if (!_builtApplication.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) + { + app.UseAuthentication(); + app.UseAuthorization(); + } } // Wire the source pipeline to run in the destination pipeline diff --git a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs index 8efc58c59305..a164cee3db00 100644 --- a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs +++ b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Builder; /// public static class AuthAppBuilderExtensions { + private const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet"; + /// /// Adds the to the specified , which enables authentication capabilities. /// @@ -22,6 +24,7 @@ public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app throw new ArgumentNullException(nameof(app)); } + app.Properties[AuthenticationMiddlewareSetKey] = true; return app.UseMiddleware(); } } diff --git a/src/Security/Authentication/test/SharedAuthenticationTests.cs b/src/Security/Authentication/test/SharedAuthenticationTests.cs index 4ee984335d59..f1085b604169 100644 --- a/src/Security/Authentication/test/SharedAuthenticationTests.cs +++ b/src/Security/Authentication/test/SharedAuthenticationTests.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication.Tests; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Authentication; @@ -25,6 +26,7 @@ public abstract class SharedAuthenticationTests where TOptions : Authe public async Task CanForwardDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { @@ -165,6 +167,7 @@ public async Task ForwardSignOutWinsOverDefault() public async Task ForwardForbidWinsOverDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { o.DefaultScheme = DefaultScheme; @@ -214,6 +217,7 @@ public Task TransformAsync(ClaimsPrincipal principal) public async Task ForwardAuthenticateOnlyRunsTransformOnceByDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var transform = new RunOnce(); var builder = services.AddSingleton(transform).AddAuthentication(o => { @@ -244,6 +248,7 @@ public async Task ForwardAuthenticateOnlyRunsTransformOnceByDefault() public async Task ForwardAuthenticateWinsOverDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { o.DefaultScheme = DefaultScheme; @@ -283,6 +288,7 @@ public async Task ForwardAuthenticateWinsOverDefault() public async Task ForwardChallengeWinsOverDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { o.DefaultScheme = DefaultScheme; @@ -322,6 +328,7 @@ public async Task ForwardChallengeWinsOverDefault() public async Task ForwardSelectorWinsOverDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { o.DefaultScheme = DefaultScheme; @@ -391,6 +398,7 @@ public async Task ForwardSelectorWinsOverDefault() public async Task NullForwardSelectorUsesDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { o.DefaultScheme = DefaultScheme; @@ -460,6 +468,7 @@ public async Task NullForwardSelectorUsesDefault() public async Task SpecificForwardWinsOverSelectorAndDefault() { var services = new ServiceCollection().AddLogging(); + services.AddSingleton(new ConfigurationManager()); var builder = services.AddAuthentication(o => { o.DefaultScheme = DefaultScheme; From 1f3a990500cd76091552c6fc8c281206117dbbe4 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 30 May 2022 21:20:31 -0700 Subject: [PATCH 14/18] Update AzureAdB2C tests and auth app builder --- ...B2CAuthenticationBuilderExtensionsTests.cs | 5 +++++ .../src/Microsoft.AspNetCore.csproj | 4 ++++ .../src/WebApplicationBuilder.cs | 7 ++----- .../Core/src/AuthAppBuilderExtensions.cs | 12 +++++++++--- .../test/AuthenticationMiddlewareTests.cs | 19 +++++++++++++++++++ ...soft.AspNetCore.Authentication.Test.csproj | 1 + 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs b/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs index 8c61fdb79844..36abc8d6d58d 100644 --- a/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs +++ b/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -262,6 +263,7 @@ public void AddAzureADB2CBearer_AddsAllAuthenticationHandlers() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -279,6 +281,7 @@ public void AddAzureADB2CBearer_ConfiguresAllOptions() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -315,6 +318,7 @@ public void AddAzureADB2CBearer_CanOverrideJwtBearerOptionsConfiguration() // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() @@ -348,6 +352,7 @@ public void AddAzureADB2CBearer_RegisteringJwtBearerHasNoImpactOnAzureAAExtensio // Arrange var services = new ServiceCollection(); services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new ConfigurationManager()); // Act services.AddAuthentication() diff --git a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj index b54de7e85b80..669ecb8037bb 100644 --- a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj +++ b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj @@ -34,4 +34,8 @@ + + + + diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index e3b581b01564..a0926ab6487f 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -177,11 +177,8 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui if (_webAuthBuilder.IsAuthenticationConfigured) { - if (!_builtApplication.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) - { - app.UseAuthentication(); - app.UseAuthorization(); - } + _builtApplication.UseAuthentication(); + _builtApplication.UseAuthorization(); } // Wire the source pipeline to run in the destination pipeline diff --git a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs index a164cee3db00..b3400413330c 100644 --- a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs +++ b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Builder; /// public static class AuthAppBuilderExtensions { - private const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet"; + internal const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet"; /// /// Adds the to the specified , which enables authentication capabilities. @@ -24,7 +24,13 @@ public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app throw new ArgumentNullException(nameof(app)); } - app.Properties[AuthenticationMiddlewareSetKey] = true; - return app.UseMiddleware(); + // Don't add more than one instance of the middleware + if (!app.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) + { + app.Properties[AuthenticationMiddlewareSetKey] = true; + return app.UseMiddleware(); + } + + return app; } } diff --git a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs index f6ac9acce57b..eb883f111d7a 100644 --- a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs +++ b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs @@ -151,6 +151,25 @@ public async Task IAuthenticateResultFeature_SettingResultSetsUser() Assert.Same(context.User, newTicket.Principal); } + [Fact] + public async Task WebApplicationBuilder_RegistersAuthenticationMiddlewares() + { + var builder = WebApplication.CreateBuilder(); + builder.Authentication.AddJwtBearer(); + await using var app = builder.Build(); + + var webAppAuthBuilder = Assert.IsType(builder.Authentication); + Assert.True(webAppAuthBuilder.IsAuthenticationConfigured); + + // Authentication middleware isn't registered until application + // is built on startup + Assert.False(app.Properties.ContainsKey("__AuthenticationMiddlewareSet")); + + await app.StartAsync(); + + Assert.True(app.Properties.ContainsKey("__AuthenticationMiddlewareSet")); + } + private HttpContext GetHttpContext( Action registerServices = null, IAuthenticationService authenticationService = null) diff --git a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj index a65c8ad9271f..72dad5f713a3 100644 --- a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj +++ b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -36,6 +36,7 @@ + From 23e9b0c270ce7537b97f6f1206c9455d6045c3c3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 30 May 2022 21:53:15 -0700 Subject: [PATCH 15/18] Fix build and move registration check --- src/DefaultBuilder/src/WebApplicationBuilder.cs | 8 ++++++-- .../Core/src/AuthAppBuilderExtensions.cs | 10 ++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index a0926ab6487f..b96b15117432 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -177,8 +177,12 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui if (_webAuthBuilder.IsAuthenticationConfigured) { - _builtApplication.UseAuthentication(); - _builtApplication.UseAuthorization(); + // Don't add more than one instance of the middleware + if (!_builtApplication.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) + { + _builtApplication.UseAuthentication(); + _builtApplication.UseAuthorization(); + } } // Wire the source pipeline to run in the destination pipeline diff --git a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs index b3400413330c..5c210ef7e907 100644 --- a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs +++ b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs @@ -24,13 +24,7 @@ public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app throw new ArgumentNullException(nameof(app)); } - // Don't add more than one instance of the middleware - if (!app.Properties.ContainsKey(AuthenticationMiddlewareSetKey)) - { - app.Properties[AuthenticationMiddlewareSetKey] = true; - return app.UseMiddleware(); - } - - return app; + app.Properties[AuthenticationMiddlewareSetKey] = true; + return app.UseMiddleware(); } } From f0aa38679c6b0685ae88a54b8a8ca78a713ef6e3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 30 May 2022 22:45:50 -0700 Subject: [PATCH 16/18] Fix up resolution for Certificate test sources --- src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs | 1 + src/Security/Authentication/test/CertificateTests.cs | 1 + src/Shared/test/Certificates/Certificates.cs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs index a729ef21e0e5..332fe321aaf2 100644 --- a/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs +++ b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs index 03fd439a3660..018ccb139171 100644 --- a/src/Security/Authentication/test/CertificateTests.cs +++ b/src/Security/Authentication/test/CertificateTests.cs @@ -6,6 +6,7 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; diff --git a/src/Shared/test/Certificates/Certificates.cs b/src/Shared/test/Certificates/Certificates.cs index 8124e9cdf0f1..d0c2a0c043b6 100644 --- a/src/Shared/test/Certificates/Certificates.cs +++ b/src/Shared/test/Certificates/Certificates.cs @@ -4,6 +4,8 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +namespace Microsoft.AspNetCore.Authentication.Certificate; + public static class Certificates { private static string ServerEku = "1.3.6.1.5.5.7.3.1"; From 98b504ff112ce55cd5c47d690b55bb70b4ef3f6a Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 31 May 2022 10:30:03 -0700 Subject: [PATCH 17/18] Fix write stream configuration for writing key material --- src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 621ec228d76b..40e35506b27c 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -104,7 +104,7 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = } secrets.Add(DevJwtsDefaults.SigningKeyConfigurationKey, Convert.ToBase64String(newKeyMaterial)); - using var secretsWriteStream = new FileStream(secretsFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + using var secretsWriteStream = new FileStream(secretsFilePath, FileMode.Create, FileAccess.Write); JsonSerializer.Serialize(secretsWriteStream, secrets); return newKeyMaterial; From 50a3cdaabc41e980e0d98983e77a688e9a41e5b3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 31 May 2022 14:26:55 -0700 Subject: [PATCH 18/18] Fix handling missing config section when processing options --- .../Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs index 5cb7ce6dfbd4..7004f06a40be 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs @@ -37,7 +37,7 @@ public void Configure(string? name, JwtBearerOptions options) var configSection = _authenticationConfigurationProvider.GetAuthenticationSchemeConfiguration(name); - if (configSection is null) + if (configSection is null || !configSection.GetChildren().Any()) { return; }