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

AAD B2C updates #1961

Merged
merged 9 commits into from
Aug 1, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Microsoft.DotNet.MSIdentity.AuthenticationParameters
{
public class PropertyNames
public static class PropertyNames
{
public const string Domain = nameof(Domain);
public const string TenantId = nameof(TenantId);
Expand All @@ -12,12 +12,14 @@ public class PropertyNames
public const string ClientCertificates = nameof(ClientCertificates);
public const string CallbackPath = nameof(CallbackPath);
public const string Instance = nameof(Instance);

public const string Authority = nameof(Authority);
public const string ValidateAuthority = nameof(ValidateAuthority);

public const string BaseUrl = nameof(BaseUrl);
public const string Scopes = nameof(Scopes);
public const string SignUpSignInPolicyId = nameof(SignUpSignInPolicyId);
public const string ResetPasswordPolicyId = nameof(ResetPasswordPolicyId);
public const string EditProfilePolicyId = nameof(EditProfilePolicyId);
public const string SignedOutCallbackPath = nameof(SignedOutCallbackPath);
}

// getting default properties from
Expand All @@ -30,10 +32,14 @@ public static class DefaultProperties
public const string Instance = "https://login.microsoftonline.com/";
public const string CallbackPath = "/signin-oidc";
public const string ClientSecret = "Client secret from app-registration. Check user secrets/azure portal.";

public const string Authority = "https://login.microsoftonline.com/22222222-2222-2222-2222-222222222222";
public const bool ValidateAuthority = true;

// B2C properties
public const string SignUpSignInPolicyId = "b2c_1_susi";
public const string ResetPasswordPolicyId = "b2c_1_reset";
public const string EditProfilePolicyId = "b2c_1_edit_profile";
public const string SignedOutCallbackPath = "/signout/B2C_1_susi";

public const string MicrosoftGraphBaseUrl = "https://graph.microsoft.com/v1.0";
public const string MicrosoftGraphScopes = "user.read";
public const string ApiScopes = "access_as_user";
Expand All @@ -43,59 +49,49 @@ public class AzureAdBlock
{
public bool IsBlazorWasm;
public bool IsWebApi;
public bool IsB2C;

public string? ClientId;
public string? Instance = DefaultProperties.Instance;
public string? Instance;
public string? Domain;
public string? TenantId;
public string? Authority;
public string? CallbackPath = DefaultProperties.CallbackPath;
public string? CallbackPath;
public string? SignUpSignInPolicyId;
public string? ResetPasswordPolicyId = DefaultProperties.ResetPasswordPolicyId;
public string? EditProfilePolicyId = DefaultProperties.EditProfilePolicyId;
public string? SignedOutCallbackPath = DefaultProperties.SignedOutCallbackPath;

public string? Scopes;

public string? ClientSecret = DefaultProperties.ClientSecret;
public string? ClientSecret;
public string[]? ClientCertificates;

public AzureAdBlock(ApplicationParameters applicationParameters)
public AzureAdBlock(ApplicationParameters applicationParameters, JObject? existingBlock = null)
{
IsBlazorWasm = applicationParameters.IsBlazorWasm;
IsWebApi = applicationParameters.IsWebApi.GetValueOrDefault();

Domain = !string.IsNullOrEmpty(applicationParameters.Domain) ? applicationParameters.Domain : null;
TenantId = !string.IsNullOrEmpty(applicationParameters.TenantId) ? applicationParameters.TenantId : null;
ClientId = !string.IsNullOrEmpty(applicationParameters.ClientId) ? applicationParameters.ClientId : null;
Instance = !string.IsNullOrEmpty(applicationParameters.Instance) ? applicationParameters.Instance : null;
Authority = !string.IsNullOrEmpty(applicationParameters.Authority) ? applicationParameters.Authority : null;
CallbackPath = !string.IsNullOrEmpty(applicationParameters.CallbackPath) ? applicationParameters.CallbackPath : null;
Scopes = !string.IsNullOrEmpty(applicationParameters.CalledApiScopes) ? applicationParameters.CalledApiScopes : null;
}

/// <summary>
/// Updates AzureAdBlock object from existing appSettings.json
/// </summary>
/// <param name="azureAdToken"></param>
public AzureAdBlock UpdateFromJToken(JToken azureAdToken)
{
JObject azureAdObj = JObject.FromObject(azureAdToken);

ClientId ??= azureAdObj.GetValue(PropertyNames.ClientId)?.ToString(); // here, if the applicationparameters value is null, we use the existing app settings value
Instance ??= azureAdObj.GetValue(PropertyNames.Instance)?.ToString();
Domain ??= azureAdObj.GetValue(PropertyNames.Domain)?.ToString();
TenantId ??= azureAdObj.GetValue(PropertyNames.TenantId)?.ToString();
Authority ??= azureAdObj.GetValue(PropertyNames.Authority)?.ToString();
CallbackPath ??= azureAdObj.GetValue(PropertyNames.CallbackPath)?.ToString();
Scopes ??= azureAdObj.GetValue(PropertyNames.Scopes)?.ToString();
ClientSecret ??= azureAdObj.GetValue(PropertyNames.ClientSecret)?.ToString();
ClientCertificates ??= azureAdObj.GetValue(PropertyNames.ClientCertificates)?.ToObject<string[]>();

return this;
IsB2C = applicationParameters.IsB2C;

Domain = !string.IsNullOrEmpty(applicationParameters.Domain) ? applicationParameters.Domain : existingBlock?.GetValue(PropertyNames.Domain)?.ToString() ?? DefaultProperties.Domain;
TenantId = !string.IsNullOrEmpty(applicationParameters.TenantId) ? applicationParameters.TenantId : existingBlock?.GetValue(PropertyNames.TenantId)?.ToString() ?? DefaultProperties.TenantId;
ClientId = !string.IsNullOrEmpty(applicationParameters.ClientId) ? applicationParameters.ClientId : existingBlock?.GetValue(PropertyNames.ClientId)?.ToString() ?? DefaultProperties.ClientId;
Instance = !string.IsNullOrEmpty(applicationParameters.Instance) ? applicationParameters.Instance : existingBlock?.GetValue(PropertyNames.Instance)?.ToString() ?? DefaultProperties.Instance;
CallbackPath = !string.IsNullOrEmpty(applicationParameters.CallbackPath) ? applicationParameters.CallbackPath : existingBlock?.GetValue(PropertyNames.CallbackPath)?.ToString() ?? DefaultProperties.CallbackPath;
Scopes = !string.IsNullOrEmpty(applicationParameters.CalledApiScopes) ? applicationParameters.CalledApiScopes : existingBlock?.GetValue(PropertyNames.Scopes)?.ToString()
?? (applicationParameters.CallsDownstreamApi ? DefaultProperties.ApiScopes : applicationParameters.CallsMicrosoftGraph ? DefaultProperties.MicrosoftGraphScopes : null);
SignUpSignInPolicyId = !string.IsNullOrEmpty(applicationParameters.SusiPolicy) ? applicationParameters.SusiPolicy : existingBlock?.GetValue(PropertyNames.SignUpSignInPolicyId)?.ToString() ?? DefaultProperties.SignUpSignInPolicyId;
// TODO determine the SusiPolicy from the graph beta
Authority = IsB2C ? $"{Instance}{TenantId}/{SignUpSignInPolicyId}" : $"{Instance}{TenantId}";
ClientSecret = existingBlock?.GetValue(PropertyNames.ClientSecret)?.ToString() ?? DefaultProperties.ClientSecret;
ClientCertificates = existingBlock?.GetValue(PropertyNames.ClientCertificates)?.ToObject<string[]>();
}

public dynamic BlazorSettings => new
{
ClientId = ClientId ?? DefaultProperties.ClientId, // here, if a value is null, we could use the default properties
Authority = Authority ?? (string.IsNullOrEmpty(Instance) || string.IsNullOrEmpty(TenantId) ? DefaultProperties.Authority : $"{Instance}{TenantId}"),
ValidateAuthority = true
ClientId,
Authority,
ValidateAuthority = !IsB2C
};

public dynamic WebAppSettings => new
Expand All @@ -119,18 +115,30 @@ public AzureAdBlock UpdateFromJToken(JToken azureAdToken)
ClientCertificates = ClientCertificates ?? Array.Empty<string>()
};

public dynamic B2CSettings => new
{
SignUpSignInPolicyId = SignUpSignInPolicyId ?? DefaultProperties.SignUpSignInPolicyId,
SignedOutCallbackPath = SignedOutCallbackPath ?? DefaultProperties.SignedOutCallbackPath,
ResetPasswordPolicyId = ResetPasswordPolicyId ?? DefaultProperties.ResetPasswordPolicyId,
EditProfilePolicyId = EditProfilePolicyId ?? DefaultProperties.EditProfilePolicyId,
EnablePiiLogging = true
};

public JObject ToJObject()
{
if (IsBlazorWasm)
{
return JObject.FromObject(BlazorSettings);
}
if (IsWebApi)

var jObject = IsWebApi ? JObject.FromObject(WebApiSettings) : JObject.FromObject(WebAppSettings);

if (IsB2C)
{
return JObject.FromObject(WebApiSettings);
jObject.Merge(JObject.FromObject(B2CSettings));
}

return JObject.FromObject(WebAppSettings);
return jObject;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,34 +132,29 @@ public void ModifyAppSettings(ApplicationParameters applicationParameters, IEnum
/// <param name="appSettings"></param>
/// <param name="applicationParameters"></param>
/// <returns>(bool changesMade, JObject? updatedBlock)</returns>
internal (bool changesMade, JObject? updatedBlock) GetModifiedAzureAdBlock(JObject appSettings, ApplicationParameters applicationParameters)
internal static (bool changesMade, JObject? updatedBlock) GetModifiedAzureAdBlock(JObject appSettings, ApplicationParameters applicationParameters)
{
var azureAdBlock = new AzureAdBlock(applicationParameters);
if (!appSettings.TryGetValue("AzureAd", out var azureAdToken))
var azAdToken = appSettings.GetValue("AzureAd") ?? appSettings.GetValue("AzureAdB2C"); // TODO test "AzureAdB2C" string, make sure that blazor WASM works
if (azAdToken is null)
{
// Create and return AzureAd block if none exists, differs for Blazor apps
return (true, azureAdBlock.ToJObject());
return (true, new AzureAdBlock(applicationParameters).ToJObject());
}

var existingBlock = JObject.FromObject(azureAdToken);
var updatedBlock = azureAdBlock.UpdateFromJToken(azureAdToken).ToJObject();
if (NeedsUpdate(existingBlock, updatedBlock))
{
return (true, updatedBlock);
}
var existingParameters = JObject.FromObject(azAdToken);
var newBlock = new AzureAdBlock(applicationParameters, existingParameters).ToJObject();

return (false, null); // If no changes were made, return null
return (NeedsUpdate(existingParameters, newBlock), newBlock);
}

/// <summary>
/// Checks all keys in updatedBlock, if any differ from existingBlock then update is necessary
/// </summary>
/// <param name="existingBlock"></param>
/// <param name="updatedBlock"></param>
/// <param name="newBlock"></param>
/// <returns></returns>
internal bool NeedsUpdate(JObject existingBlock, JObject updatedBlock)
internal static bool NeedsUpdate(JObject existingBlock, JObject newBlock)
{
foreach ((var key, var updatedValue) in updatedBlock)
foreach ((var key, var updatedValue) in newBlock)
{
if (existingBlock.GetValue(key) != updatedValue)
{
Expand All @@ -170,7 +165,7 @@ internal bool NeedsUpdate(JObject existingBlock, JObject updatedBlock)
return false;
}

private JObject? GetApiBlock(JObject appSettings, string key, string? scopes, string? baseUrl)
internal static JObject? GetApiBlock(JObject appSettings, string key, string? scopes, string? baseUrl)
{
var inputParameters = JObject.FromObject(new ApiSettingsBlock
{
Expand All @@ -189,7 +184,7 @@ internal bool NeedsUpdate(JObject existingBlock, JObject updatedBlock)
return inputParameters;
}

private bool ModifyAppSettingsObject(JObject existingSettings, JObject inputProperties)
internal static bool ModifyAppSettingsObject(JObject existingSettings, JObject inputProperties)
{
bool changesMade = false;
foreach ((var propertyName, var newValue) in inputProperties)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,18 @@ public class MicrosoftIdentityPlatformApplicationManager
.Request()
.AddAsync(servicePrincipal).ConfigureAwait(false);

if (applicationParameters.IsB2C) // TODO B2C not fully supported at the moment
// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (applicationParameters.IsB2C)
{
// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (applicationParameters.IsB2C)
{
IEnumerable<IGrouping<string, ResourceAndScope>>? scopesPerResource = await AddApiPermissions(
applicationParameters,
graphServiceClient,
application).ConfigureAwait(false);

await AddAdminConsentToApiPermissions(
graphServiceClient,
createdServicePrincipal,
scopesPerResource);
}
IEnumerable<IGrouping<string, ResourceAndScope>>? 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,
Expand Down Expand Up @@ -188,6 +185,7 @@ internal async Task<JsonResponse> UpdateApplication(

var graphServiceClient = GetGraphServiceClient(tokenCredential);

// TODO: Add if it's B2C, acquire or create the SUSI Policy
var remoteApp = (await graphServiceClient.Applications.Request()
.Filter($"appId eq '{parameters.ClientId}'").GetAsync()).FirstOrDefault(app => app.AppId.Equals(parameters.ClientId));

Expand All @@ -196,7 +194,7 @@ internal async Task<JsonResponse> UpdateApplication(
return new JsonResponse(commandName, State.Fail, output: string.Format(Resources.NotFound, parameters.ClientId));
}

(bool needsUpdates, Application appUpdates) = GetApplicationUpdates(remoteApp, toolOptions);
(bool needsUpdates, Application appUpdates) = GetApplicationUpdates(remoteApp, toolOptions, parameters);
if (!needsUpdates)
{
return new JsonResponse(commandName, State.Success, output: string.Format(Resources.NoUpdateNecessary, remoteApp.DisplayName, remoteApp.AppId));
Expand All @@ -220,7 +218,10 @@ internal async Task<JsonResponse> UpdateApplication(
/// <param name="existingApplication"></param>
/// <param name="toolOptions"></param>
/// <returns>Updated Application if changes were made, otherwise null</returns>
internal static (bool needsUpdate, Application appUpdates) GetApplicationUpdates(Application existingApplication, ProvisioningToolOptions toolOptions)
internal static (bool needsUpdate, Application appUpdates) GetApplicationUpdates(
Application existingApplication,
ProvisioningToolOptions toolOptions,
ApplicationParameters parameters)
{
bool needsUpdate = false;

Expand All @@ -233,7 +234,7 @@ internal static (bool needsUpdate, Application appUpdates) GetApplicationUpdates

// Make updates if necessary
needsUpdate |= UpdateRedirectUris(updatedApp, toolOptions);
needsUpdate |= UpdateImplicitGrantSettings(updatedApp, toolOptions);
needsUpdate |= UpdateImplicitGrantSettings(updatedApp, toolOptions, parameters.IsB2C);
if (toolOptions.IsBlazorWasmHostedServer)
{
needsUpdate |= PreAuthorizeBlazorWasmClientApp(existingApplication, toolOptions, updatedApp);
Expand Down Expand Up @@ -347,32 +348,30 @@ private static string UpdateCallbackPath(string redirectUri, bool isBlazorWasm =
/// <param name="app"></param>
/// <param name="toolOptions"></param>
/// <returns>true if ImplicitGrantSettings require updates, else false</returns>
internal static bool UpdateImplicitGrantSettings(Application app, ProvisioningToolOptions toolOptions)
internal static bool UpdateImplicitGrantSettings(Application app, ProvisioningToolOptions toolOptions, bool isB2C = false)
{
bool needsUpdate = false;
var currentSettings = app.Web.ImplicitGrantSettings;

if (toolOptions.IsBlazorWasm) // In the case of Blazor WASM, Access Tokens and Id Tokens must both be true.
// In the case of Blazor WASM and B2C, Access Tokens and Id Tokens must both be true.
if ((toolOptions.IsBlazorWasm || isB2C)
&& (currentSettings.EnableAccessTokenIssuance is false
|| currentSettings.EnableAccessTokenIssuance is false))
{
if (currentSettings.EnableAccessTokenIssuance is true || currentSettings.EnableIdTokenIssuance is true)
{
app.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = false;
app.Web.ImplicitGrantSettings.EnableIdTokenIssuance = false;

needsUpdate = true;
}
app.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = true;
app.Web.ImplicitGrantSettings.EnableIdTokenIssuance = true;
needsUpdate = true;
}
else // Otherwise we make changes only when the tool options differ from the existing settings.
// Otherwise we make changes only when the tool options differ from the existing settings.
else
{
if (toolOptions.EnableAccessToken.HasValue &&
currentSettings.EnableAccessTokenIssuance != toolOptions.EnableAccessToken.Value)
if (toolOptions.EnableAccessToken.HasValue && toolOptions.EnableAccessToken.Value != currentSettings.EnableAccessTokenIssuance)
{
app.Web.ImplicitGrantSettings.EnableAccessTokenIssuance = toolOptions.EnableAccessToken.Value;
needsUpdate = true;
}

if (toolOptions.EnableIdToken.HasValue &&
currentSettings.EnableIdTokenIssuance != toolOptions.EnableIdToken.Value)
if (toolOptions.EnableIdToken.HasValue && toolOptions.EnableIdToken.Value != currentSettings.EnableIdTokenIssuance)
{
app.Web.ImplicitGrantSettings.EnableIdTokenIssuance = toolOptions.EnableIdToken.Value;
needsUpdate = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Microsoft.DotNet.MSIdentity
{
public static class ProjectTypes
{
public const string BlazorServer = "blazorserver";
public const string WebApp = "webapp";
public const string WebApi = "webapi";
public const string BlazorWasmClient = "blazorwasm-client";
public const string BlazorWasm = "blazorwasm";
}
}
Loading