I am trying to use the addKey
method to add a certificate to an App Registration via Graph API. This requires you to generate a proof of ownership of an existing certificate that’s present in the App Registration by creating a JWT token signed with that cert. The token should contain the following claims:
aud
– Audience needs to be00000002-0000-0000-c000-000000000000
.iss
– Issuer needs to be the Azure AD ObjectId of the application that is making the call (not the applicationId or clientId).nbf
– Not before time.exp
– Expiration time should be “nbf” + 10 mins.
And there’s some sample code to generate this at this document. The code, however is in C#, but I want to generate the proof as part of my PowerShell code so that’s not entirely helpful.
Here’s the code from the document btw in case it changes/ goes down:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.JsonWebTokens; namespace MicrosoftIdentityPlatformProofTokenGenerator { class Program { static void Main(string[] args) { // Configure the following string pfxFilePath = "<Path to your certificate file"; string password = "<Certificate password>"; string objectId = "<id of the application or servicePrincipal object>"; // Get signing certificate X509Certificate2 signingCert = new X509Certificate2(pfxFilePath, password); // audience string aud = $"00000002-0000-0000-c000-000000000000"; // aud and iss are the only required claims. var claims = new Dictionary<string, object>() { { "aud", aud }, { "iss", objectId } }; // token validity should not be more than 10 minutes var now = DateTime.UtcNow; var securityTokenDescriptor = new SecurityTokenDescriptor { Claims = claims, NotBefore = now, Expires = now.AddMinutes(10), SigningCredentials = new X509SigningCredentials(signingCert) }; var handler = new JsonWebTokenHandler(); var x = handler.CreateToken(securityTokenDescriptor); Console.WriteLine(x); } } } |
Turns out there’s a PowerShell module someone’s helpfully created that can create JWT tokens. Using it is straight-forward too:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Install the module Install-Module JWT # Create a hash table of the claims $proof = @{ "aud" = "00000002-0000-0000-c000-000000000000" "iss" = "2c71b413-c365-4dd2-8c8f-375d5f56c59f" "nbf" = [datetime]::now "exp" = [datetime]::now.AddMinutes(10) } # Read in the cert $certName = "xxx" if ($IsWindows) { $pathSeparator = "\" } else { $pathSeparator = "/" } $cert = Get-PfxCertificate "$($Global:CertPath)${pathSeparator}${certName}" # Generate the signed token $token = New-Jwt -Cert $cert -PayloadJson (ConvertTo-Json $proof) |
The end-result is a token like the following:
1 |
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJpc3MiOiAiMmM3MWI0MTMtYzM2NS00ZGQyLThjOGYtMzc1ZDVmNTZjNTlmIiwKICAibmJmIjogIjIwMjEtMTAtMTFUMTA6Mjk6MzIuNjYwMzM5KzAxOjAwIiwKICAiZXhwIjogIjIwMjEtMTAtMTFUMTA6Mzk6MzIuNjYwMzQ0KzAxOjAwIiwKICAiYXVkIjogIjAwMDAwMDAyLTAwMDAtMDAwMC1jMDAwLTAwMDAwMDAwMDAwMCIKfQ.QfeTo1mu4EzjRHkscrJExEstpk9XkcmU87FFfVqz2eqf7q-Qep062pQdj0CXN17ZK9DyjnugfG3F6Neg2wZa2LhBtDgkYG_Hl0EJ1kHHaz60jfEjIKtsP54T97y-fAyf5oRLHNM1RbNIhDFp_7f4_FB4lUztK5luH_RcyMoxRgfQhc0f0IfmPcyv6qo2n9V_eSnpO1KBQwzehwZR6diruyYMdDJbB3KMwKOp8-FXQQd6NlXbXUaspGcNWkpdBMMU7tbM7FBdRLj2n0c3jdnYCNdYMWx4nZ8XXZUTKp-hSQwAEAkxF-rN9EPEzDzKs3GB60Z08smblSnA2cMZMK8k2YJA59gYO-ad4_fSmdPGLghYWpdJzfm7dRsfK0giNJNK-CJMhJo553wp9fHhJ1dBe8UObvRrutku_UHbH0XTNjAXZNJq2VwWuh6SFqGmRTZi5ilOR5NkvzHgNEQPt1IHDl-mr-LmcnR6kOhpCAWxpeM9rv2Q_U39X1L1LKDcc_oeZ-dOcqaAB4RzQaECPU4Q6FnmEe69kcIb-dPNuZdkRY9MLhuxptHK58o53bDsf7lzf9YpKYBMYYVubkt9AqMxqKnBBtwXlMPC1NJpCtsrQ5BdzEPabUTjiG6xubCDysHkl7Z6kLLnt-pbjzDKRgxdWyT8-AAEwQ0Jg2EtksnSogA |
Which has the following claims correctly set (I can view this using a site such as https://jwt.ms/):
1 2 3 4 5 6 7 8 9 |
{ "alg": "RS256", "typ": "JWT" }.{ "iss": "2c71b413-c365-4dd2-8c8f-375d5f56c59f", "nbf": "2021-10-11T10:29:32.660339+01:00", "exp": "2021-10-11T10:39:32.660344+01:00", "aud": "00000002-0000-0000-c000-000000000000" }.[Signature] |
Great!
However, I wanted to see if I could use the official C# version itself in PowerShell. I know you can use a lot of C# in PowerShell as the latter is based on .NET. I use PowerShell Core on macOS mainly, so hopefully all the .NET classes above were present in .NET Core too (which is what PowerShell Core is based on). Luckily they are!
The three classes I need are System.Security.Cryptography.X509Certificates
, Microsoft.IdentityModel.Tokens
, and Microsoft.IdentityModel.JsonWebTokens
. And turns out I can just create new objects in PowerShell based on these classes and do the same as the C# code.
Here’s what I came up with through a fair bit of trial and error yesterday:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$signingCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$($Global:CertPath)${pathSeparator}${certName}", "$certPassword") $signingCreds = New-Object Microsoft.IdentityModel.Tokens.X509SigningCredentials($signingCert) # Thanks https://stackoverflow.com/questions/37160534/how-to-declare-a-system-collections-generic-idictionary-in-powershell $claimsDict = New-Object System.Collections.Generic.Dictionary"[String,Object]" $claimsDict."aud" = "00000002-0000-0000-c000-000000000000" $claimsDict."iss" = "9db7dfa6-4f4c-430b-a573-d64d5dc172fd" $securityToken = New-Object Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor $securityToken.claims = $claimsDict $securityToken.NotBefore = [datetime]::now $securityToken.Expires = [datetime]::now.AddMinutes(10) $securityToken.SigningCredentials = $signingCreds $handler = New-Object Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler $token = $handler.CreateToken($securityToken) |
I had to Google a bit on creating the IDictionary object as everything pointed me towards using a hash table instead, but that didn’t do the trick.
Also when it came to certificates initially I was doing something along these lines:
1 2 3 |
$signingCert.Import("$($Global:CertPath)${pathSeparator}${certName}", "$certPassword", [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) MethodInvocationException: Exception calling "Import" with "3" argument(s): "X509Certificate is immutable on this platform. Use the equivalent constructor instead." |
But that gave the above error until I realized rather than create the object and then instantiate it with an import()
I should just create the object with the certificate. Thanks to this blog post for pointing me to that.
The above PowerShell code successfully generates a JWT token and it looks similar to the one generated by New-JWT
so I guess I am doing things alright.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "alg": "RS256", "kid": "6E8E784FCFFDD2C69E27D468C8B814E7C4ED808B", "typ": "JWT", "x5t": "bo54T8_90saeJ9RoyLgU58TtgIs" }.{ "aud": "00000002-0000-0000-c000-000000000000", "iss": "9db7dfa6-4f4c-430b-a573-d64d5dc172fd", "exp": 1633982021, "nbf": 1633981421, "iat": 1633981422 }.[Signature] |
If anything it also includes the thumbprint of the certificate used to sign the token (the kid
parameter) so that’s better I guess.