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

Enhance Intune Win32 App Lifecycle Automation with Improved programmatic Auth and upload Retry Logic #162

Open
wants to merge 23 commits into
base: master
Choose a base branch
from

Conversation

tjgruber
Copy link

@tjgruber tjgruber commented Jun 4, 2024

Summary

Overview:

This pull request introduces several key changes aimed at enhancing the reliability, flexibility, and automation capabilities (e.g., GitHub Actions, etc.) of the Intune Win32 app lifecycle management using modern authentication (MSAL.PS is not recommended). The changes primarily address issues related to token handling, authentication flow, Azure Storage blob upload processes, and error handling. Designed to automate via Azure Service Principal auth.

Key Changes:

  1. Enhanced Token Handling and Authentication Flow:

    • Addition of ExpiresOn Property: The New-ClientCredentialsAccessToken function now calculates and adds an ExpiresOn property to the token object for easier expiration checks.
    • Improved Token Expiration Check: The Test-AccessToken function now uses the ExpiresOn property to determine token expiration, enhancing reliability.
    • Refinements in Connect-MSIntuneGraph: Improved error handling and dynamic installation of the MSAL.PS module if still required for other auth methods.
    • New-AuthenticationHeader: Updated to support tokens with the ExpiresOn property directly.
  2. Content-Type Header Addition:

    • Invoke-AzureStorageBlobUploadFinalize: Added a content-type header to the REST request to ensure correct handling of the request body during upload finalization.
  3. Fixing System.DateTime Error:

    • Addressed a specific error related to invalid System.DateTime usage in Azure blob upload processes.
  4. Retry Logic and SAS URI Renewal:

    • Retry Mechanism: Implemented a retry mechanism for chunk uploads and finalization steps, with a maximum of 5 attempts.
    • SAS URI Renewal Adjustments: Initially added but removed SAS URI renewal on the first retry, opting for straightforward retries instead, and maintaining renewal attempts if needed based on a timer.
  5. Module Configuration:

    • Removed MSAL.PS from the required modules to support environments where it is dynamically loaded.
  6. This PR also introduces retry logic to improve the robustness of the Add-IntuneWin32App function, in hopes to help mitigate some transient errors in the following potential areas: (see Issue #8)

    • Win32 App Creation
    • Content Version Creation
    • File Content Creation

Detailed Changes:

  1. Token Handling Enhancements:

    • New-ClientCredentialsAccessToken: Added to handle modern OAuth2.0 client credentials flow without using MSAL.PS which is not recommended to use anymore.
    • Test-AccessToken: Updated to utilize the ExpiresOn property.
    • New-AuthenticationHeader: Updated to handle the ExpiresOn property directly.
    • Connect-MSIntuneGraph: Improved integration with new token functions and error handling.
  2. Azure Storage Blob Upload:

    • Invoke-AzureStorageBlobUpload: Enhanced retry logic for chunk uploads and finalization.
    • Invoke-AzureStorageBlobUploadChunk: Added exception throwing to support retry logic.
    • Invoke-AzureStorageBlobUploadFinalize: Added exception throwing to support retry logic during finalization.
    • Invoke-AzureStorageBlobUploadRenew: Added loop to check the status of the SAS URI renewal process.
  3. Module Configuration:

    • IntuneWin32App.psd1: Removed MSAL.PS from the required modules to allow dynamic loading.
  4. Error Handling Improvements:

    • Improved error messages and handling in various functions to provide more informative feedback and enhance robustness.
  5. Add-IntuneWin32App: (see Issue #8) Adds retry logic to the following:

    • Win32 App Creation
    • Content Version Creation
    • File Content Creation

Impact:

These improvements are expected to significantly enhance the automation capabilities of the Intune Win32 app lifecycle management process. They address previous limitations and errors, making the system more robust against issues like authentication, throttling / rate limitations, blob upload issues, and some other minor fixes.

Testing:

I have tested this using a fully automated GitHub Actions Intune app management lifecycle workflow, and locally on up to date PS 7. If others can help test other scenarios I would appreciate the help.

Example of the fix working successfully during a GitHub Action (throttling / network / etc issue):
image


Please review the changes and provide feedback. Thank you!

@obuolinis
Copy link

obuolinis commented Sep 27, 2024

I believe the upload retry logic is a badly needed feature of IntuneWin32App since Intune gets unpredictable too often. Especially for automation scenarios. So good job on implementing that Tim.

I'm now testing specifically this part of the PR but all the uploads end up with:

WARNING: An error occurred while creating the Win32 application. Error message: Cannot convert the "9/27/2024 10:30:07 AM +00:00" value of type "System.DateTimeOffset" to type "System.DateTime".

As it turns out there's a problem with a cast in this line

$TokenExpireMinutes = [System.Math]::Round(([datetime]$Global:AccessToken.ExpiresOn.ToUniversalTime() - $UTCDateTime).TotalMinutes)

Looking at the original it looks like you just lost a call to the UtcDateTime property :)

$TokenExpireMinutes = [System.Math]::Round(([datetime]$Global:AccessToken.ExpiresOn.ToUniversalTime().UtcDateTime - $UTCDateTime).TotalMinutes)

And honestly I believe a cast to datetime is redundant here since $Global:AccessToken.ExpiresOn.UtcDateTime property is already a DateTime. We can just rewrite the line like this:

$TokenExpireMinutes = [System.Math]::Round(($Global:AccessToken.ExpiresOn.UtcDateTime - $UTCDateTime).TotalMinutes)

@tjgruber
Copy link
Author

On my end, I got errors when using the original:

$TokenExpireMinutes = [System.Math]::Round(([datetime]$Global:AccessToken.ExpiresOn.ToUniversalTime().UtcDateTime - $UTCDateTime).TotalMinutes)

Once I removed .UtcDateTime, it worked without issue locally and in GitHub Actions. I'm strictly using it in an automation scenario there since the PR. I'm curious why that part doesn't work for you but does for me and in automation?

I'll test your below suggestion to see if that works for me as well:

$TokenExpireMinutes = [System.Math]::Round(($Global:AccessToken.ExpiresOn.UtcDateTime - $UTCDateTime).TotalMinutes)`

@obuolinis
Copy link

My end goal is Github Actions as well - I already have an instance of Intune App Factory running there. But I'm testing all the code changes in Windows Sandbox, that's where I got the error above.

Not sure why the statement behaves differently for us, but it could be due to different locales and date/time formats.

@tjgruber
Copy link
Author

tjgruber commented Nov 16, 2024

My end goal is Github Actions as well - I already have an instance of Intune App Factory running there. But I'm testing all the code changes in Windows Sandbox, that's where I got the error above.

Not sure why the statement behaves differently for us, but it could be due to different locales and date/time formats.

@obuolinis Are you able to test this latest change? It is working well on everything I can test it on. I'd appreciate it if you could try it when you have time.

@tjgruber
Copy link
Author

Next up, I need to add retry logic to almost all calls to MS. It's actually fairly frequent that I get request timeouts, and simply waiting a few seconds fixes it.

@obuolinis
Copy link

@obuolinis Are you able to test this latest change? It is working well on everything I can test it on. I'd appreciate it if you could try it when you have time.

Managed to test it just now. It does work for me as well, both in Github and within Windows Sandbox. However, I'm really puzzled about this "black magic" trick you went for :)

# Convert ExpiresOn to DateTimeOffset in UTC
$ExpiresOnUTC = [DateTimeOffset]::Parse(
$Global:AccessToken.ExpiresOn.ToString(),
[System.Globalization.CultureInfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AssumeUniversal
).ToUniversalTime()

Why did you have to convert a DateTimeOffset to String and then parse back to DateTimeOffset? I don't think DateTimeOffset is culture-dependent.

In my eyes token expiration calculation here has got to be just a simple subtraction of either two Datetimes or two Datetimeoffsets, whatever the preference.

If you like Datetimeoffset, then ExpiresOn property is already what you need:

PS C:\> $AccessToken.ExpiresOn.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     DateTimeOffset                           System.ValueType

If you go for Datetime, like in the original, then there's UtcDateTime property:

PS C:\> $AccessToken.ExpiresOn.UtcDateTime.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     DateTime                                 System.ValueType

Did you test any of the above?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants