Skip to content
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 @@ -35,15 +35,18 @@ private static Command CreateInitSubcommand(ILogger logger, string configDir, IC
{
new Option<string?>(new[] { "-c", "--configfile" }, "Path to an existing config file to import"),
new Option<bool>(new[] { "--global", "-g" }, "Create config in global directory (AppData) instead of current directory"),
new Option<bool>(new[] { "--yes", "-y" }, "Skip confirmation prompts and apply any required app registration changes automatically"),
};

cmd.SetHandler(async (System.CommandLine.Invocation.InvocationContext context) =>
{
var configFileOption = cmd.Options.OfType<Option<string?>>().First(opt => opt.HasAlias("-c"));
var globalOption = cmd.Options.OfType<Option<bool>>().First(opt => opt.HasAlias("--global"));
var yesOption = cmd.Options.OfType<Option<bool>>().First(opt => opt.HasAlias("--yes"));

string? configFile = context.ParseResult.GetValueForOption(configFileOption);
bool useGlobal = context.ParseResult.GetValueForOption(globalOption);
bool yes = context.ParseResult.GetValueForOption(yesOption);

// Determine config path
string configPath = useGlobal
Expand Down Expand Up @@ -95,7 +98,8 @@ private static Command CreateInitSubcommand(ILogger logger, string configDir, IC
await clientAppValidator.EnsureValidClientAppAsync(
importedConfig.ClientAppId,
importedConfig.TenantId,
context.GetCancellationToken());
skipConfirmation: yes,
ct: context.GetCancellationToken());
}
catch (ClientAppValidationException ex)
{
Expand All @@ -108,9 +112,10 @@ await clientAppValidator.EnsureValidClientAppAsync(
}
if (ex.MitigationSteps.Count > 0)
{
logger.LogInformation("");
foreach (var step in ex.MitigationSteps)
{
logger.LogError(step);
logger.LogInformation(" {Step}", step.TrimEnd());
}
}
logger.LogError("");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public static Command CreateCommand(
await RequirementsSubcommand.RunChecksOrExitAsync(
checks, setupConfig, logger, ct);
}
catch (Exception reqEx) when (reqEx is not OperationCanceledException)
catch (Exception reqEx) when (reqEx is not OperationCanceledException && reqEx is not CleanExitException)
{
logger.LogError(reqEx, "Requirements check failed: {Message}", reqEx.Message);
logger.LogError("Rerun with --skip-requirements to bypass.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public static Command CreateCommand(
await RequirementsSubcommand.RunChecksOrExitAsync(
checks, setupConfig, logger, ct);
}
catch (Exception reqEx) when (reqEx is not OperationCanceledException)
catch (Exception reqEx) when (reqEx is not OperationCanceledException && reqEx is not CleanExitException)
{
logger.LogError(reqEx, "Requirements check failed with an unexpected error: {Message}", reqEx.Message);
logger.LogError("If you want to bypass requirement validation, rerun this command with the --skip-requirements flag.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public static Command CreateCommand(
await RequirementsSubcommand.RunChecksOrExitAsync(
checks, setupConfig, logger, ct);
}
catch (Exception reqEx) when (reqEx is not OperationCanceledException)
catch (Exception reqEx) when (reqEx is not OperationCanceledException && reqEx is not CleanExitException)
{
logger.LogError(reqEx, "Requirements check failed with an unexpected error: {Message}", reqEx.Message);
logger.LogError("If you want to bypass requirement validation, rerun this command with the --skip-requirements flag.");
Expand Down Expand Up @@ -1061,12 +1061,17 @@ public static async Task<bool> EnsureDelegatedConsentWithRetriesAsync(
var spPropagated = await retryHelper.ExecuteWithRetryAsync(
async ct =>
{
// Probe oauth2PermissionGrants directly — a 200 (even empty list) confirms
// the SP's clientId is visible to the grants API replication layer.
// GET /servicePrincipals resolves too fast and gives false confidence.
using var checkResp = await httpClient.GetAsync(
$"{Constants.GraphApiConstants.BaseUrl}/v1.0/oauth2PermissionGrants?$filter=clientId eq '{servicePrincipalId}'", ct);
return checkResp.IsSuccessStatusCode;
// Probe oauth2PermissionGrants via GraphApiService with explicit delegated
// scopes (DelegatedPermissionGrant.ReadWrite.All). A non-null response —
// even an empty list — confirms the SP's clientId is visible to the grants
// API replication layer. Using the raw httpClient here (Application.ReadWrite.All
// scope only) caused 403s on every probe, wasting 8+ minutes of retries.
using var checkDoc = await graphApiService.GraphGetAsync(
setupConfig.TenantId!,
$"/v1.0/oauth2PermissionGrants?$filter=clientId eq '{servicePrincipalId}'",
ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
return checkDoc != null;
},
result => !result,
maxRetries: 12,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ public static class AuthenticationConstants
public const string LocalhostRedirectUri = "http://localhost:8400/";

/// <summary>
/// Required redirect URIs for Microsoft Graph PowerShell SDK authentication.
/// The SDK requires both http://localhost and http://localhost:8400/ for different auth flows.
/// Required redirect URIs for authentication.
/// <list type="bullet">
/// <item><term>http://localhost</term><description>Required by the Microsoft Graph PowerShell SDK
/// (<c>Connect-MgGraph -ClientId</c>). Without this URI, PowerShell-based operations (OAuth2 grants,
/// service principal lookups) fall back to the Azure CLI token, which lacks required delegated
/// permissions and causes 403 errors on inheritable permissions operations.</description></item>
/// <item><term>http://localhost:8400/</term><description>Required by MSAL for interactive browser
/// authentication. Uses a fixed port to ensure consistent OAuth callbacks.</description></item>
/// </list>
/// See also <see cref="WamBrokerRedirectUriFormat"/> for the Windows WAM broker URI.
/// </summary>
public static readonly string[] RequiredRedirectUris = new[]
{
Expand Down
Loading
Loading