RestConnect.psm1
class Token { #region Token Data [string]$AccessToken [System.DateTime]$ValidAfter [System.DateTime]$ValidUntil [string[]]$Scopes #endregion Token Data #region Connection Data [string]$Type [string]$ClientID [string]$TenantID [string]$ServiceUrl # Workflow: Client Secret [System.Security.SecureString]$ClientSecret # Workflow: Certificate [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate # Workflow: Username & Password [PSCredential]$Credential #endregion Connection Data #region Extension Data [hashtable]$Data = @{ } [scriptblock]$GetHeaderCode [scriptblock]$RefreshTokenCode #endregion Extension Data #region Constructors Token([string]$ClientID, [string]$TenantID, [Securestring]$ClientSecret, [string]$ServiceUrl) { $this.ClientID = $ClientID $this.TenantID = $TenantID $this.ClientSecret = $ClientSecret $this.ServiceUrl = $ServiceUrl $this.Type = 'ClientSecret' } Token([string]$ClientID, [string]$TenantID, [pscredential]$Credential, [string]$ServiceUrl) { $this.ClientID = $ClientID $this.TenantID = $TenantID $this.Credential = $Credential $this.ServiceUrl = $ServiceUrl $this.Type = 'UsernamePassword' } Token([string]$ClientID, [string]$TenantID, [bool]$DeviceCode, [string]$ServiceUrl) { $this.ClientID = $ClientID $this.TenantID = $TenantID $this.ServiceUrl = $ServiceUrl $this.Type = 'DeviceCode' } Token([string]$ClientID, [string]$TenantID, [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [string]$ServiceUrl) { $this.ClientID = $ClientID $this.TenantID = $TenantID $this.Certificate = $Certificate $this.ServiceUrl = $ServiceUrl $this.Type = 'Certificate' } #endregion Constructors [void]SetTokenMetadata([PSObject] $AuthToken) { $this.AccessToken = $AuthToken.AccessToken $this.ValidAfter = $AuthToken.ValidAfter $this.ValidUntil = $AuthToken.ValidUntil $this.Scopes = $AuthToken.Scopes } [hashtable]GetHeader() { if ($this.GetHeaderCode) { return & $this.GetHeaderCode $this } return @{ Authorization = "Bearer $($this.AccessToken)" } } } function Connect-ServiceCertificate { <# .SYNOPSIS Connects to AAD using a application ID and a certificate. .DESCRIPTION Connects to AAD using a application ID and a certificate. .PARAMETER ServiceUrl The base url to the service connecting to. Used for authentication, scopes and executing requests. .PARAMETER Certificate The certificate to use for authentication. .PARAMETER TenantID The ID of the tenant/directory to connect to. .PARAMETER ClientID The ID of the registered application used to authenticate as. .EXAMPLE PS C:\> Connect-ServiceCertificate -Service MyAPI -ServiceUrl $url -Certificate $cert -TenantID $tenantID -ClientID $clientID Connects to the specified tenant using the specified app & cert. .LINK https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [uri] $ServiceUrl, [Parameter(Mandatory = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [string] $ClientID ) #region Build Signature Payload $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 '.' #endregion Build Signature Payload $body = @{ client_id = $ClientID client_assertion = $jwt client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' scope = '{0}://{1}/.default' -f $ServiceUrl.Scheme, $ServiceUrl.Host grant_type = 'client_credentials' } $header = @{ Authorization = "Bearer $jwt" } $uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" try { $authResponse = Invoke-RestMethod -Method Post -Uri $uri -Body $body -Headers $header -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop } catch { throw } [pscustomobject]@{ AccessToken = $authResponse.access_token ValidAfter = Get-Date ValidUntil = (Get-Date).AddSeconds($authResponse.expires_in) Scopes = @() } } function Connect-ServiceClientSecret { <# .SYNOPSIS Connets using a client secret. .DESCRIPTION Connets using a client secret. .PARAMETER ServiceUrl The base url to the service connecting to. Used for authentication, scopes and executing requests. .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. .EXAMPLE PS C:\> Connect-ServiceClientSecret -ServiceUrl $url -ClientID '<ClientID>' -TenantID '<TenantID>' -ClientSecret $secret Connects to the specified tenant using the specified client and secret. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [uri] $ServiceUrl, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [securestring] $ClientSecret ) process { $body = @{ resource = '{0}://{1}' -f $ServiceUrl.Scheme, $ServiceUrl.Host client_id = $ClientID client_secret = [PSCredential]::new('NoMatter', $ClientSecret).GetNetworkCredential().Password grant_type = 'client_credentials' } try { $authResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$TenantId/oauth2/token" -Body $body -ErrorAction Stop } catch { throw } [pscustomobject]@{ AccessToken = $authResponse.access_token ValidAfter = (Get-Date -Date '1970-01-01').AddSeconds($authResponse.not_before).ToLocalTime() ValidUntil = (Get-Date -Date '1970-01-01').AddSeconds($authResponse.expires_on).ToLocalTime() Scopes = @() } } } function Connect-ServiceDeviceCode { <# .SYNOPSIS Connects to Azure AD using the Device Code authentication workflow. .DESCRIPTION Connects to Azure AD using the Device Code authentication workflow. .PARAMETER ServiceUrl The base url to the service connecting to. Used for authentication, scopes and executing requests. .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 The scopes to request. Automatically scoped to the service specified via Service Url. Defaults to ".Default" .EXAMPLE PS C:\> Connect-ServiceDeviceCode -ServiceUrl $url -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)] [uri] $ServiceUrl, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [string[]] $Scopes = '.default' ) $actualScopes = foreach ($scope in $Scopes) { if ($scope -like 'https://*/*') { $scope } else { "{0}://{1}/{2}" -f $ServiceUrl.Scheme, $ServiceUrl.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 { continue } if ($authResponse) { break } } [pscustomobject]@{ AccessToken = $authResponse.access_token ValidAfter = (Get-Date -Date '1970-01-01').AddSeconds($authResponse.not_before).ToLocalTime() ValidUntil = (Get-Date -Date '1970-01-01').AddSeconds($authResponse.expires_on).ToLocalTime() Scopes = $authResponse.scope -split " " } } function Connect-ServicePassword { <# .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 ServiceUrl The base url to the service connecting to. Used for authentication, scopes and executing requests. .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 -Service MyAPI -ServiceUrl $url -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)] [uri] $ServiceUrl, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [string[]] $Scopes = '.default' ) $actualScopes = foreach ($scope in $Scopes) { if ($scope -like 'https://*/*') { $scope } else { "{0}://{1}/{2}" -f $ServiceUrl.Scheme, $ServiceUrl.Host, $scope } } $request = @{ client_id = $ClientID scope = $actualScopes -join " " username = $Credential.UserName password = $Credential.GetNetworkCredential().Password grant_type = 'password' } try { $authResponse = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" -Body $request -ErrorAction Stop } catch { throw } [pscustomobject]@{ AccessToken = $authResponse.access_token ValidAfter = (Get-Date).AddMinutes(-5) ValidUntil = (Get-Date).AddSeconds($authResponse.expires_in) Scopes = @($scope -split " ") } } function ConvertTo-Base64 { <# .SYNOPSIS Converts the input-string to its base 64 encoded string form. .DESCRIPTION Converts the input-string to its base 64 encoded string form. .PARAMETER Text The text to convert. .PARAMETER Encoding The encoding of the input text. Used to correctly translate the input string into bytes before converting those to base 64. Defaults to UTF8 .EXAMPLE PS C:\> Get-Content .\code.ps1 -Raw | ConvertTo-Base64 Reads the input file and converts its content into base64. #> [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-QueryString { <# .SYNOPSIS Convert conditions in a hashtable to a Query string to append to a webrequest. .DESCRIPTION Convert conditions in a hashtable to a Query string to append to a webrequest. .PARAMETER QueryHash Hashtable of query modifiers - usually filter conditions - to include in a web request. .EXAMPLE PS C:\> ConvertTo-QueryString -QueryHash $Query Converts the conditions in the specified hashtable to a Query string to append to a webrequest. #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [Hashtable] $QueryHash ) process { $elements = foreach ($pair in $QueryHash.GetEnumerator()) { '{0}={1}' -f $pair.Name, ($pair.Value -join ",") } '?{0}' -f ($elements -join '&') } } function ConvertTo-SignedString { <# .SYNOPSIS Signs input string with the offered certificate. .DESCRIPTION Signs input string with the offered certificate. .PARAMETER Text The text to sign. .PARAMETER Certificate The certificate to sign with. The Private Key must be available. .PARAMETER Padding What RSA Signature padding to use. Defaults to Pkcs1 .PARAMETER Algorithm What algorithm to use for signing. Defaults to SHA256 .PARAMETER Encoding The encoding to use for transforming the text to bytes before signing it. Defaults to UTF8 .EXAMPLE PS C:\> ConvertTo-SignedString -Text $token Signs the specified token #> [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 ) process { foreach ($entry in $Text) { $inBytes = $Encoding.GetBytes($entry) $outBytes = $Certificate.PrivateKey.SignData($inBytes, $Algorithm, $Padding) [convert]::ToBase64String($outBytes) } } } function Get-Token { <# .SYNOPSIS Retrieve the OAuth token to use for rest queries. .DESCRIPTION Retrieve the OAuth token to use for rest queries. .PARAMETER Service The service for which the token should be returned. .PARAMETER RequiredScopes Which scopes are needed for the token. In user-delegate authentication workflows, it will automatically try to add thoose scopes if not present yet. NOT YET IMPLEMENTED (no user-delegate authentication workflows implemented yet) .PARAMETER Cmdlet The $PSCmdlet variable of the caller. Used to kill the caller with in case of error. .EXAMPLE PS C:\> Get-Token -Service Endpoint -Cmdlet $PSCmdlet Retrieve the current access token for defender for endpoint. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Service, [string[]] $RequiredScopes = @(), [Parameter(Mandatory = $true)] $Cmdlet ) begin { #region Utility Functions function Update-Token { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Service ) $token = $script:tokens[$Service] switch ($token.Type) { 'Certificate' { try { Connect-RestService -Service $Service -ServiceUrl $token.ServiceUrl -ClientID $token.ClientID -TenantID $token.TenantID -Certificate $token.Certificate } catch { throw } } 'ClientSecret' { try { Connect-RestService -Service $Service -ServiceUrl $token.ServiceUrl -ClientID $token.ClientID -TenantID $token.TenantID -ClientSecret $token.ClientSecret } catch { throw } } 'UsernamePassword' { try { Connect-RestService -Service $Service -ServiceUrl $token.ServiceUrl -ClientID $token.ClientID -TenantID $token.TenantID -Credential $token.Credential } catch { throw } } 'DeviceCode' { try { Connect-RestService -Service $Service -ServiceUrl $token.ServiceUrl -ClientID $token.ClientID -TenantID $token.TenantID -DeviceCode } catch { throw } } default { if (-not $token.RefreshTokenCode) { throw "Unable to refresh connection to $Service - no refresh logic registered!" } try { & $token.RefreshTokenCode $token } catch { throw } } } } #endregion Utility Functions } process { $token = $script:tokens[$Service] if (-not $token) { Invoke-TerminatingException -Cmdlet $Cmdlet -Message "No token found for Service $Service. Establish a connection first!" } if ($token.ValidUntil -gt (Get-Date).AddMinutes(2)) { return $token } try { Update-Token -Service $Service -ErrorAction Stop } catch { Invoke-TerminatingException -Cmdlet $Cmdlet -Message "Failed to refresh the access token" -ErrorRecord $_ } $script:tokens[$Service] } } 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 Resolve-Certificate { <# .SYNOPSIS Resolves the certificate to use for authentication. .DESCRIPTION Resolves the certificate to use for authentication. Offers a centralized way to resolve certificate based on parameters passed through. Silently returns nothing if no match was found, which needs to be handled by the caller. .PARAMETER BoundParameters The parameters passed to the calling command. Will pick certificate-related parameters and figure out the cert to use from that. - Certificate: Assumes a full certificate specified and returns it - CertificateThumbprint: Searches current user and system cert store for a matching cert to use - CertificateName: Searches current user and system cert store for a matching cert to use (based on subject) - CertificatePath & CertificatePassword: Loads certificate from file Will be processed in the order above if multiple options are specified. .EXAMPLE PS C:\> Resolve-Certificate -BoundParameters $PSBoundParameters Resolves the certificate to use for authentication. #> [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] [CmdletBinding()] param ( $BoundParameters ) if ($BoundParameters.Certificate) { return $BoundParameters.Certificate } if ($BoundParameters.CertificateThumbprint) { if (Test-Path -Path "cert:\CurrentUser\My\$($BoundParameters.CertificateThumbprint)") { return Get-Item "cert:\CurrentUser\My\$($BoundParameters.CertificateThumbprint)" } if (Test-Path -Path "cert:\LocalMachine\My\$($BoundParameters.CertificateThumbprint)") { return Get-Item "cert:\LocalMachine\My\$($BoundParameters.CertificateThumbprint)" } } if ($BoundParameters.CertificateName) { if ($certificate = (Get-ChildItem 'Cert:\CurrentUser\My\').Where{ $_.Subject -eq $BoundParameters.CertificateName -and $_.HasPrivateKey }) { return $certificate | Sort-Object NotAfter -Descending | Select-Object -First 1 } if ($certificate = (Get-ChildItem 'Cert:\LocalMachine\My\').Where{ $_.Subject -eq $BoundParameters.CertificateName -and $_.HasPrivateKey }) { return $certificate | Sort-Object NotAfter -Descending | Select-Object -First 1 } } if ($BoundParameters.CertificatePath) { try { [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($BoundParameters.CertificatePath, $BoundParameters.CertificatePassword) } catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Unable to load certificate from file '$($BoundParameters.CertificatePath)': $_" -ErrorRecord $_ } } } function Assert-RestConnection { <# .SYNOPSIS Asserts a connection to the specified rest api has been established. .DESCRIPTION Asserts a connection to the specified rest api has been established. Fails the calling command in a terminating exception if not connected yet. .PARAMETER Service The service to which a connection needs to be established. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. Used to execute the terminating exception in the caller scope if needed. .EXAMPLE PS C:\> Assert-Connection -Service 'Endpoint' -Cmdlet $PSCmdlet Silently does nothing if already connected to the service 'Endpoint'. Kills the calling command if not yet connected. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Service, [Parameter(Mandatory = $true)] $Cmdlet ) process { if ($script:tokens[$Service]) { return } Invoke-TerminatingException -Cmdlet $Cmdlet -Message "Not connected yet! Use Connect-RestService (or a service specific connection method) to establish a connection first." -Category ConnectionError } } function Connect-RestService { <# .SYNOPSIS Establish a connection to a REST API. .DESCRIPTION Establish a connection to a REST API. Prerequisite before executing any requests / commands. Note: Used for authenticating against Microsoft Authentication services. For other authentication services, use "Set-RestServiceConnection" instead. .PARAMETER Service The name of the service to connect to. Label associated with the token generated, the same must be used when callng the Invoke-RestCommand command to associate the request with the connection. .PARAMETER ServiceUrl The base url to the service connecting to. Used for authentication, scopes and executing requests. .PARAMETER ClientID ID of the registered/enterprise application used for authentication. .PARAMETER TenantID The ID of the tenant/directory to connect to. .PARAMETER Scopes Any scopes to include in the request. Only used for interactive/delegate workflows, ignored for Certificate based authentication or when using Client Secrets. .PARAMETER DeviceCode Use the Device Code delegate authentication flow. This will prompt the user to complete login via browser. .PARAMETER Certificate The Certificate object used to authenticate with. Part of the Application Certificate authentication workflow. .PARAMETER CertificateThumbprint Thumbprint of the certificate to authenticate with. The certificate must be stored either in the user or computer certificate store. Part of the Application Certificate authentication workflow. .PARAMETER CertificateName The name/subject of the certificate to authenticate with. The certificate must be stored either in the user or computer certificate store. The newest certificate with a private key will be chosen. Part of the Application Certificate authentication workflow. .PARAMETER CertificatePath Path to a PFX file containing the certificate to authenticate with. Part of the Application Certificate authentication workflow. .PARAMETER CertificatePassword Password to use to read a PFX certificate file. Only used together with -CertificatePath. Part of the Application Certificate authentication workflow. .PARAMETER ClientSecret The client secret configured in the registered/enterprise application. Part of the Client Secret Certificate authentication workflow. .PARAMETER Credential The credentials to use to authenticate as a user. Part of the Username and Password delegate authentication workflow. Note: This workflow only works with cloud-only accounts and requires scopes to be pre-approved. .EXAMPLE PS C:\> Connect-RestService -Service MyAPI -ServiceUrl $url -ClientID $clientID -TenantID $tenantID -Certificate $cert Establish a connection to a rest API using the provided certificate. .EXAMPLE PS C:\> Connect-RestService -Service MyAPI -ServiceUrl $url -ClientID $clientID -TenantID $tenantID -CertificatePath C:\secrets\certs\mde.pfx -CertificatePassword (Read-Host -AsSecureString) Establish a connection to a rest API using the provided certificate file. Prompts you to enter the certificate-file's password first. .EXAMPLE PS C:\> Connect-RestService -Service MyAPI -ServiceUrl $url -ClientID $clientID -TenantID $tenantID -ClientSecret $secret Establish a connection to a rest API using a client secret. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Service, [Parameter(Mandatory = $true)] [string] $ServiceUrl, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [string[]] $Scopes, [Parameter(ParameterSetName = 'DeviceCode')] [switch] $DeviceCode, [Parameter(ParameterSetName = 'AppCertificate')] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(ParameterSetName = 'AppCertificate')] [string] $CertificateThumbprint, [Parameter(ParameterSetName = 'AppCertificate')] [string] $CertificateName, [Parameter(ParameterSetName = 'AppCertificate')] [string] $CertificatePath, [Parameter(ParameterSetName = 'AppCertificate')] [System.Security.SecureString] $CertificatePassword, [Parameter(Mandatory = $true, ParameterSetName = 'AppSecret')] [System.Security.SecureString] $ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = 'UsernamePassword')] [PSCredential] $Credential ) process { switch ($PSCmdlet.ParameterSetName) { 'AppSecret' { $serviceToken = [Token]::new($ClientID, $TenantID, $ClientSecret, $ServiceUrl) try { $authToken = Connect-ServiceClientSecret -ServiceUrl $ServiceUrl -ClientID $ClientID -TenantID $TenantID -ClientSecret $ClientSecret -ErrorAction Stop } catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -ErrorRecord $_ } $serviceToken.SetTokenMetadata($authToken) $script:tokens[$Service] = $serviceToken } 'AppCertificate' { try { $cert = Resolve-Certificate -BoundParameters $PSBoundParameters } catch { throw } if (-not $cert) { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "No certificate found to authenticate with!" } if (-not $cert.HasPrivateKey) { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Certificate has no private key: $($cert.Thumbprint)" } if (-not $cert.PrivateKey) { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Failed to access private key on Certificate $($cert.Thumbprint)" } $serviceToken = [Token]::new($ClientID, $TenantID, $cert, $ServiceUrl) try { $authToken = Connect-ServiceCertificate -ServiceUrl $ServiceUrl -ClientID $ClientID -TenantID $TenantID -Certificate $cert -ErrorAction Stop } catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -ErrorRecord $_ } $serviceToken.SetTokenMetadata($authToken) $script:tokens[$Service] = $serviceToken } 'UsernamePassword' { $serviceToken = [Token]::new($ClientID, $TenantID, $Credential, $ServiceUrl) try { $authToken = Connect-ServicePassword -ServiceUrl $ServiceUrl -ClientID $ClientID -TenantID $TenantID -Credential $Credential -ErrorAction Stop } catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -ErrorRecord $_ } $serviceToken.SetTokenMetadata($authToken) $script:tokens[$Service] = $serviceToken } 'DeviceCode' { $serviceToken = [Token]::new($ClientID, $TenantID, $true, $ServiceUrl) try { $authToken = Connect-ServiceDeviceCode -ServiceUrl $ServiceUrl -ClientID $ClientID -TenantID $TenantID -Scopes $Scopes -ErrorAction Stop } catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -ErrorRecord $_ } $serviceToken.SetTokenMetadata($authToken) $script:tokens[$Service] = $serviceToken } } } } function Invoke-RestRequest { <# .SYNOPSIS Executes a web request against a rest API. .DESCRIPTION Executes a web request against a rest API. Handles all the authentication details once connected using Connect-RestService. .PARAMETER Path The relative path of the endpoint to query. .PARAMETER Body Any body content needed for the request. .PARAMETER Query Any query content to include in the request. In opposite to -Body this is attached to the request Url and usually used for filtering. .PARAMETER Method The Rest Method to use. Defaults to GET .PARAMETER RequiredScopes NOT IMPLEMENTED YET Any authentication scopes needed. When connected as a user, it will automatically try to re-authenticate with the correct scopes if they are missing in the current session. .PARAMETER Service Which service to execute against. .PARAMETER Header Additional header data to include. .EXAMPLE PS C:\> Invoke-RestRequest -Path 'alerts' -RequiredScopes 'Alert.Read' -Service mde Executes a GET request against the "mde" service's alerts endpoint. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Hashtable] $Body = @{ }, [Hashtable] $Query = @{ }, [string] $Method = 'GET', [string[]] $RequiredScopes, [Parameter(Mandatory = $true)] [string] $Service, [Hashtable] $Header = @{ } ) begin{ Assert-RestConnection -Service $Service -Cmdlet $PSCmdlet $token = Get-Token -Service $Service -RequiredScopes $RequiredScopes -Cmdlet $PSCmdlet $baseUri = $token.ServiceUrl } process { $parameters = @{ Method = $Method Uri = "$($baseUri)/$($Path.TrimStart('/'))" Headers = $token.GetHeader() + $Header } if ($Body.Count -gt 0) { $parameters.Body = $Body | ConvertTo-Json -Compress } if ($Query.Count -gt 0) { $parameters.Uri += ConvertTo-QueryString -QueryHash $Query } while ($parameters.Uri) { try { $result = Invoke-RestMethod @parameters -ErrorAction Stop } catch { Write-Error $_ break } if ($result.Value) { $result.Value } else { $result } $parameters.Uri = $result.'@odata.nextLink' } } } function Set-RestConnection { <# .SYNOPSIS Register an externally provided authenticated connection. .DESCRIPTION Register an externally provided authenticated connection. Use this to connect to services not covered behind Azure AD authentication. .PARAMETER Service Name of the service to configure. Creates a new service/connection registration if the name doesn't exist yet. .PARAMETER ServiceUrl Url used to connect to the service. For example, "https://graph.microsoft.com/beta" is the ServiceUrl for the beta version of the MS graph api. .PARAMETER ValidAfter Starting when the token is valid .PARAMETER ValidUntil Until when the token is valid .PARAMETER AccessToken The token string used for authentication. Optional if your connection is established via data provided in -Data .PARAMETER Scopes The scopes/permissions applicable to your session. For documentation purposes only at the moment. .PARAMETER GetHeaderCode A scriptblock that will return a hashtable used as header in each webrequest. This is generally the "Authorization" header used to authenticate individual requests. Receives the token object as argument. .PARAMETER RefreshTokenCode Logic used to refresh the connection. Receives the token object as argument. .PARAMETER Data A hashtable of additional data to store in the token object. Use this for any data needed by the scriptblock logic for producing the header or refreshing the connection. .EXAMPLE PS C:\> Set-RestConnection -Service MyService -ServiceUrl "https://MyService.contoso.com/api" -Data $data -GetHeaderCode $headerCode -RefreshTokenCode $refreshCode Registers a new service connection to the MyService API #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Service, [string] $ServiceUrl, [DateTime] $ValidAfter, [datetime] $ValidUntil, [string] $AccessToken, [string[]] $Scopes, [scriptblock] $GetHeaderCode, [scriptblock] $RefreshTokenCode, [hashtable] $Data ) $commonParameters = 'Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable','InformationVariable','OutVariable','OutBuffer','PipelineVariable' $token = $script:tokens[$Service] if (-not $token) { $token = [Token]::new() $token.Type = 'Custom' $script:tokens[$Service] = $token } foreach ($key in $PSBoundParameters.Keys) { if ($key -eq 'Service') { continue } if ($key -in $commonParameters) { continue } $token.$key = $PSBoundParameters.$key } } # Store the tokens connected through $script:tokens = @{ } |