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

Refactor console logger and catch app provisioning exceptions #2189

Merged
merged 4 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -50,8 +50,8 @@ public async Task AddAuthCodeAsync()
if (csProjFiles.Count() != 1)
{
var errorMsg = string.Format(Resources.ProjectPathError, _toolOptions.ProjectFilePath);
_consoleLogger.LogFailure(errorMsg, Commands.UPDATE_PROJECT_COMMAND);
return;
_consoleLogger.LogFailure(errorMsg);
Environment.Exit(1);
deepchoudhery marked this conversation as resolved.
Show resolved Hide resolved
}

_toolOptions.ProjectFilePath = csProjFiles.First();
Expand Down Expand Up @@ -89,7 +89,7 @@ public async Task AddAuthCodeAsync()
await HandleCodeFileAsync(file, project, options, codeModifierConfig.Identifier);
}

_consoleLogger.LogJsonMessage(new JsonResponse(Commands.UPDATE_PROJECT_COMMAND, State.Success, output: _output.ToString().TrimEnd()));
_consoleLogger.LogJsonMessage(State.Success, output: _output.ToString().TrimEnd());
}

internal static string GetCodeFileString(CodeFile file, string identifier) // todo make all code files strings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext r
$"No valid tokens found in the cache.\n" +
$"Please sign-in to Visual Studio with this account: {Username}.\n\n" +
$"After signing-in, re-run the tool.");
Environment.Exit(1);
}
result = await app.AcquireTokenInteractive(requestContext.Scopes)
.WithAccount(account)
Expand All @@ -120,21 +121,30 @@ public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext r
{
if (ex.Message.Contains("AADSTS70002")) // "The client does not exist or is not enabled for consumers"
zahalzel marked this conversation as resolved.
Show resolved Hide resolved
{
// We want to exit here because this is probably an MSA without an AAD tenant.
_consoleLogger.LogFailure(
"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. ");
Environment.Exit(1); // we want to exit here because this is probably an MSA without an AAD tenant.
Environment.Exit(1);
}

// we want to exit here. Re-sign in will not resolve the issue.
_consoleLogger.LogFailure($"Error encountered with sign-in. See error message for details:\n{ex.Message}");
Environment.Exit(1); // we want to exit here. Re-sign in will not resolve the issue.
Environment.Exit(1);
}
catch (Exception ex)
{
_consoleLogger.LogFailure($"Error encountered with sign-in. See error message for details:\n{ex.Message}");
Environment.Exit(1);
}

if (result is null)
{
_consoleLogger.LogFailure($"Failed to acquire a token");
Environment.Exit(1);
}

return new AccessToken(result.AccessToken, result.ExpiresOn);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.DotNet.MSIdentity.Properties;
using Microsoft.DotNet.MSIdentity.Shared;
using Microsoft.DotNet.MSIdentity.Tool;
using Microsoft.DotNet.Scaffolding.Shared;
using Microsoft.Graph;

namespace Microsoft.DotNet.MSIdentity.MicrosoftIdentityPlatformApplication
Expand All @@ -27,109 +28,106 @@ public class MicrosoftIdentityPlatformApplicationManager
internal async Task<ApplicationParameters?> CreateNewAppAsync(
TokenCredential tokenCredential,
ApplicationParameters applicationParameters,
IConsoleLogger consoleLogger,
string commandName)
IConsoleLogger consoleLogger)
{
var graphServiceClient = GetGraphServiceClient(tokenCredential);

// Get the tenant
Organization? tenant = await GetTenant(graphServiceClient);
if (tenant != null && tenant.TenantType.Equals("AAD B2C", StringComparison.OrdinalIgnoreCase))
{
applicationParameters.IsB2C = true;
}
// Create the app.
Application application = new Application()
try
{
DisplayName = applicationParameters.ApplicationDisplayName,
SignInAudience = AppParameterAudienceToMicrosoftIdentityPlatformAppAudience(applicationParameters.SignInAudience!),
Description = applicationParameters.Description
};
var graphServiceClient = GetGraphServiceClient(tokenCredential);

if (applicationParameters.IsWebApi.GetValueOrDefault())
{
application.Api = new ApiApplication()
// Get the tenant
Organization? tenant = await GetTenant(graphServiceClient);
if (tenant != null && tenant.TenantType.Equals("AAD B2C", StringComparison.OrdinalIgnoreCase))
{
RequestedAccessTokenVersion = 2,
applicationParameters.IsB2C = true;
}
// Create the app.
Application application = new Application()
{
DisplayName = applicationParameters.ApplicationDisplayName,
SignInAudience = AppParameterAudienceToMicrosoftIdentityPlatformAppAudience(applicationParameters.SignInAudience!),
Description = applicationParameters.Description
};
}

if (applicationParameters.IsWebApp.GetValueOrDefault())
{
AddWebAppPlatform(application, applicationParameters);
}
else if (applicationParameters.IsBlazorWasm)
{
AddSpaPlatform(application, applicationParameters.WebRedirectUris);
}
if (applicationParameters.IsWebApi.GetValueOrDefault())
{
application.Api = new ApiApplication()
{
RequestedAccessTokenVersion = 2,
};
}

var createdApplication = await graphServiceClient.Applications
.Request()
.AddAsync(application);
if (applicationParameters.IsWebApp.GetValueOrDefault())
{
AddWebAppPlatform(application, applicationParameters);
}
else if (applicationParameters.IsBlazorWasm)
{
AddSpaPlatform(application, applicationParameters.WebRedirectUris);
}

// Create service principal, necessary for Web API applications
// and useful for Blazorwasm hosted applications. We create it always.
ServicePrincipal servicePrincipal = new ServicePrincipal
{
AppId = createdApplication.AppId,
};
var createdApplication = await graphServiceClient.Applications
.Request()
.AddAsync(application);

ServicePrincipal? createdSp = await graphServiceClient.ServicePrincipals
.Request()
.AddAsync(servicePrincipal);
// Create service principal, necessary for Web API applications
// and useful for Blazorwasm hosted applications. We create it always.
var createdSp = await GetOrCreateSP(graphServiceClient, createdApplication.AppId, consoleLogger);

if (createdSp is null)
{
consoleLogger.LogFailure(Resources.FailedToGetServicePrincipal, commandName);
return null;
}
// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (applicationParameters.IsB2C)
{
string scopes = GetMsGraphScopes(applicationParameters); // Explicit usage of MicrosoftGraph openid and offline_access in the case of Azure AD B2C.
await AddDownstreamApiPermissions(scopes, graphServiceClient, application, createdSp);
}

// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (applicationParameters.IsB2C)
{
string scopes = GetMsGraphScopes(applicationParameters); // Explicit usage of MicrosoftGraph openid and offline_access in the case of Azure AD B2C.
await AddDownstreamApiPermissions(scopes, graphServiceClient, application, createdSp);
}
// For web API, we need to know the appId of the created app to compute the Identifier URI,
// and therefore we need to do it after the app is created (updating the app)
if (applicationParameters.IsWebApi.GetValueOrDefault() && createdApplication.Api != null)
{
await ExposeWebApiScopes(graphServiceClient, createdApplication, applicationParameters);

// For web API, we need to know the appId of the created app to compute the Identifier URI,
// and therefore we need to do it after the app is created (updating the app)
if (applicationParameters.IsWebApi.GetValueOrDefault() && createdApplication.Api != null)
{
await ExposeWebApiScopes(graphServiceClient, createdApplication, applicationParameters);
// Re-reading the app to be sure to have everything.
createdApplication = (await graphServiceClient.Applications
.Request()
.Filter($"appId eq '{createdApplication.AppId}'")
.GetAsync()).FirstOrDefault();
}

// Re-reading the app to be sure to have everything.
createdApplication = (await graphServiceClient.Applications
.Request()
.Filter($"appId eq '{createdApplication.AppId}'")
.GetAsync()).FirstOrDefault();
}
// log json console message inside this method since we need the Microsoft.Graph.Application
if (createdApplication is null)
{
consoleLogger.LogFailure(Resources.FailedToCreateApp);
Environment.Exit(1);
}

// log json console message inside this method since we need the Microsoft.Graph.Application
if (createdApplication is null)
{
consoleLogger.LogFailure(Resources.FailedToCreateApp, commandName);
return null;
}
if (applicationParameters.IsB2C)
{
createdApplication.AdditionalData.Add("IsB2C", true);
}

if (applicationParameters.IsB2C)
{
createdApplication.AdditionalData.Add("IsB2C", true);
}
ApplicationParameters? effectiveApplicationParameters = GetEffectiveApplicationParameters(tenant!, createdApplication, applicationParameters);

ApplicationParameters? effectiveApplicationParameters = GetEffectiveApplicationParameters(tenant!, createdApplication, applicationParameters);
// Add password credentials
if (applicationParameters.CallsMicrosoftGraph || applicationParameters.CallsDownstreamApi)
{
await AddPasswordCredentialsAsync(
graphServiceClient,
createdApplication.Id,
effectiveApplicationParameters,
consoleLogger);
}

var output = string.Format(Resources.CreatedAppRegistration, effectiveApplicationParameters.ApplicationDisplayName, effectiveApplicationParameters.ClientId);
consoleLogger.LogJsonMessage(State.Success, content: createdApplication, output: output);

// Add password credentials
if (applicationParameters.CallsMicrosoftGraph || applicationParameters.CallsDownstreamApi)
return effectiveApplicationParameters;
}
catch (Exception ex)
zahalzel marked this conversation as resolved.
Show resolved Hide resolved
{
await AddPasswordCredentialsAsync(
graphServiceClient,
createdApplication.Id,
effectiveApplicationParameters,
consoleLogger);
var errorMessage = string.IsNullOrEmpty(ex.Message) ? ex.ToString() : ex.Message;
consoleLogger.LogFailure(errorMessage);
zahalzel marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

consoleLogger.LogJsonMessage(new JsonResponse(commandName, State.Success, createdApplication));
return effectiveApplicationParameters;
}

private static string GetMsGraphScopes(ApplicationParameters applicationParameters)
Expand Down Expand Up @@ -189,15 +187,17 @@ private static string GetMsGraphScopes(ApplicationParameters applicationParamete
/// <param name="toolOptions"></param>
/// <param name="commandName"></param>
/// <returns></returns>
internal async Task<JsonResponse> UpdateApplication(
internal async Task UpdateApplication(
TokenCredential tokenCredential,
ApplicationParameters? parameters,
ProvisioningToolOptions toolOptions,
string commandName)
IConsoleLogger consoleLogger,
StringBuilder? output = null)
{
if (parameters is null)
{
return new JsonResponse(commandName, State.Fail, output: string.Format(Resources.FailedToUpdateAppNull, nameof(ApplicationParameters)));
consoleLogger.LogFailure(string.Format(Resources.FailedToUpdateAppNull, nameof(ApplicationParameters)));
Environment.Exit(1);
}

var graphServiceClient = GetGraphServiceClient(tokenCredential);
Expand All @@ -207,41 +207,40 @@ internal async Task<JsonResponse> UpdateApplication(

if (remoteApp is null)
{
return new JsonResponse(commandName, State.Fail, output: string.Format(Resources.NotFound, parameters.ClientId));
consoleLogger.LogFailure(string.Format(Resources.NotFound, parameters.ClientId));
Environment.Exit(1);
}

(bool needsUpdates, Application appUpdates) = GetApplicationUpdates(remoteApp, toolOptions, parameters);
StringBuilder output = new StringBuilder();
output ??= new StringBuilder();
// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (parameters.IsB2C && parameters.CallsDownstreamApi && !string.IsNullOrEmpty(toolOptions.ApiScopes))
{
// TODO: Add if it's B2C, acquire or create the SUSI Policy
var servicePrincipal = await GetOrCreateSP(graphServiceClient, parameters.ClientId);
if (servicePrincipal is null)
{
return new JsonResponse(commandName, State.Fail, output: Resources.FailedToGetServicePrincipal);
}
var servicePrincipal = await GetOrCreateSP(graphServiceClient, parameters.ClientId, consoleLogger);

await AddDownstreamApiPermissions(toolOptions.ApiScopes, graphServiceClient, appUpdates, servicePrincipal, output);
needsUpdates = true;
}

if (!needsUpdates)
{
return new JsonResponse(commandName, State.Success, output: string.Format(Resources.NoUpdateNecessary, remoteApp.DisplayName, remoteApp.AppId));
consoleLogger.LogJsonMessage(State.Success, output: string.Format(Resources.NoUpdateNecessary, remoteApp.DisplayName, remoteApp.AppId));
return;
}

try
{
// TODO: update other fields, see https://github.com/jmprieur/app-provisonning-tool/issues/10
var updatedApp = await graphServiceClient.Applications[remoteApp.Id].Request().UpdateAsync(appUpdates);
output.Append(string.Format(Resources.SuccessfullyUpdatedApp, remoteApp.DisplayName, remoteApp.AppId));
return new JsonResponse(commandName, State.Success, output.ToString());
consoleLogger.LogJsonMessage(State.Success, output: output.ToString());
}
catch (ServiceException se)
{
output.Append(se.Error?.Message);
return new JsonResponse(commandName, State.Fail, output.ToString());
consoleLogger.LogFailure(output.ToString());
Environment.Exit(1);
}
}

Expand All @@ -260,7 +259,7 @@ await AddAdminConsentToApiPermissions(
output);
}

private static async Task<ServicePrincipal?> GetOrCreateSP(GraphServiceClient graphServiceClient, string? clientId)
private static async Task<ServicePrincipal> GetOrCreateSP(GraphServiceClient graphServiceClient, string? clientId, IConsoleLogger consoleLogger)
{
var servicePrincipal = (await graphServiceClient.ServicePrincipals
.Request()
Expand All @@ -281,6 +280,12 @@ await AddAdminConsentToApiPermissions(
.AddAsync(sp);
}

if (servicePrincipal is null)
{
consoleLogger.LogFailure(Resources.FailedToGetServicePrincipal);
Environment.Exit(1);
}

return servicePrincipal;
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
<comment>0 = File name, 1 = Exception message</comment>
</data>
<data name="FailedToProvisionClientApp" xml:space="preserve">
<value>Failed to provision Client Application</value>
<value>Failed to provision Client Application for Blazor WASM hosted project</value>
</data>
<data name="FailedToRetrieveADObjectsError" xml:space="preserve">
<value>Failed to retrieve all Azure AD/AD B2C objects(apps/service principals</value>
Expand All @@ -265,7 +265,7 @@
<comment>0 = null object</comment>
</data>
<data name="FailedToUpdateClientAppCode" xml:space="preserve">
<value>Failed to update client app program.cs file</value>
<value>Failed to update client app program.cs file for Blazor WASM hosted project</value>
</data>
<data name="InitializeUserSecrets" xml:space="preserve">
<value>Initializing User Secrets . . .</value>
Expand Down
Loading