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)
  • Loading branch information
jmprieur authored May 3, 2023
1 parent 6ef91da commit 7e3443f
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public string? Domain1
{
get
{
return Domain?.Replace(".onmicrosoft.com", string.Empty);
return Domain?.Replace(".onmicrosoft.com", string.Empty, StringComparison.OrdinalIgnoreCase);
}
}

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
{
Expand All @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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);
}
Expand Down Expand Up @@ -258,7 +258,8 @@ private static void UpdatePropertyRepresents(
index,
length,
replaceFrom,
propertyMapping.Represents);
propertyMapping.Represents,
propertyMapping.Property!);
}
}

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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":
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ");
Expand Down
Loading

0 comments on commit 7e3443f

Please sign in to comment.