GraphApiRequests.psm1
### --- PUBLIC FUNCTIONS --- ### #Region - Get-GraphCertToken.ps1 Function Get-GraphCertToken { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $AppId, [Parameter(Mandatory=$true)] [string] $TenantID, [Parameter(Mandatory=$true)] [string] $CertificatePath ) $Scope = "https://graph.microsoft.com/.default" $Certificate = Get-Item $CertificatePath $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash()) $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime() $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds $JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0) $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0) $JWTHeader = @{ alg = "RS256" typ = "JWT" x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '=' } $JWTPayLoad = @{ aud = "https://login.microsoftonline.com/$TenantID/oauth2/token" exp = $JWTExpiration iss = $AppId jti = [guid]::NewGuid() nbf = $NotBefore sub = $AppId } $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json)) $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json)) $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) $JWT = $EncodedHeader + "." + $EncodedPayload $PrivateKey = $Certificate.PrivateKey # $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1 # $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 $Signature = Get-SignData -inputObject $PrivateKey -JWT $JWT # $Signature = [Convert]::ToBase64String( # $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding) # ) -replace '\+','-' -replace '/','_' -replace '=' $JWT = $JWT + "." + $Signature $Body = @{ client_id = $AppId client_assertion = $JWT client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" scope = $Scope grant_type = "client_credentials" } $Url = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" $Header = @{ Authorization = "Bearer $JWT" } $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' Body = $Body Uri = $Url Headers = $Header } Invoke-RestMethod @PostSplat } Export-ModuleMember -Function Get-GraphCertToken #EndRegion - Get-GraphCertToken.ps1 #Region - Get-GraphDeviceAuthToken.ps1 <# .SYNOPSIS Function to get a device authentication token. .DESCRIPTION This function works only if your tenant satisfy the pre-requisites below: - Registered Graph API application with required permissions (depends of the requests that you need) - Enabled redirection for Mobile and desktop applications. More details here: https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-a-new-application-using-the-azure-portal - Configured redirect URL: https://localhost - '"allowPublicClient": true' in application Manifest json .EXAMPLE PS C:\> Get-GraphDeviceAuthToken -TenantName 'contoso' -AppId '246c7445-eee6-4d60-968d-f83d67183753' Getting the device auth token for Contoso tenant using application ID registered in Azure AD .PARAMETER TenantName You can find your tenant name using Azure AD portal > Overview > Basic information > Name .PARAMETER AppId Фpplication ID registered in Azure AD .INPUTS None. You cannot pipe objects to Get-GraphDeviceAuthToke .OUTPUTS System.Array. Returns the array with token .LINK Source code of this function: https://github.com/aslan-im/GraphApiRequests/blob/main/Functions/Public/Get-GraphDeviceAuthToken.ps1 .LINK Source code of whole project: https://github.com/aslan-im/GraphApiRequests #> function Get-GraphDeviceAuthToken { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $AppId, [Parameter(Mandatory)] [string] $TenantName, [Parameter(Mandatory=$false)] [string] $ApiUrl = "https://graph.microsoft.com/" ) $TenantUrl = "$TenantName.onmicrosoft.com" $AuthUrl = "https://login.microsoftonline.com/$TenantUrl" $CodeRequestSplat = @{ TenantName = $TenantName AppId = $AppId } $DeviceCodeObject = Get-GraphDeviceAuthCode @CodeRequestSplat Write-Output $DeviceCodeObject.message $Code = ($DeviceCodeObject.message -split "code " | Select-Object -Last 1) -split " to authenticate." Set-Clipboard -Value $Code New-GraphAuthFormWindow $TokenParamsSplat = @{ Method = "POST" URI = "$Authurl/oauth2/token" ErrorAction = "Stop" body = @{ grant_type = 'device_code' resource = $ApiUrl client_id = $AppId code = $($DeviceCodeObject.device_code) } } $TokenResponse = $null try { $TokenResponse = Invoke-RestMethod @TokenParamsSplat return $TokenResponse } catch [System.Net.WebException]{ if ($null -eq $_.Exception.Response){ throw } $Result = $_.Exception.Response.GetResponseStream() $Reader = New-Object System.IO.StreamReader($Result) $Reader.BaseStream.Position = 0 $ErrorBody = ConvertFrom-Json $Reader.ReadToEnd() if ($ErrorBody.Error -ne "authorization_pending"){ throw } } } Export-ModuleMember -Function Get-GraphDeviceAuthToken #EndRegion - Get-GraphDeviceAuthToken.ps1 #Region - Get-GraphToken.ps1 <# .SYNOPSIS Function for getting token using client secret .DESCRIPTION For using this function you need to have a generated AppSecret (Client secret) in registered application in Azure AD .EXAMPLE PS C:\> Get-GraphToken -AppId '246c7445-eee6-4d60-968d-f83d67183753' -AppSecret '6R[O)5D8sHZ^pt"3' -TenantId 'd1ee13a4-c9d0-4ab0-bff5-c011dfc20717' Example of getting the token .INPUTS None. You cannot pipe objects to Get-GraphDeviceAuthToke .OUTPUTS Returns an array with token .LINK Source code of this function: https://github.com/aslan-im/GraphApiRequests/blob/main/Functions/Public/Get-GraphToken.ps1 .LINK Source code of whole project: https://github.com/aslan-im/GraphApiRequests #> Function Get-GraphToken { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $AppId, [Parameter(Mandatory=$true)] [string] $AppSecret, [Parameter(Mandatory=$true)] [string] $TenantID ) $AuthUrl = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" $Scope = "https://graph.microsoft.com/.default" $Body = @{ client_id = $AppId client_secret = $AppSecret scope = $Scope grant_type = 'client_credentials' } $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' Body = $Body Uri = $AuthUrl ErrorAction = "Stop" } try { Invoke-RestMethod @PostSplat } catch { throw "Exception was caught: $($_.Exception.Message)" break } } Export-ModuleMember -Function Get-GraphToken #EndRegion - Get-GraphToken.ps1 #Region - Invoke-GraphApiRequest.ps1 <# .SYNOPSIS Function to invoke Graph API Request .DESCRIPTION Using this function you can invoke any request to the Graph API both versions .EXAMPLE PS C:\> $Token = Get-GraphToken -AppId 246c7445-eee6-4d60-968d-f83d67183753 -AppSecret ?2mwmHICkx8j -TenantID d1ee13a4-c9d0-4ab0-bff5-c011dfc20717 PS C:\> Invoke-GraphApiRequest -Token $Token -Resource groups -Method Get Example of using Invoke-GraphApiRequest to get the list of Azure AD Groups. In this example first command is required to get the token using app secret. .EXAMPLE PS C:\> $BodyObject = [PSCustomObject]@{ description = "Self help community for golf" displayName = "Golf Assist" groupTypes = @( "Unified" ) mailEnabled = $true mailNickname = "golfassist" securityenabled = $false } PS C:\> $BodyJson = ConvertTo-Json $BodyObject PS C:\> $Token = Get-GraphToken -AppId 246c7445-eee6-4d60-968d-f83d67183753 -AppSecret ?2mwmHICkx8j -TenantID d1ee13a4-c9d0-4ab0-bff5-c011dfc20717 PS C:\> Invoke-GraphApiRequest -Token $Token -Resource groups -Method POST -Body $BodyJson Steps in this example: 1. Creating a PSCustomObject with payload of MailEnalbed Security Group 2. Converting PSCustomObject to Json 3. Getting the token using Secret (if you already have a token and it is not expired, this step can be skipped) 4. Invoking a request to the Graph API for creating a new group with predefined properties .INPUTS None. You cannot pipe objects to Get-GraphDeviceAuthToke .OUTPUTS Usually it is JSON .LINK Source code of this function: https://github.com/aslan-im/GraphApiRequests/blob/main/Functions/Public/Invoke-GraphApiRequest.ps1 .LINK Source code of whole project: https://github.com/aslan-im/GraphApiRequests #> function Invoke-GraphApiRequest { [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [PSCustomObject] $Token, [Parameter(Mandatory=$True)] [string] $Resource, [Parameter(Mandatory=$false)] [ValidateSet('beta', 'v1.0')] [string] $ApiVersion = 'beta', [Parameter(Mandatory=$false)] [ValidateSet('GET', 'PATCH', 'POST', 'PUT', 'DELETE')] [string] $Method = 'GET', [Parameter(Mandatory=$false)] [string] $TenantId, [Parameter(Mandatory=$false)] [string] $Body, [Parameter(Mandatory=$false)] [string] $AppID, [Parameter(Mandatory=$false)] [string] $AppSecret, [Parameter(Mandatory=$false)] [string] $ApiUrl = 'https://graph.microsoft.com' ) if (!$Token -and $AppId -and $AppSecret -and $TenantId) { try{ $Token = Get-GraphToken -TenantID $TenantId -AppId $AppId -AppSecret $AppSecret -ErrorAction Stop } catch{ throw $_.Exception break } } elseif (!$Token -and !$AppId -and $AppSecret -and $TenantId) { throw "There is no AppId parameter specified. Please run commandlet again with -AppId specified." break } elseif (!$Token -and $AppId -and !$AppSecret) { throw "There is no AppSecret parameter specified. Please run commandlet again with -AppSecret specified." break } elseif (!$Token -and $AppId -and $AppSecret -and !$TenantId) { throw "There is no TenantID parameter specified. Please run commandlet again with -TenantId specified." break } elseif (!$Token -and !$AppId -and !$AppSecret) { throw "Token, AppId, AppSecret or TenantID are not specified. Please run commandlet with Token specified or with AppId and AppSecret." break } if ($Resource[0] -eq '/') { $Resource = $Resource -replace '^.' } $Url = "$ApiUrl/$ApiVersion/$($Resource)" $Header = @{ Authorization = "$($Token.token_type) $($Token.access_token)" } $PostSplat = @{ ContentType = 'application/json' Method = $Method Header = $Header Uri = $Url } if ($Body) { $PostSplat.Add('Body', $Body) } $Result = @() try { $ResultResponse = Invoke-RestMethod @PostSplat -ErrorAction Stop if([bool]($ResultResponse -match "value")){ $Result = $ResultResponse.value } else{ $Result = $ResultResponse } if([bool]($ResultResponse -match "@odata.nextLink")){ $ResultNextLink = $ResultResponse."@odata.nextLink" } if ($ResultNextLink) { while ($null -ne $ResultNextLink){ $PostSplat = @{ ContentType = 'application/json' Method = $Method Header = $Header Uri = $ResultNextLink } $ResultResponse = Invoke-RestMethod @PostSplat -ErrorAction Stop $ResultNextLink = $ResultResponse."@odata.nextLink" $Result += $ResultResponse.value } } return $Result } catch { throw $_.Exception break } } Export-ModuleMember -Function Invoke-GraphApiRequest #EndRegion - Invoke-GraphApiRequest.ps1 #Region - New-GraphCertificate.ps1 function New-GraphCertificate { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [String] $TenantName, [Parameter(Mandatory=$false)] [string] $StoreLocation = 'Cert:\CurrentUser\My', [Parameter(Mandatory=$False)] [string] $CertificateOutputPath = "C:\Temp", [Parameter(Mandatory=$false)] [DateTime][ValidateScript({$_ -ge (Get-Date)})] $ExpirationDate = (Get-Date).AddYears(1), [Parameter(Mandatory=$false)] [string] $FriendlyName = "GraphCert" ) $CreateCertSplat = @{ FriendlyName = $FriendlyName DnsName = $TenantName CertStoreLocation = $StoreLocation NotAfter = $ExpirationDate KeyExportPolicy = "Exportable" KeySpec = "Signature" Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider" HashAlgorithm = "SHA256" ErrorAction = "Stop" } $Certificate = New-SelfSignedCertificate @CreateCertSplat $CertificatePath = Join-Path -Path $StoreLocation -ChildPath $Certificate.Thumbprint If (!(Test-Path -Path $CertificateOutputPath)){ New-Item -ItemType Folder -Path $CertificateOutputPath } $CerOutPath = "$CertificateOutputPath\$FriendlyName.cer" Export-Certificate -Cert $CertificatePath -FilePath $CerOutPath } Export-ModuleMember -Function New-GraphCertificate #EndRegion - New-GraphCertificate.ps1 ### --- PRIVATE FUNCTIONS --- ### #Region - Get-GraphDeviceAuthCode.ps1 function Get-GraphDeviceAuthCode { [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [string] $ApiUrl = "https://graph.microsoft.com/", [Parameter(Mandatory)] [string] $TenantName, [Parameter(Mandatory)] [string] $AppId ) $TenantUrl = "$TenantName.onmicrosoft.com" $AuthUrl = "https://login.microsoftonline.com/$TenantUrl" $PostSPlat = @{ method = 'POST' uri = "$AuthUrl/oauth2/devicecode" ErrorAction = "STOP" body = @{ resource = $ApiUrl client_id = $AppId } } try { Invoke-RestMethod @PostSPlat } catch [System.Net.WebException]{ throw $_.Exception } } #EndRegion - Get-GraphDeviceAuthCode.ps1 #Region - Get-SignData.ps1 function Get-SignData { [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [System.Security.Cryptography.RSACng] $InputObject, [Parameter(Mandatory=$false)] [string] $JWT ) $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1 $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 [Convert]::ToBase64String( $InputObject.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding) ) -replace '\+','-' -replace '/','_' -replace '=' } #EndRegion - Get-SignData.ps1 #Region - New-GraphAuthFormWindow.ps1 function New-GraphAuthFormWindow { [CmdletBinding()] param ( ) Add-Type -AssemblyName System.Windows.Forms $Form = New-Object -TypeName System.Windows.Forms.Form -Property @{ Width = 440; Height = 640 } $Web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{ Width = 440; Height = 600; Url = "https://www.microsoft.com/devicelogin" } $Web.Add_DocumentCompleted($DocComp) $Web.DocumentText $Form.Controls.Add($Web) $Form.Add_Shown({ $Form.Activate()}) $Web.ScriptErrorsSuppressed = $true $Form.AutoScaleMode = 'Dpi' $Form.Text = "Graph API Authentication" $Form.ShowIcon = $False $Form.AutoSizeMode = 'GrowAndShrink' $Form.StartPosition = 'CenterScreen' $Form.ShowDialog() | Out-Null } #EndRegion - New-GraphAuthFormWindow.ps1 |