File: Commands\CreateCommand.cs
Web Access
Project: src\src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj (dotnet-user-jwts)
// 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;
    }
}