MSEndpointMgr / IntuneWin32App

Provides a set of functions to manage all aspects of Win32 apps in Microsoft Intune.
MIT License
357 stars 92 forks source link

Authenticating using an AccessToken #114

Open jdarre opened 1 year ago

jdarre commented 1 year ago

Thanks for making such a great module!

Now that GDAP is used for accessing tenants, using the Secure Application Module, authenticating using a AccessToken directly would be very useful, I think. Any thoughts on that forward?

NickolajA commented 1 year ago

This feels like news to me, could you expand a bit on what you mean?

jdarre commented 1 year ago

Sure :-) I am thinking something similar to the Mg-module (Connect-MgGraph), where you can authenticate using the param -AccessToken

Having an AccessToken parameter, you can create the accessToken first, and then use that to connect directly, without the need for the Get-MsalToken used in the module. This gives the module more flexible ways to authenticating, f ex using the Get-AzAccessToken, or in my case, using a clientId, secret, tenantId and refreshToken to create an accesstoken. The last one is used when you have a multi-tenant app, and are a partner. I.e, I can connect to all tenants where I have permission (GDAP) without a login-prompt for every tenant I want to update an application for.

I tried quickly by adding two new functions described below, and it looks like it is working. I'll see if I can add this into the Connect-MSIntuneGraph.

Function Connect-MSIntuneGraphAccessToken { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $AccessToken, [Parameter(Mandatory = $true)] [string]$TenantID, [Parameter(Mandatory = $true)] $ExpiresOn )

$Global:AccessToken = $AccessToken
$Global:AccessTokenTenantID = $TenantID
Write-Verbose -Message "Successfully retrieved access token"

try {
    # Construct the required authentication header
    $Global:AuthenticationHeader = New-AuthenticationHeaderAccessToken -AccessToken $Global:AccessToken -ExpiresOn $ExpiresOn
    Write-Verbose -Message "Successfully constructed authentication header"

    # Handle return value
    return $Global:AuthenticationHeader
} catch [System.Exception] {
    Write-Warning -Message "An error occurred while attempting to construct authentication header. Error message: $($PSItem.Exception.Message)"
}

}

function New-AuthenticationHeaderAccessToken {

param(
    [parameter(Mandatory = $true, HelpMessage = "Pass the AuthenticationResult object returned from Get-AccessToken cmdlet.")]
    [ValidateNotNullOrEmpty()]
    [string]$AccessToken,
    [parameter(Mandatory = $true, HelpMessage = "Datetime when token expires")]
    [ValidateNotNullOrEmpty()]
    $ExpiresOn
)
Process {
    # Construct default header parameters
    $AuthenticationHeader = @{
        "Content-Type"  = "application/json"
        "Authorization" = $AccessToken
        "ExpiresOn"     = $ExpiresOn
    }

    # Handle return value
    return $AuthenticationHeader
}

}

rodw-wilkins commented 1 year ago

If I'm understanding correctly, I second this.

Right now I have to authenticate separately in AzureAD in order to retrieve and assign groups to the apps - I'm thinking having an access token that can authorize all together without separate login prompts would be much better.

NickolajA commented 1 year ago

Have you tested with creating your own app registration, providing it with the required Graph API permissions to read e.g. groups/users etc from Entra ID, but also giving it the required Intune permissions to create Win32 apps? If you do that, you can authenticate this way:

Connect-MSIntuneGraph -TenantID -ClientID [-ClientSecret ]

For the multi-tenant purpose, I can see where that makes sense. Let me think about this a bit to see how I can modify the connect function.

jdarre commented 1 year ago

Yes, I know, and can, but as a Partner we want to update for many tenants :-). I tested, and it works, but gives a warning when uploading the app.

Is it possible to push a branch here? I can upload the code changes I made, in case.

I created new private function, and made a few changes in the Connect-function, as below. I ran this on many tenants, but have not tested extensively using the other (normalt methods.

function New-AuthenticationHeaderAccessToken { <# .SYNOPSIS Construct a required header hash-table based on the access token from Get-AccessToken function.

.DESCRIPTION
    Construct a required header hash-table based on the access token from Get-AccessToken function.

.PARAMETER AccessToken
    Pass the AuthenticationResult object returned from Get-AccessToken cmdlet.

.NOTES
    Author:      Nickolaj Andersen
    Contact:     @NickolajA
    Created:     2021-04-08
    Updated:     2021-09-08

    Version history:
    1.0.0 - (2021-04-08) Script created
    1.0.1 - (2021-09-08) Fixed issue reported by Paul DeArment Jr where the local date time set for ExpiresOn should be UTC to not cause any time related issues
#>
param(
    [parameter(Mandatory = $true, HelpMessage = "Pass the AuthenticationResult object returned from Get-AccessToken cmdlet.")]
    [ValidateNotNullOrEmpty()]
    [string]$AccessToken,
    [parameter(Mandatory = $true, HelpMessage = "Datetime when token expires")]
    [ValidateNotNullOrEmpty()]
    $ExpiresOn
)
Process {
    # Construct default header parameters
    $AuthenticationHeader = @{
        "Content-Type"  = "application/json"
        "Authorization" = $AccessToken
        "ExpiresOn"     = $ExpiresOn
    }

    # Handle return value
    return $AuthenticationHeader
}

}

function Connect-MSIntuneGraph { <# .SYNOPSIS Get or refresh an access token using either authorization code flow or device code flow, that can be used to authenticate and authorize against resources in Graph API.

.DESCRIPTION
    Get or refresh an access token using either authorization code flow or device code flow, that can be used to authenticate and authorize against resources in Graph API.

.PARAMETER TenantID
    Specify the tenant name or ID, e.g. tenant.onmicrosoft.com or <GUID>.

.PARAMETER ClientID
    Application ID (Client ID) for an Azure AD service principal. Uses by default the 'Microsoft Intune PowerShell' service principal Application ID.

.PARAMETER ClientSecret
    Application secret (Client Secret) for an Azure AD service principal.

.PARAMETER ClientCert
    A Certificate object (not just thumbprint) representing the client certificate for an Azure AD service principal.

.PARAMETER RedirectUri
    Specify the Redirect URI (also known as Reply URL) of the custom Azure AD service principal.

.PARAMETER DeviceCode
    Specify delegated login using devicecode flow, you will be prompted to navigate to https://microsoft.com/devicelogin

.PARAMETER Interactive
    Specify to force an interactive prompt for credentials.

.PARAMETER Refresh
    Specify to refresh an existing access token.

.NOTES
    Author:      Nickolaj Andersen
    Contact:     @NickolajA
    Created:     2021-08-31
    Updated:     2022-09-03

    Version history:
    1.0.0 - (2021-08-31) Script created
    1.0.1 - (2022-03-28) Added ClientSecret parameter input to support client secret auth flow
    1.0.2 - (2022-09-03) Added new global variable to hold the tenant id passed as parameter input for access token refresh scenario
    1.0.3 - (2023-04-07) Added support for client certificate auth flow (apcsb)
#>
[CmdletBinding(DefaultParameterSetName = "Interactive")]
param(
    [parameter(Mandatory = $true, ParameterSetName = "Interactive", HelpMessage = "Specify the tenant name or ID, e.g. tenant.onmicrosoft.com or <GUID>.")]
    [parameter(Mandatory = $true, ParameterSetName = "DeviceCode")]
    [parameter(Mandatory = $true, ParameterSetName = "ClientSecret")]
    [parameter(Mandatory = $true, ParameterSetName = "ClientCert")]
    [parameter(Mandatory = $true, ParameterSetName = "AccessToken")]
    [ValidateNotNullOrEmpty()]
    [string]$TenantID,

    [parameter(Mandatory = $false, ParameterSetName = "Interactive", HelpMessage = "Application ID (Client ID) for an Azure AD service principal. Uses by default the 'Microsoft Intune PowerShell' service principal Application ID.")]
    [parameter(Mandatory = $false, ParameterSetName = "DeviceCode")]
    [parameter(Mandatory = $true, ParameterSetName = "ClientSecret")]
    [parameter(Mandatory = $true, ParameterSetName = "ClientCert")]
    [ValidateNotNullOrEmpty()]
    [string]$ClientID,

    [parameter(Mandatory = $false, HelpMessage = "Application secret (Client Secret) for an Azure AD service principal.")]
    [parameter(Mandatory = $true, ParameterSetName = "ClientSecret")]
    [ValidateNotNullOrEmpty()]
    [string]$ClientSecret,

    [parameter(Mandatory = $false, HelpMessage = "A Certificate object (not just thumbprint) representing the client certificate for an Azure AD service principal.")]
    [parameter(Mandatory = $true, ParameterSetName = "ClientCert")]
    [ValidateNotNullOrEmpty()]
    [System.Security.Cryptography.X509Certificates.X509Certificate2]$ClientCert,

    [parameter(Mandatory = $false, ParameterSetName = "Interactive", HelpMessage = "Specify the Redirect URI (also known as Reply URL) of the custom Azure AD service principal.")]
    [parameter(Mandatory = $false, ParameterSetName = "DeviceCode")]
    [ValidateNotNullOrEmpty()]
    [string]$RedirectUri = [string]::Empty,

    [parameter(Mandatory = $false, ParameterSetName = "Interactive", HelpMessage = "Specify to force an interactive prompt for credentials.")]
    [switch]$Interactive,

    [parameter(Mandatory = $true, ParameterSetName = "DeviceCode", HelpMessage = "Specify to do delegated login using devicecode flow, you will be prompted to navigate to https://microsoft.com/devicelogin")]
    [switch]$DeviceCode,

    [parameter(Mandatory = $false, ParameterSetName = "Interactive", HelpMessage = "Specify to refresh an existing access token.")]
    [parameter(Mandatory = $false, ParameterSetName = "DeviceCode")]
    [switch]$Refresh,

    [parameter(Mandatory = $true, ParameterSetName = "AccessToken", HelpMessage = "Directly add the AccessToken")]
    [parameter(Mandatory = $true, ParameterSetName = "ExpiresOn")]
    [ValidateNotNullOrEmpty()]
    [string]$AccessToken,

    [parameter(Mandatory = $false, ParameterSetName = "ExpiresOn", HelpMessage = "Datetime when AccessToken expires")]
    [parameter(Mandatory = $true, ParameterSetName = "AccessToken")]
    [ValidateNotNullOrEmpty()]
    [datetime]$ExpiresOn
)
Begin {
    # Determine the correct RedirectUri (also known as Reply URL) to use with MSAL.PS
    if (-not([string]::IsNullOrEmpty($ClientID))) {
        Write-Verbose -Message "Using custom Azure AD service principal specified with Application ID: $($ClientID)"

        # Adjust RedirectUri parameter input in case non was passed on command line
        if ([string]::IsNullOrEmpty($RedirectUri)) {
            switch -Wildcard ($PSVersionTable["PSVersion"]) {
                "5.*" {
                    $RedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"
                }
                "7.*" {
                    $RedirectUri = "http://localhost"
                }
            }
        }
    }
    else {
        # Define static variables
        $ClientID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"
        $RedirectUri = "urn:ietf:wg:oauth:2.0:oob"

        Write-Verbose -Message "Using the default 'Microsoft Intune PowerShell' service principal with Application ID: $($ClientID)"
        Write-Verbose -Message "Using RedirectUri with value: $($RedirectUri)"

        # Set default error action preference configuration
        $ErrorActionPreference = "Stop"
    }
}
Process {
    Write-Verbose -Message "Using authentication flow: $($PSCmdlet.ParameterSetName)"

    # Direct AccessToken access
    if ($PSCmdlet.ParameterSetName -eq "AccessToken"){
        $Global:AccessToken = $AccessToken
        $Global:AccessTokenTenantID = $TenantID
        Write-Verbose -Message "Successfully retrieved access token"

        try {
            # Construct the required authentication header
            $Global:AuthenticationHeader = New-AuthenticationHeaderAccessToken -AccessToken $Global:AccessToken -ExpiresOn $ExpiresOn
            Write-Verbose -Message "Successfully constructed authentication header using direct access token"

            # Handle return value
            return $Global:AuthenticationHeader
        } catch [System.Exception] {
            Write-Warning -Message "An error occurred while attempting to construct authentication header using direct access token. Error message: $($PSItem.Exception.Message)"
        }
    } else {

        try {
            # Construct table with common parameter input for Get-MsalToken cmdlet
            $AccessTokenArguments = @{
                "TenantId"    = $TenantID
                "ClientId"    = $ClientID
                "RedirectUri" = $RedirectUri
                "ErrorAction" = "Stop"
            }

            # Dynamically add parameter input for Get-MsalToken based on parameter set name
            switch ($PSCmdlet.ParameterSetName) {
                "Interactive" {
                    if ($PSBoundParameters["Refresh"]) {
                        $AccessTokenArguments.Add("ForceRefresh", $true)
                        $AccessTokenArguments.Add("Silent", $true)
                    }
                }
                "DeviceCode" {
                    if ($PSBoundParameters["Refresh"]) {
                        $AccessTokenArguments.Add("ForceRefresh", $true)
                    }
                }
                "ClientSecret" {
                    Write-Verbose "Using clientSecret"
                    $AccessTokenArguments.Add("ClientSecret", $(ConvertTo-SecureString $clientSecret -AsPlainText -Force))
                }
                "ClientCert" {
                    Write-Verbose "Using clientCert"
                    $AccessTokenArguments.Add("ClientCertificate", $ClientCert)
                }

            }

            # Dynamically add parameter input for Get-MsalToken based on command line input
            if ($PSBoundParameters["Interactive"]) {
                $AccessTokenArguments.Add("Interactive", $true)
            }
            if ($PSBoundParameters["DeviceCode"]) {
                if (-not($PSBoundParameters["Refresh"])) {
                    $AccessTokenArguments.Add("DeviceCode", $true)
                }
            }

            try {
                # Attempt to retrieve or refresh an access token
                $Global:AccessToken = Get-MsalToken @AccessTokenArguments
                $Global:AccessTokenTenantID = $TenantID
                Write-Verbose -Message "Successfully retrieved access token"

                try {
                    # Construct the required authentication header
                    $Global:AuthenticationHeader = New-AuthenticationHeader -AccessToken $Global:AccessToken
                    Write-Verbose -Message "Successfully constructed authentication header"

                    # Handle return value
                    return $Global:AuthenticationHeader
                } catch [System.Exception] {
                    Write-Warning -Message "An error occurred while attempting to construct authentication header. Error message: $($PSItem.Exception.Message)"
                }
            } catch [System.Exception] {
                Write-Warning -Message "An error occurred while attempting to retrieve or refresh access token, or direct access token was used. Error message: $($PSItem.Exception.Message)"
            }
        } catch [System.Exception] {
            Write-Warning -Message "An error occurred while constructing parameter input for access token retrieval. Error message: $($PSItem.Exception.Message)"
        }
    }
}

}

jdarre commented 1 year ago

If I'm understanding correctly, I second this.

Right now I have to authenticate separately in AzureAD in order to retrieve and assign groups to the apps - I'm thinking having an access token that can authorize all together without separate login prompts would be much better.

Yes, you can use the same AccessToken in e.g. the Mg-module to all the stuff the application have permissions to do.

Djaymam commented 1 year ago

thank you so much @jdarre for pointing this, i'm currently trying to find a solution for this, I'm currently working on an application that will publish Intune application on behalf of the user, and prompting user for login credentials not in the table and temporary token would be great in my case. I hope this will be implemented.

Tank you @NickolajA for this amazing module, i'm having a blast with it.