Public/Get-MsGraphAuthTokenWithCert.ps1

Function Get-MsGraphAuthTokenWithCert {
    [CmdletBinding()]
    param (
        [Parameter (mandatory=$true)][string]$TenantName,
        [Parameter (mandatory=$true)][string]$AppId,
        [Parameter (mandatory=$true)][string]$CertificateThumbprint,
        [Parameter (mandatory=$false)][string]$CertificateLocation = "Cert:\CurrentUser\My\",
        [Parameter (mandatory=$false)][string]$Scope = "https://graph.microsoft.com/.default"
    )

    # IMPORTANT: This function builds on the excellent code by Alex Asplund.
    # Their original post can be found here : https://adamtheautomator.com/powershell-graph-api/

    BEGIN {
        # Set the error action response for this function
        $objErrActionPref = $ErrorActionPreference
        $ErrorActionPreference = "Stop"

        # Used to check the signature loaded from the private cert
        $bSigFailed = $false

        # Show the function being run in verbose
        Write-Verbose "*** Function: Get-MsGraphAuthTokenWithCert"

        # Set a value for the return object (if case it fails to be created)
        $objAuth = $null

        # Get the certificate from the local certificate store
        try {
            $sCertPath = ($CertificateLocation + $CertificateThumbprint)
            $Certificate = Get-Item $sCertPath

            Write-Verbose "`t+++ Certificate discovered $Certificate.Subject"
        }
        catch {
            $ErrorActionPreference = $objErrActionPref

            if (!($null -eq $Global:Error[0])) {
                $sErrMsg = ("Certificate was not found or could not be loaded. Error: " + $Global:Error[0].Exception.toString())
            }
            else {
                $sErrMsg = ("Certificate was not found or could not be loaded. No more details available")
            }

            Throw $sErrMsg
        }
    }
    PROCESS {
        # Create the request object
        try {
            Write-Verbose "`t+++ Creating the JWT object"

            # Create base64 hash of certificate
            $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

            # Create JWT timestamp for expiration
            $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)

            # Create JWT validity start timestamp
            $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
            $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

            # Create JWT header
            $JWTHeader = @{
                alg = "RS256"
                typ = "JWT"
                # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
                x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
            }

            # Create JWT payload
            $JWTPayLoad = @{
                # What endpoint is allowed to use this JWT
                aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"

                # Expiration timestamp
                exp = $JWTExpiration

                # Issuer = your application
                iss = $AppId

                # JWT ID: random guid
                jti = [guid]::NewGuid()

                # Not to be used before
                nbf = $NotBefore

                # JWT Subject
                sub = $AppId
            }

            # Convert header and payload to base64
            $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)

            # Join header and Payload with "." to create a valid (unsigned) JWT
            $JWT = $EncodedHeader + "." + $EncodedPayload

            # Get the private key object of your certificate
            $PrivateKey = $Certificate.PrivateKey
        }
        catch {
            $ErrorActionPreference = $objErrActionPref

            if (!($null -eq $Global:Error[0])) {
                $sErrMsg = ("Failed to create the JWT Payload. Error: " + $Global:Error[0].Exception.toString())
            }
            else {
                $sErrMsg = ("Failed to create the JWT Payload. No error details returned")
            }

            Throw $sErrMsg
        }

        try {
            # Define RSA signature and hashing algorithm
            $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
            $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

            # Create a signature of the JWT
            $Signature = [Convert]::ToBase64String(
                $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
            ) -replace '\+','-' -replace '/','_' -replace '='

            # Join the signature to the JWT with "."
            $JWT = $JWT + "." + $Signature

        }
        catch {
            Write-Verbose "`t+++ Failed to use hash algorithm SHA256... attempting with SHA1"
            $bSigFailed = $true
        }

        if ($bSigFailed -eq $true) {
            try {
                # Define RSA signature and hashing algorithm
                $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
                $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA1

                # Create a signature of the JWT
                $Signature = [Convert]::ToBase64String(
                    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
                ) -replace '\+','-' -replace '/','_' -replace '='

                # Join the signature to the JWT with "."
                $JWT = $JWT + "." + $Signature

                Write-Verbose "`t+++ Successfully created the Signature with SHA1"
            }
            catch {
                $ErrorActionPreference = $objErrActionPref

                if (!($null -eq $Global:Error[0])) {
                    $sErrMsg = ("Failed to create private signature. Error: " + $Global:Error[0].Exception.toString())
                }
                else {
                    $sErrMsg = ("Failed to create private signature. No error details returned")
                }

                Throw $sErrMsg
            }
        }

        try {
            # Create a hash with body parameters
            $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"
            }

            Write-Verbose "`t+++ JWT Object created"

            $Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

            Write-Verbose "`t+++ Uri to connect: $Url"

            # Use the self-generated JWT as Authorization
            $Header = @{
                Authorization = "Bearer $JWT"
            }

            # Splat the parameters for Invoke-Restmethod for cleaner code
            $PostSplat = @{
                ContentType = 'application/x-www-form-urlencoded'
                Method = 'POST'
                Body = $Body
                Uri = $Url
                Headers = $Header
            }
        }
        catch {
            $ErrorActionPreference = $objErrActionPref

            if (!($null -eq $Global:Error[0])) {
                $sErrMsg = ("Failed to create token request. Error: " + $Global:Error[0].Exception.toString())
            }
            else {
                $sErrMsg = ("Failed to create token request.")
            }

            Throw $sErrMsg
        }


        try {
            Write-Verbose "`t+++ Attempting to request auth token"

            $objAuth = Invoke-RestMethod @PostSplat

            Write-Verbose "`t+++ Successfully requested auth token"
        }
        catch {
            $ErrorActionPreference = $objErrActionPref

            if (!($null -eq $Global:Error[0])) {
                $sErrMsg = ("Failed to request a token from MSGraph. Error: " + $Global:Error[0].Exception.toString())
            }
            else {
                $sErrMsg = ("Failed to request a token from MSGraph. No further details available")
            }

            Throw $sErrMsg
        }
    }
    END {
        # Reset the error action prefence
        $ErrorActionPreference = $objErrActionPref

        Write-Verbose "`t+++ Returning auth token - function finishing"

        return Write-Output $objAuth -NoEnumerate
    }

    <#
        .SYNOPSIS
        Function to connect to MSGraph using a certificate and retuns an authentication token. This function was written by Alex Asplund and reused from https://adamtheautomator.com/powershell-graph-api/ - and then I modified it to use paramaters for the connection details. His website is at https://automativity.com.
 
        .DESCRIPTION
        This function accepts a certificate and creates a JWT auth token before attempting to authenticate against MSGraph. For this to work you must first create an Azure App Registration that has permission to MsGraph and you must also upload the certificate to Azure. See https://adamtheautomator.com/powershell-graph-api/ for more information on this.
 
        .PARAMETER TenantName
        Mandatory. This string value is the Azure tenancy Name (not the id guid)
 
        .PARAMETER AppId
        Mandatory. This is the guid id of the App Registrtaion that the authentication certificate and permissions have been assigned to.
 
        .PARAMETER CertificateThumbprint
        Mandatory. This is the Certificate Thumbprint to use to authenticate with the Azure App Proxy. See this site for info on how to get the thumbprint of a cert using PowerShell: https://devblogs.microsoft.com/scripting/powertip-use-powershell-to-discover-certificate-thumbprints/
 
        .PARAMETER CertificateLocation
        Optional. The function will, by default, check for the certificate in the current user certificate store. To specify another store use this parameter. Default = Cert:\CurrentUser\My\
 
        .PARAMETER Scope
        Optional. This is the MSGraph scope - you shouldn't need to change this but have the option to here.
 
        .EXAMPLE
        $objAuth = Connect-MsGraphWithCert -TenantName "mytenant.onmicrosoft.com" -AppId "b5c3dab3-634a-49f9-9e70-d87faadf7a2c" -CertificateThumbprint "48504E974C0DAC5B5CD476C8202274B24C8C7172"
 
        This example shows the minimum requirements for connecting to MSGraph and returning an authentication token
 
        .EXAMPLE
        $objAuth = Connect-MsGraphWithCert -TenantName "mytenant.onmicrosoft.com" -AppId "b5c3dab3-634a-49f9-9e70-d87faadf7a2c" -CertificateThumbprint "48504E974C0DAC5B5CD476C8202274B24C8C7172" -CertificateLocation = "Cert:\LocalMachine\My\"
 
        As above but specifying the computer certificate store rather than the user store.
 
        .LINK
        Alex Asplund's original code : https://adamtheautomator.com/powershell-graph-api/
 
        .LINK
        Martin Vogwell - https://github.com/mvogwell/ps_Module_MSGraph
    #>

}