Skip to content

Commit

Permalink
Add a webhook-jwks endpoint (#1692)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Nov 20, 2024
1 parent 06f663e commit bf75077
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ static GetAnIdentityEndpoints()

public static IEndpointConventionBuilder MapIdentityEndpoints(this IEndpointRouteBuilder builder)
{
return builder.MapGroup("/identity")
return builder
.MapPost(
"",
"/webhooks/identity",
async (
HttpContext context,
IOptions<GetAnIdentityOptions> identityOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ public static class WebHookEndpoints
{
public static IEndpointConventionBuilder MapWebHookEndpoints(this IEndpointRouteBuilder builder)
{
return builder.MapGroup("/webhooks")
.MapIdentityEndpoints();
return builder.MapIdentityEndpoints();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Extensions.Options;
using TeachingRecordSystem.Core.Services.Webhooks;

namespace TeachingRecordSystem.Api.Endpoints;

public static class WebhookJwks
{
public static IEndpointConventionBuilder MapWebhookJwks(this IEndpointRouteBuilder endpoints)
{
return endpoints.MapGet("/webhook-jwks", async ctx =>
{
var webhookOptions = ctx.RequestServices.GetRequiredService<IOptions<WebhookOptions>>();
var jsonWebKeySet = webhookOptions.Value.GetJsonWebKeySet();
await ctx.Response.WriteAsJsonAsync(jsonWebKeySet);
});
}
}
6 changes: 5 additions & 1 deletion TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.PowerPlatform.Dataverse.Client;
using Optional;
using TeachingRecordSystem.Api.Endpoints;
using TeachingRecordSystem.Api.Endpoints.IdentityWebHooks;
using TeachingRecordSystem.Api.Infrastructure.ApplicationModel;
using TeachingRecordSystem.Api.Infrastructure.Filters;
Expand All @@ -30,6 +31,7 @@
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
using TeachingRecordSystem.Core.Services.NameSynonyms;
using TeachingRecordSystem.Core.Services.TrnGenerationApi;
using TeachingRecordSystem.Core.Services.Webhooks;
using TeachingRecordSystem.ServiceDefaults;
using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging;

Expand Down Expand Up @@ -221,7 +223,8 @@ public static void Main(string[] args)
.AddDistributedLocks()
.AddIdentityApi()
.AddNameSynonyms()
.AddDqtOutboxMessageSerializer();
.AddDqtOutboxMessageSerializer()
.AddWebhookOptions();

services.AddAccessYourTeachingQualificationsOptions(configuration, env);
services.AddCertificateGeneration();
Expand Down Expand Up @@ -274,6 +277,7 @@ public static void Main(string[] args)
});

app.MapWebHookEndpoints();
app.MapWebhookJwks();

app.MapControllers();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@
"Username": "admin",
"Password": "test"
},
"StorageConnectionString": "UseDevelopmentStorage=true"
"StorageConnectionString": "UseDevelopmentStorage=true",
"Webhooks": {
"CanonicalDomain": "https://localhost:5001"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace TeachingRecordSystem.Core.Services.Webhooks;

public static class HostApplicationBuilderExtensions
{
public static IHostApplicationBuilder AddWebhookOptions(this IHostApplicationBuilder builder)
{
builder.Services.AddOptions<WebhookOptions>()
.Bind(builder.Configuration.GetSection("Webhooks"))
.ValidateDataAnnotations()
.ValidateOnStart();

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;

namespace TeachingRecordSystem.Core.Services.Webhooks;

Expand All @@ -12,6 +14,35 @@ public class WebhookOptions

[Required]
public required WebhookOptionsKey[] Keys { get; set; }

public JsonWebKeySet GetJsonWebKeySet()
{
// FUTURE memoize this

var keySet = new JsonWebKeySet();

foreach (var key in Keys)
{
using var certificate = X509Certificate2.CreateFromPem(key.CertificatePem);
var securityKey = new ECDsaSecurityKey(certificate.GetECDsaPublicKey());

var jsonWebKey = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(securityKey);
jsonWebKey.Use = "sig";
jsonWebKey.Alg = "ES384";
jsonWebKey.KeyId = key.KeyId;

var certChain = certificate.ExportCertificatePem().Split("\n")
.Skip(1) // Remove -----BEGIN CERTIFICATE-----
.SkipLast(1) // Remove -----END CERTIFICATE-----
.Aggregate((l, r) => l + r);

jsonWebKey.X5c.Add(certChain);

keySet.Keys.Add(jsonWebKey);
}

return keySet;
}
}

public class WebhookOptionsKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ IOptions<MessageSigningOptions> GetMessageSigningOptions(IServiceProvider servic
.WithCreatedNow()
.WithExpires(DateTimeOffset.UtcNow.AddMinutes(5))
.WithAlgorithm(SignatureAlgorithm.EcdsaP384Sha384)
.WithKeyId("test")
.WithKeyId(keyId)
.WithNonce(Guid.NewGuid().ToString("N"));

return Options.Create(options);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using JustEat.HttpClientInterception;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc.Testing;
Expand All @@ -13,6 +14,7 @@
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
using TeachingRecordSystem.Core.Services.TrnGenerationApi;
using TeachingRecordSystem.Core.Services.TrsDataSync;
using TeachingRecordSystem.Core.Services.Webhooks;

namespace TeachingRecordSystem.Api.Tests;

Expand Down Expand Up @@ -95,6 +97,25 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
options.WebHookClientSecret = "dummy";
});

services.Configure<WebhookOptions>(options =>
{
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP384);
var certRequest = new CertificateRequest("CN=Teaching Record System Tests", key, HashAlgorithmName.SHA384);
using var cert = certRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(1));
var certPem = cert.ExportCertificatePem();
var keyPem = key.ExportECPrivateKeyPem();

options.CanonicalDomain = "http://localhost";
options.SigningKeyId = "testkey";
options.Keys = [
new WebhookOptionsKey()
{
KeyId = "testkey",
CertificatePem = certPem,
PrivateKeyPem = keyPem,
}];
});

services.AddHttpClient("EvidenceFiles")
.AddHttpMessageHandler(_ => EvidenceFilesHttpClientInterceptorOptions.CreateHttpMessageHandler())
.ConfigurePrimaryHttpMessageHandler(_ => new NotFoundHandler());
Expand Down

0 comments on commit bf75077

Please sign in to comment.