diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs index a52f3b0591..d2f8af167e 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/ApplicationParameters.cs @@ -86,9 +86,6 @@ public string? Domain1 /// Is authenticated with Azure AD B2C (set by reflection). /// public bool IsB2C { get; set; } - - - // TODO: propose a fix for the blazorwasm project template /// /// Sign-up/sign-in policy in the case of B2C. @@ -157,7 +154,7 @@ public string? Domain1 /// /// Identifier URIs for web APIs. /// - public string? AppIdUri { set; get; } + public string? AppIdUri { get; set; } /// /// API permissions. diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeModifierConfigs/cm_dotnet_blazorwasm_client.json b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeModifierConfigs/cm_dotnet_blazorwasm_client.json new file mode 100644 index 0000000000..6ef298c304 --- /dev/null +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/CodeModifierConfigs/cm_dotnet_blazorwasm_client.json @@ -0,0 +1,32 @@ +{ + "Identifier": "dotnet-blazorwasm-client", + "Files": [ + { + "FileName": "Program.cs", + "Options": [ + "MinimalApp" + ], + "Methods": { + "Global": { + "CodeChanges": [ + { + "MultiLineBlock": [ + "{", + " builder.Configuration.Bind(\"AzureAd\", options.ProviderOptions.Authentication);", + " options.ProviderOptions.DefaultAccessTokenScopes.Add(builder.Configuration[\"ServerApi\"]);", + "}" + ], + "Replace": true, + "CodeChangeType": "Lambda", + "Parameter": "options", + "Parent": "builder.Services.AddMsalAuthentication", + "TrailingTrivia": { + "Semicolon": false + } + } + ] + } + } + } + ] +} diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs index f0c82a5ed6..fc07eeeef7 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs @@ -44,7 +44,16 @@ public async Task AddAuthCodeAsync() { if (string.IsNullOrEmpty(_toolOptions.ProjectFilePath)) { - return; + var csProjFiles = _files.Where(file => file.EndsWith(".csproj")); + if (csProjFiles.Count() != 1) + { + var errorMsg = string.Format(Resources.ProjectPathError, _toolOptions.ProjectFilePath); + _consoleLogger.LogJsonMessage(new JsonResponse(Commands.UPDATE_PROJECT_COMMAND, State.Fail, errorMsg)); + _consoleLogger.LogMessage(errorMsg, LogMessageType.Error); + return; + } + + _toolOptions.ProjectFilePath = csProjFiles.First(); } CodeModifierConfig? codeModifierConfig = GetCodeModifierConfig(); @@ -98,7 +107,7 @@ public async Task AddAuthCodeAsync() CodeModifierConfig? codeModifierConfig = ReadCodeModifierConfigFromFileContent(content); if (codeModifierConfig is null) { - throw new FormatException($"Resource file { CodeModifierConfigPropertyInfo.Name } could not be parsed. "); + throw new FormatException(string.Format(Resources.ResourceFileParseError, CodeModifierConfigPropertyInfo.Name)); } if (!string.Equals(codeModifierConfig.Identifier, _toolOptions.ProjectTypeIdentifier, StringComparison.OrdinalIgnoreCase)) @@ -133,7 +142,7 @@ private PropertyInfo? CodeModifierConfigPropertyInfo } catch (Exception e) { - _consoleLogger.LogMessage($"Error parsing Code Modifier Config for project type { _toolOptions.ProjectType }, exception: { e.Message }"); + _consoleLogger.LogMessage(string.Format(Resources.CodeModifierConfigParsingError, _toolOptions.ProjectType, e.Message)); return null; } } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/AppSettingsModifier.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/AppSettingsModifier.cs index 0d5838c014..020c4610aa 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/AppSettingsModifier.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/AppSettingsModifier.cs @@ -41,7 +41,13 @@ public void ModifyAppSettings(ApplicationParameters applicationParameters, IEnum * }; */ - _provisioningToolOptions.AppSettingsFilePath = GetAppSettingsFilePath(files); + if (!File.Exists(_provisioningToolOptions.AppSettingsFilePath)) + { + // If default appsettings file does not exist, try to find it, if not found, create in the default location + var defaultAppSettingsPath = DefaultAppSettingsPath; + _provisioningToolOptions.AppSettingsFilePath = File.Exists(defaultAppSettingsPath) ? defaultAppSettingsPath + : files.FirstOrDefault(f => f.Contains(AppSettingsFileName)) ?? defaultAppSettingsPath; + } JObject appSettings; try @@ -62,26 +68,9 @@ public void ModifyAppSettings(ApplicationParameters applicationParameters, IEnum } } - /// - /// First checks if default appsettings file exists, if not searches for the file. - /// If the file does not exist anywhere, it will be created later. - /// - private string GetAppSettingsFilePath(IEnumerable files) - { - if (!File.Exists(_provisioningToolOptions.AppSettingsFilePath)) - { - // If default appsettings file does not exist, try to find it, if not found, create in the default location - _provisioningToolOptions.AppSettingsFilePath = File.Exists(DefaultAppSettingsPath) - ? DefaultAppSettingsPath - : files.Where(f => f.Contains(AppSettingsFileName)).FirstOrDefault(); - } - - return _provisioningToolOptions.AppSettingsFilePath ??= DefaultAppSettingsPath; - } - private string DefaultAppSettingsPath => _provisioningToolOptions.IsBlazorWasm - ? Path.Combine(_provisioningToolOptions.ProjectPath, "wwwroot", AppSettingsFileName) - : Path.Combine(_provisioningToolOptions.ProjectPath, AppSettingsFileName); + ? Path.Combine(_provisioningToolOptions.ProjectPath, "wwwroot", AppSettingsFileName) + : Path.Combine(_provisioningToolOptions.ProjectPath, AppSettingsFileName); /// /// Modifies AppSettings.json if necessary, helper method for testing @@ -101,7 +90,7 @@ private string GetAppSettingsFilePath(IEnumerable files) appSettings["AzureAd"] = updatedAzureAdBlock; } - if (_provisioningToolOptions.CallsGraph) // TODO blazor + if (_provisioningToolOptions.CallsGraph) { // update MicrosoftGraph Block var microsoftGraphBlock = GetModifiedMicrosoftGraphBlock(appSettings); @@ -112,7 +101,7 @@ private string GetAppSettingsFilePath(IEnumerable files) } } - if (_provisioningToolOptions.CallsDownstreamApi) // TODO blazor + if (_provisioningToolOptions.CallsDownstreamApi) { // update DownstreamAPI Block var updatedDownstreamApiBlock = GetModifiedDownstreamApiBlock(appSettings); @@ -123,6 +112,13 @@ private string GetAppSettingsFilePath(IEnumerable files) } } + if (!string.IsNullOrEmpty(_provisioningToolOptions.HostedApiScopes)) + { + // update ServerAPI Block + changesMade = true; + appSettings["ServerApi"] = _provisioningToolOptions.HostedApiScopes; + } + return changesMade ? appSettings : null; } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs index e226dc5494..dfa8ff6116 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs @@ -60,49 +60,41 @@ public class MicrosoftIdentityPlatformApplicationManager } else if (applicationParameters.IsBlazorWasm) { - // In .NET Core 3.1, Blazor uses MSAL.js 1.x (web redirect URIs) - // whereas in .NET 5.0 and .NET 6.0, Blazor uses MSAL.js 2.x (SPA redirect URIs) - switch (applicationParameters.TargetFramework) - { - case "net5.0": - case "net6.0": - AddSpaPlatform(application, applicationParameters.WebRedirectUris); - break; - default: - AddWebAppPlatform(application, applicationParameters, withImplicitFlow: true); - break; - } + AddSpaPlatform(application, applicationParameters.WebRedirectUris); } - IEnumerable>? scopesPerResource = await AddApiPermissions( - applicationParameters, - graphServiceClient, - application).ConfigureAwait(false); - - Application createdApplication = await graphServiceClient.Applications + var createdApplication = await graphServiceClient.Applications .Request() .AddAsync(application); - // Creates a service principal (needed for B2C) - ServicePrincipal servicePrincipal = new ServicePrincipal + if (applicationParameters.IsB2C) // TODO B2C not fully supported at the moment { - AppId = createdApplication.AppId, - }; + // Creates a service principal (needed for B2C) // TODO: What if it's not B2C? + ServicePrincipal servicePrincipal = new ServicePrincipal + { + AppId = createdApplication.AppId, + }; - // B2C does not allow user consent, and therefore we need to explicity create - // a service principal and permission grants. It's also useful for Blazorwasm hosted - // applications. We create it always. - var createdServicePrincipal = await graphServiceClient.ServicePrincipals - .Request() - .AddAsync(servicePrincipal).ConfigureAwait(false); + // B2C does not allow user consent, and therefore we need to explicity create + // a service principal and permission grants. It's also useful for Blazorwasm hosted + // applications. We create it always. + ServicePrincipal? createdServicePrincipal = await graphServiceClient.ServicePrincipals + .Request() + .AddAsync(servicePrincipal).ConfigureAwait(false); - // B2C does not allow user consent, and therefore we need to explicity grant permissions - if (applicationParameters.IsB2C) - { - await AddAdminConsentToApiPermissions( - graphServiceClient, - createdServicePrincipal, - scopesPerResource); + // B2C does not allow user consent, and therefore we need to explicity grant permissions + if (applicationParameters.IsB2C) + { + IEnumerable>? scopesPerResource = await AddApiPermissions( + applicationParameters, + graphServiceClient, + application).ConfigureAwait(false); + + await AddAdminConsentToApiPermissions( + graphServiceClient, + createdServicePrincipal, + scopesPerResource); + } } // For web API, we need to know the appId of the created app to compute the Identifier URI, @@ -113,48 +105,36 @@ await AddAdminConsentToApiPermissions( { await ExposeScopes(graphServiceClient, createdApplication); - // Blazorwasm hosted: add permission to server web API from client SPA - if (applicationParameters.IsBlazorWasm) - { - await AddApiPermissionFromBlazorwasmHostedSpaToServerApi( - graphServiceClient, - createdApplication, - createdServicePrincipal, - applicationParameters.IsB2C); - } + // Re-reading the app to be sure to have everything. + createdApplication = (await graphServiceClient.Applications + .Request() + .Filter($"appId eq '{createdApplication.AppId}'") + .GetAsync()).FirstOrDefault(); } - 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(); - // 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) - { - await AddPasswordCredentialsAsync( - graphServiceClient, - createdApplication.Id, - effectiveApplicationParameters, - consoleLogger); - } - } - else + if (createdApplication is null) { jsonResponse.State = State.Fail; jsonResponse.Content = Resources.FailedToCreateApp; consoleLogger.LogJsonMessage(jsonResponse); + return null; + } + + ApplicationParameters? effectiveApplicationParameters = GetEffectiveApplicationParameters(tenant!, createdApplication, applicationParameters); + + // Add password credentials + if (applicationParameters.CallsMicrosoftGraph || applicationParameters.CallsDownstreamApi) + { + await AddPasswordCredentialsAsync( + graphServiceClient, + createdApplication.Id, + effectiveApplicationParameters, + consoleLogger); } + jsonResponse.State = State.Success; + jsonResponse.Content = createdApplication; consoleLogger.LogJsonMessage(jsonResponse); return effectiveApplicationParameters; } @@ -222,22 +202,22 @@ internal async Task UpdateApplication( return new JsonResponse(commandName, State.Fail, string.Format(Resources.NotFound, parameters.ClientId)); } - var appUpdates = GetApplicationUpdates(remoteApp, toolOptions); - if (appUpdates != null) + (bool needsUpdates, Application appUpdates) = GetApplicationUpdates(remoteApp, toolOptions); + if (!needsUpdates) { - try - { - // TODO: update other fields, see https://github.com/jmprieur/app-provisonning-tool/issues/10 - await graphServiceClient.Applications[remoteApp.Id].Request().UpdateAsync(appUpdates).ConfigureAwait(false); - return new JsonResponse(commandName, State.Success, string.Format(Resources.SuccessfullyUpdatedApp, remoteApp.DisplayName, remoteApp.AppId)); - } - catch (ServiceException se) - { - return new JsonResponse(commandName, State.Fail, se.Error?.Message); - } + return new JsonResponse(commandName, State.Success, string.Format(Resources.NoUpdateNecessary, remoteApp.DisplayName, remoteApp.AppId)); } - return new JsonResponse(commandName, State.Success, string.Format(Resources.NoUpdateNecessary, remoteApp.DisplayName, remoteApp.AppId)); + try + { + // TODO: update other fields, see https://github.com/jmprieur/app-provisonning-tool/issues/10 + var updatedApp = await graphServiceClient.Applications[remoteApp.Id].Request().UpdateAsync(appUpdates); + return new JsonResponse(commandName, State.Success, string.Format(Resources.SuccessfullyUpdatedApp, remoteApp.DisplayName, remoteApp.AppId)); + } + catch (ServiceException se) + { + return new JsonResponse(commandName, State.Fail, se.Error?.Message); + } } /// @@ -246,7 +226,7 @@ internal async Task UpdateApplication( /// /// /// Updated Application if changes were made, otherwise null - private Application? GetApplicationUpdates(Application existingApplication, ProvisioningToolOptions toolOptions) + internal static (bool needsUpdate, Application appUpdates) GetApplicationUpdates(Application existingApplication, ProvisioningToolOptions toolOptions) { bool needsUpdate = false; @@ -260,8 +240,46 @@ internal async Task UpdateApplication( // Make updates if necessary needsUpdate |= UpdateRedirectUris(updatedApp, toolOptions); needsUpdate |= UpdateImplicitGrantSettings(updatedApp, toolOptions); + if (toolOptions.IsBlazorWasmHostedServer) + { + needsUpdate |= PreAuthorizeBlazorWasmClientApp(existingApplication, toolOptions, updatedApp); + } - return needsUpdate ? updatedApp : null; + return (needsUpdate, updatedApp); + } + + internal static bool PreAuthorizeBlazorWasmClientApp(Application existingApplication, ProvisioningToolOptions toolOptions, Application updatedApp) + { + if (string.IsNullOrEmpty(toolOptions.BlazorWasmClientAppId)) + { + return false; + } + + var delegatedPermissionId = existingApplication.Api?.Oauth2PermissionScopes?.FirstOrDefault()?.Id.ToString(); + if (string.IsNullOrEmpty(delegatedPermissionId)) + { + return false; + } + + if (existingApplication.Api?.PreAuthorizedApplications?.Any( + app => string.Equals(toolOptions.BlazorWasmClientAppId, app.AppId) + && app.DelegatedPermissionIds.Any(id => id.Equals(delegatedPermissionId))) is true) + { + return false; + } + + var preAuthorizedApp = new PreAuthorizedApplication + { + AppId = toolOptions.BlazorWasmClientAppId, + DelegatedPermissionIds = new List { delegatedPermissionId } + }; + + updatedApp.Api = existingApplication.Api ?? new ApiApplication(); + + updatedApp.Api.PreAuthorizedApplications = updatedApp.Api.PreAuthorizedApplications?.Append(preAuthorizedApp) + ?? new List { preAuthorizedApp }; + + return true; } /// @@ -330,22 +348,22 @@ private static string UpdateCallbackPath(string redirectUri, bool isBlazorWasm = } /// - /// Updates implicit grant settings if necessary + /// Updates application's implicit grant settings if necessary /// - /// + /// /// /// true if ImplicitGrantSettings require updates, else false - private bool UpdateImplicitGrantSettings(Application updatedApp, ProvisioningToolOptions toolOptions) + internal static bool UpdateImplicitGrantSettings(Application app, ProvisioningToolOptions toolOptions) { bool needsUpdate = false; - var currentSettings = updatedApp.Web.ImplicitGrantSettings; + var currentSettings = app.Web.ImplicitGrantSettings; if (toolOptions.IsBlazorWasm) // In the case of Blazor WASM, Access Tokens and Id Tokens must both be true. { - if (currentSettings.EnableAccessTokenIssuance != true || currentSettings.EnableIdTokenIssuance != true) + if (currentSettings.EnableAccessTokenIssuance is true || currentSettings.EnableIdTokenIssuance is true) { - updatedApp.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = true; - updatedApp.Web.ImplicitGrantSettings.EnableIdTokenIssuance = true; + app.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = false; + app.Web.ImplicitGrantSettings.EnableIdTokenIssuance = false; needsUpdate = true; } @@ -355,14 +373,14 @@ private bool UpdateImplicitGrantSettings(Application updatedApp, ProvisioningToo if (toolOptions.EnableAccessToken.HasValue && currentSettings.EnableAccessTokenIssuance != toolOptions.EnableAccessToken.Value) { - updatedApp.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = toolOptions.EnableAccessToken.Value; + app.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = toolOptions.EnableAccessToken.Value; needsUpdate = true; } if (toolOptions.EnableIdToken.HasValue && currentSettings.EnableIdTokenIssuance != toolOptions.EnableIdToken.Value) { - updatedApp.Web.ImplicitGrantSettings.EnableIdTokenIssuance = toolOptions.EnableIdToken.Value; + app.Web.ImplicitGrantSettings.EnableIdTokenIssuance = toolOptions.EnableIdToken.Value; needsUpdate = true; } } @@ -373,9 +391,14 @@ private bool UpdateImplicitGrantSettings(Application updatedApp, ProvisioningToo private async Task AddApiPermissionFromBlazorwasmHostedSpaToServerApi( GraphServiceClient graphServiceClient, Application createdApplication, - ServicePrincipal createdServicePrincipal, + ServicePrincipal? createdServicePrincipal, bool isB2C) { + if (createdServicePrincipal is null) + { + throw new ArgumentNullException(nameof(createdServicePrincipal)); + } + var requiredResourceAccess = new List(); var resourcesAccessAndScopes = new List { @@ -459,15 +482,19 @@ internal static async Task AddPasswordCredentialsAsync( /// Expose scopes for the web API. /// /// - /// - /// - private static async Task ExposeScopes(GraphServiceClient graphServiceClient, Application createdApplication) + /// + /// + /// existing scopes + /// Identifier URI for exposed scope + internal static async Task ExposeScopes(GraphServiceClient graphServiceClient, string? appId, string? graphEntityId, List? scopes = null) { + var scope = $"api://{appId}"; var updatedApp = new Application { - IdentifierUris = new[] { $"api://{createdApplication.AppId}" }, + IdentifierUris = new[] { scope } }; - var scopes = createdApplication.Api.Oauth2PermissionScopes?.ToList() ?? new List(); + + scopes ??= new List(); var newScope = new PermissionScope { Id = Guid.NewGuid(), @@ -479,12 +506,26 @@ private static async Task ExposeScopes(GraphServiceClient graphServiceClient, Ap UserConsentDisplayName = "Access the API on your behalf", Value = "access_as_user", }; + scopes.Add(newScope); updatedApp.Api = new ApiApplication { Oauth2PermissionScopes = scopes }; - - await graphServiceClient.Applications[createdApplication.Id] + await graphServiceClient.Applications[graphEntityId] .Request() - .UpdateAsync(updatedApp).ConfigureAwait(false); + .UpdateAsync(updatedApp); + + return scope; + } + + /// + /// Expose scopes for the web API. + /// + /// + /// + /// + internal static async Task ExposeScopes(GraphServiceClient graphServiceClient, Application createdApplication) + { + var scopes = createdApplication.Api.Oauth2PermissionScopes?.ToList() ?? new List(); + await ExposeScopes(graphServiceClient, createdApplication.AppId, createdApplication.Id, scopes); } /// @@ -646,6 +687,7 @@ private async Task AddPermission( .Filter($"AppId eq '{MicrosoftGraphAppId}'") .GetAsync(); } + ServicePrincipal? spWithScopes = spsWithScopes.FirstOrDefault(); if (spWithScopes == null) @@ -673,6 +715,7 @@ private async Task AddPermission( Type = ScopeType })) }; + apiRequests.Add(requiredResourceAccess); } @@ -742,23 +785,39 @@ internal GraphServiceClient GetGraphServiceClient(TokenCredential tokenCredentia return _graphServiceClient; } - public async Task ReadApplication(TokenCredential tokenCredential, ApplicationParameters applicationParameters) + /// + /// Reads application parameters from Azure AD for a given app registration client ID + /// + /// + /// + /// + /// + public async Task ReadApplication(TokenCredential tokenCredential, ApplicationParameters applicationParameters, IConsoleLogger consoleLogger) { + if (string.IsNullOrEmpty(applicationParameters.EffectiveClientId) && + (string.IsNullOrEmpty(applicationParameters.ClientId) || DefaultProperties.ClientId.Equals(applicationParameters.ClientId, StringComparison.OrdinalIgnoreCase))) + { + var exception = new ArgumentException(nameof(applicationParameters.ClientId)); + consoleLogger.LogMessage(exception.Message, LogMessageType.Error); + return null; + } + var graphServiceClient = GetGraphServiceClient(tokenCredential); - // Get the tenant Organization? tenant = await GetTenant(graphServiceClient); var application = await GetApplication(tokenCredential, applicationParameters); - if (application != null) + if (application is null) { - ApplicationParameters effectiveApplicationParameters = GetEffectiveApplicationParameters( - tenant!, - application, - applicationParameters); - - return effectiveApplicationParameters; + var errorMsg = string.Format(Resources.AppNotFound, applicationParameters.EffectiveClientId, applicationParameters.EffectiveTenantId); + consoleLogger.LogMessage(errorMsg, LogMessageType.Error); + return null; } - return null; + ApplicationParameters effectiveApplicationParameters = GetEffectiveApplicationParameters( + tenant!, + application, + applicationParameters); + + return effectiveApplicationParameters; } public async Task GetApplication(TokenCredential tokenCredential, ApplicationParameters applicationParameters) @@ -769,13 +828,7 @@ internal GraphServiceClient GetGraphServiceClient(TokenCredential tokenCredentia .Filter($"appId eq '{applicationParameters.ClientId}'") .GetAsync(); - var readApplication = apps.FirstOrDefault(); - - if (readApplication == null) - { - return null; - } - return readApplication; + return apps.FirstOrDefault(); } private ApplicationParameters GetEffectiveApplicationParameters( @@ -801,7 +854,7 @@ private ApplicationParameters GetEffectiveApplicationParameters( GraphEntityId = application.Id, // Parameters that cannot be infered from the registered app - IsWebApp = originalApplicationParameters.IsWebApp, // TODO + IsWebApp = originalApplicationParameters.IsWebApp, IsBlazorWasm = originalApplicationParameters.IsBlazorWasm, SusiPolicy = originalApplicationParameters.SusiPolicy, SecretsId = originalApplicationParameters.SecretsId, diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/DependencyGraphService.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/DependencyGraphService.cs index 8a56695f11..4ef9df5f4b 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/DependencyGraphService.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/DependencyGraphService.cs @@ -1,17 +1,14 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using Microsoft.Extensions.Internal; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NuGet.ProjectModel; namespace Microsoft.DotNet.MSIdentity.Project { internal class DependencyGraphService : IDependencyGraphService { - private string? _projectFilePath; + private readonly string? _projectFilePath; public DependencyGraphService(string? projectPath = null) { _projectFilePath = projectPath; diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescription.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescription.cs index 8555cec805..393762dfa4 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescription.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescription.cs @@ -12,7 +12,7 @@ public class ProjectDescription /// /// Empty files /// - static readonly ConfigurationProperties[] s_emptyFiles = new ConfigurationProperties[0]; + static readonly ConfigurationProperties[] s_emptyFiles = Array.Empty(); /// /// Identifier of the project description. diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescriptionReader.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescriptionReader.cs index 8e815303a3..bc75c14f4f 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescriptionReader.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescription/ProjectDescriptionReader.cs @@ -13,7 +13,7 @@ namespace Microsoft.DotNet.MSIdentity.Project { public class ProjectDescriptionReader { - private const string ProjectTypeIdSuffix = "dotnet-"; + private const string ProjectTypeIdPrefix = "dotnet-"; private readonly IEnumerable _files; private List? _projectDescriptions; @@ -23,11 +23,11 @@ public ProjectDescriptionReader(IEnumerable files) _files = files; } - public ProjectDescription? GetProjectDescription(string projectTypeId) + public ProjectDescription? GetProjectDescription(string? projectTypeId) { - if (!string.IsNullOrEmpty(projectTypeId) && !projectTypeId.Equals(ProjectTypeIdSuffix)) + if (!string.IsNullOrEmpty(projectTypeId) && !projectTypeId.Equals(ProjectTypeIdPrefix)) { - return ProjectDescriptions.FirstOrDefault(p => p.Identifier == projectTypeId); + return ProjectDescriptions.FirstOrDefault(p => string.Equals(projectTypeId, p.Identifier)); } // TODO: could be both a Web app and WEB API. @@ -86,13 +86,6 @@ internal static ProjectDescription GetProjectDescription(PropertyInfo propertyIn return projectDescription; } - private string? InferProjectType() - { - - - return null; - } - /// /// Checks for a match given a list of matches and a list of files /// diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescriptions/dotnet_blazorwasm_client.json b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescriptions/dotnet_blazorwasm_client.json new file mode 100644 index 0000000000..a09a4775ff --- /dev/null +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/ProjectDescriptions/dotnet_blazorwasm_client.json @@ -0,0 +1,9 @@ +{ + "Identifier": "dotnet-blazorwasm-client", + "ProjectRelativeFolder": "", + "BasedOnProjectDescription": "", + "ConfigurationProperties": [ + ], + "MatchesForProjectType": [ + ] +} diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs index 6c00ed8df9..c107132f77 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs @@ -271,6 +271,15 @@ internal static string AddingPackage { } } + /// + /// Looks up a localized string similar to Couldn't find app {0} in tenant {1}. + /// + internal static string AppNotFound { + get { + return ResourceManager.GetString("AppNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to 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).. /// @@ -309,6 +318,16 @@ internal static byte[] cm_dotnet_blazorwasm { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] cm_dotnet_blazorwasm_client { + get { + object obj = ResourceManager.GetObject("cm_dotnet_blazorwasm_client", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -349,6 +368,24 @@ internal static byte[] cm_dotnet_webapp { } } + /// + /// Looks up a localized string similar to Error parsing Code Modifier Config for project type {0}, exception: {1}. + /// + internal static string CodeModifierConfigParsingError { + get { + return ResourceManager.GetString("CodeModifierConfigParsingError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configured Blazor WASM client app registration "{0}" ({1}). + /// + internal static string ConfiguredBlazorWasmClient { + get { + return ResourceManager.GetString("ConfiguredBlazorWasmClient", resourceCulture); + } + } + /// /// Looks up a localized string similar to Created app {0} - {1}.. /// @@ -387,6 +424,16 @@ internal static byte[] dotnet_blazorwasm { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] dotnet_blazorwasm_client { + get { + object obj = ResourceManager.GetObject("dotnet_blazorwasm_client", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// @@ -446,6 +493,15 @@ internal static string DotnetUserSecretsError { } } + /// + /// Looks up a localized string similar to Exposing API scopes for Server App Registration "{0}" ({1}). + /// + internal static string ExposingScopes { + get { + return ResourceManager.GetString("ExposingScopes", resourceCulture); + } + } + /// /// Looks up a localized string similar to FAILED\n\n. /// @@ -545,6 +601,15 @@ internal static string MismatchedProjectTypeIdentifier { } } + /// + /// Looks up a localized string similar to No valid project description found with project type identifier "{0}". + /// + internal static string NoProjectDescriptionFound { + get { + return ResourceManager.GetString("NoProjectDescriptionFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to No project found in {0}.. /// @@ -586,7 +651,7 @@ internal static string Policies { } /// - /// Looks up a localized string similar to Specify one .csproj file for the --project-path. + /// Looks up a localized string similar to Failed to validate project path {0}. /// internal static string ProjectPathError { get { @@ -594,6 +659,15 @@ internal static string ProjectPathError { } } + /// + /// Looks up a localized string similar to Resource file {0} could not be parsed.. + /// + internal static string ResourceFileParseError { + get { + return ResourceManager.GetString("ResourceFileParseError", resourceCulture); + } + } + /// /// Looks up a localized string similar to SUCCESS. /// diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx index 29ab855cd7..feabfa3213 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx @@ -148,6 +148,10 @@ ..\CodeReaderWriter\CodeFiles\Blazor\wasm\UserProfile.razor.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + Couldn't find app {0} in tenant {1} + 0 = EffectiveClientId, 1 = EffectiveTenantId + 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). @@ -160,6 +164,9 @@ ..\CodeReaderWriter\CodeModifierConfigs\cm_dotnet_blazorwasm.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\CodeReaderWriter\CodeModifierConfigs\cm_dotnet_blazorwasm_client.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\CodeReaderWriter\CodeModifierConfigs\cm_dotnet_blazorwasm_hosted.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -172,6 +179,14 @@ ..\CodeReaderWriter\CodeModifierConfigs\cm_dotnet_webapp.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Error parsing Code Modifier Config for project type {0}, exception: {1} + 0 = projectType, 1 = exception.Message + + + Configured Blazor WASM client app registration "{0}" ({1}) + 0 = ApplicationDisplayName, 1 = ClientId + Created app {0} - {1}. @@ -184,6 +199,9 @@ ..\ProjectDescriptions\dotnet_blazorwasm.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\ProjectDescriptions\dotnet_blazorwasm_client.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\ProjectDescriptions\dotnet_blazorwasm_hosted.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -202,6 +220,10 @@ ..\ProjectDescriptions\dotnet_webapp.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Exposing API scopes for Server App Registration "{0}" ({1}) + 0 = ApplicationDisplayName, 1 = ClientId + FAILED\n\n @@ -238,6 +260,10 @@ Config identifier: {0} does not match toolOptions identifier: {1} 0 = codeModifierConfig.Identifier, 1 = _toolOptions.ProjectTypeIdentifier + + No valid project description found with project type identifier "{0}" + 0 = ProjectTypeIdentifier + No project found in {0}. @@ -253,7 +279,12 @@ ..\Resources\Policies.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 - Specify one .csproj file for the --project-path + Failed to validate project path {0} + 0 = projectPath + + + Resource file {0} could not be parsed. + 0 = CodeModifierConfigPropertyInfo.Name SUCCESS diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs index 90eb93a61b..4a835d62dc 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Azure.Core; using Microsoft.DotNet.MSIdentity.AuthenticationParameters; @@ -22,18 +23,12 @@ namespace Microsoft.DotNet.MSIdentity { - /// - /// - /// public class AppProvisioningTool : IMsAADTool { - private static readonly string[] s_fileExtensions = new string[] { "*.cs", "*.cshtml", "*.razor", "*.html" }; - + private static readonly string[] s_fileExtensions = new string[] { "*.cs", "*.cshtml", "*.razor", "*.html", "*.csproj" }; private ProvisioningToolOptions ProvisioningToolOptions { get; set; } private string CommandName { get; } private MicrosoftIdentityPlatformApplicationManager MicrosoftIdentityPlatformApplicationManager { get; } = new MicrosoftIdentityPlatformApplicationManager(); - internal AppSettingsModifier AppSettingsModifier { get => new AppSettingsModifier(ProvisioningToolOptions); } - internal static IEnumerable? _properties; internal static IEnumerable Properties => _properties ??= typeof(Resources).GetProperties(BindingFlags.Static | BindingFlags.NonPublic); @@ -46,34 +41,32 @@ public class AppProvisioningTool : IMsAADTool private ProjectDescriptionReader? _projectDescriptionReader; private ProjectDescriptionReader ProjectDescriptionReader => _projectDescriptionReader ??= new ProjectDescriptionReader(FilePaths); - public AppProvisioningTool(string commandName, ProvisioningToolOptions provisioningToolOptions) + public AppProvisioningTool(string commandName, ProvisioningToolOptions provisioningToolOptions, bool silent = false) // TODO silent is temporary { CommandName = commandName; ProvisioningToolOptions = provisioningToolOptions; - ConsoleLogger = new ConsoleLogger(ProvisioningToolOptions.Json); + ConsoleLogger = new ConsoleLogger(ProvisioningToolOptions.Json, silent); } public async Task Run() { - if (!ValidateProjectFilePath()) + if (!ValidateProjectPath()) { - ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, State.Fail, Resources.ProjectPathError)); - ConsoleLogger.LogMessage(Resources.ProjectPathError, LogMessageType.Error); Environment.Exit(1); } var projectDescription = ProjectDescriptionReader.GetProjectDescription(ProvisioningToolOptions.ProjectTypeIdentifier); - if (projectDescription == null) { - ConsoleLogger.LogMessage(string.Format(Resources.NoProjectFound, ProvisioningToolOptions.ProjectPath), LogMessageType.Error); - } - else - { - ConsoleLogger.LogMessage(string.Format(Resources.DetectedProjectType, projectDescription.Identifier)); - ProvisioningToolOptions.ProjectType ??= projectDescription.Identifier?.Replace("dotnet-", ""); + var errorMessage = string.Format(Resources.NoProjectDescriptionFound, ProvisioningToolOptions.ProjectTypeIdentifier); + ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, State.Fail, errorMessage)); + ConsoleLogger.LogMessage(errorMessage, LogMessageType.Error); + Environment.Exit(1); } + ConsoleLogger.LogMessage(string.Format(Resources.DetectedProjectType, projectDescription.Identifier)); + ProvisioningToolOptions.ProjectType ??= projectDescription.Identifier?.Replace("dotnet-", ""); + ProjectAuthenticationSettings projectSettings = InferApplicationParameters( ProvisioningToolOptions, ProjectDescriptionReader.ProjectDescriptions, @@ -84,62 +77,30 @@ public AppProvisioningTool(string commandName, ProvisioningToolOptions provision ProvisioningToolOptions, ProvisioningToolOptions.TenantId ?? projectSettings.ApplicationParameters.EffectiveTenantId ?? projectSettings.ApplicationParameters.EffectiveDomain); - // 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.GetValueOrDefault()) + ApplicationParameters applicationParameters; + switch (CommandName) { - // 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); - ApplicationParameters? applicationParametersServer = await appProvisioningToolBlazorServer.Run(); - - /// 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"; - AppProvisioningTool appProvisioningToolBlazorClient = new AppProvisioningTool(CommandName, provisioningToolOptionsBlazorClient); - return await appProvisioningToolBlazorClient.Run(); - } + case Commands.CREATE_APP_REGISTRATION_COMMAND: + return await CreateAppRegistration(tokenCredential, projectSettings.ApplicationParameters); + case Commands.UPDATE_APP_REGISTRATION_COMMAND: + applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); + await UpdateAppRegistration(tokenCredential, applicationParameters); + return applicationParameters; - //for now, update project command is handlded seperately. - ApplicationParameters? applicationParameters; - switch (CommandName) - { case Commands.UPDATE_PROJECT_COMMAND: applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); await UpdateProject(tokenCredential, applicationParameters, projectDescription); return applicationParameters; - case Commands.UPDATE_APP_REGISTRATION_COMMAND: + case Commands.ADD_CLIENT_SECRET: applicationParameters = await ReadMicrosoftIdentityApplication(tokenCredential, projectSettings.ApplicationParameters); - await UpdateApplication(tokenCredential, applicationParameters); + await AddClientSecret(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 @@ -195,13 +156,17 @@ await WriteApplicationRegistration( } /// - /// Ensures existence of csproj file and ensures that projectFilePath and projectPath are set correctly, + /// Ensures that ProjectPath is updated if ProjectFilePath argument exists /// /// true if valid else false - private bool ValidateProjectFilePath() + private bool ValidateProjectPath() { - if (!string.IsNullOrEmpty(ProvisioningToolOptions.ProjectFilePath) - && System.IO.File.Exists(ProvisioningToolOptions.ProjectFilePath) + if (string.IsNullOrEmpty(ProvisioningToolOptions.ProjectFilePath)) + { + return true; + } + + if (System.IO.File.Exists(ProvisioningToolOptions.ProjectFilePath) && Path.GetDirectoryName(ProvisioningToolOptions.ProjectFilePath) is string projectPath) { if (!projectPath.Equals(ProvisioningToolOptions.ProjectPath)) @@ -212,177 +177,12 @@ private bool ValidateProjectFilePath() return true; } - if (string.IsNullOrEmpty(ProvisioningToolOptions.ProjectFilePath)) - { - var csProjFiles = Directory.EnumerateFiles(ProvisioningToolOptions.ProjectPath, "*.csproj"); - if (csProjFiles.Count() == 1) - { - ProvisioningToolOptions.ProjectFilePath = csProjFiles.First(); - return true; - } - } - + var errorMsg = string.Format(Resources.ProjectPathError, ProvisioningToolOptions.ProjectFilePath); + ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, State.Fail, errorMsg)); + ConsoleLogger.LogMessage(errorMsg, LogMessageType.Error); return false; } - 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(string.Format(Resources.CreatedAppRegistration, resultAppParameters.ApplicationDisplayName, resultAppParameters.ClientId)); - } - else - { - string failMessage = Resources.FailedToCreateApp; - ConsoleLogger.LogMessage(failMessage, LogMessageType.Error); - } - } - - return resultAppParameters; - } - - /// - /// Converts an AAD application to a B2C application - /// - /// - /// - /// - private ProjectAuthenticationSettings ConvertAadApplicationToB2CApplication(ProjectDescription projectDescription, ProjectAuthenticationSettings projectSettings) - { - // Get all the files in which "AzureAD" needs to be replaced by "AzureADB2C" - IEnumerable filesWithReplacementsForB2C = projectSettings.Replacements - .Where(r => r.ReplaceBy == "Application.ConfigurationSection") - .Select(r => r.FilePath); - - foreach (string filePath in filesWithReplacementsForB2C) - { - string fileContent = System.IO.File.ReadAllText(filePath); - string updatedContent = fileContent.Replace("AzureAd", "AzureAdB2C"); - - // Add the policies to the appsettings.json - if (filePath.EndsWith(AppSettingsModifier.AppSettingsFileName)) - { - // Insert the policies - int indexCallbackPath = updatedContent.IndexOf("\"CallbackPath\""); - if (indexCallbackPath > 0) - { - updatedContent = updatedContent.Substring(0, indexCallbackPath) - + Resources.Policies - + updatedContent.Substring(indexCallbackPath); - } - } - System.IO.File.WriteAllText(filePath, updatedContent); - } - - if (projectSettings.ApplicationParameters.CallsMicrosoftGraph) - { - ConsoleLogger.LogMessage(Resources.MicrosoftGraphNotSupported, LogMessageType.Error); - } - - // reevaulate the project settings - projectSettings = InferApplicationParameters( - ProvisioningToolOptions, - ProjectDescriptionReader.ProjectDescriptions, - projectDescription); - return projectSettings; - } - - private void WriteSummary(Summary summary) - { - ConsoleLogger.LogMessage(Resources.Summary); - foreach (Change change in summary.changes) - { - ConsoleLogger.LogMessage($"{change.Description}"); - } - } - - private async Task WriteApplicationRegistration(Summary summary, ApplicationParameters reconcialedApplicationParameters, TokenCredential tokenCredential) - { - summary.changes.Add(new Change($"Writing the project AppId = {reconcialedApplicationParameters.ClientId}")); - await MicrosoftIdentityPlatformApplicationManager.UpdateApplication(tokenCredential, reconcialedApplicationParameters, ProvisioningToolOptions, CommandName); - } - - private void WriteProjectConfiguration(Summary summary, ProjectAuthenticationSettings projectSettings, ApplicationParameters reconcialedApplicationParameters) - { - CodeWriter.WriteConfiguration(summary, projectSettings.Replacements, reconcialedApplicationParameters, ConsoleLogger); - } - - private bool Reconciliate(ApplicationParameters applicationParameters, ApplicationParameters effectiveApplicationParameters) - { - // Redirect URIs that are needed by the code, but not yet registered - IEnumerable missingRedirectUri = applicationParameters.WebRedirectUris.Except(effectiveApplicationParameters.WebRedirectUris); - - bool needUpdate = missingRedirectUri.Any(); - - if (needUpdate) - { - effectiveApplicationParameters.WebRedirectUris.AddRange(missingRedirectUri); - } - - // TODO: - // See also https://github.com/jmprieur/app-provisonning-tool/issues/10 - /* - string? audience = ComputeAudienceToSet(applicationParameters.SignInAudience, effectiveApplicationParameters.SignInAudience); - IEnumerable missingApiPermission = null; - IEnumerable missingExposedScopes = null; - bool needUpdate = missingRedirectUri != null || audience != null || missingApiPermission != null || missingExposedScopes != null; - */ - return needUpdate; - } - - private async Task ReadMicrosoftIdentityApplication( - TokenCredential tokenCredential, - ApplicationParameters applicationParameters) - { - ApplicationParameters? currentApplicationParameters = null; - if (!string.IsNullOrEmpty(applicationParameters.EffectiveClientId) - || (!string.IsNullOrEmpty(applicationParameters.ClientId) - && !DefaultProperties.ClientId.Equals(applicationParameters.ClientId, StringComparison.OrdinalIgnoreCase))) - { - currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.ReadApplication(tokenCredential, applicationParameters); - if (currentApplicationParameters == null) - { - ConsoleLogger.LogMessage($"Couldn't find app {applicationParameters.EffectiveClientId} in tenant {applicationParameters.EffectiveTenantId}. ", LogMessageType.Error); - } - } - - return currentApplicationParameters; - } - - private async Task ReadOrProvisionMicrosoftIdentityApplication( - TokenCredential tokenCredential, - ApplicationParameters applicationParameters) - { - ApplicationParameters? currentApplicationParameters = null; - - if (!string.IsNullOrEmpty(applicationParameters.EffectiveClientId) || (!string.IsNullOrEmpty(applicationParameters.ClientId) && !DefaultProperties.ClientId.Equals(applicationParameters.ClientId, StringComparison.OrdinalIgnoreCase))) - { - currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.ReadApplication(tokenCredential, applicationParameters); - if (currentApplicationParameters == null) - { - ConsoleLogger.LogMessage($"Couldn't find app {applicationParameters.EffectiveClientId} in tenant {applicationParameters.EffectiveTenantId}. ", LogMessageType.Error); - } - } - - if (currentApplicationParameters == null && !ProvisioningToolOptions.Unregister) - { - currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.CreateNewAppAsync(tokenCredential, applicationParameters, ConsoleLogger, CommandName); - if (currentApplicationParameters != null) - { - ConsoleLogger.LogMessage($"Created app {currentApplicationParameters.ApplicationDisplayName} - {currentApplicationParameters.ClientId}. "); - } - else - { - ConsoleLogger.LogMessage(Resources.FailedToCreateApp, LogMessageType.Error); - } - } - return currentApplicationParameters; - } - private ProjectAuthenticationSettings InferApplicationParameters( ProvisioningToolOptions provisioningToolOptions, IEnumerable projectDescriptions, @@ -396,10 +196,11 @@ private ProjectAuthenticationSettings InferApplicationParameters( 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; + projectSettings.ApplicationParameters.CalledApiScopes = !string.IsNullOrEmpty(provisioningToolOptions.HostedApiScopes) ? provisioningToolOptions.HostedApiScopes : projectSettings.ApplicationParameters.CalledApiScopes; projectSettings.ApplicationParameters.IsBlazorWasm = provisioningToolOptions.IsBlazorWasm; + projectSettings.ApplicationParameters.WebRedirectUris.AddRange(ProvisioningToolOptions.RedirectUris); - // there can mutliple project types + // there can multiple project types if (!string.IsNullOrEmpty(provisioningToolOptions.ProjectType)) { if (provisioningToolOptions.ProjectType.Equals("webapp", StringComparison.OrdinalIgnoreCase) @@ -407,24 +208,26 @@ private ProjectAuthenticationSettings InferApplicationParameters( { projectSettings.ApplicationParameters.IsWebApp = projectSettings.ApplicationParameters.IsWebApp ?? true; } - if (provisioningToolOptions.ProjectType.Equals("webapi", StringComparison.OrdinalIgnoreCase)) + if (provisioningToolOptions.ProjectType.Equals("webapi", StringComparison.OrdinalIgnoreCase) || provisioningToolOptions.IsBlazorWasmHostedServer) { projectSettings.ApplicationParameters.IsWebApi = projectSettings.ApplicationParameters.IsWebApi ?? true; } - if (provisioningToolOptions.ProjectType.Equals("blazorwasm-hosted", StringComparison.OrdinalIgnoreCase)) - { - projectSettings.ApplicationParameters.IsBlazorWasm = true; - } } - if (!string.IsNullOrEmpty(provisioningToolOptions.AppIdUri)) + if (!string.IsNullOrEmpty(provisioningToolOptions.HostedAppIdUri)) { - projectSettings.ApplicationParameters.AppIdUri = provisioningToolOptions.AppIdUri; + projectSettings.ApplicationParameters.AppIdUri = provisioningToolOptions.HostedAppIdUri; } return projectSettings; } - private TokenCredential GetTokenCredential(ProvisioningToolOptions provisioningToolOptions, string? currentApplicationTenantId) + /// + /// Gets Token Credential + /// + /// + /// + /// + internal static TokenCredential GetTokenCredential(ProvisioningToolOptions provisioningToolOptions, string? currentApplicationTenantId) { DeveloperCredentialsReader developerCredentialsReader = new DeveloperCredentialsReader(); return developerCredentialsReader.GetDeveloperCredentials( @@ -432,111 +235,144 @@ private TokenCredential GetTokenCredential(ProvisioningToolOptions provisioningT currentApplicationTenantId ?? provisioningToolOptions.TenantId); } - private async Task UnregisterApplication(TokenCredential tokenCredential, ApplicationParameters applicationParameters) + /// + /// Creates a new Azure AD App registration + /// + /// + /// + /// + private async Task CreateAppRegistration(TokenCredential tokenCredential, ApplicationParameters 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 + ApplicationParameters? resultAppParameters = await MicrosoftIdentityPlatformApplicationManager.CreateNewAppAsync(tokenCredential, applicationParameters, ConsoleLogger, CommandName); + if (resultAppParameters is null || string.IsNullOrEmpty(resultAppParameters.ClientId)) { - 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); + string failMessage = Resources.FailedToCreateApp; + ConsoleLogger.LogMessage(failMessage, LogMessageType.Error); + return null; } + + ConsoleLogger.LogMessage(string.Format(Resources.CreatedAppRegistration, resultAppParameters.ApplicationDisplayName, resultAppParameters.ClientId)); + return resultAppParameters; } - private async Task UpdateApplication(TokenCredential tokenCredential, ApplicationParameters? applicationParameters) + /// + /// Reads application parameters from Azure AD for a given app registration client ID + /// + /// + /// + /// + private async Task ReadMicrosoftIdentityApplication( + TokenCredential tokenCredential, + ApplicationParameters applicationParameters) { - if (applicationParameters != null) + var currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.ReadApplication(tokenCredential, applicationParameters, ConsoleLogger); + if (currentApplicationParameters is null) { - var jsonResponse = await MicrosoftIdentityPlatformApplicationManager.UpdateApplication( - tokenCredential, - applicationParameters, - ProvisioningToolOptions, - CommandName); - - ConsoleLogger.LogMessage(jsonResponse.Content as string); - ConsoleLogger.LogJsonMessage(jsonResponse); + Environment.Exit(1); } + + return currentApplicationParameters; } - private async Task AddClientSecret(TokenCredential tokenCredential, ApplicationParameters? applicationParameters) + /// + /// Updates app registration paramters in Azure AD + /// + /// + /// + /// + private async Task UpdateAppRegistration(TokenCredential tokenCredential, ApplicationParameters applicationParameters) { - JsonResponse jsonResponse = new JsonResponse(CommandName); - string? output; - - if (applicationParameters == null || string.IsNullOrEmpty(applicationParameters.GraphEntityId)) - { - output = Resources.FailedClientSecret; - jsonResponse.State = State.Fail; - jsonResponse.Content = output; - } - else + StringBuilder output = new StringBuilder(); // TODO: implement streaming output + if (ProvisioningToolOptions.IsBlazorWasmHostedServer) // Provision Blazor WASM Hosted client app registration { - var graphServiceClient = MicrosoftIdentityPlatformApplicationManager.GetGraphServiceClient(tokenCredential); - - try - { - string? password = await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentialsAsync( - graphServiceClient, - applicationParameters.GraphEntityId, - applicationParameters, - ConsoleLogger); - - //if user wants to update user secrets - if (ProvisioningToolOptions.UpdateUserSecrets) - { - CodeWriter.AddUserSecrets(applicationParameters.IsB2C, ProvisioningToolOptions.ProjectPath, password, ConsoleLogger); - } - - if (!string.IsNullOrEmpty(password)) - { - output = string.Format(Resources.ClientSecret, password); - jsonResponse.State = State.Success; - jsonResponse.Content = new KeyValuePair("ClientSecret", password); - } - else - { - output = string.Format(Resources.FailedClientSecretWithApp, applicationParameters.ApplicationDisplayName, applicationParameters.ClientId); - jsonResponse.State = State.Fail; - jsonResponse.Content = "TODO Empty password"; - } - } - catch (ServiceException se) + if (string.IsNullOrEmpty(applicationParameters.AppIdUri)) // Expose server API scopes { - output = se.Error?.ToString(); - jsonResponse.State = State.Fail; - jsonResponse.Content = se.Error?.Code; + var graphServiceClient = MicrosoftIdentityPlatformApplicationManager.GetGraphServiceClient(tokenCredential); + output.AppendLine(string.Format(Resources.ExposingScopes, applicationParameters.ApplicationDisplayName, applicationParameters.ClientId)); + applicationParameters.AppIdUri = await MicrosoftIdentityPlatformApplicationManager.ExposeScopes(graphServiceClient, applicationParameters.ClientId, applicationParameters.GraphEntityId); } - ConsoleLogger.LogMessage(output); - ConsoleLogger.LogJsonMessage(jsonResponse); + var clientApplicationParameters = await ConfigureBlazorWasmHostedClientAsync(serverApplicationParameters: applicationParameters); + output.AppendLine(string.Format(Resources.ConfiguredBlazorWasmClient, applicationParameters.ApplicationDisplayName, applicationParameters.ClientId)); + + ProvisioningToolOptions.BlazorWasmClientAppId = clientApplicationParameters.ClientId; } + + var jsonResponse = await MicrosoftIdentityPlatformApplicationManager.UpdateApplication( + tokenCredential, + applicationParameters, + ProvisioningToolOptions, + CommandName); + + output.AppendLine(jsonResponse.Content.ToString()); + var response = new JsonResponse(CommandName, jsonResponse.State, output.ToString()); + + ConsoleLogger.LogMessage(response.Content as string); + ConsoleLogger.LogJsonMessage(response); } - private async Task UpdateProject(TokenCredential tokenCredential, ApplicationParameters? applicationParameters, ProjectDescription? projectDescription) + /// + /// For Blazor WASM Hosted scenario, provisions and configures client app registration and client code updates + /// + /// + /// + /// + private async Task ConfigureBlazorWasmHostedClientAsync(ApplicationParameters serverApplicationParameters) { - if (applicationParameters is null || string.IsNullOrEmpty(ProvisioningToolOptions.ProjectFilePath) || projectDescription is null) + // Processes the Blazorwasm client + var clientToolOptions = ProvisioningToolOptions.Clone(); + + clientToolOptions.CodeUpdate = true; + clientToolOptions.ProjectPath = Path.GetDirectoryName(ProvisioningToolOptions.ClientProject) ?? string.Empty; + clientToolOptions.ProjectFilePath = ProvisioningToolOptions.ClientProject ?? string.Empty; + clientToolOptions.ClientId = null; + clientToolOptions.ClientProject = null; + clientToolOptions.ProjectType = "blazorwasm-client"; + clientToolOptions.AppDisplayName = string.Concat(clientToolOptions.AppDisplayName ?? serverApplicationParameters.ApplicationDisplayName, "-Client"); + clientToolOptions.HostedAppIdUri = serverApplicationParameters.AppIdUri; + clientToolOptions.HostedApiScopes = $"{serverApplicationParameters.AppIdUri}/access_as_user"; + + // Provision client app registration + var provisionClientAppRegistration = new AppProvisioningTool(Commands.CREATE_APP_REGISTRATION_COMMAND, clientToolOptions, silent: true); + var clientApplicationParameters = await provisionClientAppRegistration.Run(); + if (clientApplicationParameters == null) { - return; + var exception = new ArgumentNullException(nameof(clientApplicationParameters)); + ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, State.Fail, exception.Message)); + throw exception; } + // Update program.cs file + clientToolOptions.ClientId = clientApplicationParameters.ClientId; + var updateCode = new AppProvisioningTool(Commands.UPDATE_PROJECT_COMMAND, clientToolOptions, silent: true); + clientApplicationParameters = await updateCode.Run(); + if (clientApplicationParameters == null) + { + var exception = new ArgumentNullException(nameof(clientApplicationParameters)); + ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, State.Fail, exception.Message)); + throw exception; + } + + return clientApplicationParameters; + } + + /// + /// + /// + /// + /// + /// + /// + private async Task UpdateProject(TokenCredential tokenCredential, ApplicationParameters applicationParameters, ProjectDescription projectDescription) + { if (ProvisioningToolOptions.CodeUpdate || ProvisioningToolOptions.ConfigUpdate) { ConsoleLogger.LogMessage("============================================="); ConsoleLogger.LogMessage(Resources.UpdatingAppSettingsJson); ConsoleLogger.LogMessage("=============================================\n"); // modify appsettings.json. - AppSettingsModifier.ModifyAppSettings(applicationParameters, FilePaths); + var appSettingsModifier = new AppSettingsModifier(ProvisioningToolOptions); + appSettingsModifier.ModifyAppSettings(applicationParameters, FilePaths); } if (ProvisioningToolOptions.ConfigUpdate) @@ -624,5 +460,236 @@ await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentialsAsync( await startupModifier.AddAuthCodeAsync(); } } + + /// + /// + /// + /// + /// + /// + private async Task AddClientSecret(TokenCredential tokenCredential, ApplicationParameters applicationParameters) + { + string? output; + JsonResponse jsonResponse = new JsonResponse(CommandName); + + if (string.IsNullOrEmpty(applicationParameters.GraphEntityId)) + { + output = Resources.FailedClientSecret; + jsonResponse.State = State.Fail; + jsonResponse.Content = output; + } + else + { + var graphServiceClient = MicrosoftIdentityPlatformApplicationManager.GetGraphServiceClient(tokenCredential); + try + { + string? password = await MicrosoftIdentityPlatformApplicationManager.AddPasswordCredentialsAsync( + graphServiceClient, + applicationParameters.GraphEntityId, + applicationParameters, + ConsoleLogger); + + // if user wants to update user secrets + if (ProvisioningToolOptions.UpdateUserSecrets) + { + CodeWriter.AddUserSecrets(applicationParameters.IsB2C, ProvisioningToolOptions.ProjectPath, password, ConsoleLogger); + } + + if (!string.IsNullOrEmpty(password)) + { + output = string.Format(Resources.ClientSecret, password); + jsonResponse.State = State.Success; + jsonResponse.Content = new KeyValuePair("ClientSecret", password); + } + else + { + output = string.Format(Resources.FailedClientSecretWithApp, applicationParameters.ApplicationDisplayName, applicationParameters.ClientId); + jsonResponse.State = State.Fail; + jsonResponse.Content = "TODO Empty password"; + } + } + catch (ServiceException se) + { + output = se.Error?.ToString(); + jsonResponse.State = State.Fail; + jsonResponse.Content = se.Error?.Code; + } + + ConsoleLogger.LogMessage(output); + ConsoleLogger.LogJsonMessage(jsonResponse); + } + } + + /// + /// + /// + /// + /// + /// + private async Task UnregisterApplication(TokenCredential tokenCredential, ApplicationParameters 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); + } + } + + /// + /// Converts an AAD application to a B2C application + /// + /// + /// + /// + private ProjectAuthenticationSettings ConvertAadApplicationToB2CApplication(ProjectDescription projectDescription, ProjectAuthenticationSettings projectSettings) + { + // Get all the files in which "AzureAD" needs to be replaced by "AzureADB2C" + IEnumerable filesWithReplacementsForB2C = projectSettings.Replacements + .Where(r => string.Equals(r.ReplaceBy, "Application.ConfigurationSection", StringComparison.OrdinalIgnoreCase)) + .Select(r => r.FilePath); + + foreach (string filePath in filesWithReplacementsForB2C) + { + string fileContent = System.IO.File.ReadAllText(filePath); + string updatedContent = fileContent.Replace("AzureAd", "AzureAdB2C"); + + // Add the policies to the appsettings.json + if (filePath.EndsWith(AppSettingsModifier.AppSettingsFileName)) + { + // Insert the policies + int indexCallbackPath = updatedContent.IndexOf("\"CallbackPath\""); + if (indexCallbackPath > 0) + { + updatedContent = updatedContent.Substring(0, indexCallbackPath) + + Resources.Policies + + updatedContent.Substring(indexCallbackPath); + } + } + System.IO.File.WriteAllText(filePath, updatedContent); + } + + if (projectSettings.ApplicationParameters.CallsMicrosoftGraph) + { + ConsoleLogger.LogMessage(Resources.MicrosoftGraphNotSupported, LogMessageType.Error); + } + + // reevaulate the project settings + projectSettings = InferApplicationParameters( + ProvisioningToolOptions, + ProjectDescriptionReader.ProjectDescriptions, + projectDescription); + return projectSettings; + } + + /// + /// + /// + /// + private void WriteSummary(Summary summary) + { + ConsoleLogger.LogMessage(Resources.Summary); + foreach (Change change in summary.changes) + { + ConsoleLogger.LogMessage($"{change.Description}"); + } + } + + /// + /// + /// + /// + /// + /// + /// + private async Task WriteApplicationRegistration(Summary summary, ApplicationParameters reconcialedApplicationParameters, TokenCredential tokenCredential) + { + summary.changes.Add(new Change($"Writing the project AppId = {reconcialedApplicationParameters.ClientId}")); + await MicrosoftIdentityPlatformApplicationManager.UpdateApplication(tokenCredential, reconcialedApplicationParameters, ProvisioningToolOptions, CommandName); + } + + /// + /// + /// + /// + /// + /// + private void WriteProjectConfiguration(Summary summary, ProjectAuthenticationSettings projectSettings, ApplicationParameters reconcialedApplicationParameters) + { + CodeWriter.WriteConfiguration(summary, projectSettings.Replacements, reconcialedApplicationParameters, ConsoleLogger); + } + + /// + /// + /// + /// + /// + /// + private bool Reconciliate(ApplicationParameters applicationParameters, ApplicationParameters effectiveApplicationParameters) + { + // Redirect URIs that are needed by the code, but not yet registered + IEnumerable missingRedirectUri = applicationParameters.WebRedirectUris.Except(effectiveApplicationParameters.WebRedirectUris); + + bool needUpdate = missingRedirectUri.Any(); + + if (needUpdate) + { + effectiveApplicationParameters.WebRedirectUris.AddRange(missingRedirectUri); + } + + // TODO: + // See also https://github.com/jmprieur/app-provisonning-tool/issues/10 + /* + string? audience = ComputeAudienceToSet(applicationParameters.SignInAudience, effectiveApplicationParameters.SignInAudience); + IEnumerable missingApiPermission = null; + IEnumerable missingExposedScopes = null; + bool needUpdate = missingRedirectUri != null || audience != null || missingApiPermission != null || missingExposedScopes != null; + */ + return needUpdate; + } + + /// + /// + /// + /// + /// + /// + private async Task ReadOrProvisionMicrosoftIdentityApplication( + TokenCredential tokenCredential, + ApplicationParameters applicationParameters) + { + ApplicationParameters? currentApplicationParameters = null; + + if (!string.IsNullOrEmpty(applicationParameters.EffectiveClientId) || (!string.IsNullOrEmpty(applicationParameters.ClientId) && !DefaultProperties.ClientId.Equals(applicationParameters.ClientId, StringComparison.OrdinalIgnoreCase))) + { + currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.ReadApplication(tokenCredential, applicationParameters, ConsoleLogger); + } + + if (currentApplicationParameters == null && !ProvisioningToolOptions.Unregister) + { + currentApplicationParameters = await MicrosoftIdentityPlatformApplicationManager.CreateNewAppAsync(tokenCredential, applicationParameters, ConsoleLogger, CommandName); + if (currentApplicationParameters != null) + { + ConsoleLogger.LogMessage($"Created app {currentApplicationParameters.ApplicationDisplayName} - {currentApplicationParameters.ClientId}. "); + } + else + { + ConsoleLogger.LogMessage(Resources.FailedToCreateApp, LogMessageType.Error); + } + } + return currentApplicationParameters; + } } } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs index dc141a658e..8d23ab72c9 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/ProvisioningToolOptions.cs @@ -110,9 +110,9 @@ public string ProjectTypeIdentifier public bool Unregister { get; set; } /// - /// Scopes for the called web API. + /// Scopes for the Blazor WASM hosted API. /// - public string? CalledApiScopes { get; set; } + public string? HostedApiScopes { get; set; } /// /// Url for the called web API. @@ -134,12 +134,6 @@ public string ProjectTypeIdentifier /// public bool UpdateUserSecrets { get; set; } - /// - /// The App ID Uri for the blazorwasm hosted API. It's only used - /// on the case of a blazorwasm hosted application. - /// - public string? AppIdUri { get; set; } - /// /// Format for console output for list commands. /// @@ -156,14 +150,35 @@ public string ProjectTypeIdentifier public bool CodeUpdate { get; set; } = false; /// - /// Make PackageReferences in .csproj (add using `dotnet add package` + /// Make PackageReferences in .csproj (add using `dotnet add package`) /// public bool PackagesUpdate { get; set; } = false; + /// + /// The App ID Uri for the blazorwasm hosted API. It's only used + /// in the case of a blazorwasm hosted application. + /// + public string? HostedAppIdUri { get; set; } + + /// + /// Provisions app registrations and applies code updates for Blazor WASM client and server in hosted scenario + /// + public bool IsBlazorWasmHostedServer => !string.IsNullOrEmpty(ClientProject); + + /// + /// Path to csproj file of the Blazor WASM hosted client + /// + public string? ClientProject { get; set; } + /// /// Determines if the project type is blazor wasm /// - public bool IsBlazorWasm => "blazorwasm".Equals(ProjectType); + public bool IsBlazorWasm => "blazorwasm".Equals(ProjectType) || "blazorwasm-client".Equals(ProjectType); + + /// + /// App registration ID associated with the Blazor WASM hosted client, Used for the Blazor WASM hosted server API in order to pre-authorize the client app + /// + public string? BlazorWasmClientAppId { get; internal set; } /// /// Clones the options @@ -173,7 +188,7 @@ public ProvisioningToolOptions Clone() { return new ProvisioningToolOptions() { - CalledApiScopes = CalledApiScopes, + HostedApiScopes = HostedApiScopes, CalledApiUrl = CalledApiUrl, CallsDownstreamApi = CallsDownstreamApi, UpdateUserSecrets = UpdateUserSecrets, @@ -191,9 +206,10 @@ public ProvisioningToolOptions Clone() ProjectFilePath = ProjectFilePath, AppSettingsFilePath = AppSettingsFilePath, WebApiClientId = WebApiClientId, - AppIdUri = AppIdUri, + HostedAppIdUri = HostedAppIdUri, Json = Json, - AppDisplayName = AppDisplayName + AppDisplayName = AppDisplayName, + RedirectUris = RedirectUris }; } } diff --git a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs index d1d408fd15..aed97fdf93 100644 --- a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs +++ b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs @@ -349,7 +349,6 @@ private static SyntaxTriviaList GetLeadingTrivia(Formatting codeFormatting) return statementLeadingTrivia; } - private static SyntaxTriviaList GetTrailingTrivia(Formatting codeFormatting) { var statementLeadingTrivia = SyntaxFactory.TriviaList(); diff --git a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/ConsoleLogger.cs b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/ConsoleLogger.cs index 82634bfb06..5bd755a8f9 100644 --- a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/ConsoleLogger.cs +++ b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/ConsoleLogger.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Text; namespace Microsoft.DotNet.MSIdentity.Shared @@ -7,15 +6,22 @@ namespace Microsoft.DotNet.MSIdentity.Shared internal class ConsoleLogger : IConsoleLogger { private bool _jsonOutput; + private bool _silent; - public ConsoleLogger(bool jsonOutput = false) + public ConsoleLogger(bool jsonOutput = false, bool silent = false) { _jsonOutput = jsonOutput; + _silent = silent; Console.OutputEncoding = Encoding.UTF8; } public void LogMessage(string message, LogMessageType level, bool removeNewLine = false) { + if (_silent) + { + return; + } + //if json output is enabled, don't write to console at all. if (!_jsonOutput) { @@ -47,6 +53,11 @@ public void LogMessage(string message, LogMessageType level, bool removeNewLine public void LogJsonMessage(JsonResponse jsonMessage) { + if (_silent) + { + return; + } + if (_jsonOutput) { Console.WriteLine(jsonMessage.ToJsonString()); @@ -55,6 +66,11 @@ public void LogJsonMessage(JsonResponse jsonMessage) public void LogMessage(string message, bool removeNewLine = false) { + if (_silent) + { + return; + } + LogMessage(message, LogMessageType.Information, removeNewLine); } } diff --git a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs index fa60619579..a22040f140 100644 --- a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs +++ b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs @@ -17,9 +17,10 @@ internal static class ProjectModifierHelper { internal static char[] CodeSnippetTrimChars = new char[] { ' ', '\r', '\n', ';' }; internal static IEnumerable CodeSnippetTrimStrings = CodeSnippetTrimChars.Select(c => c.ToString()); - internal static char[] Parentheses = new char[] { '(', ')' }; + internal static char[] Parentheses = new char[] { '(', ')' }; internal const string VarIdentifier = "var"; internal const string WebApplicationBuilderIdentifier = "WebApplicationBuilder"; + /// /// Check if Startup.cs or similar file exists. /// @@ -418,8 +419,8 @@ internal static async Task ModifyDocumentText(Document fileDoc, IEnume return null; } - var trimmedSourceFile = ProjectModifierHelper.TrimStatement(sourceFileString); - var applicableCodeChanges = codeChanges.Where(c => !trimmedSourceFile.Contains(ProjectModifierHelper.TrimStatement(c.Block))); + var trimmedSourceFile = TrimStatement(sourceFileString); + var applicableCodeChanges = codeChanges.Where(c => !trimmedSourceFile.Contains(TrimStatement(c.Block))); if (!applicableCodeChanges.Any()) { return null; @@ -454,8 +455,9 @@ internal static async Task UpdateDocument(Document document, IConsoleLogger cons { var classFileTxt = await document.GetTextAsync(); - // Note: Here, document.Name is the full filepath - File.WriteAllText(document.Name, classFileTxt.ToString(), new UTF8Encoding(false)); + // Note: For files without .cs extension, document.Name is the full filepath + var filePath = document.Name.EndsWith(".cs") ? document.FilePath : document.Name; + File.WriteAllText(filePath, classFileTxt.ToString(), new UTF8Encoding(false)); consoleLogger.LogMessage($"Modified {document.Name}.\n"); } diff --git a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs index f4b27ee23e..a61b5024a9 100644 --- a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs +++ b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/MicrosoftIdentityPlatformApplicationManagerTests.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.DotNet.MSIdentity.MicrosoftIdentityPlatformApplication; +using Microsoft.DotNet.MSIdentity.Tool; +using Microsoft.Graph; using Xunit; namespace Microsoft.DotNet.MSIdentity.UnitTests.Tests @@ -50,5 +53,179 @@ public void ValidateUrisTests(List urisToValidate, List validUri } } }; + + [Fact] + public void PreAuthorizeBlazorWasmClientAppTest_WhenBlazorWasmClientAppIdEmpty_ReturnFalse() + { + var originalApp = new Graph.Application { Api = new Graph.ApiApplication() }; + var toolOptions = new ProvisioningToolOptions + { + BlazorWasmClientAppId = "" + }; + var output = MicrosoftIdentityPlatformApplicationManager.PreAuthorizeBlazorWasmClientApp(originalApp, toolOptions, null); + Assert.False(output); + Assert.Null(originalApp.Api.PreAuthorizedApplications); + } + + [Fact] + public void PreAuthorizeBlazorWasmClientAppTest_WhenExistingAppHasNoPermissionScopes_ReturnFalse() + { + var originalApp = new Graph.Application { Api = new Graph.ApiApplication() }; + var toolOptions = new ProvisioningToolOptions + { + BlazorWasmClientAppId = "id" + }; + + var output = MicrosoftIdentityPlatformApplicationManager.PreAuthorizeBlazorWasmClientApp(originalApp, toolOptions, null); + Assert.False(output); + Assert.Null(originalApp.Api.PreAuthorizedApplications); + } + + [Fact] + public void PreAuthorizeBlazorWasmClientAppTest_WhenExistingAppHasMatchingPreAuthorizedApplications_ReturnFalse() + { + var clientId = "id"; + var permissionId = "permissionId"; + + var originalApp = new Graph.Application { + Api = new Graph.ApiApplication + { + PreAuthorizedApplications = new List + { + new PreAuthorizedApplication + { + AppId = clientId, + DelegatedPermissionIds = new List + { + permissionId + } + } + } + } + }; + + var toolOptions = new ProvisioningToolOptions + { + BlazorWasmClientAppId = clientId + }; + + var output = MicrosoftIdentityPlatformApplicationManager.PreAuthorizeBlazorWasmClientApp(originalApp, toolOptions, null); + Assert.False(output); + Assert.Equal(originalApp.Api.PreAuthorizedApplications.First().AppId, clientId); + Assert.Equal(originalApp.Api.PreAuthorizedApplications.First().DelegatedPermissionIds.First(), permissionId); + } + + [Fact] + public void PreAuthorizeBlazorWasmClientAppTest_NoMatchingPreAuthorizedApplications_ReturnTrueAndUpdate() + { + var clientId = "id"; + var permissionId = Guid.NewGuid(); + + var originalApp = new Graph.Application + { + Api = new Graph.ApiApplication + { + Oauth2PermissionScopes = new List + { + new PermissionScope + { + Id = permissionId + } + }, + PreAuthorizedApplications = new List + { + new PreAuthorizedApplication + { + AppId = "existingClientId", + DelegatedPermissionIds = new List + { + "existingPermissionId" + } + } + } + } + }; + + var updatedApp = new Graph.Application(); + + var toolOptions = new ProvisioningToolOptions + { + BlazorWasmClientAppId = clientId + }; + + var output = MicrosoftIdentityPlatformApplicationManager.PreAuthorizeBlazorWasmClientApp(originalApp, toolOptions, updatedApp); + + Assert.True(output); + // Preauthorized application should be added, not replaced + Assert.Equal(2, updatedApp.Api.PreAuthorizedApplications.Count()); + Assert.Contains(updatedApp.Api.PreAuthorizedApplications, + app => app.AppId.Equals(clientId) + && app.DelegatedPermissionIds.Any( + id => id.ToString().Equals(permissionId.ToString()))); + } + + [Fact] + public void UpdateImplicitGrantSettingsTest_WhenBlazorWasm_SetCheckboxesFalse() + { + var originalApp = new Graph.Application + { + Web = new WebApplication + { + ImplicitGrantSettings = new ImplicitGrantSettings + { + EnableAccessTokenIssuance = true, + EnableIdTokenIssuance = true + } + } + }; + var toolOptions = new ProvisioningToolOptions + { + ProjectType = "blazorwasm" + }; + + var output = MicrosoftIdentityPlatformApplicationManager.UpdateImplicitGrantSettings(originalApp, toolOptions); // TODO unit tests + Assert.True(output); + Assert.False(originalApp.Web.ImplicitGrantSettings.EnableAccessTokenIssuance); + Assert.False(originalApp.Web.ImplicitGrantSettings.EnableIdTokenIssuance); + } + + [Theory] + [InlineData(false, false, false, false, false)] + [InlineData(true, true, true, true, false)] + [InlineData(false, false, true, true, false)] + [InlineData(true, true, false, false, false)] + [InlineData(false, true, false, true, true)] + [InlineData(false, true, false, false, true)] + [InlineData(false, false, true, false, true)] + [InlineData(false, false, false, true, true)] + [InlineData(true, false, true, false, true)] + [InlineData(true, false, true, true, true)] + [InlineData(true, true, false, true, true)] + [InlineData(true, true, true, false, true)] + public void UpdateImplicitGrantSettingsTest_SetCheckboxes(bool appAccessToken, bool toolAccessToken, bool appIdToken, bool toolIdToken, bool expected) + { + var originalApp = new Graph.Application + { + Web = new WebApplication + { + ImplicitGrantSettings = new ImplicitGrantSettings + { + EnableAccessTokenIssuance = appAccessToken, + EnableIdTokenIssuance = appIdToken + } + } + }; + + var toolOptions = new ProvisioningToolOptions + { + EnableAccessToken = toolAccessToken, + EnableIdToken = toolIdToken + }; + + var needsUpdate = MicrosoftIdentityPlatformApplicationManager.UpdateImplicitGrantSettings(originalApp, toolOptions); + Assert.Equal(expected, needsUpdate); + Assert.Equal(originalApp.Web.ImplicitGrantSettings.EnableAccessTokenIssuance, toolAccessToken); + Assert.Equal(originalApp.Web.ImplicitGrantSettings.EnableIdTokenIssuance, toolIdToken); + } } } diff --git a/tools/dotnet-msidentity/Program.cs b/tools/dotnet-msidentity/Program.cs index 67cc0326b9..15d77eb12d 100644 --- a/tools/dotnet-msidentity/Program.cs +++ b/tools/dotnet-msidentity/Program.cs @@ -212,7 +212,7 @@ private static Command RegisterApplicationCommand() => description: "Register an Azure AD or Azure AD B2C app registration in Azure and update the project." + "\n\t- Updates the appsettings.json file.\n") { - TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), ClientSecretOption(), AppIdUriOption(), ApiClientIdOption(), SusiPolicyIdOption(), ProjectFilePathOption() + TenantOption(), UsernameOption(), JsonOption(), ClientIdOption(), ClientSecretOption(), HostedAppIdUriOption(), ApiClientIdOption(), SusiPolicyIdOption(), ProjectFilePathOption() }; private static Command UpdateProjectCommand() => @@ -223,7 +223,7 @@ private static Command UpdateProjectCommand() => "\n\t- Updates the Startup.cs file." + "\n\t- Updates the user secrets.\n") { - TenantOption(), UsernameOption(), ClientIdOption(), JsonOption(), ProjectFilePathOption(), ConfigUpdateOption(), CodeUpdateOption(), PackagesUpdateOption(), CallsGraphOption(), CallsDownstreamApiOption(), UpdateUserSecretsOption(), RedirectUriOption(), + TenantOption(), UsernameOption(), ClientIdOption(), JsonOption(), ProjectFilePathOption(), ConfigUpdateOption(), CodeUpdateOption(), PackagesUpdateOption(), CallsGraphOption(), CallsDownstreamApiOption(), UpdateUserSecretsOption(), RedirectUriOption() }; private static Command UpdateAppRegistrationCommand() => @@ -231,7 +231,7 @@ private static Command UpdateAppRegistrationCommand() => name: Commands.UPDATE_APP_REGISTRATION_COMMAND, description: "Update an Azure AD/AD B2C app registration in Azure.\n") { - TenantOption(), UsernameOption(), JsonOption(), AppIdUriOption(), ClientIdOption(), RedirectUriOption(), EnableIdTokenOption(), EnableAccessToken() + TenantOption(), UsernameOption(), JsonOption(), HostedAppIdUriOption(), ClientIdOption(), RedirectUriOption(), EnableIdTokenOption(), EnableAccessToken(), ClientProjectOption() }; private static Command CreateAppRegistrationCommand() => @@ -239,7 +239,7 @@ private static Command CreateAppRegistrationCommand() => name: Commands.CREATE_APP_REGISTRATION_COMMAND, description: "Create an Azure AD/AD B2C app registration in Azure.\n") { - TenantOption(), UsernameOption(), JsonOption(), AppDisplayName(), ProjectFilePathOption(), ProjectType() + TenantOption(), UsernameOption(), JsonOption(), AppDisplayName(), ProjectFilePathOption(), ProjectType(), ClientProjectOption() }; private static Command UnregisterApplicationCommand() => @@ -249,7 +249,7 @@ private static Command UnregisterApplicationCommand() => description: "Unregister an Azure AD or Azure AD B2C app registration in Azure." + "\n\t- Updates the appsettings.json file.\n") { - TenantOption(), UsernameOption(), JsonOption(), AppIdUriOption(), ProjectFilePathOption(), ClientIdOption() + TenantOption(), UsernameOption(), JsonOption(), HostedAppIdUriOption(), ProjectFilePathOption(), ClientIdOption() }; private static Option JsonOption() => @@ -377,9 +377,17 @@ private static Option ProjectFilePathOption() => IsRequired = false }; - private static Option AppIdUriOption() => + private static Option ClientProjectOption() => new Option( - aliases: new[] { "--app-id-uri" }, + aliases: new[] { "--client-project" }, + description: "Path to the project file (.csproj file) for a hosted Blazor WASM client. If provided, implies that project is Blazor WASM Hosted\n") + { + IsRequired = false + }; + + private static Option HostedAppIdUriOption() => + new Option( + aliases: new[] { "--hosted-app-id-uri" }, description: "The App ID Uri for the Blazor WebAssembly hosted API. This parameter will only be used for Blazor WebAssembly hosted applications.\n") { IsRequired = false