MSGraphAppOnlyEssentials.psm1
#Requires -Version 5.1 using Namespace System.Security.Cryptography.X509Certificates using Namespace System.Management.Automation.Host # v0.0.0.2 function New-MSGraphAccessToken { [CmdletBinding( DefaultParameterSetName = 'Certificate' )] param ( [Parameter(Mandatory)] [Alias('Tenant', 'TenantName', 'TenantDomain', 'TenantDomainName')] [string]$TenantId, [Parameter(Mandatory)] [Alias('ClientId')] [Guid]$ApplicationId, [Parameter( Mandatory, ParameterSetName = 'Certificate', HelpMessage = 'E.g. Use $Certificate, where `$Certificate = Get-ChildItem cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317' )] [X509Certificate2]$Certificate, [Parameter( Mandatory, ParameterSetName = 'CertificateStorePath', HelpMessage = 'E.g. cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317; E.g. cert:\LocalMachine\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317' )] [ValidateScript( { if (Test-Path -Path $_) { $true } else { throw "An example proper path would be 'cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317'." } } )] [string]$CertificateStorePath, [ValidateRange(1, 10)] [int16]$JWTExpMinutes = 2 ) if ($PSCmdlet.ParameterSetName -eq 'CertificateStorePath') { try { $Script:Certificate = Get-ChildItem -Path $CertificateStorePath -ErrorAction Stop } catch { throw $_ } } else { $Script:Certificate = $Certificate } if (-not (Test-CertificateProvider -Certificate $Script:Certificate)) { $ErrorMessage = "The supplied certificate does not use the provider 'Microsoft Enhanced RSA and AES Cryptographic Provider'. " + "For best luck, use a certificate generated using New-SelfSignedAzureADAppRegistrationCertificate." throw $ErrorMessage } $NowUTC = [datetime]::UtcNow $JWTHeader = @{ alg = 'RS256' typ = 'JWT' x5t = ConvertTo-Base64UrlFriendly -String ([System.Convert]::ToBase64String($Script:Certificate.GetCertHash())) } $JWTClaims = @{ aud = "https://login.microsoftonline.com/$TenantId/oauth2/token" exp = (Get-Date $NowUTC.AddMinutes($JWTExpMinutes) -UFormat '%s') -replace '\..*' iss = $ApplicationId.Guid jti = [Guid]::NewGuid() nbf = (Get-Date $NowUTC -UFormat '%s') -replace '\..*' sub = $ApplicationId.Guid } $EncodedJWTHeader = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject $JWTHeader)) ) $EncodedJWTClaims = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject $JWTClaims)) ) $JWT = ConvertTo-Base64UrlFriendly -String ($EncodedJWTHeader + '.' + $EncodedJWTClaims) $Signature = ConvertTo-Base64UrlFriendly -String ([System.Convert]::ToBase64String( $Script:Certificate.PrivateKey.SignData( [System.Text.Encoding]::UTF8.GetBytes($JWT), [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1 ) ) ) $JWT = $JWT + '.' + $Signature $Body = @{ client_id = $ApplicationId 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" } $TokenRequestParams = @{ Method = 'POST' Uri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" Body = $Body Headers = @{ Authorization = "Bearer $($JWT)" } ContentType = 'application/x-www-form-urlencoded' ErrorAction = 'Stop' } try { Invoke-RestMethod @TokenRequestParams } catch { throw $_ } } function New-MSGraphRequest { [CmdletBinding()] param ( [Parameter(Mandatory)] [Alias('Query')] [string]$Request, [Parameter(Mandatory)] [ValidateScript( { if ($_.token_type -eq 'Bearer' -and $_.access_token -match '^[-\w]+\.[-\w]+\.[-\w]+$') { $true } else { throw 'Invalid access token. For best results, supply $AccessToken where: $AccessToken = New-MSGraphAccessToken ...' } } )] [Object]$AccessToken, [Alias('API', 'Version', 'Endpoint')] [ValidateSet('v1.0', 'beta')] [string]$ApiVersion = 'v1.0', [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')] [string]$Method = 'GET', [string]$Body, [ValidateSet('Warn', 'Inquire', 'Continue', 'SilentlyContinue')] [string]$nextLinkAction = 'Warn' ) $RequestParams = @{ Headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } Uri = "https://graph.microsoft.com/$($ApiVersion)/$($Request)" Method = $Method ContentType = 'application/json' ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Body')) { if ($Method -notmatch '(POST)|(PATCH)') { throw "Body is not allowed when the method is $($Method), only POST or PATCH." } else { $RequestParams['Body'] = $Body } } try { Invoke-RestMethod @RequestParams -OutVariable requestResponse } catch { throw $_ } if ($requestResponse.'@odata.nextLink') { $Script:Continue = $true switch ($nextLinkAction) { Warn { Write-Warning -Message "There are more results available. Next page: $($requestResponse.'@odata.nextLink')" $Script:Continue = $false } Continue { Write-Information -MessageData 'There are more results available. Getting the next page' -InformationAction Continue } Inquire { switch ( $host.UI.PromptForChoice( 'There are more results available (i.e. response included @odata.nextLink).', 'Get more results?', [ChoiceDescription[]]@('&Yes', 'Yes to &All', '&No'), 2 ) ) { 0 {} # Will prompt for choice again if the next response includes another @odata.nextLink. 1 { $nextLinkAction = 'SilentlyContinue' } 2 { $Script:Continue = $false } } } } if ($Script:Continue) { $nextLinkRequestParams = @{ AccessToken = $AccessToken ApiVersion = $ApiVersion Request = "$($requestResponse.'@odata.nextLink' -replace 'https://graph.microsoft.com/(v1\.0|beta)/')" nextLinkAction = $nextLinkAction ErrorAction = 'Stop' } try { New-MSGraphRequest @nextLinkRequestParams } catch { throw $_ } } } } New-Alias -Name New-MSGraphQuery -Value New-MSGraphRequest function New-SelfSignedMSGraphApplicationCertificate { [CmdletBinding()] param ( [ValidatePattern('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)')] [string]$DnsName, [Parameter(Mandatory)] [string]$FriendlyName, [ValidateScript( { if (Test-Path -Path $_) { $true } else { throw "An example proper location would be 'cert:\CurrentUser\My'." } } )] [string]$CertStoreLocation = 'cert:\CurrentUser\My', [datetime]$NotAfter = [datetime]::Now.AddDays(90), [ValidateSet('Signature', 'KeyExchange')] [string]$KeySpec = 'Signature' ) $NewCertParams = @{ DnsName = $DnsName FriendlyName = $FriendlyName CertStoreLocation = $CertStoreLocation NotAfter = $NotAfter KeyExportPolicy = 'Exportable' KeySpec = $KeySpec Provider = 'Microsoft Enhanced RSA and AES Cryptographic Provider' HashAlgorithm = 'SHA256' ErrorAction = 'Stop' } try { New-SelfSignedCertificate @NewCertParams } catch { throw $_ } } New-Alias -Name 'New-SelfSignedAzureADAppRegistrationCertificate' -Value New-SelfSignedMSGraphApplicationCertificate function New-MSGraphPoPToken { [CmdletBinding( DefaultParameterSetName = 'Certificate' )] param ( [Parameter(Mandatory)] [Alias('ClientId')] [Guid]$ApplicationObjectId, [Parameter( Mandatory, ParameterSetName = 'Certificate', HelpMessage = 'E.g. Use $Certificate, where `$Certificate = Get-ChildItem cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317' )] [X509Certificate2]$Certificate, [Parameter( Mandatory, ParameterSetName = 'CertificateStorePath', HelpMessage = 'E.g. cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317; E.g. cert:\LocalMachine\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317' )] [ValidateScript( { if (Test-Path -Path $_) { $true } else { throw "An example proper path would be 'cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317'." } } )] [string]$CertificateStorePath, [ValidateRange(1, 10)] [int16]$JWTExpMinutes = 2 ) if ($PSCmdlet.ParameterSetName -eq 'CertificateStorePath') { try { $Script:Certificate = Get-ChildItem -Path $CertificateStorePath -ErrorAction Stop } catch { throw $_ } } else { $Script:Certificate = $Certificate } if (-not (Test-CertificateProvider -Certificate $Script:Certificate)) { $ErrorMessage = "The supplied certificate does not use the provider 'Microsoft Enhanced RSA and AES Cryptographic Provider'. " + "For best luck, use a certificate generated using New-SelfSignedAzureADAppRegistrationCertificate." throw $ErrorMessage } $NowUTC = [datetime]::UtcNow $JWTHeader = @{ alg = 'RS256' typ = 'JWT' x5t = ConvertTo-Base64UrlFriendly -String ([System.Convert]::ToBase64String($Script:Certificate.GetCertHash())) } $JWTClaims = @{ aud = '00000002-0000-0000-c000-000000000000' iss = $ApplicationObjectId.Guid exp = (Get-Date $NowUTC.AddMinutes($JWTExpMinutes) -UFormat '%s') -replace '\..*' nbf = (Get-Date $NowUTC -UFormat '%s') -replace '\..*' } $EncodedJWTHeader = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json)) ) $EncodedJWTClaims = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes(($JWTClaims | ConvertTo-Json)) ) $JWT = ConvertTo-Base64UrlFriendly ($EncodedJWTHeader + '.' + $EncodedJWTClaims) $Signature = ConvertTo-Base64UrlFriendly ([System.Convert]::ToBase64String( $Script:Certificate.PrivateKey.SignData( [System.Text.Encoding]::UTF8.GetBytes($JWT), [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1 ) ) ) $JWT = $JWT + '.' + $Signature $JWT } function Add-MSGraphApplicationKeyCredential { [CmdletBinding( DefaultParameterSetName = 'Certificate' )] param ( [Parameter(Mandatory)] [Guid]$ApplicationObjectId, [Parameter(Mandatory)] [ValidateScript( { if ($_.token_type -eq 'Bearer' -and $_.access_token -match '^[-\w]+\.[-\w]+\.[-\w]+$') { $true } else { throw 'Invalid access token. For best results, supply $AccessToken where: $AccessToken = New-MSGraphAccessToken ...' } } )] [Object]$AccessToken, [Parameter(Mandatory)] [ValidatePattern('^[-\w]+\.[-\w]+\.[-\w]+$')] [string]$PoPToken, [Parameter( Mandatory, ParameterSetName = 'Certificate', HelpMessage = 'E.g. Use $Certificate, where `$Certificate = Get-ChildItem cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317' )] [X509Certificate2]$Certificate, [Parameter( Mandatory, ParameterSetName = 'CertificateStorePath', HelpMessage = 'E.g. cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317; E.g. cert:\LocalMachine\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317' )] [ValidateScript( { if (Test-Path -Path $_) { $true } else { throw "An example proper path would be 'cert:\CurrentUser\My\C3E7F30B9DD50B8B09B9B539BC41F8157642D317'." } } )] [string]$CertificateStorePath ) if ($PSCmdlet.ParameterSetName -eq 'CertificateStorePath') { try { $Script:Certificate = Get-ChildItem -Path $CertificateStorePath -ErrorAction Stop } catch { throw $_ } } else { $Script:Certificate = $Certificate } if (-not (Test-CertificateProvider -Certificate $Script:Certificate)) { $ErrorMessage = "The supplied certificate does not use the provider 'Microsoft Enhanced RSA and AES Cryptographic Provider'. " + "For best luck, use a certificate generated using New-SelfSignedAzureADAppRegistrationCertificate." throw $ErrorMessage } $Body = @{ proof = $PoPToken keyCredential = @{ type = "AsymmetricX509Cert" usage = "Verify" key = [System.Convert]::ToBase64String($Script:Certificate.GetRawCertData()) } } $AddKeyParams = @{ AccessToken = $AccessToken Method = 'POST' Request = "applications/$($ApplicationObjectId)/addKey" Body = (ConvertTo-Json $Body) ErrorAction = 'Stop' } try { New-MSGraphRequest @AddKeyParams } catch { throw $_ } } function Remove-MSGraphApplicationKeyCredential { [CmdletBinding( DefaultParameterSetName = 'CertificateThumbprint' )] param ( [Parameter(Mandatory)] [Guid]$ApplicationObjectId, [Parameter(Mandatory)] [ValidateScript( { if ($_.token_type -eq 'Bearer' -and $_.access_token -match '^[-\w]+\.[-\w]+\.[-\w]+$') { $true } else { throw 'Invalid access token. For best results, supply $AccessToken where: $AccessToken = New-MSGraphAccessToken ...' } } )] [Object]$AccessToken, [Parameter(Mandatory)] [ValidatePattern('^[-\w]+\.[-\w]+\.[-\w]+$')] [string]$PoPToken, [Parameter( Mandatory, ParameterSetName = 'CertificateThumbprint' )] [ValidatePattern('^[a-fA-F0-9]{40,40}$')] [string]$CertificateThumbprint, [Parameter( Mandatory, ParameterSetName = 'KeyId' )] [Guid]$KeyId ) if ($PSCmdlet.ParameterSetName -eq 'CertificateThumbprint') { $ListKeysParams = @{ AccessToken = $AccessToken Request = "applications/$($ApplicationObjectId)" ErrorAction = 'Stop' } try { $KeyCredentials = New-MSGraphRequest @ListKeysParams } catch { throw $_ } $Script:KeyId = ($KeyCredentials.KeyCredentials | Where-Object { $_.customKeyIdentifier -eq $CertificateThumbprint }).KeyId if (-not $Script:KeyId) { throw "No KeyCredential was found with certificate thumbprint $($CertificateThumbprint)." } } else { $Script:KeyId = $KeyId.Guid } $Body = @{ keyId = $Script:KeyId proof = $PoPToken } $RemoveKeyParams = @{ AccessToken = $AccessToken Request = "applications/$($ApplicationObjectId)/removeKey" Method = 'POST' Body = (ConvertTo-Json $Body) ErrorAction = 'Stop' } try { New-MSGraphRequest @RemoveKeyParams } catch { throw $_ } } function ConvertTo-Base64UrlFriendly ([string]$String) { $String -replace '\+', '-' -replace '/', '_' -replace '=' } function Test-CertificateProvider ([X509Certificate2]$Certificate) { if ($PSVersionTable.PSEdition -eq 'Desktop') { $certProvider = $Certificate.PrivateKey.CspKeyContainerInfo.ProviderName } else { $certProvider = $Certificate.PrivateKey.Key.Provider } if ($certProvider -eq 'Microsoft Enhanced RSA and AES Cryptographic Provider') { $true } else { $false } } |