Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CIAM to the app registration tool #2219

Merged
merged 5 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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