Webhook Validation

Hey, I am validating my user.create webhook as per the webhooks guide, checking the id and timestamp against the api/v1/events/{event_id} endpoint, but I am getting a 403 status code response. I am using the content of the webhook (the encoded JWT) as my token for the validation. Am I doing something wrong?
public async Task<bool> ValidateWebhook(string eventId, DateTime timestamp, string accessToken, CancellationToken cToken = default)
{
try
{
ArgumentNullException.ThrowIfNullOrEmpty(nameof(eventId));
ArgumentNullException.ThrowIfNull(nameof(timestamp));

string endpoint = $"api/v1/events/{eventId}";
string domain = _kindeSettings.Domain;

string absoluteUrl = $"{domain}/{endpoint}";
bool uriIsValid = Uri.TryCreate(absoluteUrl, UriKind.Absolute, out Uri uri);
if (!uriIsValid) throw new Exception("The uri is not valid");
using (HttpClient client = new HttpClient())
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
cToken.ThrowIfCancellationRequested();

HttpResponseMessage message = await client.SendAsync(request, cToken);
}
return true; //not yet finished
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
public async Task<bool> ValidateWebhook(string eventId, DateTime timestamp, string accessToken, CancellationToken cToken = default)
{
try
{
ArgumentNullException.ThrowIfNullOrEmpty(nameof(eventId));
ArgumentNullException.ThrowIfNull(nameof(timestamp));

string endpoint = $"api/v1/events/{eventId}";
string domain = _kindeSettings.Domain;

string absoluteUrl = $"{domain}/{endpoint}";
bool uriIsValid = Uri.TryCreate(absoluteUrl, UriKind.Absolute, out Uri uri);
if (!uriIsValid) throw new Exception("The uri is not valid");
using (HttpClient client = new HttpClient())
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
cToken.ThrowIfCancellationRequested();

HttpResponseMessage message = await client.SendAsync(request, cToken);
}
return true; //not yet finished
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
5 Replies
skywalker-kiwi#02131
skywalker-kiwi#02131OP•6mo ago
I am validating the JWT against my open-id config as I would with any other JWT and so is there a need to validate webhook again (see below):
[HttpPost("register")]
public async Task<Results<Ok, UnauthorizedHttpResult, BadRequest, ProblemHttpResult>> RegisterAccount(CancellationToken cToken = default)
{
string content = string.Empty;
using (MemoryStream ms = new MemoryStream())
{
await HttpContext.Request.Body.CopyToAsync(ms);
byte[] bytes = ms.ToArray();
content = Encoding.UTF8.GetString(bytes);
}
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
RSA rsa = await Utils.GetRSAFromJwks(_kindeSettings.Domain);
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _kindeSettings.Issuer,
ValidAudience = _kindeSettings.Audience,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
try
{
JwtSecurityToken jwtToken = handler.ReadJwtToken(content);
string payload = jwtToken.Payload.SerializeToJson();
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};

RegisterAccountWebhookBody? deserialisedResponse = JsonSerializer.Deserialize<RegisterAccountWebhookBody>(payload, options);
if(deserialisedResponse == null)
{
throw new InvalidDataException("The web request body was invalid");
}
bool isValid = await _authenticationService.ValidateWebhook(deserialisedResponse.EventId, deserialisedResponse.Timestamp, content, cToken);
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
return TypedResults.Problem("The webhook could not be validated");
}
return TypedResults.Ok();
}
[HttpPost("register")]
public async Task<Results<Ok, UnauthorizedHttpResult, BadRequest, ProblemHttpResult>> RegisterAccount(CancellationToken cToken = default)
{
string content = string.Empty;
using (MemoryStream ms = new MemoryStream())
{
await HttpContext.Request.Body.CopyToAsync(ms);
byte[] bytes = ms.ToArray();
content = Encoding.UTF8.GetString(bytes);
}
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
RSA rsa = await Utils.GetRSAFromJwks(_kindeSettings.Domain);
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _kindeSettings.Issuer,
ValidAudience = _kindeSettings.Audience,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
try
{
JwtSecurityToken jwtToken = handler.ReadJwtToken(content);
string payload = jwtToken.Payload.SerializeToJson();
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};

RegisterAccountWebhookBody? deserialisedResponse = JsonSerializer.Deserialize<RegisterAccountWebhookBody>(payload, options);
if(deserialisedResponse == null)
{
throw new InvalidDataException("The web request body was invalid");
}
bool isValid = await _authenticationService.ValidateWebhook(deserialisedResponse.EventId, deserialisedResponse.Timestamp, content, cToken);
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
return TypedResults.Problem("The webhook could not be validated");
}
return TypedResults.Ok();
}
onderay
onderay•6mo ago
Thanks for providing all the details. It looks like you are encountering a 403 status code, which indicates that the credentials provided are invalid. Are you able to double the following items? Invalid Access Token: Ensure that the access token you are using is valid and has not expired. The token should be a valid bearer token with the necessary permissions to access the /api/v1/events/{event_id} endpoint. Incorrect Token Usage: The token used for the Authorization header should be a valid access token obtained through the proper authentication flow, not the encoded JWT from the webhook payload. The JWT from the webhook payload is used to verify the authenticity of the webhook request, not for API authentication. Scope and Permissions: Verify that the access token has the correct scopes and permissions to access the event details. You might need to check the permissions associated with the token. Here is a revised version of your method, ensuring that the access token is correctly used for the API call:
public async Task<bool> ValidateWebhook(string eventId, DateTime timestamp, string accessToken, CancellationToken cToken = default)
{
try
{
ArgumentNullException.ThrowIfNullOrEmpty(nameof(eventId));
ArgumentNullException.ThrowIfNull(nameof(timestamp));

string endpoint = $"api/v1/events/{eventId}";
string domain = _kindeSettings.Domain;

string absoluteUrl = $"{domain}/{endpoint}";
bool uriIsValid = Uri.TryCreate(absoluteUrl, UriKind.Absolute, out Uri uri);
if (!uriIsValid) throw new Exception("The uri is not valid");
using (HttpClient client = new HttpClient())
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
cToken.ThrowIfCancellationRequested();

HttpResponseMessage message = await client.SendAsync(request, cToken);
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
throw new Exception("Access forbidden: Invalid credentials or insufficient permissions.");
}
message.EnsureSuccessStatusCode();
}
return true; //not yet finished
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
public async Task<bool> ValidateWebhook(string eventId, DateTime timestamp, string accessToken, CancellationToken cToken = default)
{
try
{
ArgumentNullException.ThrowIfNullOrEmpty(nameof(eventId));
ArgumentNullException.ThrowIfNull(nameof(timestamp));

string endpoint = $"api/v1/events/{eventId}";
string domain = _kindeSettings.Domain;

string absoluteUrl = $"{domain}/{endpoint}";
bool uriIsValid = Uri.TryCreate(absoluteUrl, UriKind.Absolute, out Uri uri);
if (!uriIsValid) throw new Exception("The uri is not valid");
using (HttpClient client = new HttpClient())
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
cToken.ThrowIfCancellationRequested();

HttpResponseMessage message = await client.SendAsync(request, cToken);
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
throw new Exception("Access forbidden: Invalid credentials or insufficient permissions.");
}
message.EnsureSuccessStatusCode();
}
return true; //not yet finished
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
Let me know this helps you with what you are seeing
skywalker-kiwi#02131
skywalker-kiwi#02131OP•6mo ago
Thanks @Andre @ Kinde . That explains things a bit more. The flow I am trying to achieve is: user registers -> triggers webhook -> backend receives webhook payload and adds user to database -> returns 200 status code. As it is the webhook triggering my custom endpoint and not a user, there is not a token being passed in. As a user is registering and they don't have an access token yet, what token do I use to hit that events endpoint? I assume it could be an admin token?
Daniel_Kinde
Daniel_Kinde•6mo ago
You would need to create a M2M application and use a token from this to pass to the endpoint
skywalker-kiwi#02131
skywalker-kiwi#02131OP•4mo ago
Ah cool, thanks. That clarifies things Hey @Daniel_Kinde, so I implemented the suggestion above. I tried using my m2m token with read:events enabled as a permission, and I get a 403 response. I am setting the token as a bearer token Authorization header value. I then tried giving my m2m app all the scopes available and I still get a 403. Any suggestions as to what's up with that? See below for implementation 😃
public async Task<Result<EventResponse>> GetEventAsync(string eventId, string accessToken, CancellationToken cancellationToken)
{
_logger.Information("Getting webhook {Id}", eventId);
try
{
HttpClient client = _clientFactory.CreateClient(ClientName);
string endpoint = $"{_kindeSettings.Domain}/api/v1/events/{eventId}";
bool createUri = Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri);
if (!createUri)
{
_logger.Error("The uri was invalid");
return Result<EventResponse>.Fail(new Error(400, "The webhook endpoint was invalid"));

}
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, endpoint);
requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken);
HttpResponseMessage response = await client.SendAsync(requestMessage, cancellationToken);
response.EnsureSuccessStatusCode();
JsonSerializerOptions options = _jsonOptionsFactory.CreateOptions(ClientName);
EventResponse responseValue = await response.Content.ReadFromJsonAsync<EventResponse>(options: options, cancellationToken) ?? throw new InvalidDataException("The json response was invalid");
_logger.Information("Successfully retrieved webhook value");
return Result<EventResponse>.SucceedWithValue(responseValue);
}
catch (Exception exception)
{
_logger.Error(exception, exception.Message);
return Result<EventResponse>.Fail(new Error(500, "The webhook could not be retrieved"));
}
}
public async Task<Result<EventResponse>> GetEventAsync(string eventId, string accessToken, CancellationToken cancellationToken)
{
_logger.Information("Getting webhook {Id}", eventId);
try
{
HttpClient client = _clientFactory.CreateClient(ClientName);
string endpoint = $"{_kindeSettings.Domain}/api/v1/events/{eventId}";
bool createUri = Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri);
if (!createUri)
{
_logger.Error("The uri was invalid");
return Result<EventResponse>.Fail(new Error(400, "The webhook endpoint was invalid"));

}
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, endpoint);
requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken);
HttpResponseMessage response = await client.SendAsync(requestMessage, cancellationToken);
response.EnsureSuccessStatusCode();
JsonSerializerOptions options = _jsonOptionsFactory.CreateOptions(ClientName);
EventResponse responseValue = await response.Content.ReadFromJsonAsync<EventResponse>(options: options, cancellationToken) ?? throw new InvalidDataException("The json response was invalid");
_logger.Information("Successfully retrieved webhook value");
return Result<EventResponse>.SucceedWithValue(responseValue);
}
catch (Exception exception)
{
_logger.Error(exception, exception.Message);
return Result<EventResponse>.Fail(new Error(500, "The webhook could not be retrieved"));
}
}
Want results from more Discord servers?
Add your server