|
// 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.Linq;
using System.Text.Json;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
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", "yyyy/MM/dd HH:mm", "yyyy-MM-ddTHH:mm:ss.fffffffzzz" };
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(ProjectCommandLineApplication app, Program program)
{
app.Command("create", cmd =>
{
cmd.Description = Resources.CreateCommand_Description;
var schemeNameOption = cmd.Option(
"--scheme",
Resources.CreateCommand_SchemeOption_Description,
CommandOptionType.SingleValue
);
var nameOption = cmd.Option(
"-n|--name",
Resources.CreateCommand_NameOption_Description,
CommandOptionType.SingleValue);
var audienceOption = cmd.Option(
"--audience",
Resources.CreateCommand_AudienceOption_Description,
CommandOptionType.MultipleValue);
var issuerOption = cmd.Option(
"--issuer",
Resources.CreateCommand_IssuerOption_Description,
CommandOptionType.SingleValue);
var scopesOption = cmd.Option(
"--scope",
Resources.CreateCommand_ScopeOption_Description,
CommandOptionType.MultipleValue);
var rolesOption = cmd.Option(
"--role",
Resources.CreateCommand_RoleOption_Description,
CommandOptionType.MultipleValue);
var claimsOption = cmd.Option(
"--claim",
Resources.CreateCommand_ClaimOption_Description,
CommandOptionType.MultipleValue);
var notBeforeOption = cmd.Option(
"--not-before",
Resources.CreateCommand_NotBeforeOption_Description,
CommandOptionType.SingleValue);
var expiresOnOption = cmd.Option(
"--expires-on",
Resources.CreateCommand_ExpiresOnOption_Description,
CommandOptionType.SingleValue);
var validForOption = cmd.Option(
"--valid-for",
Resources.CreateCommand_ValidForOption_Description,
CommandOptionType.SingleValue);
cmd.HelpOption("-h|--help");
cmd.OnExecute(() =>
{
var (options, isValid, optionsString) = ValidateArguments(
cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption);
if (!isValid)
{
return 1;
}
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, cmd.OutputOption.Value(), program);
});
});
}
private static (JwtCreatorOptions, bool, string) ValidateArguments(
IReporter reporter,
CommandOption projectOption,
CommandOption schemeNameOption,
CommandOption nameOption,
CommandOption audienceOption,
CommandOption issuerOption,
CommandOption notBeforeOption,
CommandOption expiresOnOption,
CommandOption validForOption,
CommandOption rolesOption,
CommandOption scopesOption,
CommandOption claimsOption)
{
var isValid = true;
var finder = new MsBuildProjectFinder(Directory.GetCurrentDirectory());
var project = finder.FindMsBuildProject(projectOption.Value());
if (project == null)
{
reporter.Error(Resources.ProjectOption_ProjectNotFound);
isValid = false;
// Break out early if we haven't been able to resolve a project
// since we depend on it for the managing of JWT tokens
return (
null,
isValid,
string.Empty
);
}
var scheme = schemeNameOption.HasValue() ? schemeNameOption.Value() : "Bearer";
var optionsString = schemeNameOption.HasValue() ? $"{Resources.JwtPrint_Scheme}: {scheme}{Environment.NewLine}" : string.Empty;
var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName;
optionsString += $"{Resources.JwtPrint_Name}: {name}{Environment.NewLine}";
var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project);
optionsString += audienceOption.HasValue() ? $"{Resources.JwtPrint_Audiences}: {string.Join(", ", audience)}{Environment.NewLine}" : string.Empty;
if (audience is null || audience.Count == 0)
{
reporter.Error(Resources.CreateCommand_NoAudience_Error);
isValid = false;
}
var issuer = issuerOption.HasValue() ? issuerOption.Value() : DevJwtsDefaults.Issuer;
optionsString += issuerOption.HasValue() ? $"{Resources.JwtPrint_Issuer}: {issuer}{Environment.NewLine}" : string.Empty;
var notBefore = DateTime.UtcNow;
if (notBeforeOption.HasValue())
{
if (!ParseDate(notBeforeOption.Value(), out notBefore))
{
reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--not-before"));
isValid = false;
}
optionsString += $"{Resources.JwtPrint_NotBefore}: {notBefore:O}{Environment.NewLine}";
}
var expiresOn = notBefore.AddMonths(3);
if (expiresOnOption.HasValue())
{
if (!ParseDate(expiresOnOption.Value(), out expiresOn))
{
reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--expires-on"));
isValid = false;
}
if (validForOption.HasValue())
{
reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error);
isValid = false;
}
else
{
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
}
}
if (validForOption.HasValue())
{
if (!TimeSpan.TryParseExact(validForOption.Value(), _timeSpanFormats, CultureInfo.InvariantCulture, out var validForValue))
{
reporter.Error(Resources.FormatCreateCommand_InvalidPeriod_Error("--valid-for"));
}
expiresOn = notBefore.Add(validForValue);
if (expiresOnOption.HasValue())
{
reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error);
isValid = false;
}
else
{
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
}
}
var roles = rolesOption.HasValue() ? rolesOption.Values : new List<string>();
optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(", ", roles)}]{Environment.NewLine}" : string.Empty;
var scopes = scopesOption.HasValue() ? scopesOption.Values : new List<string>();
optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(", ", scopes)}{Environment.NewLine}" : string.Empty;
var claims = new Dictionary<string, string>();
if (claimsOption.HasValue())
{
if (!DevJwtCliHelpers.TryParseClaims(claimsOption.Values, out claims))
{
reporter.Error(Resources.CreateCommand_InvalidClaims_Error);
isValid = false;
}
optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(", ", claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}";
}
return (
new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims),
isValid,
optionsString);
static bool ParseDate(string datetime, out DateTime parsedDateTime) =>
DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime);
}
private static int Execute(
IReporter reporter,
string projectPath,
JwtCreatorOptions options,
string optionsString,
string outputFormat,
Program program)
{
if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
{
return 1;
}
var keyMaterial = SigningKeysHandler.GetOrCreateSigningKeyMaterial(userSecretsId, options.Scheme, options.Issuer);
var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial);
var jwtToken = jwtIssuer.Create(options);
var jwtStore = new JwtStore(userSecretsId, program);
var jwt = Jwt.Create(options.Scheme, 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.Audiences, options.Issuer);
settingsToWrite.Save(appsettingsFilePath);
switch (outputFormat)
{
case "token":
reporter.Output(jwt.Token);
break;
case "json":
reporter.Output(JsonSerializer.Serialize(jwt, JwtSerializerOptions.Default));
break;
default:
reporter.Output(Resources.FormatCreateCommand_Confirmed(jwtToken.Id));
reporter.Output(optionsString);
reporter.Output($"{Resources.JwtPrint_Token}: {jwt.Token}");
break;
}
return 0;
}
}
|