{"id":603,"date":"2024-03-30T22:23:15","date_gmt":"2024-03-30T21:23:15","guid":{"rendered":"https:\/\/sparrow365.de\/?p=603"},"modified":"2025-02-12T18:39:17","modified_gmt":"2025-02-12T17:39:17","slug":"going-loopy-rotating-entra-id-app-certificates-with-powershell","status":"publish","type":"post","link":"https:\/\/sparrow365.de\/index.php\/en\/2024\/03\/30\/going-loopy-rotating-entra-id-app-certificates-with-powershell\/","title":{"rendered":"Going Loopy rotating Entra ID App Certificates with PowerShell"},"content":{"rendered":"<p>While working on my <a href=\"https:\/\/sparrow365.de\/index.php\/en\/2024\/03\/04\/you-probably-dont-need-application-readwrite-all\/\">appeal against <em>Application.ReadWrite.All<\/em><\/a>, I stumbled upon a potential way to rotate an application&#8217;s authentication certificate without Graph API permissions. After some experimentation, I was unable to get it running in PowerShell, so I set it aside for the time being to finish the article. However, the topic continued to intrigue me, as this method would have several advantages:<\/p>\n<ul>\n<li>Application managers would have a quick self-service option to change an application&#8217;s certificate<\/li>\n<li>Automations could replace a certificate shortly before its expiration, without user interaction<\/li>\n<li>I would have another reason to grant, and use, fewer additional permissions<\/li>\n<\/ul>\n<blockquote>\n<p>&quot;Authentication certificate&quot; refers to the credentials of the App Registration &#8211; not SAML Signing Certificates, which are used for Single Sign-On!<\/p>\n<\/blockquote>\n<p>For these reasons, I set out to write this article:<br \/>\nBoth as a guide for those who should, or would like to, use this method (<a href=\"https:\/\/sparrow365.de\/index.php\/en\/2024\/03\/30\/going-loopy-rotating-entra-id-app-certificates-with-powershell\/#toc-6\"><strong>see the result<\/strong><\/a>), as well as hopefully offering an interesting insight into challenges with Microsoft documentation and PowerShell troubleshooting. \ud83d\ude01<\/p>\n<p><br class=\"\"><\/p>\n<h2>Challenge: Microsoft&#8217;s Code Suggestion for Proof of Possession<\/h2>\n<p>Adding certificate credentials to an App Registration via Graph API is nothing special; this part of the process is <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/applications-how-to-add-certificate?tabs=http\">well documented<\/a>. The real challenge lies in the peculiarity of the addKey endpoint &#8211; proving to the application that we currently have a valid certificate.<\/p>\n<p>Fundamentally, this authentication process also relies on a JSON Web Token (JWT), which must first be constructed. Fortunately, Microsoft also offers <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/application-rollkey-prooftoken?tabs=powershell#prerequisite\">documentation of this process<\/a>. However, it quickly becomes clear that their PowerShell snippet is not optimal &#8211; usually, it will only lead to error messages when executed.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/2_JustExecute.png\" alt=\"ExecutionError\" \/><\/p>\n<blockquote>\n<p><em>Unable to find type [&#8230;]<\/em><\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<blockquote>\n<p>&quot;Short&quot; Primer:<\/p>\n<ul>\n<li><strong>.NET Assemblies<\/strong> are the compiled code libraries in .NET, mostly as .dll files, which contain metadata and <strong>.NET Types<\/strong>. They facilitate code reuse and support versioning<\/li>\n<li><strong>.NET Types<\/strong> represent data structures or methods in the code. They make it easier to represent objects and processes and contribute to simplifying programming<\/li>\n<\/ul>\n<p>Example: The <strong>Assembly<\/strong> <em>Microsoft.IdentityModel.JsonWebTokens<\/em> includes, among other things, the <strong>Type<\/strong> [Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler], which provides a series of methods and properties to simplify the creation, validation, and processing of JWTs<br \/>\nBy using this Type, developers can implement more complex processes around JWTs &#8211; without having to dive deep into the details of token creation and validation<\/p>\n<\/blockquote>\n<p>The issue arises from the use of certain .NET Types that are not included in the <em>Microsoft.Graph.Authentication<\/em> module or the PowerShell environment. It&#8217;s unclear to me where these Types are supposed to come from, as I couldn&#8217;t find the associated assemblies in any of the usual PowerShell modules. This leaves me to suspect that the code was translated from C# to PowerShell 1:1, without verifying its executability for the average user. I am very interested in other explanations. \ud83d\ude09<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/1_MSSnippet-e1711638684250.png\" alt=\"SnippetExample\" \/><\/p>\n<p>At this point, I could have started installing and loading the required assemblies. However, I prefer functions that are immediately executable on a wider range of systems, even if it makes the code seem more complex. Additionally, working with NuGet, the package manager for .NET libraries, is really not the best in PowerShell, as we will see later in the article.<\/p>\n<p><br class=\"\"><\/p>\n<h2>My Integration Attempts<\/h2>\n<blockquote>\n<p><strong>Important: the code snippets in the current chapter are primarily for illustrating the development process, and do not represent a complete script<\/strong><br \/>\nThe full source code can be found at the <a href=\"https:\/\/sparrow365.de\/index.php\/en\/2024\/03\/30\/going-loopy-rotating-entra-id-app-certificates-with-powershell\/#toc-6\">end of this article<\/a><\/p>\n<\/blockquote>\n<p>While implementing <a href=\"https:\/\/sparrow365.de\/index.php\/en\/2024\/01\/28\/connect-mggraph-with-username-and-password\/\">Graph API authentication with username + password<\/a>, I already successfully relied on the <a href=\"https:\/\/adamtheautomator.com\/powershell-graph-api\/#Acquire_an_Access_Token_Using_a_Certificate\">work of Adam The Automator<\/a> when constructing JWTs, so I adapted it for this use case as well.<\/p>\n<p>My goal now is to transfer the missing Types from the Microsoft example into generally available code:<\/p>\n<p><br class=\"\"><\/p>\n<h3>Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor<\/h3>\n<p>We start with this Code from the Microsoft Dokumentation:<\/p>\n<pre><code class=\"language-powershell\"># aud and iss are the only required claims.\n$claims = [System.Collections.Generic.Dictionary[string,object]]::new()\n$claims.aud = $aud\n$claims.iss = $objectId # In my case $clientID\n\n$now = (Get-Date).ToUniversalTime()\n$securityTokenDescriptor = [Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor]@{\n    Claims = $claims\n    NotBefore = $now\n    Expires = $now.AddMinutes(10)\n}<\/code><\/pre>\n<p>So this Type is used to define the contents of the token. There are several points that are dictated by the standard and do not need to be explicitly listed &#8211; useful if you want cleaner code, but a hindrance if you&#8217;re trying to recreate it. Ultimately, my equivalent looked as follows:<\/p>\n<pre><code class=\"language-powershell\">$StartDate = (Get-Date &quot;1970-01-01T00:00:00Z&quot;).ToUniversalTime()\n$now = (Get-Date).ToUniversalTime()\n# Create JWT timestamp for expiration\n$JWTExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now.AddMinutes(2) ).TotalSeconds\n$JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)\n\n# Create JWT timestamp for beginning of validity\n$NotBeforeExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now ).TotalSeconds\n$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)\n\n# Create JWT header\n$JWTHeader = @{\n    alg = &quot;RS256&quot;\n    typ = &quot;JWT&quot;\n    x5t = $CertificateBase64Hash \n}\n\n# Create JWT payload\n$JWTPayLoad = @{\n    aud = &quot;00000002-0000-0000-c000-000000000000&quot;\n    exp = $JWTExpiration\n    iss = $clientID\n    jti = [guid]::NewGuid()\n    nbf = $NotBefore\n    sub = $clientID\n}<\/code><\/pre>\n<blockquote>\n<p><em>Upon closer inspection, you might recognize the Application ID of the now-discontinued &quot;Azure AD Graph&quot; API as the Audience (aud) of the claim, <strong>this is necessary<\/strong><\/em><\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<p>By using the assemblies you save the effort of crafting the header and converting time formats. While this doesn&#8217;t represent a dramatic upside yet, it becomes evident that code becomes shorter when utilizing libraries \u2013 shocking, I know. Our next example will illustrate this even clearer.<\/p>\n<p><br class=\"\"><\/p>\n<h3>Microsoft.IdentityModel.Tokens.X509SigningCredentials and Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler<\/h3>\n<p>The replacement of the next two Types is handled together because &quot;X509SigningCredentials&quot; only represents the form of the certificate that the JsonWebTokenHandler uses to construct the final JWT. Here most of the work is taken off the developer&#8217;s hands &#8211; in the original, we have only two lines:<\/p>\n<pre><code class=\"language-powershell\">$handler = [Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler]::new()\n$token = $handler.CreateToken($securityTokenDescriptor)<\/code><\/pre>\n<p>From the code, we can&#8217;t really learn what&#8217;s being used in the background &#8211; thankfully, we&#8217;ve already got the groundwork from <a href=\"https:\/\/adamtheautomator.com\/powershell-graph-api\/#Acquire_an_Access_Token_Using_a_Certificate\">Adam the Automator<\/a>. So, we know that the header and payload need to be base64 encoded and cryptographically signed.<\/p>\n<pre><code class=\"language-powershell\"># Convert header and payload to base64\n$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) \n\n$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)\n\n$JWT = $EncodedHeader + &quot;.&quot; + $EncodedPayload\n\n# Define RSA signature and hashing algorithm\n$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1\n$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256\n\n# Sign the JWT\n$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)\n$Signature = [Convert]::ToBase64String(\n    $rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)\n) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n# Add Signature to JWT\n$JWT = $JWT + &quot;.&quot; + $Signature<\/code><\/pre>\n<p>TaDa &#8211; we now have our Credential Token, or &quot;Proof&quot;.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/JWT.png\" alt=\"JWT\" \/><\/p>\n<blockquote>\n<p>And this is where I initially overlooked something critical. The <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/application-rollkey-prooftoken?tabs=powershell#prerequisite\">Microsoft documentation<\/a> clearly states what needs to be considered &#8211; Pause here to see if you can spot the issue. \ud83d\ude09<\/p>\n<\/blockquote>\n<p><br class=\"\">  <\/p>\n<p>Everything seems reasonable, but this error would haunt me for the next few hours:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/ErrorMessage.png\" alt=\"errorMessage\" \/><\/p>\n<blockquote>\n<p><strong><em>Authentication_MissingOrMalformed<\/em><\/strong><\/p>\n<\/blockquote>\n<p><br class=\"\"> <\/p>\n<h2>Temporary Surrender<\/h2>\n<p>After many unsuccessful attempts, including experiments with the claims in the header and the signature, I had enough. A few days&#8216; break brought a new perspective, and I decided to get the Microsoft example working &#8211; to obtain comparative values.<\/p>\n<p>First, we need the .NET assemblies &#8211; these can be obtained from the NuGet Package Manager from the NuGet Package Source, both often not pre-installed.<br \/>\n<img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/0_InstallNuget-1.png\" alt=\"CheckNugetAvailability\" \/><\/p>\n<blockquote>\n<p>In brief, as I am not a fan of this path &#8211; it can lead to manifold problems:<\/p>\n<ul>\n<li>Under PowerShell 7 (pwsh \/ Core), I only had to add the repository: <code>Register-PackageSource -Name &quot;NuGet&quot; -Location &quot;https:\/\/api.nuget.org\/v3\/index.json&quot; -ProviderName NuGet<\/code><\/li>\n<li>The Package Provider can be added if necessary with <code>Find-PackageProvider -Name NuGet | Install-PackageProvider -Scope CurrentUser -Force<\/code><\/li>\n<li>The v3 repository cannot be added under older PowerShell versions &#8211; alternatively <code>Register-PackageSource -Name &quot;NuGetOld&quot; -Location &quot;https:\/\/www.nuget.org\/api\/v2\/&quot; -ProviderName NuGet<\/code><\/li>\n<\/ul>\n<\/blockquote>\n<p>Adding the provider and the repository does not conclude the extra work &#8211; NuGet in PowerShell is known for causing circular references when resolving dependencies within the assemblies. Thus, the installation runs indefinitely &#8211; or, after a while, you might encounter the error <code>Dependency loop detected for package ...<\/code><\/p>\n<p>Therefore, we must install the packages with <code>-SkipDependencies<\/code> &#8211; and then for every subsequent <code>Type not Found<\/code> error, seek the associated assembly.<br \/>\nSkipping forward a bit, the dependencies are limited; we need:<\/p>\n<ul>\n<li>Microsoft.IdentityModel.JsonWebTokens<\/li>\n<li>Microsoft.IdentityModel.Logging<\/li>\n<li>Microsoft.IdentityModel.Tokens<\/li>\n<\/ul>\n<p>All other dependencies are already covered by the Microsoft.Graph.Authentication module or the global catalog.<\/p>\n<p>To install and then load the assemblies, I wrote the following helper code:<\/p>\n<pre><code class=\"language-powershell\"># Change to your liking, be aware that this will create directories\n$destination = &quot;$env:OneDrive\\Dokumente\\WindowsPowerShell\\Modules\\&quot;\n\n# Versions are relevant, since they might have specific dependencies\n$packages = @{\n&quot;Microsoft.IdentityModel.JsonWebTokens&quot; = &quot;7.3.1&quot;\n&quot;Microsoft.IdentityModel.Logging&quot; = &quot;7.5.0&quot;\n&quot;Microsoft.IdentityModel.Tokens&quot; = &quot;7.5.0&quot;\n}\n\n# Install \/ Load Packages\nforeach ($p in $packages.keys){ \n    $dllPath = $destination + &quot;\\$p.$($packages.$p)\\lib\\netstandard2.0\\$p.dll&quot;\n    if ( -not (Test-Path $dllPath) ){\n        Install-Package -Name $p -ProviderName NuGet -Scope CurrentUser -Destination $destination -RequiredVersion $packages.$p -Force -SkipDependencies\n    }    \n    [System.Reflection.Assembly]::LoadFrom($dllPath) | Out-Null\n}<\/code><\/pre>\n<blockquote>\n<p><strong>!Caution!<\/strong>: The assemblies must be reloaded each session &#8211; I only want to use them for troubleshooting, not permanently<\/p>\n<\/blockquote>\n<p>After this preparation, the <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/application-rollkey-prooftoken?tabs=powershell#prerequisite\">Microsoft example<\/a> works, and we obtain a valid token (parsed via <a href=\"https:\/\/jwt.ms\">https:\/\/jwt.ms<\/a>):<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/MSToken.png\" alt=\"MicrosoftToken\" \/><\/p>\n<p><br class=\"\"><\/p>\n<h2>The Ah-Hah Moment<\/h2>\n<p>After I had completed the necessary groundwork, I wanted to verify that everything generally works on a new PC. &quot;Works on my machine&quot; does not fit my philosophy. When copying the code snippet fresh from the Microsoft documentation again, I first consciously read a sentence that is at the very end of the article:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/DERSatz.png\" alt=\"CriticalSentence\" \/><\/p>\n<p>In my defense &#8211; I had stopped reading at &quot;Azure KeyVault&quot; at the beginning, as it seemed irrelevant to me. However, the listed error message &quot;Authentication_MissingOrMalformed&quot; was burned into my eyes by that point, so I could not miss it.<\/p>\n<p>Let`s take another look at the Token my Code generates:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/JWTHighlight.png\" alt=\"IssueHighlighted\" \/><\/p>\n<p>Huh. Sometimes, I wish these mistakes wouldn&#8217;t look so ridiculously simple in hindsight. \ud83e\udd26 If anyone discovered (or not) the mistake when I mentioned it earlier in the article, I&#8217;d love to hear about it.<\/p>\n<p>So, all that was missing was removing the &quot;=&quot; sign when we convert our header and payload to Base64:<\/p>\n<pre><code class=\"language-powershell\">#...\n$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) -replace &#039;=&#039;\n\n$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) -replace &#039;=&#039;\n#...<\/code><\/pre>\n<p>At least the NuGet detour wasn&#8217;t for nought &#8211; now that I had a Microsoft example of the token, I could match both the header and the claims. Besides these adjustments, I&#8217;ve cleaned up the code and turned the generation of the prooftoken into a function.<\/p>\n<p><br class=\"\"><\/p>\n<h2>The Result<\/h2>\n<p>After a considerable detour (or not, if you&#8217;ve jumped straight to the result \ud83d\ude09) \u2013 the process now looks like this when I update the certificate of an Enterprise App:<\/p>\n<h3>Function for Prooftokens<\/h3>\n<p>This part is the most extensive and needs to be used twice, so functionalization is the word:<\/p>\n<blockquote>\n<p>I will keep updates in my <a href=\"https:\/\/gist.github.com\/dreadsend\/b257928af29f8827259f435104986a01\">GitHub gist<\/a><\/p>\n<\/blockquote>\n<pre><code class=\"language-powershell\">function New-Prooftoken {\n    param (\n        [Parameter(Mandatory = $true)]\n        [string]$clientId,\n\n        [Parameter(Mandatory = $true)]\n        [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert\n    )\n\n    # Very far removed, but still based on https:\/\/adamtheautomator.com\/powershell-graph-api\/\n\n    # Get base64 hash of certificate in Web Encoding\n    $CertificateBase64Hash = [System.Convert]::ToBase64String( $cert.GetCertHash() ) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n    $StartDate = (Get-Date &quot;1970-01-01T00:00:00Z&quot;).ToUniversalTime()\n    $now = (Get-Date).ToUniversalTime()\n\n    # Create JWT timestamp for expiration - 5 Minute Lifetime here\n    $JWTExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now.AddMinutes(5) ).TotalSeconds\n    $JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)\n\n    # Create JWT validity start timestamp\n    $NotBeforeExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now ).TotalSeconds\n    $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)\n\n    # Create JWT header\n    $JWTHeader = @{\n        alg = &quot;RS256&quot;\n        typ = &quot;JWT&quot;\n        x5t = $CertificateBase64Hash \n        kid = $cert.Thumbprint\n    }\n\n    # Create JWT payload\n    $JWTPayLoad = @{\n        aud = &quot;00000002-0000-0000-c000-000000000000&quot;\n        exp = $JWTExpiration\n        iss = $clientID\n        nbf = $NotBefore\n        iat = $NotBefore\n    }\n\n    # Convert header and payload to base64\n    $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n    $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) -replace &#039;=&#039;\n\n    $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n    $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) -replace &#039;=&#039;\n\n    $JWT = $EncodedHeader + &quot;.&quot; + $EncodedPayload\n\n    # Define RSA signature and hashing algorithm\n    $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1\n    $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256\n\n    # Sign the JWT\n    $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)\n    $Signature = [Convert]::ToBase64String(\n        $rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)\n    ) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n    # Add Signature to JWT\n    $JWT = $JWT + &quot;.&quot; + $Signature\n\n    return ConvertTo-SecureString $JWT -AsPlainText -Force \n\n    end {\n        # Clear Senstive Values\n        $sensitiveVars = @(&quot;Signature&quot;,&quot;JWT&quot;)\n        Remove-Variable $sensitiveVars\n        [gc]::collect()\n    }\n}<\/code><\/pre>\n<h3>Rotating the Certificate<\/h3>\n<p>In summary, we do the following:<\/p>\n<ol>\n<li>Load the certificates<\/li>\n<li>Connect to the Enterprise App<\/li>\n<li>Add the new certificate<\/li>\n<li><em>[Switch the certificates in the application that uses the Enterprise App!]<\/em><\/li>\n<li>Reconnect for safety<\/li>\n<li>Remove the old certificate<\/li>\n<\/ol>\n<p><strong>None of this requires additional permissions on the Enterprise App!<\/strong><\/p>\n<pre><code class=\"language-powershell\">###### Configure Here #####\n\n# Load Certificates, Alternative: Get-PfxCertificate for .cer Files\n$oldcert = Get-Item &quot;Cert:\\CurrentUser\\My\\$($graphConfig.thumb)&quot;\n$newCert = Get-Item &quot;Cert:\\CurrentUser\\My\\$($graphConfig.newThumb)&quot;\n\n# Replace to your Values\n$clientID = $graphconfig.clientID\n$tenantID = $graphconfig.tenantID\n\n###########################\n\nConnect-MgGraph -TenantId $tenantID -ClientId $clientID -Certificate $oldCert -NoWelcome\n\n# Add new Certificate, store information to Rotate\n$proof = New-Prooftoken -clientId $clientID -cert $oldcert\n$params = @{\n    keyCredential      = @{\n        type  = &quot;AsymmetricX509Cert&quot;\n        usage = &quot;Verify&quot;\n        key   = [convert]::ToBase64String($newCert.GetRawCertData())\n    }\n    passwordCredential = $null\n    proof              = [System.Net.NetworkCredential]::new(&#039;&#039;,$proof).Password\n}\n$newKeyCredential = Invoke-MgGraphRequest POST &quot;\/v1.0\/applications(appId=&#039;$clientID&#039;)\/addKey&quot; -Body $params -OutputType PSObject\n\n# Reconnect to make sure that the new certificate is loaded\nDisconnect-MgGraph | Out-Null\nConnect-MgGraph -TenantId $tenantID -ClientId $clientID -Certificate $newCert -NoWelcome\n\n# Rotate the Certificates in your Application NOW ;)\n\n# Read all currently Registered Certificate Credentials, Filter to only the old Credentials\n$currentKeys = Invoke-MgGraphRequest GET &quot;\/v1.0\/applications(appId=&#039;$clientID&#039;)?`$select=id,appId,keyCredentials&quot; -OutputType PSObject\n$oldKeys = $currentKeys.keyCredentials | Where-Object keyId -ne $newKeyCredential.keyId\n\n# Remove the old Certificate(s)\nforeach ($k in $oldKeys){\n    $proof = New-Prooftoken -clientId $clientID -cert $newcert\n    $params = @{\n        keyId = $oldkeys.keyId\n        proof = [System.Net.NetworkCredential]::new(&#039;&#039;,$proof).Password\n    }\n    $null = Invoke-MgGraphRequest POST &quot;\/v1.0\/applications(appId=&#039;$clientID&#039;)\/removeKey&quot; -Body $params\n}\n\nDisconnect-MgGraph | Out-Null\nRemove-Variable @(&quot;params&quot;, &quot;proof&quot;)\n[gc]::collect()<\/code><\/pre>\n<p><br class=\"\"><\/p>\n<h2>In Review<\/h2>\n<p>This topic was extensive and at times very challenging, yet there are few things as satisfying as the working result of hard work. Have you gained new insights or were you able to simplify your credential management? Let me know.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>While working on my appeal against Application.ReadWrite.All, I stumbled upon a potential way to rotate an application&#8217;s authentication certificate without Graph API permissions. After some experimentation, I was unable to get it running in PowerShell, so I set it aside for the time being to finish the article. However, the topic continued to intrigue me,&#8230; &raquo; <a class=\"read-more-link\" href=\"https:\/\/sparrow365.de\/index.php\/en\/2024\/03\/30\/going-loopy-rotating-entra-id-app-certificates-with-powershell\/\">weiterlesen<\/a><\/p>\n","protected":false},"author":2,"featured_media":595,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[76,78],"tags":[80,84,165,88,90,96,98,228],"class_list":["post-603","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-me-id-en","category-powershell-en","tag-aad-en","tag-azure-ad-en","tag-best-practices-en","tag-entra-en","tag-entra-id-en","tag-graph-en","tag-graph-api-en","tag-least-privilege"],"_links":{"self":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/603","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/comments?post=603"}],"version-history":[{"count":5,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/603\/revisions"}],"predecessor-version":[{"id":928,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/603\/revisions\/928"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media\/595"}],"wp:attachment":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media?parent=603"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/categories?post=603"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/tags?post=603"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}