In my last blog post I talked about how to use PowerShell to instantiate an MSAL Confidential Client Application to acquire an access token using Client Credentials Grant flow. In this post we will use PowerShell to instantiate an MSAL Public Client Application to perform an Authorization Code Grant flow to obtain a delegated permission Access Token for Microsoft Graph. We will then use that access token to call Microsoft Graph to configure a signing certificate for our SAML Application Service Principal. Just a quick refresher that a certificate is always required when setting up SAML Sigle Sign-On feature for an Enterprise App in Azure AD.

Pre-requisites

To run the script in this blog you should have the following:

1) A SAML-based SSO Enterprise Application you want to configure a signing certificate for. Get the Object ID of this Application in the Properties blade of the Enterprise App – we will need it for the script. The script in this blog performs the same thing as doing the following UI action in the portal:

Azure Active Directory -> Enterprise Application -> Pick the correct App -> Single sign-on -> click ‘Edit’ link in the SAML Signing Certificate section -> Import Certificate


If you are trying to automate a SAML-based SSO Application, take a look at the documentation Automate SAML-based SSO app configuration with Microsoft Graph API. This blog can help with step 4, Configure Signing Certifcate, of that article.

2) An app registration to sign in a user and get an access token for Microsoft Graph. Get the Application (client) ID of this app in the Overview section – we will need it for the script. This application should have the following App Registration configuration:

Supported account typesAccounts in this organizational directory only
Redirect URIshttp://localhost under ‘Mobile and desktop applications’ platform
API permissionsMicrosoft Graph – Delegated permissions: Application.ReadWrite.All and User.Read
(Make sure you grant Admin consent to these permissions)

3) The user who logs in to get the MS Graph Access Token should be one of the following Azure AD Administrative Role – this is required in order to make a change to the Service Principal:

  • Cloud Application Administrator
  • Application Administrator
  • Global Administrator

Signing Certificate

We will need to have a certificate to configure for our application. You can either create a self-signed certificate (using either PowerShell or OpenSSL as shown below) or obtain one from your Trusted Certificate Authority. We will need the following certificate components for our script:

  1. public key (typically a .cer file)
  2. private key in PKCS#12 format (in .pfx file)
  3. password for the private key (pfx file)

Note: It is important to have the private key in PKCS#12 format since Azure AD does not support other format types. Using the wrong format can result in the the error “Invalid certificate: Key value is invalid certificate” when using MS Graph to PATCH the Service Principal with a keyCredentials containing the certificate info

Using PowerShell to create a self-signed certificate

The following PowerShell script can be used to create a self-signed certificate and then export both the private key and public key out to a .pfx and and a .cer files

# fqdn - this is used for the 'issued to' and 'issued by' field of the certificate
# pwd - password for exporting the certificate private key
# location - path to folder where both the pfx and cer file will be written to, for example C:\users\john\Documents

Param(
    [Parameter(Mandatory=$true)]
    [string]$fqdn,
    [Parameter(Mandatory=$true)]
    [string]$pwd,
    [Parameter(Mandatory=$true)]
    [string]$location
) 

if (!$PSBoundParameters.ContainsKey('location'))
{
    $location = "."
} 

$cert = New-SelfSignedCertificate -certstorelocation cert:\currentuser\my -DnsName $fqdn
$pwdSecure = ConvertTo-SecureString -String $pwd -Force -AsPlainText
$path = 'cert:\currentuser\my\' + $cert.Thumbprint
$cerFile = $location + "\\" + $fqdn + ".cer"
$pfxFile = $location + "\\" + $fqdn + ".pfx" 

Export-PfxCertificate -cert $path -FilePath $pfxFile -Password $pwdSecure
Export-Certificate -cert $path -FilePath $cerFile

Using OpenSSL to create a self-signed certificate

If you don’t have OpenSSL installed already, refer to the OpenSSL documentation for building and installation instruction. For Windows users, this StackOverflow discussion has some useful information on how to download OpenSSL for Windows.

1) Run the following openssl command to create a public key file (crt) and private key file (pem) with your info.  See https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm for some openssl reference guide.

openssl req-x509 -sha256 -days 365 -newkey rsa:2048 -keyout "C:\Users\path\privateKey.key" -out "C:\Users\path\certificate.crt" -subj '/C=your country/ST=your state/L=your locality/O=Your Company, Inc./OU=your Organizational Unit/CN=yourdomain.com'

2) convert the pem file to pfx file with your info:

openssl pkcs12 -export -out "C:\Users\path\certificate.pfx" -inkey "C:\Users\path\privateKey.key" -in "C:\Users\path\certificate.crt" -passout pass:your password -passin pass:your password

3) convert the crt file to DER encoded binary X.509 .cer file:

On Windows, double-click on the crt file to launch the certificate wizard. Go to ‘Details’ tab and click on ‘Copy to File…’ button:

Click ‘Next’ on the ‘Welcome to the Certificate Export Wizard’ page. Choose ‘DER encoded binary X.509 (cer)’ option and save to cer file.

Configuring the signing certificate using the MS Graph request

At this point we should have a pfx file, a cer file, and a pfx file password. We will use all of this information to construct the following MS Graph request:

PATCH https://graph.microsoft.com/v1.0/serviceprincipals/<Service Principal Object ID>

Request body:

Note: This is not a complete request body. For brevity, I am only emphasizing the main part related to private and public key.

{
    "keyCredentials":[
        {
	    ...
            "type": "AsymmetricX509Cert",
            "usage": "Sign",
            "key": Base64-encoded private key

        },
        {
	    ...
            "type": "AsymmetricX509Cert",
            "usage": "Verify",
            "key": Base64-encoded public key

        }
    ],
    "passwordCredentials": [
        {
	    ...
            "secretText": "password for the pfx file"
        }
    ]
}

Note: the passwordCredentials is required in the above request. Omitting it may result in the error “The value for the property \”usage\” in one of your credentials is invalid. Acceptable values are Sign, Verify.”

There is also a PowerShell script to generate the above json body from the certificate files located here.

Use the following PowerShell script with your info to send a PATCH request to configure the signing certificate for this SAML Application. The script will attempt to download MSAL.Net module if it does not already exist on the machine and then use MSAL.Net to sign in to Azure AD. You may need to run the following commands first to update both Nuget package and PowerShellGet.

## Update Nuget Package and PowerShellGet Module
Install-PackageProvider NuGet -Force
Install-Module PowerShellGet -Force

Note: this script will replace the current keyCredentials and passwordCredentials configured for this Service Principal. If you need to do an append operation, you will need to get the current selection first and append it to the payload.

$ClientID = "<client ID>"
$loginURL       = "https://login.microsoftonline.com"
$tenantdomain   = "<tenant>.onmicrosoft.com"
$redirectURL = "http://localhost" # this reply URL is needed for PowerShell Core 
[string[]] $Scopes = "https://graph.microsoft.com/.default"
$pfxpath = "C:\Users\path\certificate.pfx" # path to pfx file
$cerpath = "C:\Users\path\certificate.cer" # path to cer file
$SPOID = "<Service Principal Object ID>"
$graphuri = "https://graph.microsoft.com/v1.0/serviceprincipals/$SPOID"
$password = "<password for pfx file>"  # password for the pfx file
$CertDisplayName = "CN = <Your Cert Display Name>" # "CN=contoso"


# choose the correct folder name for MSAL based on PowerShell version 5.1 (.Net) or PowerShell Core (.Net Core)
# script is tested on both PS 5.1 and PS Core 7

if ($PSVersionTable.PSVersion.Major -gt 5)
    { 
        $core = $true
        $foldername =  "netcoreapp2.1"
    }
else 
    { 
        $core = $false
        $foldername = "net45"
    }


# Download MSAL.Net module to a local folder if it does not exist there
if ( ! (Get-ChildItem $HOME/MSAL/lib/Microsoft.Identity.Client.* -erroraction ignore) ) {
    install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.Identity.Client -Destination $HOME/MSAL/lib -force -forcebootstrap | out-null
}
 
# Load the MSAL assembly -- needed once per PowerShell session
[System.Reflection.Assembly]::LoadFrom((Get-ChildItem $HOME/MSAL/lib/Microsoft.Identity.Client.*/lib/$foldername/Microsoft.Identity.Client.dll).fullname) | out-null
 
$global:app = $null
 
$ClientApplicationBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientID)
[void]$ClientApplicationBuilder.WithAuthority($("$loginURL/$tenantdomain"))
[void]$ClientApplicationBuilder.WithRedirectUri($redirectURL)


$global:app = $ClientApplicationBuilder.Build()
 
Function Get-GraphAccessTokenFromMSAL {
    [Microsoft.Identity.Client.AuthenticationResult] $authResult  = $null
    $AquireTokenParameters = $global:app.AcquireTokenInteractive($Scopes)
    [IntPtr] $ParentWindow = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle
    if ($ParentWindow)
    {
        [void]$AquireTokenParameters.WithParentActivityOrWindow($ParentWindow)
    }
    try {
        $authResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult()
    }
    catch {
        $ErrorMessage = $_.Exception.Message
        Write-Host $ErrorMessage
    }
    
    return $authResult
}
 
$myvar = Get-GraphAccessTokenFromMSAL
if ($myvar)
{
    $GraphAccessToken = $myvar.AccessToken
    Write-Host "Access Token: " $myvar.AccessToken

    #  this is for PowerShell Core
    $Secure_String_Pwd = ConvertTo-SecureString $password -AsPlainText -Force

    # reading certificate files and creating Certificate Object
    if ($core)
    {
        $pfx_cert = get-content $pfxpath -AsByteStream -Raw
        $cer_cert = get-content $cerpath -AsByteStream -Raw
        $cert = Get-PfxCertificate -FilePath $pfxpath -Password $Secure_String_Pwd
    }
    else 
    {
        $pfx_cert = get-content $pfxpath -Encoding Byte
        $cer_cert = get-content $cerpath -Encoding Byte
        # Write-Host "Enter password for the pfx file..."
        # calling Get-PfxCertificate in PowerShell 5.1 prompts for password
        # $cert = Get-PfxCertificate -FilePath $pfxpath
        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxpath, $password)
    }

    # base 64 encode the private key and public key
    $base64pfx = [System.Convert]::ToBase64String($pfx_cert)
    $base64cer = [System.Convert]::ToBase64String($cer_cert)

    # getting id for the keyCredential object
    $guid1 = New-Guid
    $guid1 = $guid1.ToString()
    $guid2 = New-Guid
    $guid2 = $guid2.ToString()

    # get the custom key identifier from the certificate thumbprint:
    $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
    $hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($cert.Thumbprint))
    $customKeyIdentifier = [System.Convert]::ToBase64String($hash)

    # get end date and start date for our keycredentials
    $endDateTime = ($cert.NotAfter).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
    $startDateTime = ($cert.NotBefore).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )

    # building our json payload
    $object = [ordered]@{    
    keyCredentials = @(       
         [ordered]@{            
            customKeyIdentifier = $customKeyIdentifier
            endDateTime = $endDateTime
            keyId = $guid1
            startDateTime = $startDateTime  
            type = "AsymmetricX509Cert" 
            usage = "Sign"
            key = $base64pfx
            displayName = $CertDisplayName  
        },
        [ordered]@{            
            customKeyIdentifier = $customKeyIdentifier
            endDateTime = $endDateTime
            keyId = $guid2
            startDateTime = $startDateTime  
            type = "AsymmetricX509Cert" 
            usage = "Verify"
            key = $base64cer
            displayName = $CertDisplayName    
        }
        )  
    passwordCredentials = @(
        [ordered]@{
            customKeyIdentifier = $customKeyIdentifier
            keyId = $guid1            
            endDateTime = $endDateTime
            startDateTime = $startDateTime
            secretText = $password 
        }
    )
    }

    $json = $object | ConvertTo-Json -Depth 99
    Write-Host "JSON Payload:"
    Write-Output $json

    # Request Header
    $Header = @{}
    $Header.Add("Authorization","Bearer $($GraphAccessToken)")
    $Header.Add("Content-Type","application/json")

    try 
    {
        Invoke-RestMethod -Uri $graphuri -Method "PATCH" -Headers $Header -Body $json
    } 
    catch 
    {
        # Dig into the exception to get the Response details.
        # Note that value__ is not a typo.
        Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ 
        Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
    }

    Write-Host "Complete Request"
}
else 
{
    Write-Host "Fail to get Access Token"
}

If the script completes successfully we should see the thumbprint of our certificate showing up for this application:


Last Updated on 8/19/2022

Leave a Comment