From acfa933f0255b61cf722bc6e175f8cb165164748 Mon Sep 17 00:00:00 2001 From: Conrad Hunter Date: Tue, 3 Sep 2024 18:56:18 +1000 Subject: [PATCH 1/3] Add documention example for handling Clerk webhooks to README.md --- README.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3054ef0..ffe58fc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ _Looking for ASP.NET Core w/ Clerk JWTs? [See below](#what-about-jwt-auth)._ ### Packages + **`Clerk.Net`**: Provides the standalone API Client as a Kiota-generated wrapper over Clerk's OpenAPI spec. Compatible with .NET 6+ and .NET Framework 4.7.2+. **`Clerk.Net.DependencyInjection`**: Extensions to register the `ClerkApiClient` into your DI container. Compatible with .NET 6+. @@ -20,12 +21,14 @@ Make sure to add your `SecretKey` to your application configuration, ideally via 1. Install `Clerk.Net.DependencyInjection` from Nuget. 2. Add the following code to your service configuration: + ```cs builder.Services.AddClerkApiClient(config => { config.SecretKey = builder.Configuration["Clerk:SecretKey"]! }); ``` + 3. Request the `ClerkApiClient` in your services ```cs @@ -50,7 +53,7 @@ public class MyBackgroundWorker : BackgroundService ### Standalone Client -If you want to use the client by itself, install `Clerk.Net` and call `ClerkApiClientFactory.Create`, passing in your secret key. +If you want to use the client by itself, install `Clerk.Net` and call `ClerkApiClientFactory.Create`, passing in your secret key. The returned client should be treated as a singleton and created once for the lifetime of your application. @@ -80,7 +83,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) { // Disable audience validation as we aren't using it ValidateAudience = false, - NameClaimType = ClaimTypes.NameIdentifier + NameClaimType = ClaimTypes.NameIdentifier }; x.Events = new JwtBearerEvents() { @@ -99,6 +102,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) ``` If you're sending requests from a SPA, it should call Clerk-JS's `getToken` as part of its HTTP middleware and append the token (prefixed with `Bearer`) to the `Authorization` header, for example: + ```ts async onRequestInit({ requestInit }) { requestInit.headers = { @@ -108,6 +112,119 @@ async onRequestInit({ requestInit }) { } ``` +If you're accepting webhooks from Clerk, you will need to validate the incoming webhook signature. This can be done like this: + +```cs +using Newtonsoft.Json; +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; +using Svix; +using Svix.Models; +using System.Net; + +[ApiController] +[Route("api/[controller]")] +public class ClerkController : ControllerBase +{ + private readonly IConfiguration _configuration; + + public ClerkController(IConfiguration configuration) + { + _configuration = configuration; + } + + [HttpPost("webhook")] + public async Task ClerkWebhook() + { + // Retrieve the Clerk webhook secret from the configuration + var clerkWebhookSecret = "your-webhook-secret-here"; + + // Read the request body + var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); + Console.WriteLine($"Payload: {json}"); + + // Retrieve the Svix headers + var svixId = Request.Headers["svix-id"].ToString(); + var svixTimestamp = Request.Headers["svix-timestamp"].ToString(); + var svixSignature = Request.Headers["svix-signature"].ToString(); + + Console.WriteLine($"Svix ID: {svixId}"); + Console.WriteLine($"Svix Timestamp: {svixTimestamp}"); + Console.WriteLine($"Svix Signature: {svixSignature}"); + + if (string.IsNullOrEmpty(svixId) || string.IsNullOrEmpty(svixTimestamp) || string.IsNullOrEmpty(svixSignature)) + { + return BadRequest("Missing headers"); + } + + // Attempt signature verification with the raw signature + var wh = new Webhook(clerkWebhookSecret); + WebHeaderCollection headers = new WebHeaderCollection + { + { "svix-id", svixId }, + { "svix-timestamp", svixTimestamp }, + { "svix-signature", svixSignature } + }; + + Event webhookEvent; + try + { + wh.Verify(json, headers); // Verify doesn't return an event, just verifies the signature + webhookEvent = JsonConvert.DeserializeObject(json); // Deserialize the JSON into an Event object + Console.WriteLine($"Event Type: {webhookEvent.Type}"); + } + catch (Svix.Exceptions.WebhookVerificationException ex) + { + return BadRequest("Invalid signature"); + } + catch (Exception ex) + { + return BadRequest("An error occurred"); + } + + switch (webhookEvent.Type) + { + case "user.created": + // Handle user created event + break; + case "user.updated": + // Handle user updated event + break; + case "user.deleted": + // Handle user deleted event + break; + + default: + return BadRequest("Unhandled event type"); + } + + return Ok(); + } + + // Define the Event class to represent the webhook event + public class Event + { + public string Type { get; set; } = string.Empty; // Initialize with an empty string + public ClerkUser Data { get; set; } = new ClerkUser(); // Initialize with a new instance + } + + public class ClerkUser + { + public string Id { get; set; } = string.Empty; + public string ExternalId { get; set; } = string.Empty; + public string First_Name { get; set; } = string.Empty; + public string Last_Name { get; set; } = string.Empty; + public List Email_Addresses { get; set; } = new List(); + } + + public class ClerkEmailAddress + { + public string Email_Address { get; set; } = string.Empty; + } +} +``` + If the requests are coming from an SSR environment (ie NextJS), then you can either use the session JWT via the `__session` cookie and forward it on, or use a JWT template to create a unique JWT for your scenario. ### Disclaimer From dcc90a130c33e6c58b9919889c54be965a7f48cb Mon Sep 17 00:00:00 2001 From: Conrad Hunter Date: Tue, 3 Sep 2024 21:04:43 +1000 Subject: [PATCH 2/3] Add heading for webhooks section, Remove bloated code from example, Remove Console logging, replace Newtonsoft with STJ, initialize properties as nullable --- README.md | 63 +++++++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index ffe58fc..503bf97 100644 --- a/README.md +++ b/README.md @@ -112,47 +112,31 @@ async onRequestInit({ requestInit }) { } ``` -If you're accepting webhooks from Clerk, you will need to validate the incoming webhook signature. This can be done like this: +### Sync Clerk data with Webhooks -```cs -using Newtonsoft.Json; -using System.IO; -using System.Threading.Tasks; -using System.Collections.Generic; -using Svix; -using Svix.Models; -using System.Net; - -[ApiController] -[Route("api/[controller]")] -public class ClerkController : ControllerBase -{ - private readonly IConfiguration _configuration; +If you're accepting webhooks from Clerk, you will need to validate the incoming webhook signature. To do this, you'll need to install the `Svix` package: - public ClerkController(IConfiguration configuration) - { - _configuration = configuration; - } +```bash +dotnet add package Svix +``` + +Then, you can use the following code to validate the webhook signature: +```cs [HttpPost("webhook")] - public async Task ClerkWebhook() + public async Task ClerkWebhook(IConfiguration configuration) { // Retrieve the Clerk webhook secret from the configuration - var clerkWebhookSecret = "your-webhook-secret-here"; + var clerkWebhookSecret = configuration["Clerk:WebhookSecret"]; // Read the request body var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); - Console.WriteLine($"Payload: {json}"); // Retrieve the Svix headers var svixId = Request.Headers["svix-id"].ToString(); var svixTimestamp = Request.Headers["svix-timestamp"].ToString(); var svixSignature = Request.Headers["svix-signature"].ToString(); - Console.WriteLine($"Svix ID: {svixId}"); - Console.WriteLine($"Svix Timestamp: {svixTimestamp}"); - Console.WriteLine($"Svix Signature: {svixSignature}"); - if (string.IsNullOrEmpty(svixId) || string.IsNullOrEmpty(svixTimestamp) || string.IsNullOrEmpty(svixSignature)) { return BadRequest("Missing headers"); @@ -171,8 +155,7 @@ public class ClerkController : ControllerBase try { wh.Verify(json, headers); // Verify doesn't return an event, just verifies the signature - webhookEvent = JsonConvert.DeserializeObject(json); // Deserialize the JSON into an Event object - Console.WriteLine($"Event Type: {webhookEvent.Type}"); + webhookEvent = JsonSerializer.Deserialize(json); // Deserialize the JSON into an Event object } catch (Svix.Exceptions.WebhookVerificationException ex) { @@ -205,24 +188,30 @@ public class ClerkController : ControllerBase // Define the Event class to represent the webhook event public class Event { - public string Type { get; set; } = string.Empty; // Initialize with an empty string - public ClerkUser Data { get; set; } = new ClerkUser(); // Initialize with a new instance + public string? Type { get; set; } + public ClerkUser? Data { get; set; } } public class ClerkUser { - public string Id { get; set; } = string.Empty; - public string ExternalId { get; set; } = string.Empty; - public string First_Name { get; set; } = string.Empty; - public string Last_Name { get; set; } = string.Empty; - public List Email_Addresses { get; set; } = new List(); + public string? Id { get; set; } + public string? ExternalId { get; set; } + + [JsonPropertyName("first_name")] + public string? FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string? LastName { get; set; } + + [JsonPropertyName("email_addresses")] + public List EmailAddresses { get; set; } = new List(); } public class ClerkEmailAddress { - public string Email_Address { get; set; } = string.Empty; + [JsonPropertyName("email_address")] + public string? EmailAddress { get; set; } } -} ``` If the requests are coming from an SSR environment (ie NextJS), then you can either use the session JWT via the `__session` cookie and forward it on, or use a JWT template to create a unique JWT for your scenario. From 654898236598dee4f3e030609b263c503f3301b3 Mon Sep 17 00:00:00 2001 From: Conrad Hunter Date: Tue, 3 Sep 2024 21:10:38 +1000 Subject: [PATCH 3/3] bump text block back to original position --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 503bf97..a0e58da 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ async onRequestInit({ requestInit }) { } ``` +If the requests are coming from an SSR environment (ie NextJS), then you can either use the session JWT via the `__session` cookie and forward it on, or use a JWT template to create a unique JWT for your scenario. + ### Sync Clerk data with Webhooks If you're accepting webhooks from Clerk, you will need to validate the incoming webhook signature. To do this, you'll need to install the `Svix` package: @@ -214,8 +216,6 @@ Then, you can use the following code to validate the webhook signature: } ``` -If the requests are coming from an SSR environment (ie NextJS), then you can either use the session JWT via the `__session` cookie and forward it on, or use a JWT template to create a unique JWT for your scenario. - ### Disclaimer I am not affiliated with nor represent Clerk. All support queries regarding the underlying service should go to [Clerk Support](https://clerk.com/support).