diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs index 4d2431f54..da6ef6ae1 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs @@ -107,17 +107,17 @@ public string? Domain1 /// /// The project is a web API. /// - public bool IsWebApi { get; set; } + public bool? IsWebApi { get; set; } /// /// The project is a web app. /// - public bool IsWebApp { get; set; } + public bool? IsWebApp { get; set; } /// /// The project is a blazor web assembly. /// - public bool IsBlazorWasm { get; set; } + public bool? IsBlazorWasm { get; set; } /// /// The app calls Microsoft Graph. diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeReader.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeReader.cs index 0461f2c3e..3ccb53fb2 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeReader.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeReader.cs @@ -74,8 +74,10 @@ private static void ProcessProject( private static void PostProcessWebUris(ProjectAuthenticationSettings projectAuthenticationSettings) { - bool isBlazorWasm = projectAuthenticationSettings.ApplicationParameters.IsBlazorWasm - && !projectAuthenticationSettings.ApplicationParameters.IsWebApp; + bool isBlazorWasm = projectAuthenticationSettings.ApplicationParameters.IsBlazorWasm.HasValue && + projectAuthenticationSettings.ApplicationParameters.IsBlazorWasm.Value && + projectAuthenticationSettings.ApplicationParameters.IsWebApp.HasValue && + !projectAuthenticationSettings.ApplicationParameters.IsWebApp.Value; string callbackPath = projectAuthenticationSettings.ApplicationParameters.CallbackPath ?? "/signin-oidc"; if (isBlazorWasm) { diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeWriter.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeWriter.cs index 8f37015fd..b3bd02660 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeWriter.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeWriter.cs @@ -2,18 +2,18 @@ // Licensed under the MIT License. using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using Microsoft.DotNet.MSIdentity.AuthenticationParameters; using Microsoft.DotNet.MSIdentity.Project; +using Microsoft.DotNet.MSIdentity.Tool; using Microsoft.Extensions.Internal; namespace Microsoft.DotNet.MSIdentity.CodeReaderWriter { public static class CodeWriter { - internal static void WriteConfiguration(Summary summary, IEnumerable replacements, ApplicationParameters reconciledApplicationParameters, bool jsonOutput) + internal static void WriteConfiguration(Summary summary, IEnumerable replacements, ApplicationParameters reconciledApplicationParameters, IConsoleLogger consoleLogger) { foreach (var replacementsInFile in replacements.GroupBy(r => r.FilePath)) { @@ -23,7 +23,7 @@ internal static void WriteConfiguration(Summary summary, IEnumerable r.Index)) { - string? replaceBy = ComputeReplacement(r.ReplaceBy, reconciledApplicationParameters, jsonOutput); + string? replaceBy = ComputeReplacement(r.ReplaceBy, reconciledApplicationParameters, consoleLogger); if (replaceBy != null && replaceBy!=r.ReplaceFrom) { int index = fileContent.IndexOf(r.ReplaceFrom /*, r.Index*/); @@ -51,16 +51,16 @@ internal static void WriteConfiguration(Summary summary, IEnumerable(); var output = new List(); @@ -75,11 +75,7 @@ public static void InitUserSecrets(string projectPath, bool jsonOutput) } arguments.Add("init"); - - if (!jsonOutput) - { - Console.Write("\nInitializing User Secrets . . . "); - } + consoleLogger.LogMessage("\nInitializing User Secrets . . . ", LogMessageType.Error); var result = Command.CreateDotNet( "user-secrets", @@ -90,22 +86,16 @@ public static void InitUserSecrets(string projectPath, bool jsonOutput) if (result.ExitCode != 0) { - if (!jsonOutput) - { - Console.Write("FAILED\n"); - } + consoleLogger.LogMessage("FAILED\n", LogMessageType.Error, removeNewLine: true); throw new Exception("Error while running dotnet-user-secrets init"); } else { - if (!jsonOutput) - { - Console.Write("SUCCESS\n"); - } + consoleLogger.LogMessage("SUCCESS\n", removeNewLine: true); } } - public static void AddPackage(string packageName, string packageVersion, string tfm, bool jsonOutput) + public static void AddPackage(string packageName, string packageVersion, string tfm, IConsoleLogger consoleLogger) { if (!string.IsNullOrEmpty(packageName) && ((!string.IsNullOrEmpty(packageVersion)) || (!string.IsNullOrEmpty(tfm)))) { @@ -130,10 +120,8 @@ public static void AddPackage(string packageName, string packageVersion, string arguments.Add("-f"); arguments.Add(tfm); } - if (!jsonOutput) - { - Console.Write($"\nAdding package {packageName} . . . "); - } + + consoleLogger.LogMessage($"\nAdding package {packageName} . . . "); var result = Command.CreateDotNet( "add", @@ -144,19 +132,12 @@ public static void AddPackage(string packageName, string packageVersion, string if (result.ExitCode != 0) { - if (!jsonOutput) - { - Console.Write("FAILED\n"); - Console.WriteLine($"Failed to add package {packageName}"); - } + consoleLogger.LogMessage("FAILED\n", removeNewLine: true); + consoleLogger.LogMessage($"Failed to add package {packageName}"); } else { - if (!jsonOutput) - { - Console.Write("SUCCESS\n"); - } - + consoleLogger.LogMessage("SUCCESS\n"); } } } @@ -166,7 +147,7 @@ private static bool IsTfmPreRelease(string tfm) return tfm.Equals("net6.0", StringComparison.OrdinalIgnoreCase); } - private static void SetUserSecerets(string projectPath, string key, string value, bool jsonOutput) + private static void SetUserSecerets(string projectPath, string key, string value, IConsoleLogger consoleLogger) { var errors = new List(); var output = new List(); @@ -196,14 +177,11 @@ private static void SetUserSecerets(string projectPath, string key, string value } else { - if (!jsonOutput) - { - Console.WriteLine($"\nAdded {key} to user secrets.\n"); - } + consoleLogger.LogMessage($"\nAdded {key} to user secrets.\n"); } } - private static string? ComputeReplacement(string replaceBy, ApplicationParameters reconciledApplicationParameters, bool jsonOutput) + private static string? ComputeReplacement(string replaceBy, ApplicationParameters reconciledApplicationParameters, IConsoleLogger consoleLogger) { string? replacement = replaceBy; switch(replaceBy) @@ -212,7 +190,7 @@ private static void SetUserSecerets(string projectPath, string key, string value string? password = reconciledApplicationParameters.PasswordCredentials.LastOrDefault(); if (!string.IsNullOrEmpty(reconciledApplicationParameters.SecretsId) && !string.IsNullOrEmpty(password)) { - AddUserSecrets(reconciledApplicationParameters.IsB2C, reconciledApplicationParameters.ProjectPath ?? string.Empty, password, jsonOutput); + AddUserSecrets(reconciledApplicationParameters.IsB2C, reconciledApplicationParameters.ProjectPath ?? string.Empty, password, consoleLogger); } else { diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs index b06404044..04aaf9bb9 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs @@ -19,16 +19,20 @@ public class MicrosoftIdentityPlatformApplicationManager GraphServiceClient? _graphServiceClient; - internal async Task CreateNewApp( + internal async Task CreateNewAppAsync( TokenCredential tokenCredential, ApplicationParameters applicationParameters, - bool jsonOutput) + IConsoleLogger consoleLogger, + string commandName) { var graphServiceClient = GetGraphServiceClient(tokenCredential); // Get the tenant Organization? tenant = await GetTenant(graphServiceClient); - + if (tenant != null && tenant.TenantType.Equals("AAD B2C", StringComparison.OrdinalIgnoreCase)) + { + applicationParameters.IsB2C = true; + } // Create the app. Application application = new Application() { @@ -37,7 +41,7 @@ internal async Task CreateNewApp( Description = applicationParameters.Description }; - if (applicationParameters.IsWebApi) + if (applicationParameters.IsWebApi.GetValueOrDefault()) { application.Api = new ApiApplication() { @@ -45,11 +49,11 @@ internal async Task CreateNewApp( }; } - if (applicationParameters.IsWebApp) + if (applicationParameters.IsWebApp.GetValueOrDefault()) { AddWebAppPlatform(applicationParameters, application); } - else if (applicationParameters.IsBlazorWasm) + else if (applicationParameters.IsBlazorWasm.GetValueOrDefault()) { // In .NET Core 3.1, Blazor uses MSAL.js 1.x (web redirect URIs) // whereas in .NET 5.0, Blazor uses MSAL.js 2.x (SPA redirect URIs) @@ -96,14 +100,14 @@ await AddAdminConsentToApiPermissions( // For web API, we need to know the appId of the created app to compute the Identifier URI, // and therefore we need to do it after the app is created (updating the app) - if (applicationParameters.IsWebApi + if (applicationParameters.IsWebApi.GetValueOrDefault() && createdApplication.Api != null && (createdApplication.IdentifierUris == null || !createdApplication.IdentifierUris.Any())) { await ExposeScopes(graphServiceClient, createdApplication); // Blazorwasm hosted: add permission to server web API from client SPA - if (applicationParameters.IsBlazorWasm) + if (applicationParameters.IsBlazorWasm.GetValueOrDefault()) { await AddApiPermissionFromBlazorwasmHostedSpaToServerApi( graphServiceClient, @@ -112,25 +116,39 @@ await AddApiPermissionFromBlazorwasmHostedSpaToServerApi( applicationParameters.IsB2C); } } - + ApplicationParameters? effectiveApplicationParameters = null; // Re-reading the app to be sure to have everything. createdApplication = (await graphServiceClient.Applications .Request() .Filter($"appId eq '{createdApplication.AppId}'") .GetAsync()).First(); - var effectiveApplicationParameters = GetEffectiveApplicationParameters(tenant!, createdApplication, applicationParameters); + //log json console message here since we need the Microsoft.Graph.Application + JsonResponse jsonResponse = new JsonResponse(commandName); + if (createdApplication != null) + { + jsonResponse.State = State.Success; + jsonResponse.Content = createdApplication; + effectiveApplicationParameters = GetEffectiveApplicationParameters(tenant!, createdApplication, applicationParameters); - // Add password credentials - if (applicationParameters.CallsMicrosoftGraph || applicationParameters.CallsDownstreamApi) + // Add password credentials + if (applicationParameters.CallsMicrosoftGraph || applicationParameters.CallsDownstreamApi) + { + await AddPasswordCredentialsAsync( + graphServiceClient, + createdApplication.Id, + effectiveApplicationParameters, + consoleLogger); + } + + } + else { - await AddPasswordCredentials( - graphServiceClient, - createdApplication.Id, - effectiveApplicationParameters, - jsonOutput); + jsonResponse.State = State.Fail; + jsonResponse.Content = "Failed to create Azure AD/AD B2C app registration"; + consoleLogger.LogJsonMessage(jsonResponse); } - + consoleLogger.LogJsonMessage(jsonResponse); return effectiveApplicationParameters; } @@ -166,18 +184,20 @@ await AddPasswordCredentials( return tenant; } - internal async Task UpdateApplication(TokenCredential tokenCredential, ApplicationParameters? reconcialedApplicationParameters, ProvisioningToolOptions toolOptions) + internal async Task UpdateApplication(TokenCredential tokenCredential, ApplicationParameters? reconciledApplicationParameters, ProvisioningToolOptions toolOptions) { bool updateStatus = false; - if (reconcialedApplicationParameters != null) + if (reconciledApplicationParameters != null) { var graphServiceClient = GetGraphServiceClient(tokenCredential); var existingApplication = (await graphServiceClient.Applications .Request() - .Filter($"appId eq '{reconcialedApplicationParameters.ClientId}'") + .Filter($"appId eq '{reconciledApplicationParameters.ClientId}'") .GetAsync()).First(); + bool needsUpdate = false; + // Updates the redirect URIs if (existingApplication.Web == null) { @@ -192,44 +212,55 @@ internal async Task UpdateApplication(TokenCredential tokenCredential, App //update redirect uris List existingRedirectUris = updatedApp.Web.RedirectUris.ToList(); List urisToEnsure = ValidateUris(toolOptions.RedirectUris).ToList(); + int originalUrisCount = existingRedirectUris.Count; existingRedirectUris.AddRange(urisToEnsure); updatedApp.Web.RedirectUris = existingRedirectUris.Distinct(); - - //update implicit grant settings + if (updatedApp.Web.RedirectUris.Count() > originalUrisCount) + { + needsUpdate = true; + } + if (updatedApp.Web.ImplicitGrantSettings == null) { updatedApp.Web.ImplicitGrantSettings = new ImplicitGrantSettings(); } - if (toolOptions.EnableAccessToken.HasValue) + //update implicit grant settings if need be. + if (toolOptions.EnableAccessToken.HasValue && (toolOptions.EnableAccessToken.Value != updatedApp.Web.ImplicitGrantSettings.EnableAccessTokenIssuance)) { + needsUpdate = true; updatedApp.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = toolOptions.EnableAccessToken.Value; } - if (toolOptions.EnableIdToken.HasValue) + if (toolOptions.EnableIdToken.HasValue && (toolOptions.EnableIdToken.Value != updatedApp.Web.ImplicitGrantSettings.EnableIdTokenIssuance)) { + needsUpdate = true; updatedApp.Web.ImplicitGrantSettings.EnableIdTokenIssuance = toolOptions.EnableIdToken.Value; } + - // TODO: update other fields. - // See https://github.com/jmprieur/app-provisonning-tool/issues/10 - try - { - await graphServiceClient.Applications[existingApplication.Id] - .Request() - .UpdateAsync(updatedApp).ConfigureAwait(false); - updateStatus = true; - } - //TODO update exception - catch (Exception) + if (needsUpdate) { + try + { + // TODO: update other fields. + // See https://github.com/jmprieur/app-provisonning-tool/issues/10 + await graphServiceClient.Applications[existingApplication.Id] + .Request() + .UpdateAsync(updatedApp).ConfigureAwait(false); + updateStatus = true; + } + //TODO update exception + catch (ServiceException) + { + updateStatus = false; + } } } return updateStatus; } //checks for valid https uris. - //TODO Unit test internal static IList ValidateUris(IList redirectUris) { IList validUris = new List(); @@ -301,11 +332,11 @@ await graphServiceClient.Oauth2PermissionGrants /// /// /// - internal static async Task AddPasswordCredentials( + internal static async Task AddPasswordCredentialsAsync( GraphServiceClient graphServiceClient, string applicatonId, ApplicationParameters effectiveApplicationParameters, - bool jsonOutput) + IConsoleLogger consoleLogger) { string? password = string.Empty; var passwordCredential = new PasswordCredential @@ -326,10 +357,7 @@ internal static async Task AddPasswordCredentials( } catch (ServiceException se) { - if (!jsonOutput) - { - Console.Error.WriteLine($"Failed to create password : {se.Error.Message}"); - } + consoleLogger.LogMessage($"Failed to create password : {se.Error.Message}", LogMessageType.Error); } } return password; @@ -485,7 +513,8 @@ private static void AddWebAppPlatform(ApplicationParameters applicationParameter // Explicit usage of MicrosoftGraph openid and offline_access, in the case // of Azure AD B2C. - if (applicationParameters.IsB2C && applicationParameters.IsWebApp || applicationParameters.IsBlazorWasm) + if (applicationParameters.IsB2C && (applicationParameters.IsWebApp.HasValue && applicationParameters.IsWebApp.Value) + || (applicationParameters.IsBlazorWasm.HasValue && applicationParameters.IsBlazorWasm.Value)) { if (applicationParameters.CalledApiScopes == null) { @@ -588,25 +617,33 @@ private string AppParameterAudienceToMicrosoftIdentityPlatformAppAudience(string }; } - internal async Task Unregister(TokenCredential tokenCredential, ApplicationParameters applicationParameters) + internal async Task UnregisterAsync(TokenCredential tokenCredential, ApplicationParameters applicationParameters) { + bool unregisterSuccess = false; var graphServiceClient = GetGraphServiceClient(tokenCredential); - var apps = await graphServiceClient.Applications - .Request() - .Filter($"appId eq '{applicationParameters.ClientId}'") - .GetAsync(); + var readApplication = (await graphServiceClient.Applications + .Request() + .Filter($"appId eq '{applicationParameters.ClientId}'") + .GetAsync()).FirstOrDefault(); - var readApplication = apps.FirstOrDefault(); if (readApplication != null) { - var clientId = readApplication.Id; - await graphServiceClient.Applications[$"{readApplication.Id}"] - .Request() - .DeleteAsync(); - - Console.WriteLine($"Unregistered the Azure AD w/ client id = {clientId}\n"); + try + { + var clientId = readApplication.Id; + await graphServiceClient.Applications[$"{readApplication.Id}"] + .Request() + .DeleteAsync(); + unregisterSuccess = true; + } + catch (ServiceException) + { + unregisterSuccess = false; + } } + + return unregisterSuccess; } internal GraphServiceClient GetGraphServiceClient(TokenCredential tokenCredential) diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs index 60dac2b87..143aaeb1c 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs @@ -4,10 +4,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Azure.Core; -using Microsoft.CodeAnalysis; using Microsoft.DotNet.MSIdentity.Properties; using Microsoft.DotNet.MSIdentity.AuthenticationParameters; using Microsoft.DotNet.MSIdentity.CodeReaderWriter; @@ -15,7 +13,6 @@ using Microsoft.DotNet.MSIdentity.MicrosoftIdentityPlatformApplication; using Microsoft.DotNet.MSIdentity.Project; using Microsoft.DotNet.MSIdentity.Tool; -using Microsoft.Graph; using Newtonsoft.Json.Linq; using Directory = System.IO.Directory; using File = System.IO.File; @@ -28,6 +25,7 @@ namespace Microsoft.DotNet.MSIdentity /// public class AppProvisioningTool : IMsAADTool { + internal IConsoleLogger ConsoleLogger { get; } private ProvisioningToolOptions ProvisioningToolOptions { get; set; } private string CommandName { get; } @@ -40,6 +38,7 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision { CommandName = commandName; ProvisioningToolOptions = provisioningToolOptions; + ConsoleLogger = new ConsoleLogger(ProvisioningToolOptions.Json); } public async Task Run() @@ -50,11 +49,25 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision var csProjfiles = Directory.EnumerateFiles(ProvisioningToolOptions.ProjectPath, "*.csproj"); if (csProjfiles.Any()) { + if (csProjfiles.Count() > 1) + { + string errorMsg = "Specify one .csproj file for the --project-path"; + ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, State.Fail, errorMsg)); + ConsoleLogger.LogMessage(errorMsg, LogMessageType.Error); + return null; + } var filePath = csProjfiles.First(); ProvisioningToolOptions.ProjectFilePath = filePath; } } + string currentDirectory = Directory.GetCurrentDirectory(); + //if its current directory, update it using the ProjectPath + if (ProvisioningToolOptions.ProjectPath.Equals(currentDirectory, StringComparison.OrdinalIgnoreCase)) + { + ProvisioningToolOptions.ProjectPath = Path.GetDirectoryName(ProvisioningToolOptions.ProjectFilePath) ?? currentDirectory; + } + //get appsettings.json file path var appSettingsFile = Directory.EnumerateFiles(ProvisioningToolOptions.ProjectPath, "appsettings.json"); if (appSettingsFile.Any()) @@ -63,9 +76,23 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision ProvisioningToolOptions.AppSettingsFilePath = filePath; } + ProjectDescription? projectDescription = ProjectDescriptionReader.GetProjectDescription( + ProvisioningToolOptions.ProjectTypeIdentifier, + ProvisioningToolOptions.ProjectPath); + + if (projectDescription == null) + { + ConsoleLogger.LogMessage($"No project found in {ProvisioningToolOptions.ProjectPath}.", LogMessageType.Error); + } + else + { + ConsoleLogger.LogMessage($"Detected project type {projectDescription.Identifier}."); + } + ProjectAuthenticationSettings projectSettings = InferApplicationParameters( ProvisioningToolOptions, - ProjectDescriptionReader.projectDescriptions); + ProjectDescriptionReader.projectDescriptions, + projectDescription); // Get developer credentials TokenCredential tokenCredential = GetTokenCredential( @@ -76,51 +103,17 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision //TODO: switch case to handle all the different commands. ApplicationParameters? applicationParameters = null; - switch (CommandName) - { - case Commands.UPDATE_PROJECT_COMMAND: - applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); - await UpdateProject(tokenCredential, applicationParameters); - return applicationParameters; - - case Commands.UPDATE_APPLICATION_COMMAND: - applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); - await UpdateApplication(tokenCredential, applicationParameters); - return applicationParameters; - - case Commands.UNREGISTER_APPLICATION_COMMAND: - await UnregisterApplication(tokenCredential, projectSettings.ApplicationParameters); - return null; - - case Commands.ADD_CLIENT_SECRET: - applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); - await AddClientSecret(tokenCredential, applicationParameters); - return applicationParameters; - - } - // If needed, infer project type from code - ProjectDescription? projectDescription = ProjectDescriptionReader.GetProjectDescription( - ProvisioningToolOptions.ProjectTypeIdentifier, - ProvisioningToolOptions.ProjectPath); - - if (projectDescription == null) - { - Console.WriteLine($"The code in {ProvisioningToolOptions.ProjectPath} wasn't recognized as supported by the tool. Rerun with --help for details."); - return null; - } - else - { - Console.WriteLine($"Detected project type {projectDescription.Identifier}. "); - } - // Case of a blazorwasm hosted application. We need to create two applications: // - the hosted web API // - the SPA. - if (projectSettings.ApplicationParameters.IsBlazorWasm && projectSettings.ApplicationParameters.IsWebApi) + if (projectSettings.ApplicationParameters.IsBlazorWasm.HasValue && projectSettings.ApplicationParameters.IsBlazorWasm.Value + && projectSettings.ApplicationParameters.IsWebApi.HasValue && projectSettings.ApplicationParameters.IsWebApi.Value) { // Processes the hosted web API ProvisioningToolOptions provisioningToolOptionsBlazorServer = ProvisioningToolOptions.Clone(); provisioningToolOptionsBlazorServer.ProjectPath = Path.Combine(ProvisioningToolOptions.ProjectPath, "Server"); + provisioningToolOptionsBlazorServer.AppDisplayName = string.Concat(provisioningToolOptionsBlazorServer.AppDisplayName ?? projectSettings.ApplicationParameters.ApplicationDisplayName, "-Server"); + provisioningToolOptionsBlazorServer.ProjectType = string.Empty; provisioningToolOptionsBlazorServer.ClientId = ProvisioningToolOptions.WebApiClientId; provisioningToolOptionsBlazorServer.WebApiClientId = null; AppProvisioningTool appProvisioningToolBlazorServer = new AppProvisioningTool(CommandName, provisioningToolOptionsBlazorServer); @@ -129,6 +122,8 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision /// Processes the Blazorwasm client ProvisioningToolOptions provisioningToolOptionsBlazorClient = ProvisioningToolOptions.Clone(); provisioningToolOptionsBlazorClient.ProjectPath = Path.Combine(ProvisioningToolOptions.ProjectPath, "Client"); + provisioningToolOptionsBlazorClient.AppDisplayName = string.Concat(provisioningToolOptionsBlazorClient.AppDisplayName ?? projectSettings.ApplicationParameters.ApplicationDisplayName, "-Client"); + provisioningToolOptionsBlazorClient.ProjectType = string.Empty; provisioningToolOptionsBlazorClient.WebApiClientId = applicationParametersServer?.ClientId; provisioningToolOptionsBlazorClient.AppIdUri = applicationParametersServer?.AppIdUri; provisioningToolOptionsBlazorClient.CalledApiScopes = $"{applicationParametersServer?.AppIdUri}/access_as_user"; @@ -136,17 +131,45 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision return await appProvisioningToolBlazorClient.Run(); } + switch (CommandName) + { + case Commands.UPDATE_PROJECT_COMMAND: + applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); + await UpdateProject(tokenCredential, applicationParameters); + return applicationParameters; + + case Commands.UPDATE_APP_REGISTRATION_COMMAND: + applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); + await UpdateApplication(tokenCredential, applicationParameters); + return applicationParameters; + + case Commands.UNREGISTER_APPLICATION_COMMAND: + await UnregisterApplication(tokenCredential, projectSettings.ApplicationParameters); + return null; + + case Commands.CREATE_APP_REGISTRATION_COMMAND: + return await CreateAppRegistration(tokenCredential, projectSettings.ApplicationParameters); + + case Commands.ADD_CLIENT_SECRET: + applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); + await AddClientSecret(tokenCredential, applicationParameters); + return applicationParameters; + } + // Case where the developer wants to have a B2C application, but the created application is an AAD one. The // tool needs to convert it if (!projectSettings.ApplicationParameters.IsB2C && !string.IsNullOrEmpty(ProvisioningToolOptions.SusiPolicyId)) { - projectSettings = ConvertAadApplicationToB2CApplication(projectDescription, projectSettings); + if (projectDescription != null) + { + projectSettings = ConvertAadApplicationToB2CApplication(projectDescription, projectSettings); + } } // Case where there is no code for the authentication if (!projectSettings.ApplicationParameters.HasAuthentication) { - Console.WriteLine($"Authentication is not enabled yet in this project. An app registration will " + + ConsoleLogger.LogMessage($"Authentication is not enabled yet in this project. An app registration will " + $"be created, but the tool does not add the code yet (work in progress). "); } @@ -186,6 +209,25 @@ await WriteApplicationRegistration( return effectiveApplicationParameters; } + private async Task CreateAppRegistration(TokenCredential tokenCredential, ApplicationParameters? applicationParameters) + { + ApplicationParameters? resultAppParameters = null; + if (applicationParameters != null) + { + resultAppParameters = await MicrosoftIdentityPlatformApplicationManager.CreateNewAppAsync(tokenCredential, applicationParameters, ConsoleLogger, CommandName); + if (resultAppParameters != null && !string.IsNullOrEmpty(resultAppParameters.ClientId)) + { + ConsoleLogger.LogMessage($"Created app {resultAppParameters.ApplicationDisplayName} - {resultAppParameters.ClientId}."); + } + else + { + string failMessage = "Failed to create Azure AD/AD B2C app"; + ConsoleLogger.LogMessage(failMessage, LogMessageType.Error); + } + } + return resultAppParameters; + } + // add 'AzureAd', 'MicrosoftGraph' or 'DownstreamAPI' sections as appropriate. Fill them default values if empty. // Default values can be found https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content private void ModifyAppSettings(ApplicationParameters applicationParameters) @@ -344,7 +386,7 @@ private ProjectAuthenticationSettings ConvertAadApplicationToB2CApplication(Proj if (projectSettings.ApplicationParameters.CallsMicrosoftGraph) { - Console.WriteLine("You'll need to remove the calls to Microsoft Graph as it's not supported by B2C apps."); + ConsoleLogger.LogMessage("You'll need to remove the calls to Microsoft Graph as it's not supported by B2C apps.", LogMessageType.Error); } // reevaulate the project settings @@ -357,10 +399,10 @@ private ProjectAuthenticationSettings ConvertAadApplicationToB2CApplication(Proj private void WriteSummary(Summary summary) { - Console.WriteLine("Summary"); + ConsoleLogger.LogMessage("Summary"); foreach (Change change in summary.changes) { - Console.WriteLine($"{change.Description}"); + ConsoleLogger.LogMessage($"{change.Description}"); } } @@ -372,7 +414,7 @@ private async Task WriteApplicationRegistration(Summary summary, ApplicationPara private void WriteProjectConfiguration(Summary summary, ProjectAuthenticationSettings projectSettings, ApplicationParameters reconcialedApplicationParameters) { - CodeWriter.WriteConfiguration(summary, projectSettings.Replacements, reconcialedApplicationParameters, ProvisioningToolOptions.Json); + CodeWriter.WriteConfiguration(summary, projectSettings.Replacements, reconcialedApplicationParameters, ConsoleLogger); } private bool Reconciliate(ApplicationParameters applicationParameters, ApplicationParameters effectiveApplicationParameters) @@ -408,7 +450,7 @@ private bool Reconciliate(ApplicationParameters applicationParameters, Applicati currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.ReadApplication(tokenCredential, applicationParameters); if (currentApplicationParameters == null) { - Console.Write($"Couldn't find app {applicationParameters.EffectiveClientId} in tenant {applicationParameters.EffectiveTenantId}. "); + ConsoleLogger.LogMessage($"Couldn't find app {applicationParameters.EffectiveClientId} in tenant {applicationParameters.EffectiveTenantId}. ", LogMessageType.Error); } } return currentApplicationParameters; @@ -424,14 +466,21 @@ private bool Reconciliate(ApplicationParameters applicationParameters, Applicati currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.ReadApplication(tokenCredential, applicationParameters); if (currentApplicationParameters == null) { - Console.Write($"Couldn't find app {applicationParameters.EffectiveClientId} in tenant {applicationParameters.EffectiveTenantId}. "); + ConsoleLogger.LogMessage($"Couldn't find app {applicationParameters.EffectiveClientId} in tenant {applicationParameters.EffectiveTenantId}. ", LogMessageType.Error); } } if (currentApplicationParameters == null && !ProvisioningToolOptions.Unregister) { - currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.CreateNewApp(tokenCredential, applicationParameters, ProvisioningToolOptions.Json); - Console.Write($"Created app {currentApplicationParameters.ClientId}. "); + currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.CreateNewAppAsync(tokenCredential, applicationParameters, ConsoleLogger, CommandName); + if (currentApplicationParameters != null) + { + ConsoleLogger.LogMessage($"Created app {currentApplicationParameters.ApplicationDisplayName} - {currentApplicationParameters.ClientId}. "); + } + else + { + ConsoleLogger.LogMessage("Failed to create Azure AD/AD B2C app registration", LogMessageType.Error); + } } return currentApplicationParameters; } @@ -449,10 +498,31 @@ private ProjectAuthenticationSettings InferApplicationParameters( } // Override with the tools options - projectSettings.ApplicationParameters.ApplicationDisplayName ??= Path.GetFileName(provisioningToolOptions.ProjectPath); + projectSettings.ApplicationParameters.ApplicationDisplayName ??= !string.IsNullOrEmpty(provisioningToolOptions.AppDisplayName) ? provisioningToolOptions.AppDisplayName : Path.GetFileName(provisioningToolOptions.ProjectPath); projectSettings.ApplicationParameters.ClientId = !string.IsNullOrEmpty(provisioningToolOptions.ClientId) ? provisioningToolOptions.ClientId : projectSettings.ApplicationParameters.ClientId; projectSettings.ApplicationParameters.TenantId = !string.IsNullOrEmpty(provisioningToolOptions.TenantId) ? provisioningToolOptions.TenantId : projectSettings.ApplicationParameters.TenantId; projectSettings.ApplicationParameters.CalledApiScopes = !string.IsNullOrEmpty(provisioningToolOptions.CalledApiScopes) ? provisioningToolOptions.CalledApiScopes : projectSettings.ApplicationParameters.CalledApiScopes; + + //there can mutliple project types + if (!string.IsNullOrEmpty(provisioningToolOptions.ProjectType)) + { + if (provisioningToolOptions.ProjectType.Equals("webapp", StringComparison.OrdinalIgnoreCase)) + { + projectSettings.ApplicationParameters.IsWebApp = projectSettings.ApplicationParameters.IsWebApp ?? true; + } + if (provisioningToolOptions.ProjectType.Equals("webapi", StringComparison.OrdinalIgnoreCase)) + { + projectSettings.ApplicationParameters.IsWebApi = projectSettings.ApplicationParameters.IsWebApi ?? true; + } + if (provisioningToolOptions.ProjectType.Equals("blazorwasm", StringComparison.OrdinalIgnoreCase)) + { + projectSettings.ApplicationParameters.IsBlazorWasm = projectSettings.ApplicationParameters.IsBlazorWasm ?? true; + } + if (provisioningToolOptions.ProjectType.Equals("blazorwasm-hosted", StringComparison.OrdinalIgnoreCase)) + { + projectSettings.ApplicationParameters.IsBlazorWasm = projectSettings.ApplicationParameters.IsBlazorWasm ?? true; + } + } if (!string.IsNullOrEmpty(provisioningToolOptions.AppIdUri)) { projectSettings.ApplicationParameters.AppIdUri = provisioningToolOptions.AppIdUri; @@ -470,7 +540,24 @@ private TokenCredential GetTokenCredential(ProvisioningToolOptions provisioningT private async Task UnregisterApplication(TokenCredential tokenCredential, ApplicationParameters applicationParameters) { - await MicrosoftIdentityPlatformApplicationManager.Unregister(tokenCredential, applicationParameters); + bool unregisterSuccess = await MicrosoftIdentityPlatformApplicationManager.UnregisterAsync(tokenCredential, applicationParameters); + JsonResponse jsonResponse = new JsonResponse(CommandName); + if (unregisterSuccess) + { + string outputMessage = $"Unregistered the Azure AD w/ client id = {applicationParameters.ClientId}\n"; + jsonResponse.State = State.Success; + jsonResponse.Content = outputMessage; + ConsoleLogger.LogMessage(outputMessage); + ConsoleLogger.LogJsonMessage(jsonResponse); + } + else + { + string outputMessage = $"Unable to unregister the Azure AD w/ client id = {applicationParameters.ClientId}\n"; + jsonResponse.State = State.Fail; + jsonResponse.Content = outputMessage; + ConsoleLogger.LogMessage(outputMessage); + ConsoleLogger.LogJsonMessage(jsonResponse); + } } private async Task UpdateApplication(TokenCredential tokenCredential, ApplicationParameters? applicationParameters) @@ -497,28 +584,28 @@ private async Task UpdateApplication(TokenCredential tokenCredential, Applicatio jsonResponse.State = State.Fail; } - Console.WriteLine(ProvisioningToolOptions.Json ? jsonResponse.ToJsonString() : jsonResponse.Content); + ConsoleLogger.LogMessage(jsonResponse.Content as string); + ConsoleLogger.LogJsonMessage(jsonResponse); } private async Task AddClientSecret(TokenCredential tokenCredential, ApplicationParameters? applicationParameters) { JsonResponse jsonResponse = new JsonResponse(CommandName); - string outputString; if (applicationParameters != null && !string.IsNullOrEmpty(applicationParameters.GraphEntityId)) { var graphServiceClient = MicrosoftIdentityPlatformApplicationManager.GetGraphServiceClient(tokenCredential); - string? password = await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentials( + string? password = await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentialsAsync( graphServiceClient, applicationParameters.GraphEntityId, applicationParameters, - ProvisioningToolOptions.Json); + ConsoleLogger); //if user wants to update user secrets if (ProvisioningToolOptions.UpdateUserSecrets) { - CodeWriter.AddUserSecrets(applicationParameters.IsB2C, ProvisioningToolOptions.ProjectPath, password, ProvisioningToolOptions.Json); + CodeWriter.AddUserSecrets(applicationParameters.IsB2C, ProvisioningToolOptions.ProjectPath, password, ConsoleLogger); } if (!string.IsNullOrEmpty(password)) @@ -526,7 +613,8 @@ private async Task AddClientSecret(TokenCredential tokenCredential, ApplicationP jsonResponse.State = State.Success; jsonResponse.Content = new KeyValuePair("ClientSecret", password); string secretOutput = $"Client secret - {password}."; - outputString = ProvisioningToolOptions.Json ? jsonResponse.ToJsonString() : secretOutput; + ConsoleLogger.LogMessage(secretOutput); + ConsoleLogger.LogJsonMessage(jsonResponse); } else @@ -534,7 +622,8 @@ private async Task AddClientSecret(TokenCredential tokenCredential, ApplicationP string failedOutput = $"Failed to add client secret for Azure AD app : {applicationParameters.ApplicationDisplayName}({applicationParameters.ClientId})"; jsonResponse.State = State.Fail; jsonResponse.Content = failedOutput; - outputString = ProvisioningToolOptions.Json ? jsonResponse.ToJsonString() : failedOutput; + ConsoleLogger.LogMessage(failedOutput); + ConsoleLogger.LogJsonMessage(jsonResponse); } } else @@ -542,9 +631,9 @@ private async Task AddClientSecret(TokenCredential tokenCredential, ApplicationP string failedOutput = $"Failed to add client secret."; jsonResponse.State = State.Fail; jsonResponse.Content = failedOutput; - outputString = ProvisioningToolOptions.Json ? jsonResponse.ToJsonString() : failedOutput; + ConsoleLogger.LogMessage(failedOutput); + ConsoleLogger.LogJsonMessage(jsonResponse); } - Console.WriteLine(outputString); } private async Task UpdateProject(TokenCredential tokenCredential, ApplicationParameters? applicationParameters) @@ -561,17 +650,17 @@ private async Task UpdateProject(TokenCredential tokenCredential, ApplicationPar //need ClientId and Microsoft.Graph.Application.Id(GraphEntityId) if (graphServiceClient != null && !string.IsNullOrEmpty(applicationParameters.ClientId) && !string.IsNullOrEmpty(applicationParameters.GraphEntityId)) { - await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentials( + await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentialsAsync( graphServiceClient, applicationParameters.GraphEntityId, applicationParameters, - ProvisioningToolOptions.Json); + ConsoleLogger); string? password = applicationParameters.PasswordCredentials.LastOrDefault(); //if user wants to update user secrets if (!string.IsNullOrEmpty(password) && ProvisioningToolOptions.UpdateUserSecrets) { - CodeWriter.AddUserSecrets(applicationParameters.IsB2C, ProvisioningToolOptions.ProjectPath, password, ProvisioningToolOptions.Json); + CodeWriter.AddUserSecrets(applicationParameters.IsB2C, ProvisioningToolOptions.ProjectPath, password, ConsoleLogger); } } } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/Commands.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/Commands.cs index 915e15236..57c8f971f 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/Commands.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/Commands.cs @@ -5,11 +5,11 @@ public class Commands public const string LIST_AAD_APPS_COMMAND = "--list-aad-apps"; public const string LIST_SERVICE_PRINCIPALS_COMMAND = "--list-service-principals"; public const string LIST_TENANTS_COMMAND = "--list-tenants"; - public const string ADD_CLIENT_SECRET = "--add-client-secret"; public const string REGISTER_APPLICATIION_COMMAND = "--register-app"; - public const string UPDATE_APPLICATION_COMMAND = "--update-app-registration"; - public const string UPDATE_PROJECT_COMMAND = "--update-project"; public const string UNREGISTER_APPLICATION_COMMAND = "--unregister-app"; - public const string VALIDATE_APP_PARAMS_COMMAND = "--validate-app-params"; + public const string ADD_CLIENT_SECRET = "--create-client-secret"; + public const string CREATE_APP_REGISTRATION_COMMAND = "--create-app-registration"; + public const string UPDATE_APP_REGISTRATION_COMMAND = "--update-app-registration"; + public const string UPDATE_PROJECT_COMMAND = "--update-project"; } } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ConsoleLogger.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ConsoleLogger.cs new file mode 100644 index 000000000..31ada6a0c --- /dev/null +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ConsoleLogger.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.DotNet.MSIdentity.Tool +{ + internal class ConsoleLogger : IConsoleLogger + { + private bool _jsonOutput; + + public ConsoleLogger(bool jsonOutput = false) + { + _jsonOutput = jsonOutput; + Console.OutputEncoding = Encoding.UTF8; + } + + public void LogMessage(string? message, LogMessageType level, bool removeNewLine = false) + { + //if json output is enabled, don't write to console at all. + if (!_jsonOutput) + { + switch (level) + { + case LogMessageType.Error: + if (removeNewLine) + { + Console.Error.Write(message); + } + else + { + Console.Error.WriteLine(message); + } + break; + case LogMessageType.Information: + if (removeNewLine) + { + Console.Write(message); + } + else + { + Console.WriteLine(message); + } + break; + } + } + } + + public void LogJsonMessage(JsonResponse jsonMessage) + { + if (_jsonOutput) + { + Console.WriteLine(jsonMessage.ToJsonString()); + } + } + + public void LogMessage(string? message, bool removeNewLine = false) + { + LogMessage(message, LogMessageType.Information, removeNewLine); + } + } +} diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/IConsoleLogger.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/IConsoleLogger.cs new file mode 100644 index 000000000..2aebe93b9 --- /dev/null +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/IConsoleLogger.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.DotNet.MSIdentity.Tool +{ + public interface IConsoleLogger + { + void LogMessage(string? message, LogMessageType level, bool removeNewLine = false); + void LogMessage(string? message, bool removeNewLine = false); + void LogJsonMessage(JsonResponse jsonMessage); + } + + public enum LogMessageType + { + Error, + Information + } +} diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/JsonResponse.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/JsonResponse.cs index 023d0bfdb..9dd346ed0 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/JsonResponse.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/JsonResponse.cs @@ -3,7 +3,7 @@ namespace Microsoft.DotNet.MSIdentity.Tool { - internal class JsonResponse + public class JsonResponse { public string Command { get; } public string? State { get; set; } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/MsAADTool.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/MsAADTool.cs index 412c1cd79..cdeb6a0cd 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/MsAADTool.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/MsAADTool.cs @@ -14,6 +14,7 @@ namespace Microsoft.DotNet.MSIdentity.Tool { internal class MsAADTool : IMsAADTool { + internal IConsoleLogger ConsoleLogger { get; } private ProvisioningToolOptions ProvisioningToolOptions { get; set; } private string CommandName { get; } public IGraphServiceClient GraphServiceClient { get; set; } @@ -27,6 +28,7 @@ public MsAADTool(string commandName, ProvisioningToolOptions provisioningToolOpt TokenCredential = new MsalTokenCredential(ProvisioningToolOptions.TenantId, ProvisioningToolOptions.Username); GraphServiceClient = new GraphServiceClient(new TokenCredentialAuthenticationProvider(TokenCredential)); AzureManagementAPI = new AzureManagementAuthenticationProvider(TokenCredential); + ConsoleLogger = new ConsoleLogger(ProvisioningToolOptions.Json); } public async Task Run() diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs index 624efe1a7..91ccaf72f 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs @@ -20,6 +20,11 @@ public class ProvisioningToolOptions : IDeveloperCredentialsOptions /// public string? AppSettingsFilePath { get; set; } + /// + /// Display name for Azure AD/AD B2C app registration + /// + public string? AppDisplayName { get; set; } + /// /// Web redirect URIs. /// @@ -168,7 +173,8 @@ public ProvisioningToolOptions Clone() AppSettingsFilePath = AppSettingsFilePath, WebApiClientId = WebApiClientId, AppIdUri = AppIdUri, - Json = Json + Json = Json, + AppDisplayName = AppDisplayName }; } } diff --git a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.Tests/ProjectDescriptionReaderTests.cs b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.Tests/ProjectDescriptionReaderTests.cs index 50bdba71e..6dd6d28ea 100644 --- a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.Tests/ProjectDescriptionReaderTests.cs +++ b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.Tests/ProjectDescriptionReaderTests.cs @@ -179,7 +179,10 @@ public void TestProjectDescriptionReader_TemplatesWithNoAuth(string folderPath, private void AssertAuthSettings(ProjectAuthenticationSettings authenticationSettings, bool isB2C = false) { - Assert.True(authenticationSettings.ApplicationParameters.IsWebApi || authenticationSettings.ApplicationParameters.IsWebApp || authenticationSettings.ApplicationParameters.IsBlazorWasm); + bool IsWebApi = authenticationSettings.ApplicationParameters.IsWebApi.HasValue && authenticationSettings.ApplicationParameters.IsWebApi.Value; + bool IsWebApp = authenticationSettings.ApplicationParameters.IsWebApp.HasValue && authenticationSettings.ApplicationParameters.IsWebApp.Value; + bool IsBlazorWasm = authenticationSettings.ApplicationParameters.IsBlazorWasm.HasValue && authenticationSettings.ApplicationParameters.IsBlazorWasm.Value; + Assert.True(IsWebApi || IsWebApp || IsBlazorWasm); if (isB2C) { diff --git a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs new file mode 100644 index 000000000..e1985ec0a --- /dev/null +++ b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs @@ -0,0 +1,54 @@ +using Microsoft.DotNet.MSIdentity.MicrosoftIdentityPlatformApplication; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.DotNet.MSIdentity.UnitTests.Tests +{ + public class MicrosoftIdentityPlatformApplicationManagerTests + { + [Theory] + [MemberData(nameof(UriList))] + public void ValidateUrisTests(List urisToValidate, List validUris) + { + var validatedUris = MicrosoftIdentityPlatformApplicationManager.ValidateUris(urisToValidate); + var areEquivalent = (validUris.Count == validatedUris.Count) && !validatedUris.Except(validUris).Any(); + Assert.True(areEquivalent); + } + + public static IEnumerable UriList => + new List + { + new object[] { + new List { + "https://localhost:5001/", + "https://localhost:5002/get", + "http://localhost:5001/", + "https://www.microsoft.com/", + "http://www.azure.com", + "https://www.testapi.com/get/{id}", + "http://www.skype.com", + "http://127.0.0.1/get", + "http://loopback/post", + "badstring", + null, + string.Empty, + "" + }, + + new List { + "https://localhost:5001/", + "https://localhost:5002/get", + "http://localhost:5001/", + "https://www.microsoft.com/", + "https://www.testapi.com/get/{id}", + "http://127.0.0.1/get", + "http://loopback/post", + } + } + }; + } +} diff --git a/tools/dotnet-msidentity/MsAADToolFactory.cs b/tools/dotnet-msidentity/MsAADToolFactory.cs index d5e12001b..4bc1687cb 100644 --- a/tools/dotnet-msidentity/MsAADToolFactory.cs +++ b/tools/dotnet-msidentity/MsAADToolFactory.cs @@ -4,7 +4,7 @@ internal static class MsAADToolFactory { internal static IMsAADTool CreateTool(string commandName, ProvisioningToolOptions provisioningToolOptions) { - switch(commandName) + switch (commandName) { case Commands.LIST_AAD_APPS_COMMAND: case Commands.LIST_SERVICE_PRINCIPALS_COMMAND: diff --git a/tools/dotnet-msidentity/Program.cs b/tools/dotnet-msidentity/Program.cs index 72b72e425..536de7a82 100644 --- a/tools/dotnet-msidentity/Program.cs +++ b/tools/dotnet-msidentity/Program.cs @@ -5,7 +5,6 @@ using System.CommandLine.Builder; using System.CommandLine.Invocation; using System.CommandLine.Parsing; -using System.Diagnostics; using System.Threading.Tasks; namespace Microsoft.DotNet.MSIdentity.Tool @@ -15,27 +14,35 @@ public static class Program public static async Task Main(string []args) { var rootCommand = MsIdentityCommand(); + + //internal commands var listAadAppsCommand = ListAADAppsCommand(); var listServicePrincipalsCommand = ListServicePrincipalsCommand(); var listTenantsCommand = ListTenantsCommand(); + var addClientSecretCommand = AddClientSecretCommand(); + + //exposed commands var registerApplicationCommand = RegisterApplicationCommand(); var unregisterApplicationCommand = UnregisterApplicationCommand(); - var updateApplicationCommand = UpdateApplicationCommand(); + var updateAppRegistrationCommand = UpdateAppRegistrationCommand(); var updateProjectCommand = UpdateProjectCommand(); - var addClientSecretCommand = AddClientSecretCommand(); + var createAppRegistration = CreateAppRegistrationCommand(); + //hide internal commands. listAadAppsCommand.IsHidden = true; listServicePrincipalsCommand.IsHidden = true; listTenantsCommand.IsHidden = true; updateProjectCommand.IsHidden = true; addClientSecretCommand.IsHidden = true; + createAppRegistration.IsHidden = true; listAadAppsCommand.Handler = CommandHandler.Create(HandleListApps); listServicePrincipalsCommand.Handler = CommandHandler.Create(HandleListServicePrincipals); listTenantsCommand.Handler = CommandHandler.Create(HandleListTenants); registerApplicationCommand.Handler = CommandHandler.Create(HandleRegisterApplication); unregisterApplicationCommand.Handler = CommandHandler.Create(HandleUnregisterApplication); - updateApplicationCommand.Handler = CommandHandler.Create(HandleUpdateApplication); + createAppRegistration.Handler = CommandHandler.Create(HandleCreateAppRegistration); + updateAppRegistrationCommand.Handler = CommandHandler.Create(HandleUpdateApplication); updateProjectCommand.Handler = CommandHandler.Create(HandleUpdateProject); addClientSecretCommand.Handler = CommandHandler.Create(HandleClientSecrets); @@ -45,9 +52,10 @@ public static async Task Main(string []args) rootCommand.AddCommand(listTenantsCommand); rootCommand.AddCommand(registerApplicationCommand); rootCommand.AddCommand(unregisterApplicationCommand); - rootCommand.AddCommand(updateApplicationCommand); + rootCommand.AddCommand(updateAppRegistrationCommand); rootCommand.AddCommand(updateProjectCommand); rootCommand.AddCommand(addClientSecretCommand); + rootCommand.AddCommand(createAppRegistration); //if no args are present, show default help. if (args == null || args.Length == 0) @@ -110,7 +118,7 @@ private static async Task HandleUpdateApplication(ProvisioningToolOptions p { if (provisioningToolOptions != null) { - IMsAADTool msAADTool = MsAADToolFactory.CreateTool(Commands.UPDATE_APPLICATION_COMMAND, provisioningToolOptions); + IMsAADTool msAADTool = MsAADToolFactory.CreateTool(Commands.UPDATE_APP_REGISTRATION_COMMAND, provisioningToolOptions); await msAADTool.Run(); return 0; } @@ -128,6 +136,17 @@ private static async Task HandleUnregisterApplication(ProvisioningToolOptio return -1; } + private static async Task HandleCreateAppRegistration(ProvisioningToolOptions provisioningToolOptions) + { + if (provisioningToolOptions != null) + { + IMsAADTool msAADTool = MsAADToolFactory.CreateTool(Commands.CREATE_APP_REGISTRATION_COMMAND, provisioningToolOptions); + await msAADTool.Run(); + return 0; + } + return -1; + } + private static async Task HandleUpdateProject(ProvisioningToolOptions provisioningToolOptions) { if (provisioningToolOptions != null) @@ -185,7 +204,7 @@ private static Command AddClientSecretCommand()=> name: Commands.ADD_CLIENT_SECRET, description: "Create client secret for an Azure AD/AD B2C application.") { - TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), ProjectPathOption(), ProjectFilePathOption(), UpdateUserSecretsOption() + TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), ProjectFilePathOption(), UpdateUserSecretsOption() }; private static Command RegisterApplicationCommand()=> @@ -194,7 +213,7 @@ private static Command RegisterApplicationCommand()=> description: "Register an AAD/AAD B2C application in Azure and updates .NET application." + "\n\t- Updates the appsettings.json file.") { - TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), ClientSecretOption(), AppIdUriOption(), ApiClientIdOption(), SusiPolicyIdOption(), ProjectPathOption(), ProjectFilePathOption() + TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), ClientSecretOption(), AppIdUriOption(), ApiClientIdOption(), SusiPolicyIdOption(), ProjectFilePathOption() }; private static Command UpdateProjectCommand()=> @@ -205,18 +224,25 @@ private static Command UpdateProjectCommand()=> "\n\t- Updates the Startup.cs file." + "\n\t- Updates the user secrets.") { - TenantOption(), UsernameOption(), JsonOption(), ProjectPathOption(), ClientIdOption(), CallsGraphOption(), CallsDownstreamApiOption(), UpdateUserSecretsOption(), ProjectFilePathOption(), RedirectUriOption() + TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), CallsGraphOption(), CallsDownstreamApiOption(), UpdateUserSecretsOption(), ProjectFilePathOption(), RedirectUriOption() }; - private static Command UpdateApplicationCommand() => + private static Command UpdateAppRegistrationCommand() => new Command( - name: Commands.UPDATE_APPLICATION_COMMAND, - description: "Update an AAD/AAD B2C application in Azure." + - "\n\t- Updates the appsettings.json file.") + name: Commands.UPDATE_APP_REGISTRATION_COMMAND, + description: "Update an AAD/AAD B2C application in Azure.") { TenantOption(), UsernameOption(), JsonOption(), AppIdUriOption(), ClientIdOption(), RedirectUriOption(), EnableIdTokenOption(), EnableAccessToken() }; + private static Command CreateAppRegistrationCommand() => + new Command( + name: Commands.CREATE_APP_REGISTRATION_COMMAND, + description: "Create an AAD/AAD B2C application in Azure.") + { + TenantOption(), UsernameOption(), JsonOption(), AppDisplayName(), ProjectFilePathOption(), ProjectType() + }; + private static Command UnregisterApplicationCommand() => new Command( name: Commands.UNREGISTER_APPLICATION_COMMAND, @@ -224,7 +250,7 @@ private static Command UnregisterApplicationCommand() => description: "Unregister an AAD/AAD B2C application in Azure." + "\n\t- Updates the appsettings.json file.") { - TenantOption(), UsernameOption(), JsonOption(), AppIdUriOption(), ProjectPathOption(), ClientIdOption() + TenantOption(), UsernameOption(), JsonOption(), AppIdUriOption(), ProjectFilePathOption(), ClientIdOption() }; private static Option JsonOption()=> @@ -286,6 +312,23 @@ private static Option ClientIdOption()=> IsRequired = false }; + private static Option AppDisplayName() => + new Option( + aliases: new[] { "--app-display-name" }, + description: "App display name for Azure AD/AD B2C app registration creation.") + { + IsRequired = false + }; + + private static Option ProjectType() => + new Option( + aliases: new[] { "--project-type" }, + description: "Project type for which to register the azure ad app registration." + + "\n\tFor eg., 'webapp', 'webapi', 'blazorwasm-hosted', 'blazorwasm'") + { + IsRequired = false + }; + private static Option ClientSecretOption()=> new Option( aliases: new [] {"--client-secret"}, @@ -302,17 +345,9 @@ private static Option RedirectUriOption() => IsRequired = false }; - private static Option ProjectPathOption()=> - new Option( - aliases: new [] {"-p", "--project-path"}, - description: "When specified, will analyze the application code in the specified folder. Otherwise analyzes the code in the current directory..") - { - IsRequired = false - }; - private static Option ProjectFilePathOption()=> new Option( - aliases: new [] {"--project-file-path"}, + aliases: new [] {"-p", "--project-file-path"}, description: "When specified, will analyze the application specified by the csproj file. Otherwise analyzes the csproj in the current directory..") { IsRequired = false