Reference implementation
We have a GitHub repository that contains reference implementations of webhook notifications and API clients for interacting with the Energy Coordination API. This repository contains fully working examples with auto-generated code using OpenApi Generator.
Important files
The OpenApi generated code can be a bit verbose, but this page outlines the most important implementation details in order to quickly get started. The most pertinent files in the repository are:
C#
- Webhook server implementation
- Filter for calculating valid signature of request body
- Models for client API
Java
- Webhook server implementation
- Filter for calculating valid signature of request body
- Models for client API
TypeScript
You can also see these files in the following sections.
Webhook server implementation
There are two main components to handling the webhook server implementation for receiving notifications from the Energy Coordination API; receiving the notification, and verifying the signature.
Receiving the events should be done in the following manner:
using System.ComponentModel.DataAnnotations;using System.Threading.Tasks;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;using Partner.Api.Controllers;using Partner.Api.Filters;using Partner.Api.Models;
namespace Partner.Api.Implementations;
/// <summary>/// Reference implementation of the notification event webhook/// </summary>public class NotificationApiControllerImplementation( ILogger<NotificationApiControllerImplementation> logger) : NotificationApiController{ /// <inheritdoc/> [BodySignatureFilter] public override async Task<IActionResult> NotifyPost( [FromHeader(Name = "x-payload-signature"), Required] string xPayloadSignature, [FromBody] EventNotification eventNotification ) { // Fetch calculated signature from BodySignatureFilter if ( !HttpContext.Items.TryGetValue(BodySignatureFilter.ItemsKey, out var computedHash) || computedHash == null ) { logger.LogWarning("Unable to compute hash for incoming request"); return Problem(statusCode: StatusCodes.Status500InternalServerError); } if (computedHash.ToString() != xPayloadSignature) { return BadRequest(); } // Do not perform long processing before returning a 200 OK response await SaveEventForLaterProcessingAsync(eventNotification);
// It is important to return a 200 OK response to acknowledge receipt of event return Ok(); }
private static Task SaveEventForLaterProcessingAsync(EventNotification eventNotification) { // Save the event to a queue or database for later processing // For example, you can save the event to a database for later processing // using Entity Framework Core or Dapper // or enqueue the event to a message broker like Azure Service Bus or RabbitMQ return Task.CompletedTask; }}
package com.voluespark.energycoordination.partnerapi.api;
import com.voluespark.energycoordination.partnerapi.filters.BodySignatureFilter;import com.voluespark.energycoordination.partnerapi.model.EventNotification;import java.util.Optional;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.context.request.NativeWebRequest;import org.springframework.web.context.request.RequestAttributes;
@Controller@RequestMapping("${openapi.partnerWebhook.base-path:}")public class NotifyApiController implements NotifyApi {
private static final Logger LOGGER = LoggerFactory.getLogger(NotifyApiController.class);
@Autowired private NativeWebRequest request;
@Override public Optional<NativeWebRequest> getRequest() { return Optional.ofNullable(request); }
@Override public ResponseEntity<Void> notifyPost( String xPayloadSignature, EventNotification eventNotification) { String computedHash = (String) request .getAttribute(BodySignatureFilter.REQUEST_ATTRIBUTE_KEY, RequestAttributes.SCOPE_REQUEST);
if (computedHash == null) { LOGGER.warn("Unable to compute hash for incoming request"); return ResponseEntity.internalServerError().build(); } if (!computedHash.equals(xPayloadSignature)) { return ResponseEntity.badRequest().build(); }
// Do not perform long processing before returning a 200 OK response saveEventForLaterProcessing(eventNotification);
// It is important to return a 200 OK response to acknowledge receipt of event return ResponseEntity.ok().build(); }
private void saveEventForLaterProcessing(EventNotification ignoredEventNotification) { // Save the event to a queue or database for later processing // For example, you can save the event to a database for later processing // or enqueue the event to a message broker like Azure Service Bus or RabbitMQ }}
Calculate signature:
using System;using System.Security.Cryptography;using System.Text;using System.Threading.Tasks;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;using Microsoft.Extensions.Configuration;
namespace Partner.Api.Filters;
/// <summary>/// Filter to compute the HMAC SHA256 signature of the request body./// </summary>public class BodySignatureFilter : IAsyncResourceFilter{ /// <summary> The key for the calculated signature in <see cref="HttpContext.Items"/> </summary> public const string ItemsKey = "HmacSha256Signature";
/// <summary> The config key for the signing key</summary> public const string SigningKeyConfigKey = "SIGNING_KEY";
private readonly string _signingKey;
/// <summary> /// Instantiate a new BodySignatureFilter. /// </summary> public BodySignatureFilter(IConfiguration configuration) { // Fetch signing secret from configuration, should be injected via an Environment variable string signingKey = configuration.GetValue<string>(SigningKeyConfigKey); ArgumentNullException.ThrowIfNull(signingKey); _signingKey = signingKey; }
/// <inheritdoc/> public async Task OnResourceExecutionAsync( ResourceExecutingContext context, ResourceExecutionDelegate next ) { var request = context.HttpContext.Request; if (request.ContentType == null || !request.ContentType.Contains("application/json")) { await next(); return; } request.EnableBuffering(); using var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(_signingKey)); byte[] hash = await hmacsha256.ComputeHashAsync(request.Body); request.Body.Position = 0; var signature = Convert.ToBase64String(hash);
context.HttpContext.Items[ItemsKey] = signature;
await next(); }}
/// <summary>Register to a controller to compute the HMAC SHA256 signature of the body</summary>public class BodySignatureFilterAttribute : TypeFilterAttribute{ /// <summary>Initializes a new instance of <see cref="BodySignatureFilterAttribute"/></summary> public BodySignatureFilterAttribute() : base(typeof(BodySignatureFilter)) { }}
package com.voluespark.energycoordination.partnerapi.filters;
import jakarta.servlet.Filter;import jakarta.servlet.FilterChain;import jakarta.servlet.ReadListener;import jakarta.servlet.ServletException;import jakarta.servlet.ServletInputStream;import jakarta.servlet.ServletRequest;import jakarta.servlet.ServletResponse;import jakarta.servlet.http.HttpServletRequest;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.util.Base64;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import org.springframework.web.util.ContentCachingRequestWrapper;
@Componentpublic class BodySignatureFilter implements Filter {
public static final String REQUEST_ATTRIBUTE_KEY = "HmacSha256Signature"; public static final String SIGNING_KEY_CONFIG_KEY = "SIGNING_KEY";
private static final Logger LOGGER = LoggerFactory.getLogger(BodySignatureFilter.class);
private final Mac mac;
public BodySignatureFilter(@Value("${" + SIGNING_KEY_CONFIG_KEY + "}") String signingKey) { String algorithm = "HmacSHA256"; var secretKeySpec = new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), algorithm); Mac mac; try { mac = Mac.getInstance(algorithm); mac.init(secretKeySpec); } catch (NoSuchAlgorithmException | InvalidKeyException e) { LOGGER.warn("Unable to initialize HMAC SHA256 algorithm", e); mac = null; } this.mac = mac; }
@Override public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = new ContentCachingRequest((HttpServletRequest) request); if (mac != null) { byte[] bodyBytes = requestWrapper.getInputStream().readAllBytes(); String signature = Base64.getEncoder().encodeToString(mac.doFinal(bodyBytes)); request.setAttribute(REQUEST_ATTRIBUTE_KEY, signature); } chain.doFilter(requestWrapper, response); }
// Spring really doesn't make this easy :( // https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once public static class ContentCachingRequest extends ContentCachingRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public ContentCachingRequest(HttpServletRequest request) { super(request); }
@Override public ServletInputStream getInputStream() throws IOException { if (cachedBytes == null) { cachedBytes = new ByteArrayOutputStream(); super.getInputStream().transferTo(cachedBytes); } return new CachedServletInputStream(cachedBytes.toByteArray()); }
private static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedServletInputStream(byte[] contents) { this.buffer = new ByteArrayInputStream(contents); }
@Override public int read() { return buffer.read(); }
@Override public boolean isFinished() { return buffer.available() == 0; }
@Override public boolean isReady() { return true; }
@Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException("Not implemented"); } } }}