From 7e3443fecfde795faf9112f33caa453492eb5e42 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Tue, 2 May 2023 19:50:23 -0700 Subject: [PATCH] Add support for CIAM to the app registration tool (#2219) * Add support for CIAM Update to .NET 7, and latest NuGet packages * Fixing the LICENSE path and fixing warnings * Fixing more warnings in tests * Update NuGet packages * Fixing an issue when reading a ciam appsettings.json (so far authorities with no segment were not expected) --- .gitignore | 1 + .../Directory.Build.props | 14 ++ tools/app-provisioning-tool/README.md | 2 +- .../ApplicationParameters.cs | 6 +- .../CodeReaderWriter/CodeReader.cs | 48 ++++-- .../CodeReaderWriter/CodeWriter.cs | 147 ++++++++++++++---- .../MsalTokenCredential.cs | 2 +- ...osoftIdentityPlatformApplicationManager.cs | 13 +- .../ConfigurationProperties.cs | 3 +- .../MatchesForProjectType.cs | 3 +- .../ProjectDescriptionReader.cs | 2 +- .../ProjectDescription/Replacement.cs | 7 +- .../ProjectDescriptions/dotnet-web.json | 11 ++ .../Properties/Resources.Designer.cs | 2 +- .../Tool/AppProvisioningTool.cs | 63 +++++++- .../app-provisioning-lib.csproj | 21 +-- .../app-provisioning-tool/Program.cs | 3 +- .../msidentity-app-sync.csproj | 19 +-- .../tests/ProjectDescriptionReaderTests.cs | 16 +- .../tests/TestUtilities.cs | 19 ++- .../app-provisioning-tool/tests/Tests.csproj | 4 +- 21 files changed, 303 insertions(+), 103 deletions(-) create mode 100644 tools/app-provisioning-tool/Directory.Build.props diff --git a/.gitignore b/.gitignore index 556e06553..f478d67ba 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +/tools/app-provisioning-tool/testwebapp diff --git a/tools/app-provisioning-tool/Directory.Build.props b/tools/app-provisioning-tool/Directory.Build.props new file mode 100644 index 000000000..530cadaaa --- /dev/null +++ b/tools/app-provisioning-tool/Directory.Build.props @@ -0,0 +1,14 @@ + + + + net7.0 + True + ../../../build/MSAL.snk + 10.0 + + + + + + + diff --git a/tools/app-provisioning-tool/README.md b/tools/app-provisioning-tool/README.md index 82afcdd8c..ca9d8b473 100644 --- a/tools/app-provisioning-tool/README.md +++ b/tools/app-provisioning-tool/README.md @@ -40,7 +40,7 @@ You can install the tool as an external tool in Visual Studio for this: 5. Check **Use output window** and **Prompt for arguments** 6. Select OK -![image](https://user-images.githubusercontent.com/13203188/113719161-9e2b8580-96ed-11eb-8ad8-7b02eedbd4ed.png) + ![image](https://user-images.githubusercontent.com/13203188/113719161-9e2b8580-96ed-11eb-8ad8-7b02eedbd4ed.png) `msidentity-app-sync` now appears in the **Tools** menu, and when you select an ASP.NET Core project, you can run it on that project. diff --git a/tools/app-provisioning-tool/app-provisioning-lib/AuthenticationParameters/ApplicationParameters.cs b/tools/app-provisioning-tool/app-provisioning-lib/AuthenticationParameters/ApplicationParameters.cs index d94026ca5..51496f944 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/AuthenticationParameters/ApplicationParameters.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/AuthenticationParameters/ApplicationParameters.cs @@ -43,7 +43,7 @@ public string? Domain1 { get { - return Domain?.Replace(".onmicrosoft.com", string.Empty); + return Domain?.Replace(".onmicrosoft.com", string.Empty, StringComparison.OrdinalIgnoreCase); } } @@ -82,6 +82,10 @@ public string? Domain1 /// public bool IsB2C { get; set; } + /// + /// Is the tenant a CIAM tenant? + /// + public bool IsCiam { get; set; } // TODO: propose a fix for the blazorwasm project template diff --git a/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeReader.cs b/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeReader.cs index 11d5d7a5f..c2498ed9c 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeReader.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeReader.cs @@ -117,7 +117,7 @@ private static void PostProcessWebUris(ProjectAuthenticationSettings projectAuth IEnumerable httpsProfileLaunchUrls = projectAuthenticationSettings.Replacements .Where(r => r.ReplaceBy == "profilesApplicationUrls") .SelectMany(r => r.ReplaceFrom.Split(';')) - .Where(u => u.StartsWith("https://")); + .Where(u => u.StartsWith("https://", StringComparison.OrdinalIgnoreCase)); launchUrls.AddRange(httpsProfileLaunchUrls); // Set the web redirect URIs @@ -132,7 +132,7 @@ private static void PostProcessWebUris(ProjectAuthenticationSettings projectAuth } if (!string.IsNullOrEmpty(signoutPath)) { - if (signoutPath.StartsWith("/")) + if (signoutPath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { if (launchUrls.Any()) { @@ -158,12 +158,12 @@ private static void ProcessFile( JsonElement jsonContent = default; XmlDocument? xmlDocument = null; - if (filePath.EndsWith(".json")) + if (filePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { jsonContent = JsonSerializer.Deserialize(fileContent, s_serializerOptionsWithComments); } - else if (filePath.EndsWith(".csproj")) + else if (filePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) { xmlDocument = new XmlDocument(); xmlDocument.Load(filePath); @@ -177,7 +177,7 @@ private static void ProcessFile( { string[] path = property.Split(':'); - if (filePath.EndsWith(".json")) + if (filePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { IEnumerable> elements = FindMatchingElements(jsonContent, path, 0); foreach (var pair in elements) @@ -213,7 +213,7 @@ private static void ProcessFile( } else { - int index = fileContent.IndexOf(property); + int index = fileContent.IndexOf(property, StringComparison.OrdinalIgnoreCase); if (index != -1) { UpdatePropertyRepresents( @@ -227,7 +227,7 @@ private static void ProcessFile( } if (!string.IsNullOrEmpty(propertyMapping.Sets) && (found - || (propertyMapping.MatchAny != null && propertyMapping.MatchAny.Any(m => fileContent.Contains(m))))) + || (propertyMapping.MatchAny != null && propertyMapping.MatchAny.Any(m => fileContent.Contains(m, StringComparison.OrdinalIgnoreCase))))) { projectAuthenticationSettings.ApplicationParameters.Sets(propertyMapping.Sets); } @@ -258,7 +258,8 @@ private static void UpdatePropertyRepresents( index, length, replaceFrom, - propertyMapping.Represents); + propertyMapping.Represents, + propertyMapping.Property!); } } @@ -331,19 +332,31 @@ private static void ReadCodeSetting( projectAuthenticationSettings.ApplicationParameters.EffectiveTenantId = (value != defaultValue) ? value : null; break; case "Application.Authority": - // Case of Blazorwasm where the authority is not separated :( + // Case of Blazorwasm and CIAM where the authority is not separated :( projectAuthenticationSettings.ApplicationParameters.Authority = value; - if (!string.IsNullOrEmpty(value)) + if (!string.IsNullOrEmpty(value) && !projectAuthenticationSettings.ApplicationParameters.IsCiam) { // TODO: something more generic Uri authority = new Uri(value); - string? tenantOrDomain = authority.LocalPath.Split('/', StringSplitOptions.RemoveEmptyEntries)[0]; - if (tenantOrDomain == "qualified.domain.name") + string[] segments = authority.LocalPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 0) { - tenantOrDomain = null; + string? tenantOrDomain = segments[0]; + + if (tenantOrDomain == "qualified.domain.name") + { + tenantOrDomain = null; + } + projectAuthenticationSettings.ApplicationParameters.Domain = tenantOrDomain; + projectAuthenticationSettings.ApplicationParameters.TenantId = tenantOrDomain; + } + else + { + if (value.Contains(".ciamlogin.com", StringComparison.OrdinalIgnoreCase)) + { + projectAuthenticationSettings.ApplicationParameters.IsCiam = true; + } } - projectAuthenticationSettings.ApplicationParameters.Domain = tenantOrDomain; - projectAuthenticationSettings.ApplicationParameters.TenantId = tenantOrDomain; } break; case "Directory.Domain": @@ -388,9 +401,10 @@ private static void AddReplacement( int index, int length, string replaceFrom, - string replaceBy) + string replaceBy, + string property) { - projectAuthenticationSettings.Replacements.Add(new Replacement(filePath, index, length, replaceFrom, replaceBy)); + projectAuthenticationSettings.Replacements.Add(new Replacement(filePath, index, length, replaceFrom, replaceBy, property)); } } diff --git a/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeWriter.cs b/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeWriter.cs index 739664286..9debfef4e 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeWriter.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/CodeReaderWriter/CodeWriter.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Microsoft.Identity.App.CodeReaderWriter { @@ -20,21 +22,14 @@ internal void WriteConfiguration(Summary summary, IEnumerable repla string fileContent = File.ReadAllText(filePath); bool updated = false; - foreach (Replacement r in replacementsInFile.OrderByDescending(r => r.Index)) + + if (filePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { - string? replaceBy = ComputeReplacement(r.ReplaceBy, reconcialedApplicationParameters); - if (replaceBy != null && replaceBy!=r.ReplaceFrom) - { - int index = fileContent.IndexOf(r.ReplaceFrom /*, r.Index*/); - if (index != -1) - { - fileContent = fileContent.Substring(0, index) - + replaceBy - + fileContent.Substring(index + r.Length); - updated = true; - summary.changes.Add(new Change($"{filePath}: updating {r.ReplaceBy}")); - } - } + updated = ReplaceInJSonFile(reconcialedApplicationParameters, replacementsInFile, ref fileContent); + } + else + { + updated = ReplaceInTextFile(summary, reconcialedApplicationParameters, replacementsInFile, filePath, ref fileContent); } if (updated) @@ -49,10 +44,93 @@ internal void WriteConfiguration(Summary summary, IEnumerable repla } } + private bool ReplaceInTextFile(Summary summary, ApplicationParameters reconcialedApplicationParameters, IGrouping replacementsInFile, string filePath, ref string fileContent) + { + bool updated = false; + + foreach (Replacement r in replacementsInFile.OrderByDescending(r => r.Index)) + { + string? replaceBy = ComputeReplacement(r.ReplaceBy, reconcialedApplicationParameters); + if (replaceBy != null && replaceBy != r.ReplaceFrom) + { + int index = fileContent.IndexOf(r.ReplaceFrom /*, r.Index*/, StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + fileContent = fileContent.Substring(0, index) + + replaceBy + + fileContent.Substring(index + r.Length); + updated = true; + summary.changes.Add(new Change($"{filePath}: updating {r.ReplaceBy}")); + } + } + } + return updated; + } + + private bool ReplaceInJSonFile(ApplicationParameters reconcialedApplicationParameters, IGrouping replacementsInFile, ref string fileContent) + { + bool updated = false; + JsonNode jsonNode = JsonNode.Parse(fileContent, new JsonNodeOptions() { }, new JsonDocumentOptions() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip })!; + foreach (var replacement in replacementsInFile.Where(r => r.ReplaceBy != null)) + { + string? newValue = ComputeReplacement(replacement.ReplaceBy, reconcialedApplicationParameters); + if (newValue == null) + { + continue; + } + + IEnumerable pathToParent = replacement.Property.Split(":"); + string propertyName = pathToParent.Last(); + pathToParent = pathToParent.Take(pathToParent.Count() - 1); + + JsonNode? parent = jsonNode; + foreach (string nodeName in pathToParent) + { + if (parent != null) + { + parent = parent[nodeName]; + } + } + + if (parent == null) + { + continue; + } + + JsonValue? propertyNode = parent[propertyName] as JsonValue; + + if (propertyNode == null) + { + parent.AsObject().Add(propertyName, newValue); + updated = true; + } + else if (newValue == "*Remove*") + { + JsonObject parentAsObject = parent.AsObject(); + parentAsObject.Remove(propertyName); + updated = true; + } + else + { + if (propertyNode.TryGetValue(out string? value)) + { + if (value != newValue) + { + parent[propertyName] = newValue; + updated = true; + } + } + } + } + + fileContent = jsonNode.ToJsonString(new JsonSerializerOptions() { WriteIndented = true }); + return updated; + } + private string? ComputeReplacement(string replaceBy, ApplicationParameters reconciledApplicationParameters) { string? replacement = replaceBy; - switch(replaceBy) + switch (replaceBy) { case "Application.ClientSecret": string? password = reconciledApplicationParameters.PasswordCredentials.LastOrDefault(); @@ -89,10 +167,10 @@ internal void WriteConfiguration(Summary summary, IEnumerable repla replacement = reconciledApplicationParameters.ClientId; break; case "Directory.TenantId": - replacement = reconciledApplicationParameters.TenantId; + replacement = reconciledApplicationParameters.IsCiam ? "*Remove*" : reconciledApplicationParameters.TenantId; break; case "Directory.Domain": - replacement = reconciledApplicationParameters.Domain; + replacement = reconciledApplicationParameters.IsCiam ? "*Remove*" : reconciledApplicationParameters.Domain; break; case "Application.SusiPolicy": replacement = reconciledApplicationParameters.SusiPolicy; @@ -114,8 +192,7 @@ internal void WriteConfiguration(Summary summary, IEnumerable repla case "Application.Authority": replacement = reconciledApplicationParameters.Authority; // Blazor b2C - replacement = replacement?.Replace("onmicrosoft.com.b2clogin.com", "b2clogin.com"); - + replacement = replacement?.Replace("onmicrosoft.com.b2clogin.com", "b2clogin.com", StringComparison.OrdinalIgnoreCase); break; case "MsalAuthenticationOptions": // Todo generalize with a directive: Ensure line after line, or ensure line @@ -126,27 +203,35 @@ internal void WriteConfiguration(Summary summary, IEnumerable repla replacement += "\n options.ProviderOptions.DefaultAccessTokenScopes.Add(\"User.Read\");"; - } + } break; case "Application.CalledApiScopes": replacement = reconciledApplicationParameters.CalledApiScopes - ?.Replace("openid", string.Empty) - ?.Replace("offline_access", string.Empty) + ?.Replace("openid", string.Empty, StringComparison.OrdinalIgnoreCase) + ?.Replace("offline_access", string.Empty, StringComparison.OrdinalIgnoreCase) ?.Trim(); break; case "Application.Instance": - if (reconciledApplicationParameters.Instance == "https://login.microsoftonline.com/tfp/" - && reconciledApplicationParameters.IsB2C - && !string.IsNullOrEmpty(reconciledApplicationParameters.Domain) - && reconciledApplicationParameters.Domain.EndsWith(".onmicrosoft.com")) + if (reconciledApplicationParameters.IsCiam) { - replacement = "https://"+reconciledApplicationParameters.Domain.Replace(".onmicrosoft.com", ".b2clogin.com") - .Replace("aadB2CInstance", reconciledApplicationParameters.Domain1); + replacement = "*Remove*"; } else { - replacement = reconciledApplicationParameters.Instance; + + if (reconciledApplicationParameters.Instance == "https://login.microsoftonline.com/tfp/" + && reconciledApplicationParameters.IsB2C + && !string.IsNullOrEmpty(reconciledApplicationParameters.Domain) + && reconciledApplicationParameters.Domain.EndsWith(".onmicrosoft.com", StringComparison.OrdinalIgnoreCase)) + { + replacement = "https://" + reconciledApplicationParameters.Domain.Replace(".onmicrosoft.com", ".b2clogin.com", StringComparison.OrdinalIgnoreCase) + .Replace("aadB2CInstance", reconciledApplicationParameters.Domain1, StringComparison.OrdinalIgnoreCase); + } + else + { + replacement = reconciledApplicationParameters.Instance; + } } break; case "Application.ConfigurationSection": @@ -155,6 +240,10 @@ internal void WriteConfiguration(Summary summary, IEnumerable repla case "Application.AppIdUri": replacement = reconciledApplicationParameters.AppIdUri; break; + case "Application.ExtraQueryParameters": + replacement = null; + break; + default: Console.WriteLine($"{replaceBy} not known"); diff --git a/tools/app-provisioning-tool/app-provisioning-lib/DeveloperCredentials/MsalTokenCredential.cs b/tools/app-provisioning-tool/app-provisioning-lib/DeveloperCredentials/MsalTokenCredential.cs index b3147a20a..709cfc3b8 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/DeveloperCredentials/MsalTokenCredential.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/DeveloperCredentials/MsalTokenCredential.cs @@ -109,7 +109,7 @@ public override async ValueTask GetTokenAsync(TokenRequestContext r } catch (MsalServiceException ex) { - if (ex.Message.Contains("AADSTS70002")) // "The client does not exist or is not enabled for consumers" + if (ex.Message.Contains("AADSTS70002", StringComparison.OrdinalIgnoreCase)) // "The client does not exist or is not enabled for consumers" { Console.WriteLine("An Azure AD tenant, and a user in that tenant, " + "needs to be created for this account before an application can be created. See https://aka.ms/ms-identity-app/create-a-tenant. "); diff --git a/tools/app-provisioning-tool/app-provisioning-lib/MicrosoftIdentityPlatformApplication/MicrosoftIdentityPlatformApplicationManager.cs b/tools/app-provisioning-tool/app-provisioning-lib/MicrosoftIdentityPlatformApplication/MicrosoftIdentityPlatformApplicationManager.cs index 0e64b47c5..6adcba45f 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/MicrosoftIdentityPlatformApplication/MicrosoftIdentityPlatformApplicationManager.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/MicrosoftIdentityPlatformApplication/MicrosoftIdentityPlatformApplicationManager.cs @@ -147,7 +147,7 @@ await AddPasswordCredentials( } else { - if (ex.Message.Contains("User was not found") || ex.Message.Contains("not found in tenant")) + if (ex.Message.Contains("User was not found", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("not found in tenant", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine("User was not found.\nUse both --tenant-id --username .\nAnd re-run the tool."); } @@ -352,7 +352,7 @@ await graphServiceClient.Oauth2PermissionGrants if (!string.IsNullOrEmpty(calledApiScopes)) { string[] scopes = calledApiScopes.Split(' ', '\t', StringSplitOptions.RemoveEmptyEntries); - scopesPerResource = scopes.Select(s => (!s.Contains('/')) + scopesPerResource = scopes.Select(s => (!s.Contains('/', StringComparison.OrdinalIgnoreCase)) // Microsoft Graph shortcut scopes (for instance "User.Read") ? new ResourceAndScope("https://graph.microsoft.com", s) // Proper AppIdUri/scope @@ -421,11 +421,11 @@ private static void AddWebAppPlatform(ApplicationParameters applicationParameter { applicationParameters.CalledApiScopes = string.Empty; } - if (!applicationParameters.CalledApiScopes.Contains("openid")) + if (!applicationParameters.CalledApiScopes.Contains("openid", StringComparison.OrdinalIgnoreCase)) { applicationParameters.CalledApiScopes += " openid"; } - if (!applicationParameters.CalledApiScopes.Contains("offline_access")) + if (!applicationParameters.CalledApiScopes.Contains("offline_access", StringComparison.OrdinalIgnoreCase)) { applicationParameters.CalledApiScopes += " offline_access"; } @@ -578,7 +578,7 @@ private ApplicationParameters GetEffectiveApplicationParameters( Application application, ApplicationParameters originalApplicationParameters) { - bool isB2C = (tenant.TenantType == "AAD B2C"); + bool isB2C = (tenant.TenantType == "AAD B2C") && !originalApplicationParameters.IsCiam; var effectiveApplicationParameters = new ApplicationParameters { ApplicationDisplayName = application.DisplayName, @@ -586,6 +586,7 @@ private ApplicationParameters GetEffectiveApplicationParameters( EffectiveClientId = application.AppId, IsAAD = !isB2C, IsB2C = isB2C, + IsCiam = originalApplicationParameters.IsCiam, HasAuthentication = true, IsWebApi = application.Api != null && (application.Api.Oauth2PermissionScopes != null && application.Api.Oauth2PermissionScopes.Any()) @@ -617,9 +618,11 @@ private ApplicationParameters GetEffectiveApplicationParameters( // TODO: introduce the Instance? effectiveApplicationParameters.Authority = isB2C ? $"https://{effectiveApplicationParameters.Domain1}.b2clogin.com/{effectiveApplicationParameters.Domain}/{effectiveApplicationParameters.SusiPolicy}/" + : originalApplicationParameters.IsCiam ? $"https://{effectiveApplicationParameters.Domain1}.ciamlogin.com/" : $"https://login.microsoftonline.com/{effectiveApplicationParameters.TenantId ?? effectiveApplicationParameters.Domain}/"; effectiveApplicationParameters.Instance = isB2C ? $"https://{effectiveApplicationParameters.Domain1}.b2clogin.com/" + : originalApplicationParameters.IsCiam ? $"https://{effectiveApplicationParameters.Domain1}.ciamlogin.com/" : originalApplicationParameters.Instance; effectiveApplicationParameters.PasswordCredentials.AddRange(application.PasswordCredentials.Select(p => p.Hint + "******************")); diff --git a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ConfigurationProperties.cs b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ConfigurationProperties.cs index 8fecff6fe..3f3d155db 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ConfigurationProperties.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ConfigurationProperties.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Globalization; using System.IO; using System.Linq; @@ -11,7 +12,7 @@ public class ConfigurationProperties { public string? FileRelativePath { - get { return _fileRelativePath?.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture)); } + get { return _fileRelativePath?.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); } set { _fileRelativePath = value; } } private string? _fileRelativePath; diff --git a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/MatchesForProjectType.cs b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/MatchesForProjectType.cs index 1e897365c..114e06857 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/MatchesForProjectType.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/MatchesForProjectType.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Globalization; using System.IO; using System.Linq; @@ -11,7 +12,7 @@ public class MatchesForProjectType { public string? FileRelativePath { - get { return _fileRelativePath?.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture)); } + get { return _fileRelativePath?.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); } set { _fileRelativePath = value; } } private string? _fileRelativePath; diff --git a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ProjectDescriptionReader.cs b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ProjectDescriptionReader.cs index f5e9a0653..bf850c7c2 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ProjectDescriptionReader.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/ProjectDescriptionReader.cs @@ -78,7 +78,7 @@ public class ProjectDescriptionReader string fileContent = File.ReadAllText(filePath); foreach (string match in matchesForProjectType.MatchAny!) // Valid project => { - if (fileContent.Contains(match)) + if (fileContent.Contains(match, StringComparison.OrdinalIgnoreCase)) { return projectDescription.Identifier!; } diff --git a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/Replacement.cs b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/Replacement.cs index 9b8e441b3..5de2ec6d2 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/Replacement.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescription/Replacement.cs @@ -10,25 +10,28 @@ public Replacement() FilePath = string.Empty; ReplaceFrom = string.Empty; ReplaceBy = string.Empty; + Property = string.Empty; } - public Replacement(string filePath, int index, int length, string replaceFrom, string replaceBy) + public Replacement(string filePath, int index, int length, string replaceFrom, string replaceBy, string property) { FilePath = filePath; Index = index; Length = length; ReplaceFrom = replaceFrom; ReplaceBy = replaceBy; + Property = property; } public string FilePath { get; set; } public int Index { get; set; } public int Length { get; set; } public string ReplaceFrom { get; set; } public string ReplaceBy { get; set; } + public string Property { get; set; } public override string ToString() { - return $"Replace '{ReplaceFrom}' by '{ReplaceBy}'"; + return $"Replace '{Property}' from '{ReplaceFrom}' by '{ReplaceBy}'"; } } } diff --git a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescriptions/dotnet-web.json b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescriptions/dotnet-web.json index 674215c64..c21494bff 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescriptions/dotnet-web.json +++ b/tools/app-provisioning-tool/app-provisioning-lib/ProjectDescriptions/dotnet-web.json @@ -12,6 +12,17 @@ "Sets": "IsAAD", "Default": "11111111-1111-1111-11111111111111111" }, + { + "Property": "AzureAd:Instance", + "Represents": "Application.Instance", + "Sets": "IsAAD", + "Default": "https://login.microsoftonline.com/" + }, + { + "Property": "AzureAd:Authority", + "Represents": "Application.Authority", + "Sets": "IsCiam" + }, { "Property": "AzureAd:Domain", "Represents": "Directory.Domain", diff --git a/tools/app-provisioning-tool/app-provisioning-lib/Properties/Resources.Designer.cs b/tools/app-provisioning-tool/app-provisioning-lib/Properties/Resources.Designer.cs index 1a04b4a41..5ac1b9018 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/Properties/Resources.Designer.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.Identity.App.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/tools/app-provisioning-tool/app-provisioning-lib/Tool/AppProvisioningTool.cs b/tools/app-provisioning-tool/app-provisioning-lib/Tool/AppProvisioningTool.cs index f37a3f5f8..0865adf28 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/Tool/AppProvisioningTool.cs +++ b/tools/app-provisioning-tool/app-provisioning-lib/Tool/AppProvisioningTool.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Azure.Core; +using Microsoft.Graph; using Microsoft.Identity.App.AuthenticationParameters; using Microsoft.Identity.App.CodeReaderWriter; using Microsoft.Identity.App.DeveloperCredentials; @@ -9,9 +10,12 @@ using Microsoft.Identity.App.Project; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; +using File = System.IO.File; +using Process = System.Diagnostics.Process; namespace Microsoft.Identity.App { @@ -134,9 +138,24 @@ await WriteApplicationRegistration( // Summarizes what happened WriteSummary(summary); + + Console.WriteLine("Updating NuGet packages\n"); + EnsurePackage("Microsoft.Identity.Web"); + EnsurePackage("Microsoft.Identity.Web.UI"); + return effectiveApplicationParameters; } + private static void EnsurePackage(string package) + { + ProcessStartInfo processStartInfo = new ProcessStartInfo("dotnet", $"add package {package}") + { + UseShellExecute = false, + }; + Process? process = Process.Start(processStartInfo); + process?.WaitForExit(); + } + /// /// Converts an AAD application to a B2C application /// @@ -153,13 +172,13 @@ private ProjectAuthenticationSettings ConvertAadApplicationToB2CApplication(Proj foreach (string filePath in filesWithReplacementsForB2C) { string fileContent = File.ReadAllText(filePath); - string updatedContent = fileContent.Replace("AzureAd", "AzureAdB2C"); + string updatedContent = fileContent.Replace("AzureAd", "AzureAdB2C", StringComparison.OrdinalIgnoreCase); // Add the policies to the appsettings.json - if (filePath.EndsWith("appsettings.json")) + if (filePath.EndsWith("appsettings.json", StringComparison.OrdinalIgnoreCase)) { // Insert the policies - int indexCallbackPath = updatedContent.IndexOf("\"CallbackPath\""); + int indexCallbackPath = updatedContent.IndexOf("\"CallbackPath\"", StringComparison.OrdinalIgnoreCase); if (indexCallbackPath > 0) { updatedContent = updatedContent.Substring(0, indexCallbackPath) @@ -262,7 +281,22 @@ private ProjectAuthenticationSettings InferApplicationParameters( // Override with the tools options projectSettings.ApplicationParameters.ApplicationDisplayName ??= Path.GetFileName(provisioningToolOptions.CodeFolder); projectSettings.ApplicationParameters.ClientId ??= provisioningToolOptions.ClientId; - projectSettings.ApplicationParameters.TenantId ??= provisioningToolOptions.TenantId; + + // To do: Un-comment when the Graph API returns the right tenant type. + // projectSettings.ApplicationParameters.TenantId ??= provisioningToolOptions.TenantId; + + + WorkaroundCiam(projectSettings.ApplicationParameters, provisioningToolOptions.TenantId); + if (projectSettings.ApplicationParameters.IsCiam && !projectSettings.Replacements.Any(r => r.Property == "AzureAd:Authority")) + { + Replacement? r = projectSettings.Replacements.FirstOrDefault(r => r.Property == "AzureAd"); + if (r != null) + { + projectSettings.Replacements.Remove(r); + projectSettings.Replacements.Add(new Replacement(r.FilePath, -1, -1, "", "Application.Authority", "AzureAd:Authority")); + projectSettings.Replacements.Add(new Replacement(r.FilePath, -1, -1, "", "Application.ExtraQueryParameters", "AzureAd:ExtraQueryParameters")); + } + } projectSettings.ApplicationParameters.CalledApiScopes ??= provisioningToolOptions.CalledApiScopes; if (!string.IsNullOrEmpty(provisioningToolOptions.AppIdUri)) { @@ -271,6 +305,27 @@ private ProjectAuthenticationSettings InferApplicationParameters( return projectSettings; } + /// + /// Workaround for the Graph API not returning the right Tenant type + /// + /// + /// + private void WorkaroundCiam(ApplicationParameters applicationParameters, string? tenantId) + { + bool isCiam = false; + if (!string.IsNullOrWhiteSpace(tenantId) && tenantId.EndsWith(".ciamlogin.com", StringComparison.OrdinalIgnoreCase)) + { + applicationParameters.IsCiam = true; + applicationParameters.IsB2C = false; + applicationParameters.EffectiveTenantId ??= tenantId.Replace(".ciamlogin.com", ".onmicrosoft.com", StringComparison.OrdinalIgnoreCase); + } + else + { + applicationParameters.IsCiam = false; + applicationParameters.EffectiveTenantId ??=tenantId ; + } + } + private TokenCredential GetTokenCredential(ProvisioningToolOptions provisioningToolOptions, string? currentApplicationTenantId) { DeveloperCredentialsReader developerCredentialsReader = new DeveloperCredentialsReader(); diff --git a/tools/app-provisioning-tool/app-provisioning-lib/app-provisioning-lib.csproj b/tools/app-provisioning-tool/app-provisioning-lib/app-provisioning-lib.csproj index 80f37e641..7b1752ee7 100644 --- a/tools/app-provisioning-tool/app-provisioning-lib/app-provisioning-lib.csproj +++ b/tools/app-provisioning-tool/app-provisioning-lib/app-provisioning-lib.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + net7.0 enable Microsoft.Identity.App Library @@ -16,14 +16,13 @@ - - - - - - - - + + + + + + + @@ -45,4 +44,8 @@ + + + + diff --git a/tools/app-provisioning-tool/app-provisioning-tool/Program.cs b/tools/app-provisioning-tool/app-provisioning-tool/Program.cs index 8c84f6d78..2cb8fbb97 100644 --- a/tools/app-provisioning-tool/app-provisioning-tool/Program.cs +++ b/tools/app-provisioning-tool/app-provisioning-tool/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Threading.Tasks; namespace Microsoft.Identity.App @@ -86,7 +87,7 @@ private static void GenerateTests() { foreach (string projectFolder in System.IO.Directory.GetDirectories(subFolder)) { - System.Console.WriteLine($"[InlineData(@\"{System.IO.Path.GetFileName(subFolder)}\\{System.IO.Path.GetFileName(projectFolder)}\", {projectFolder.Contains("b2c")}, \"dotnet-WebApp\")]"); + System.Console.WriteLine($"[InlineData(@\"{System.IO.Path.GetFileName(subFolder)}\\{System.IO.Path.GetFileName(projectFolder)}\", {projectFolder.Contains("b2c", StringComparison.OrdinalIgnoreCase)}, \"dotnet-WebApp\")]"); } } } diff --git a/tools/app-provisioning-tool/app-provisioning-tool/msidentity-app-sync.csproj b/tools/app-provisioning-tool/app-provisioning-tool/msidentity-app-sync.csproj index 6e1e2c2a5..bd8bfc07c 100644 --- a/tools/app-provisioning-tool/app-provisioning-tool/msidentity-app-sync.csproj +++ b/tools/app-provisioning-tool/app-provisioning-tool/msidentity-app-sync.csproj @@ -2,8 +2,8 @@ Exe - 1.0.1 - netcoreapp3.1;net5.0 + 1.1.0 + net7.0 enable Microsoft.Identity.App @@ -12,8 +12,6 @@ ./nupkg true ../../../build/MSAL.snk - - true Microsoft identity platform auto-sync app registration tool Microsoft @@ -27,7 +25,6 @@ For details see https://aka.ms/ms-identity-app-registration. © Microsoft Corporation. All rights reserved. - MIT https://github.com/AzureAD/microsoft-identity-web/blob/master/tools/app-provisioning-tool/README.md https://github.com/AzureAD/microsoft-identity-web The release notes are available at https://github.com/AzureAD/microsoft-identity-web/releases and the roadmap at https://github.com/AzureAD/microsoft-identity-web/wiki#roadmap @@ -35,13 +32,6 @@ - - - True - - - - @@ -54,6 +44,11 @@ + + + + + diff --git a/tools/app-provisioning-tool/tests/ProjectDescriptionReaderTests.cs b/tools/app-provisioning-tool/tests/ProjectDescriptionReaderTests.cs index 1b373395d..771be8d39 100644 --- a/tools/app-provisioning-tool/tests/ProjectDescriptionReaderTests.cs +++ b/tools/app-provisioning-tool/tests/ProjectDescriptionReaderTests.cs @@ -51,15 +51,15 @@ public void TestProjectDescriptionReader(string folderPath, string command, stri var projectDescription = _projectDescriptionReader.GetProjectDescription(string.Empty, createdProjectFolder); Assert.NotNull(projectDescription); - Assert.Equal(expectedProjectType, projectDescription.Identifier); + Assert.Equal(expectedProjectType, projectDescription!.Identifier); var authenticationSettings = _codeReader.ReadFromFiles( createdProjectFolder, projectDescription, _projectDescriptionReader.projectDescriptions); - bool callsGraph = folderPath.Contains(TestConstants.CallsGraph); - bool callsWebApi = folderPath.Contains(TestConstants.CallsWebApi) || callsGraph; + bool callsGraph = folderPath.Contains(TestConstants.CallsGraph, StringComparison.OrdinalIgnoreCase); + bool callsWebApi = folderPath.Contains(TestConstants.CallsWebApi, StringComparison.OrdinalIgnoreCase) || callsGraph; if (isB2C) { @@ -95,7 +95,7 @@ public void TestProjectDescriptionReader_TemplatesWithBlazorWasm(string folderPa var projectDescription = _projectDescriptionReader.GetProjectDescription(string.Empty, createdProjectFolder); Assert.NotNull(projectDescription); - Assert.Equal(expectedProjectType, projectDescription.Identifier); + Assert.Equal(expectedProjectType, projectDescription!.Identifier); var authenticationSettings = _codeReader.ReadFromFiles( createdProjectFolder, @@ -130,7 +130,7 @@ public void TestProjectDescriptionReader_TemplatesWithBlazorWasmHosted(string fo var projectDescription = _projectDescriptionReader.GetProjectDescription(string.Empty, createdProjectFolder); Assert.NotNull(projectDescription); - Assert.Equal(expectedProjectType, projectDescription.Identifier); + Assert.Equal(expectedProjectType, projectDescription!.Identifier); var authenticationSettings = _codeReader.ReadFromFiles( createdProjectFolder, @@ -155,7 +155,7 @@ public void TestProjectDescriptionReader_TemplatesWithNoAuth(string folderPath, var projectDescription = _projectDescriptionReader.GetProjectDescription(string.Empty, createdProjectFolder); Assert.NotNull(projectDescription); - Assert.Equal(expectedProjectType, projectDescription.Identifier); + Assert.Equal(expectedProjectType, projectDescription!.Identifier); var authenticationSettings = _codeReader.ReadFromFiles( createdProjectFolder, @@ -217,7 +217,7 @@ private string CreateProjectIfNeeded(string projectFolderName, string command, s string parentFolder = Path.Combine(tempFolder, "Provisioning", testName); string createdProjectFolder = Path.Combine( parentFolder, - projectFolderName.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture))); + projectFolderName.Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); if (!Directory.Exists(createdProjectFolder)) { @@ -226,7 +226,7 @@ private string CreateProjectIfNeeded(string projectFolderName, string command, s TestUtilities.RunProcess(_testOutput, command, createdProjectFolder, " --force"); // Add the capability of holding user secrets aside of appsettings.json if needed - if (command.Contains("--calls")) + if (command.Contains("--calls", StringComparison.OrdinalIgnoreCase)) { try { diff --git a/tools/app-provisioning-tool/tests/TestUtilities.cs b/tools/app-provisioning-tool/tests/TestUtilities.cs index 402440da2..a817f3a0e 100644 --- a/tools/app-provisioning-tool/tests/TestUtilities.cs +++ b/tools/app-provisioning-tool/tests/TestUtilities.cs @@ -21,19 +21,22 @@ internal static class TestUtilities public static void RunProcess(ITestOutputHelper testOutput, string command, string folder, string postFix = "") { Directory.CreateDirectory(folder); - ProcessStartInfo processStartInfo = new ProcessStartInfo("dotnet", command.Replace("dotnet ", string.Empty) + postFix); + ProcessStartInfo processStartInfo = new ProcessStartInfo("dotnet", command.Replace("dotnet ", string.Empty, StringComparison.OrdinalIgnoreCase) + postFix); processStartInfo.UseShellExecute = false; processStartInfo.RedirectStandardOutput = true; processStartInfo.RedirectStandardError = true; Environment.GetEnvironmentVariables(); processStartInfo.WorkingDirectory = folder; - Process process = Process.Start(processStartInfo); - process.WaitForExit(); - string output = process.StandardOutput.ReadToEnd(); - testOutput.WriteLine(output); - string errors = process.StandardError.ReadToEnd(); - testOutput.WriteLine(errors); - Assert.Equal(string.Empty, errors); + Process? process = Process.Start(processStartInfo); + if (process != null) + { + process.WaitForExit(); + string output = process.StandardOutput.ReadToEnd(); + testOutput.WriteLine(output); + string errors = process.StandardError.ReadToEnd(); + testOutput.WriteLine(errors); + Assert.Equal(string.Empty, errors); + } } } } diff --git a/tools/app-provisioning-tool/tests/Tests.csproj b/tools/app-provisioning-tool/tests/Tests.csproj index bcfcbcac7..bb9504b5d 100644 --- a/tools/app-provisioning-tool/tests/Tests.csproj +++ b/tools/app-provisioning-tool/tests/Tests.csproj @@ -1,9 +1,11 @@  - net5.0 + net7.0 false + + False