Open jdarre opened 1 year ago
This feels like news to me, could you expand a bit on what you mean?
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
}
}
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.
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
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.
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)"
}
}
}
}
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.
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.
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?