Skip to content

Commit

Permalink
Add support for CIAM to the app registration tool (#2219)
Browse files Browse the repository at this point in the history
* 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)
jmprieur authored May 3, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 6ef91da commit 7e3443f
Showing 21 changed files with 303 additions and 103 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -349,3 +349,4 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/tools/app-provisioning-tool/testwebapp
14 changes: 14 additions & 0 deletions tools/app-provisioning-tool/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../../'))" />
<PropertyGroup>
<TargetFrameworks>net7.0</TargetFrameworks>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>../../../build/MSAL.snk</AssemblyOriginatorKeyFile>
<LangVersion>10.0</LangVersion>
</PropertyGroup>

<ItemGroup>
<None Remove="..\..\LICENSE"/>
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion tools/app-provisioning-tool/README.md
Original file line number Diff line number Diff line change
@@ -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.

Original file line number Diff line number Diff line change
@@ -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
/// </summary>
public bool IsB2C { get; set; }

/// <summary>
/// Is the tenant a CIAM tenant?
/// </summary>
public bool IsCiam { get; set; }

// TODO: propose a fix for the blazorwasm project template

Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@ private static void PostProcessWebUris(ProjectAuthenticationSettings projectAuth
IEnumerable<string> 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<JsonElement>(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<KeyValuePair<JsonElement, int>> 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));
}

}
Original file line number Diff line number Diff line change
@@ -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<Replacement> 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<Replacement> repla
}
}

private bool ReplaceInTextFile(Summary summary, ApplicationParameters reconcialedApplicationParameters, IGrouping<string, Replacement> 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<string, Replacement> 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<string> 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<Replacement> 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<Replacement> 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<Replacement> 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<Replacement> repla
case "Application.AppIdUri":
replacement = reconciledApplicationParameters.AppIdUri;
break;
case "Application.ExtraQueryParameters":
replacement = null;
break;


default:
Console.WriteLine($"{replaceBy} not known");
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ public override async ValueTask<AccessToken> 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. ");
Loading

0 comments on commit 7e3443f

Please sign in to comment.