Created
March 4, 2020 14:31
-
-
Save AnnejanBarelds/540855428417818b5fbab83bbfa1e19f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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