MiniGraph.psm1
function Assert-GraphConnection { <# .SYNOPSIS Asserts a valid graph connection has been established. .DESCRIPTION Asserts a valid graph connection has been established. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. .EXAMPLE PS C:\> Assert-GraphConnection -Cmdlet $PSCmdlet Asserts a valid graph connection has been established. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Cmdlet ) process { if ($script:token) { return } $exception = [System.InvalidOperationException]::new('Not yet connected to MSGraph. Use Connect-Graph* to establish a connection!') $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "NotConnected", 'InvalidOperation', $null) $Cmdlet.ThrowTerminatingError($errorRecord) } } function ConvertTo-Base64 { <# .SYNOPSIS Converts input string to base 64. .DESCRIPTION Converts input string to base 64. .PARAMETER Text The text to encode. .PARAMETER Encoding The encoding of the input text. .EXAMPLE PS C:\> "Hello World" | ConvertTo-Base64 Converts the string "Hello World" to base 64. #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]] $Text, [System.Text.Encoding] $Encoding = [System.Text.Encoding]::UTF8 ) process { foreach ($entry in $Text) { $bytes = $Encoding.GetBytes($entry) [Convert]::ToBase64String($bytes) } } } function ConvertTo-SignedString { <# .SYNOPSIS Signs a string. .DESCRIPTION Signs a string. Used for certificate authentication. .PARAMETER Text The text to sign. .PARAMETER Certificate The certificate to sign with. Must have private key. .PARAMETER Padding The padding mechanism to use while signing. Defaults to "Pkcs1" .PARAMETER Algorithm The signing algorithm to use. Defaults to "SHA256" .PARAMETER Encoding Encoding of the source text. Defaults to UTF8 .EXAMPLE PS C:\> $token | ConvertTo-SignedString -Certificate $cert Signs the text stored in $token with the certificate stored in $cert #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]] $Text, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Security.Cryptography.RSASignaturePadding] $Padding = [Security.Cryptography.RSASignaturePadding]::Pkcs1, [Security.Cryptography.HashAlgorithmName] $Algorithm = [Security.Cryptography.HashAlgorithmName]::SHA256, [System.Text.Encoding] $Encoding = [System.Text.Encoding]::UTF8 ) begin { $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) } process { foreach ($entry in $Text) { $inBytes = $Encoding.GetBytes($entry) $outBytes = $privateKey.SignData($inBytes, $Algorithm, $Padding) [convert]::ToBase64String($outBytes) } } } function Invoke-TerminatingException { <# .SYNOPSIS Throw a terminating exception in the context of the caller. .DESCRIPTION Throw a terminating exception in the context of the caller. Masks the actual code location from the end user in how the message will be displayed. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. .PARAMETER Message The message to show the user. .PARAMETER Exception A nested exception to include in the exception object. .PARAMETER Category The category of the error. .PARAMETER ErrorRecord A full error record that was caught by the caller. Use this when you want to rethrow an existing error. .EXAMPLE PS C:\> Invoke-TerminatingException -Cmdlet $PSCmdlet -Message 'Unknown calling module' Terminates the calling command, citing an unknown caller. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] $Cmdlet, [string] $Message, [System.Exception] $Exception, [System.Management.Automation.ErrorCategory] $Category = [System.Management.Automation.ErrorCategory]::NotSpecified, [System.Management.Automation.ErrorRecord] $ErrorRecord ) process{ if ($ErrorRecord -and -not $Message) { $Cmdlet.ThrowTerminatingError($ErrorRecord) } $exceptionType = switch ($Category) { default { [System.Exception] } 'InvalidArgument' { [System.ArgumentException] } 'InvalidData' { [System.IO.InvalidDataException] } 'AuthenticationError' { [System.Security.Authentication.AuthenticationException] } 'InvalidOperation' { [System.InvalidOperationException] } } if ($Exception) { $newException = $Exception.GetType()::new($Message, $Exception) } elseif ($ErrorRecord) { $newException = $ErrorRecord.Exception.GetType()::new($Message, $ErrorRecord.Exception) } else { $newException = $exceptionType::new($Message) } $record = [System.Management.Automation.ErrorRecord]::new($newException, (Get-PSCallStack)[1].FunctionName, $Category, $Target) $Cmdlet.ThrowTerminatingError($record) } } function Connect-GraphAzure { <# .SYNOPSIS Connect to graph using your current Az session. .DESCRIPTION Connect to graph using your current Az session. Requires the Az.Accounts module and for the current session to already be connected via Connect-AzAccount. .PARAMETER Authority Authority to connect to. Defaults to: "https://graph.microsoft.com" .EXAMPLE PS C:\> Connect-GraphAzure Connect to graph via the current Az session #> [CmdletBinding()] param ( [string] $Authority = "https://graph.microsoft.com" ) try { $azContext = Get-AzContext -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } try { $result = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate( $azContext.Account, $azContext.Environment, "$($azContext.Tenant.id)", $null, [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, $null, $Authority ) } catch { $PSCmdlet.ThrowTerminatingError($_) } $script:token = $result.AccessToken } function Connect-GraphCertificate { <# .SYNOPSIS Connect to graph as an application using a certificate .DESCRIPTION Connect to graph as an application using a certificate .PARAMETER Certificate The certificate to use for authentication. .PARAMETER TenantID The Guid of the tenant to connect to. .PARAMETER ClientID The ClientID / ApplicationID of the application to connect as. .EXAMPLE PS C:\> $cert = Get-Item -Path 'Cert:\CurrentUser\My\082D5CB4BA31EED7E2E522B39992E34871C92BF5' PS C:\> Connect-GraphCertificate -TenantID '0639f07d-76e1-49cb-82ac-abcdefabcdefa' -ClientID '0639f07d-76e1-49cb-82ac-1234567890123' -Certificate $cert Connect to graph with the specified cert stored in the current user's certificate store. .LINK https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if (-not $_.HasPrivateKey) { throw "Certificate has no private key!" } $true })] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [string] $ClientID ) $jwtHeader = @{ alg = "RS256" typ = "JWT" x5t = [Convert]::ToBase64String($Certificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' } $encodedHeader = $jwtHeader | ConvertTo-Json | ConvertTo-Base64 $claims = @{ aud = "https://login.microsoftonline.com/$TenantID/v2.0" exp = ((Get-Date).AddMinutes(5) - (Get-Date -Date '1970.1.1')).TotalSeconds -as [int] iss = $ClientID jti = "$(New-Guid)" nbf = ((Get-Date) - (Get-Date -Date '1970.1.1')).TotalSeconds -as [int] sub = $ClientID } $encodedClaims = $claims | ConvertTo-Json | ConvertTo-Base64 $jwtPreliminary = $encodedHeader, $encodedClaims -join "." $jwtSigned = ($jwtPreliminary | ConvertTo-SignedString -Certificate $Certificate) -replace '\+', '-' -replace '/', '_' -replace '=' $jwt = $jwtPreliminary, $jwtSigned -join '.' $body = @{ client_id = $ClientID client_assertion = $jwt client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' scope = 'https://graph.microsoft.com/.default' grant_type = 'client_credentials' } $header = @{ Authorization = "Bearer $jwt" } $uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" try { $script:token = (Invoke-RestMethod -Uri $uri -Method Post -Body $body -Headers $header -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop).access_token } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Connect-GraphClientSecret { <# .SYNOPSIS Connects using a client secret. .DESCRIPTION Connects using a client secret. .PARAMETER ClientID The ID of the registered app used with this authentication request. .PARAMETER TenantID The ID of the tenant connected to with this authentication request. .PARAMETER ClientSecret The actual secret used for authenticating the request. .PARAMETER Scopes Generally doesn't need to be changed from the default 'https://graph.microsoft.com/.default' .PARAMETER Resource The resource the token grants access to. Generally doesn't need to be changed from the default 'https://graph.microsoft.com/' Only needed when connecting to another service. .EXAMPLE PS C:\> Connect-GraphClientSecret -ClientID '<ClientID>' -TenantID '<TenantID>' -ClientSecret $secret Connects to the specified tenant using the specified client and secret. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [securestring] $ClientSecret, [string[]] $Scopes = 'https://graph.microsoft.com/.default', [string] $Resource = 'https://graph.microsoft.com/' ) process { $body = @{ client_id = $ClientID client_secret = [PSCredential]::new('NoMatter', $ClientSecret).GetNetworkCredential().Password scope = $Scopes -join " " grant_type = 'client_credentials' resource = $Resource } try { $authResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$TenantId/oauth2/token" -Body $body -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } $script:token = $authResponse.access_token } } function Connect-GraphCredential { <# .SYNOPSIS Connect to graph using username and password. .DESCRIPTION Connect to graph using username and password. This logs into graph as a user, not as an application. Only cloud-only accounts can be used for this workflow. Consent to scopes must be granted before using them, as this command cannot show the consent prompt. .PARAMETER Credential Credentials of the user to connect as. .PARAMETER TenantID The Guid of the tenant to connect to. .PARAMETER ClientID The ClientID / ApplicationID of the application to use. .PARAMETER Scopes The permission scopes to request. .EXAMPLE PS C:\> Connect-GraphCredential -Credential max@contoso.com -ClientID $client -TenantID $tenant -Scopes 'user.read','user.readbasic.all' Connect as max@contoso.com with the rights to read user information. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [string[]] $Scopes = 'user.read' ) $request = @{ client_id = $ClientID scope = $Scopes -join " " username = $Credential.UserName password = $Credential.GetNetworkCredential().Password grant_type = 'password' } try { $answer = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" -Body $request -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } $script:token = $answer.access_token } function Connect-GraphDeviceCode { <# .SYNOPSIS Connects to Azure AD using the Device Code authentication workflow. .DESCRIPTION Connects to Azure AD using the Device Code authentication workflow. .PARAMETER ClientID The ID of the registered app used with this authentication request. .PARAMETER TenantID The ID of the tenant connected to with this authentication request. .PARAMETER Scopes Generally doesn't need to be changed from the default 'https://graph.microsoft.com/.default' .PARAMETER Resource The resource the token grants access to. Generally doesn't need to be changed from the default 'https://graph.microsoft.com/' Only needed when connecting to another service. .EXAMPLE PS C:\> Connect-GraphDeviceCode -ClientID '<ClientID>' -TenantID '<TenantID>' Connects to the specified tenant using the specified client, prompting the user to authorize via Browser. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [string[]] $Scopes = 'https://graph.microsoft.com/.default', [Uri] $Resource = 'https://graph.microsoft.com/' ) $actualScopes = foreach ($scope in $Scopes) { if ($scope -like 'https://*/*') { $scope } else { "{0}://{1}/{2}" -f $Resource.Scheme, $Resource.Host, $scope } } try { $initialResponse = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/devicecode" -Body @{ client_id = $ClientID scope = $actualScopes -join " " } -ErrorAction Stop } catch { throw } Write-Host $initialResponse.message $paramRetrieve = @{ Uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" Method = "POST" Body = @{ grant_type = "urn:ietf:params:oauth:grant-type:device_code" client_id = $ClientID device_code = $initialResponse.device_code } ErrorAction = 'Stop' } $limit = (Get-Date).AddSeconds($initialResponse.expires_in) while ($true) { if ((Get-Date) -gt $limit) { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Timelimit exceeded, device code authentication failed" -Category AuthenticationError } Start-Sleep -Seconds $initialResponse.interval try { $authResponse = Invoke-RestMethod @paramRetrieve } catch { if ($_ -match '"error":"authorization_pending"') { continue } $PSCmdlet.ThrowTerminatingError($_) } if ($authResponse) { break } } $script:token = $authResponse.access_token } function Connect-GraphToken { <# .SYNOPSIS Connect to graph using a token and the on behalf of flow. .DESCRIPTION Connect to graph using a token and the on behalf of flow. .PARAMETER Token The existing token to use for the request. .PARAMETER TenantID The Guid of the tenant to connect to. .PARAMETER ClientID The ClientID / ApplicationID of the application to connect as. .PARAMETER ClientSecret The secret used to authorize the OBO flow. .PARAMETER Scopes The scopes to request Defaults to: 'https://graph.microsoft.com/.default' .EXAMPLE PS C:\> Connect-GraphToken -Token $token -TenantID $tenantID -ClientID $clientID -CLientSecret $secret Connect to graph using a token and the on behalf of flow. .LINK https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Token, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [SecureString] $ClientSecret, [string[]] $Scopes = 'https://graph.microsoft.com/.default' ) $body = @{ grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer' client_id = $ClientID client_secret = ([PSCredential]::new("Whatever", $ClientSecret)).GetNetworkCredential().Password assertion = $Token scope = @($Scopes) requested_token_use = 'on_behalf_of' } $param = @{ Method = "POST" Uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" Body = $body ContentType = 'application/x-www-form-urlencoded' } try { $script:token = Invoke-RestMethod @param -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Invoke-GraphRequest { <# .SYNOPSIS Execute a request against the graph API .DESCRIPTION Execute a request against the graph API .PARAMETER Query The relative graph query with all conditions appended. .PARAMETER Method Which rest method to use. Defaults to GET. .PARAMETER ContentType Which content type to specify. Defaults to "Application/Json" .PARAMETER Body Any body to specify. Anything not a string, will be converted to json. .PARAMETER Raw Return the raw response, rather than processing the output. .PARAMETER NoPaging Only return the first set of data, rather than paging through the entire set. .PARAMETER Header Additional header entries to include beside authentication .EXAMPLE PS C:\> Invoke-GraphRequest -Query me Returns information about the current user. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Query, [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = 'GET', [string] $ContentType = 'application/json', $Body, [switch] $Raw, [switch] $NoPaging, [hashtable] $Header = @{ } ) begin { Assert-GraphConnection -Cmdlet $PSCmdlet } process { $parameters = @{ Uri = "$($script:baseEndpoint)/$($Query.TrimStart("/"))" Method = $Method Headers = @{ Authorization = "Bearer $($script:Token)" } + $Header ContentType = $ContentType } if ($Body) { if ($Body -is [string]) { $parameters.Body = $Body } else { $parameters.Body = $Body | ConvertTo-Json -Compress -Depth 99 } } do { try { $data = Invoke-RestMethod @parameters -ErrorAction Stop } catch { throw } if ($Raw) { $data } elseif ($data.Value) { $data.Value } elseif ($data -and $null -eq $data.Value) { $data } $parameters.Uri = $data.'@odata.nextLink' } until (-not $data.'@odata.nextLink' -or $NoPaging) } } function Set-GraphEndpoint { <# .SYNOPSIS Specify which graph endpoint to use for subsequent requests. .DESCRIPTION Specify which graph endpoint to use for subsequent requests. .PARAMETER Type Which kind of endpoint to use. v1 or beta .PARAMETER Url Specify a custom Url as endpoint. Used to switch to a government cloud. .EXAMPLE PS C:\> Set-GraphEndpoint -Type beta Switch to using the beta graph endpoint #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default')] [ValidateSet('v1','beta')] [string] $Type, [Parameter(Mandatory = $true, ParameterSetName = 'Url')] [string] $Url ) if ($Type) { switch ($Type) { 'v1' { $script:baseEndpoint = 'https://graph.microsoft.com/v1.0' } 'beta' { $script:baseEndpoint = 'https://graph.microsoft.com/beta' } } } if ($Url) { $script:baseEndpoint = $Url.Trim("/") } } # Graph Token used for connections $script:token = $null # Endpoint used for queries $script:baseEndpoint = 'https://graph.microsoft.com/v1.0' |