-
Notifications
You must be signed in to change notification settings - Fork 0
Home
MS Graph addKey API can be used to add a new certficate to any application. The idea is to automate rolling application expiring keys via addKey or removeKey.
Source code can be found here
⛔ applications that don’t have any existing valid certificates (i.e.: no certificates have been added yet, or all certificates have expired), won’t be able to use this service action. Update application can be used to perform an update instead.
Important
! This service actions are mainly used to help apps to rotate their own keys and only supported for app-only flow.
- Valid certificate. (For the sake of this example, we will use a self-signed certificate)
- Consent to the needed permissions.
- Registered application.
- Rest API Client tool.
❗ Caution Using a self-signed certificate is only recommended for development, not production.
Option 1: Create and export your public certificate without a private key
$cert = New-SelfSignedCertificate -Subject "CN={certificateName}" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 ## Replace {certificateName}
Export-Certificate -Cert $cert -FilePath "C:\Users\admin\Desktop\{certificateName}.cer" ## Specify your preferred location and replace {certificateName}
Option 2: Create and export your public certificate with its private key
$cert = New-SelfSignedCertificate -Subject "CN={certificateName}" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 ## Replace {certificateName}
Export-Certificate -Cert $cert -FilePath "C:\Users\admin\Desktop\{certificateName}.cer" ## Specify your preferred location and replace {certificateName}
$mypwd = ConvertTo-SecureString -String "{myPassword}" -Force -AsPlainText ## Replace {myPassword}
Export-PfxCertificate -Cert $cert -FilePath "C:\Users\admin\Desktop\{privateKeyName}.pfx" -Password $mypwd ## Specify your preferred location and replace {privateKeyName}
In this example, we will generate accessToken for authentication via certificate credentials instead of client secret. To enable this, we need to generate a JSON Web Token (JWT) assertion signed with a certificate owned by the application.
To call the addKey API we need an accessToken signed via certificate and a proof of possession as explained in the request body
First, we will generate access_token with a certificate
- We need to create a signed jwt token (aka Client Assertion).
- Then, get an Access Token Using Client Credentials Grant Flow.
To compute the assertion, you can use one of the many JWT libraries in the language of your choice - MSAL supports this using .WithCertificate(). The information is carried by the token in its Header, Claims, and Signature.
In this tutorial, we will make use of the official assertion format documented here to generate client_assertion
.
The GenerateClientAssertion
method will generate client_assertion
public string GenerateClientAssertion(string aud, string clientId, X509Certificate2 signingCert, string tenantID)
{
Guid guid = Guid.NewGuid();
// aud and iss are the only required claims.
var claims = new Dictionary<string, object>()
{
{ "aud", aud },
{ "iss", clientId },
{ "sub", clientId },
{ "jti", guid}
};
// token validity should not be more than 10 minutes
var now = DateTime.UtcNow;
var securityTokenDescriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
{
Claims = claims,
NotBefore = now,
Expires = now.AddMinutes(10),
SigningCredentials = new X509SigningCredentials(signingCert)
};
var handler = new JsonWebTokenHandler();
// Get Client Assertion
var client_assertion = handler.CreateToken(securityTokenDescriptor);
return client_assertion;;
}
Then, the client_assertion
will be used to generate accessToken
using client credentials flow via GenerateAccessTokenWithClientAssertion
method.
public string GenerateAccessTokenWithClientAssertion(string aud, string client_assertion, string clientId, X509Certificate2 signingCert, string tenantID)
{
// GET ACCESS TOKEN
var data = new[]
{
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
new KeyValuePair<string, string>("client_assertion", client_assertion),
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"),
};
var client = new HttpClient();
var url = $"https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/token";
var res = client.PostAsync(url, new FormUrlEncodedContent(data)).GetAwaiter().GetResult();
var token = "";
using (HttpResponseMessage response = res)
{
response.EnsureSuccessStatusCode();
string responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
JObject obj = JObject.Parse(responseBody);
token = (string)obj["access_token"];
}
return token;
}
Now, the access token will be returned as documented here.
Second, we will generating proof of possession tokens for rolling keys
❗ Authentication_MissingOrMalformed error will be returned if PoP is not signed with the already uploaded certificate
You can refer to this official code sample to generate proof of possession token
public string GeneratePoPToken(string objectId, string aud, X509Certificate2 signingCert)
{
Guid guid = Guid.NewGuid();
// 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 Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
{
Claims = claims,
NotBefore = now,
Expires = now.AddMinutes(10),
SigningCredentials = new X509SigningCredentials(signingCert)
};
var handler = new JsonWebTokenHandler();
var poP = handler.CreateToken(securityTokenDescriptor);
// Console.WriteLine("\n\"Generate Proof of Possession Token:\"\n--------------------------------------------");
// Console.WriteLine($"PoP: {poP}");
return poP;
}
On successful code execution, the PoP will be returned.
Now, we have both access_token and PoP which will be used to call addKey API.
All the required properties have been added to the request body and a successful 200 OK
response code should be returned.
public HttpStatusCode AddKeyWithPassword(string poP, string objectId, string api, string accessToken)
{
var client = new HttpClient();
var url = $"{api}{objectId}/addKey";
var defaultRequestHeaders = client.DefaultRequestHeaders;
if (defaultRequestHeaders.Accept == null || !defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
// Get the new certificate info which will be uploaded via the graph API
string pfxFilePath = "cert which will be added via API call\\newCertToUpload.pfx";
string password = "Test@123";
X509Certificate2 CurrentCertUsed = new X509Certificate2(pfxFilePath, password);
var key = new Helper().GetCertificateKey(CurrentCertUsed);
var payload = new
{
keyCredential = new
{
type = "X509CertAndPassword",
usage = "Sign",
key,
},
passwordCredential = new
{
secretText = password,
},
proof = poP
};
var stringPayload = JsonConvert.SerializeObject(payload);
var httpContent = new StringContent(stringPayload, Encoding.UTF8, "application/json");
var res = client.PostAsync(url, httpContent).GetAwaiter().GetResult();
return res.StatusCode;
}
- Using
GetCertificateKey
method in theHelper
class as follow :
public string GetCertificateKey(X509Certificate2 cert)
{
return Convert.ToBase64String(cert.GetRawCertData());
}
On successful code execution a
Uploaded!
message will be printed out to indicate a successful200 OK
response code returned from the API: (see below example)
To confirm the existent of the newly added certificate, we can do a
GET
request to the following Graph API, see below screenshots.
https://graph.microsoft.com/v1.0/applications/{Object ID}
- Using RemoveKey method located here to directly call the API.
- Or RemoveKey_GraphSDK to utilize Graph SDK instead.
Also you can utilize the same code to call
service principal
API instead ofapplication
API to upload a certificate via replacing the following methods.
- Open the
appsettings.json
file.- Find the app key
ApiUrl
and replacehttps://graph.microsoft.com/v1.0/applications/
withhttps://graph.microsoft.com/v1.0/servicePrincipals/
.- For Graph SDK, change the methods inside
GraphSDK.cs
fromgraphClient.Applications[objectId]
tographClient.ServicePrincipals[objectId]
as documented here.
REF: servicePrincipal: addKey Generate proof of possession tokens for rolling keys