Now this is something different and very specific. So I am writing this series on Sharepoint and how to integrate it from the Dynamics Backend. And Sharepoint Online has a strange restriction for the current state-of-the-art Azure AD App-Only authentication. It only works if you use a certificate and not with a secret.
Authenticating with a Secret
Let’s first start with this rather easy method. There are lots of resources on the internet covering this, but it’s still important to cover it as a base because a lot of concepts will be similar. Of course, we will be doing it without a NuGet.
Essentially we just need to send a POST request to https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/token with a form content. The form content has the following 4 parameters:
"grant_type": "client_credentials"
"client_id": {yourappid}
"client_secret": {yourappsecret}
"scope": {whereyouareloggininto}/.default
TenantId, AppId and AppSecret can be retrieved from the AppRegistration you created.
NOTE: Lost here? Check the section “The Portal Way” of this article for some pictures and descriptions on how to create an app registration and where the Secrets section is.
The scope might be a little more difficult. Usually, it’s the URL of the app you are trying to log in. So for Dynamics this is something like XXX.crm.dynamics.com and for Sharepoint, it is the root URL of Sharepoint. So for example I am trying to log in to the site https://crm553494.sharepoint.com/sites/CRM that means my scope is https://crm553494.sharepoint.com/.default. Don’t be fooled here, just because you authenticate to the whole Sharepoint does not mean you are authorized for all sites within it. And remember, this won’t work due to the restriction that only authentication with a certificate is allowed with Sharepoint!
What you will get back is a JSON object that contains a property “access_token” that you will pass onto the real application in the header “Authorization” and the format “Bearer {access_token}”.
Now as C#:
public string PerformSPRequest(string tenantId, string resource, string clientId, string secret, string site) {
string token;
var form = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "client_secret", secret },
{ "scope", $"{resource}/.default" }
};
FormUrlEncodedContent formContent = new FormUrlEncodedContent(form);
var url = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"
using (var aadClient = new HttpClient()) {
var response = aadClient.PostAsync(url, formContent).Result;
var responseContent = response.Content.ReadAsStringAsync().Result;
response.EnsureSuccessStatusCode();
token = new JavaScriptSerializer().Deserialize<AzureActiveDirectoryToken>(tokenString).access_token;
}
using (var spClient = new HttpClient()) {
spClient.DefaultRequestHeaders.Authorization = $"Bearer {token}";
var content = "..."
var spUrl = $"{resource}/sites/{site}";
var spResponse = spClient.PostAsync(spUrl, content).Result;
}
}
public class AzureActiveDirectoryToken
{
public string access_token { get; set; }
}
NOTE: As already mentioned, Sharepoint will reject the token due to it being created with a secret.
Debugging a token
This might become relevant when you are questioning why a token does not work. Paste your token to the website jwt.ms
So a token consists of a header (red), a body (blue) and a footer/signature (green). The header is general information like that we are dealing with a JWT token and that RSA256 was used as an encryption algorithm. The body contains all the relevant information, like for whom (sub) and by whom (iss) the token was issued, how long is it valid (exp) etc. and so on. The footer is a signature of the body. This makes sure no one alters the body, for example extending the validity of an expired token. If you then check the signature against the body it would be invalid. And in the end, all 3 parts are encoded as Base64 and split with dots, this is why they are so easy to analyze since the body is just a Base64 encoded JSON.
Also, check the claims tab, this will explain to you how Sharepoint knows this is a token generated by a secret: The property “appidacr” gives it away!
Generating a certificate token with a NuGet
There are multiple that can do it, but the NuGet Microsoft.Identity.Client
is the current official one by Microsoft.
using Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;
public string PerformSPRequest(string tenantId, string resource, string clientId, string secret, string site) {
byte[] bytes = Convert.FromBase64String(secret);
var cert = new X509Certificate2(bytes);
var client = ConfidentialClientApplicationBuilder.Create(clientId)
.WithCertificate(cert)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}").Build();
var authenticationResult = client.AcquireTokenForClient(new string[] { $"{resource}/.default" }).ExecuteAsync().Result;
using (var spClient = new HttpClient()) {
spClient.DefaultRequestHeaders.Authorization = $"Bearer {authenticationResult.AccessToken}";
var content = "..."
var spUrl = $"{resource}/sites/{site}";
var spResponse = spClient.PostAsync(spUrl, content).Result;
}
}
As you can see I am passing the Certificate as a Base64 string here because that’s easier to store for me with Dynamics. The constructor of X509Certificate2
can also take a file and you can work with a certificate store as well!
Nothing much more to say here, its just straightforward getting a token.
Why I don’t want to use the NuGet
Essentially because I’m a Dynamics Developer. Dynamics 365 historically takes exactly one assembly for plugins and everything has to be contained inside apart from the system libraries like System.Net and so on. That’s because they are loaded to an isolated sandbox that will only contain your library. Well not quite… Because the plugins you are running need to be provided with a connection to Dynamics, Microsoft’s CRM SDK is loaded as well and some more libraries, like Microsoft.IdentityModel.Clients.ActiveDirectory
which is likely used for the login. And this library can achieve a certificate authentication! Unfortunately, Microsoft has also marked this library deprecated in favor of Identity.Client, so you would rely on it. Also, this is a pretty specific dependency that could just stop working if Microsoft migrates to Identity.Client and we have no idea when it will break. So not a good option for production use cases.
But what about Microsoft.Identity.Client
? It’s not loaded to the sandboxes, we could ILMerge it to our assembly! Yes, but that’s unsupported. The new Dependent Assembly Plugins feature can actually include this by packaging it together with the real assembly as a NuGet to upload. And it does work: In my first shot post about this, this was the exact NuGet to include because of this exact challenge with Sharepoint.
But this feature is in preview at the time of writing, none of the tooling other than the PRT (Plugin Registration Tool) supports it and frankly, it does not go that well with the existing project.
When discussing this option again a colleague opened my eyes here with the comment “Did you run this with Fiddler and check what they [note: Microsoft] are doing?” Well, I closed as many programs as possible, ran an Integration Test from Visual Studio and had the result that we are going to talk about next.
Doing it without a NuGet
So what is used here is quite cool: Instead of a classic 3-way handshake where AAD sends a challenge, you encrypt it with the Certificate and send it back, a self-signed JWT token is used.
This token is issued by me for me and is signed by myself. When it is sent to AAD, it can check with the public part of the certificate that the signature is correct and can therefore be sure that the token, which only has a short lifespan, is issued by the correct client.
To generate this token I went hunting in Microsoft’s NuGet. Luckily they do publish it open-source, therefore I was just able to grab relevant parts from there. I don’t want to make a super long exhibit here, so I will provide you with links to all relevant files in their repository instead and only print my part to the article:
- JWT Models: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/src/client/Microsoft.Identity.Client/Internal/JsonWebToken.cs
- Signing a JWT Token: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/CommonCryptographyManager.cs
- Base64Conversions: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/src/client/Microsoft.Identity.Client/Utils/Base64UrlHelpers.cs
NOTE: I modified and stripped the classes to then not need other things like System.Text.Json and for example the Base64UrlHelper we only need Encode.
With these Classes in place, the code from above looks like this:
using System.Security.Cryptography.X509Certificates;
public string PerformSPRequest(string tenantId, string resource, string clientId, string secret, string site) {
byte[] bytes = Convert.FromBase64String(secret);
var cert = new X509Certificate2(bytes);
var jwt = new JsonWebToken(new CommonCryptographyManager(), clientId, $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token");
var signedToken = jwt.Sign(cert, Base64UrlHelpers.Encode(cert.GetCertHash()), false);
var form = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
{ "client_assertion", signedToken },
{ "scope", $"{resource}/.default" }
};
var tokenString = Client.Post($"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token", form);
var token = JsonSerializer.Deserialize<AzureActiveDirectoryToken>(tokenString).access_token;
using (var spClient = new HttpClient()) {
spClient.DefaultRequestHeaders.Authorization = $"Bearer {token}";
var content = "..."
var spUrl = $"{resource}/sites/{site}";
var spResponse = spClient.PostAsync(spUrl, content).Result;
}
}
Summary
So the key to doing a simple certificate authentication with Azure Active Directory was the “client_assertion_type” being jwt-bearer and then the task is to create a valid bearer token. The fact that Microsoft publishes its code is a big help here since the relevant classes are not that big, the stripped classes are only around 400 loc without comments.
Therefore skipping the NuGet is valid for the use case of authenticating with a certificate and has much fewer side effects than switching to Dependent Assembly Plugins and will help us big time in the Sharepoint Integration (Extended) series.