Skip to content

Instantly share code, notes, and snippets.

@AnnejanBarelds
Created March 4, 2020 14:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AnnejanBarelds/540855428417818b5fbab83bbfa1e19f to your computer and use it in GitHub Desktop.
Save AnnejanBarelds/540855428417818b5fbab83bbfa1e19f to your computer and use it in GitHub Desktop.
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace AzureADAppManagement
{
public class AppCertificateManager
{
public async Task RollCertificatesAsync(string appId, string appObjectId, string tenantId, X509Certificate2 existingCertificate, X509Certificate2 newCertificate)
{
var graph = GetGraphClient(() => GetTokenAsync(appId, existingCertificate, tenantId));
var keyCredential = CreateKeyCredential(newCertificate);
var proof = GetJwtTokenProof(existingCertificate, appId);
await graph.Applications[appObjectId].AddKey(keyCredential, proof).Request().PostAsync();
// Waiting 120 secs; proceeding immediately may result in failure if the new cert is not fully processed server side
// Not sure how much time is appropriate here, to be honest. 120 secs seems excessive, but 20 secs for example has proven too short
await Task.Delay(120000);
// Re-init the client to use the new cert for token retrieval
graph = GetGraphClient(() => GetTokenAsync(appId, newCertificate, tenantId));
// Find the keyId for the old certificate
var app = await graph.Applications[appObjectId].Request().GetAsync();
var keyId = app.KeyCredentials.Single(key =>
{
return Convert.ToBase64String(key.CustomKeyIdentifier).Equals(existingCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase);
}).KeyId;
// Create new proof based on the new certificate
proof = GetJwtTokenProof(newCertificate, appId);
// Remove the old certificate
await graph.Applications[appObjectId].RemoveKey(keyId.Value, proof).Request().PostAsync();
}
private GraphServiceClient GetGraphClient(Func<Task<string>> tokenDelegate)
{
return new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
var token = await tokenDelegate();
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("bearer", token);
}));
}
private async Task<string> GetTokenAsync(string appId, X509Certificate2 certificate, string tenantId)
{
var app = ConfidentialClientApplicationBuilder.Create(appId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}/")
.WithCertificate(certificate)
.Build();
var scopes = new[] { "https://graph.microsoft.com/.default" };
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
return result.AccessToken;
}
private string GetJwtTokenProof(X509Certificate2 signingCert, string appId)
{
var notBefore = DateTime.Now;
var expires = notBefore.AddMinutes(10);
var handler = new JwtSecurityTokenHandler();
var credentials = new X509SigningCredentials(signingCert);
var jwtToken = handler.CreateJwtSecurityToken(appId, "https://graph.windows.net", null, notBefore, expires, null, credentials);
return handler.WriteToken(jwtToken);
}
private KeyCredential CreateKeyCredential(X509Certificate2 certificate)
{
return new KeyCredential()
{
Key = certificate.RawData,
Usage = "Verify",
Type = "AsymmetricX509Cert"
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment