Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,44 @@ It is also built with Maven, specifically because it offers the
broadest array of compatibility. Maven artifacts can easily be used
by other build tools such as Gradle and SBT.

## Connecting to SPP TROLIE (SPP Two-Factor Authentication)

For FERC 881 compliance, many Transmission Owners (TO) and neighboring
Reliability Coordinators (RC) must connect to Southwest Power Pool (SPP)
TROLIE systems. Doing so requires adherence to the
[SPP Two-Factor Authentication (TFA) Technical Specifications v1.4](https://spp.org/documents/75215/two-factor%20authentication%20technical%20specifications%20v1.4.pdf).

The TROLIE Java Client SDK offers built-in support for SPP Authentication.
This handles the requirement to cryptographically sign every single outbound
request with a custom `X-SPP-API-Token` header using HTTP request metadata
(path, timestamp, nonce, and screen name) signed with an HMAC-SHA512 key.

### Basic Setup with SPP TFA

Configure the `TrolieClient` using your SPP-assigned screen name and Base64-encoded
API key directly in the builder:

```java
import energy.trolie.client.TrolieClient;
import energy.trolie.client.TrolieClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import java.util.Base64;

public class SppConnectionDemo {
public static void main(String[] args) throws Exception {
String base64ApiKey = Base64.getEncoder().encodeToString("your-spp-secret-key".getBytes());

try (TrolieClient client = new TrolieClientBuilder("https://trolie.spp.org", HttpClients.createDefault())
.withSppAuthentication("YourSppScreenName", base64ApiKey)
.build()) {

// The client automatically signs every outbound request
// with compliant X-SPP-API-Token signatures.
}
}
}
```

## Best Practices

This library will include best practices around usage. This includes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import java.io.FileReader;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand All @@ -45,6 +47,15 @@ private static TrolieClient initializeClient() {
// the test data provided in this example.
.httpHeaders(Map.of(
"X-TROLIE-Testing-Identity", "TO1"))

// SPP Authentication (Optional):
// This configuration is required only when connecting to SPP.
// It generates the mandatory X-SPP-API-Token header using an HMAC-SHA512
// signature based on your SPP-provided Screen Name and Base64-encoded API Key
.withSppAuthentication(
"TestScreenName",
Base64.getEncoder().encodeToString("secretkey".getBytes(StandardCharsets.UTF_8))
)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package energy.trolie.client.examples;

import energy.trolie.client.TrolieClient;
import energy.trolie.client.TrolieClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SppConnectionExample {

public static void main(String[] args) throws IOException {
try (TrolieClient client = new TrolieClientBuilder(
"http://localhost:8080",
HttpClients.createDefault())
.withSppAuthentication(
"TestScreenName",
Base64.getEncoder().encodeToString("secretkey".getBytes(StandardCharsets.UTF_8)))
.build()) {

// Client is now ready to interact with SPP system
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package energy.trolie.client;

import java.util.Map;

/**
* Interface for providing dynamic request headers (e.g., Auth tokens, nonces).
*
* <p><strong>Implementation Note:</strong> Implementations must generate
* fresh values (e.g., new nonces, current timestamps) on every call. The
* underlying {@code HttpClient} may reuse request objects during automatic
* retries. Do not cache values within the provider, as the SDK cannot
* guarantee that this method will be re-invoked for every individual
* transmission attempt.</p>
*/
public interface RequestHeaderProvider {

Map<String, String> headersFor(TrolieRequestContext requestContext);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
import energy.trolie.client.request.operatingsnapshots.ForecastSnapshotSubscribedReceiver;
import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotSubscribedReceiver;
import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotSubscribedReceiver;
import energy.trolie.client.spp.SppApiTokenHeaderProvider;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;

import java.time.Clock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
Expand Down Expand Up @@ -50,6 +54,7 @@ public class TrolieClientBuilder {
private ObjectMapper objectMapper;
private ETagStore eTagStore;
private Map<String, String> httpHeaders = new HashMap<>();
private final List<RequestHeaderProvider> providers = new ArrayList<>();
private int periodLengthMinutes = 60;

private int realTimeRatingsPollMs = 10000;
Expand Down Expand Up @@ -134,6 +139,43 @@ public TrolieClientBuilder httpHeaders(Map<String, String> httpHeaders) {
return this;
}

/**
* Adds a request header provider that can generate additional headers for each request execution.
* Providers are invoked during the initial request construction.
*
* <p><strong>Note:</strong> If the underlying {@code HttpClient} is configured with
* automatic retries, the same request object (and its headers) may be sent multiple times.
* To ensure security-sensitive headers like nonces are regenerated, users should ensure
* that automatic retries are disabled or that the client is configured to rebuild
* requests on retry.</p>
*
* @param provider provider used to generate request-specific headers
* @return fluent builder
*/
public TrolieClientBuilder addRequestHeaderProvider(RequestHeaderProvider provider) {
this.providers.add(provider);
return this;
}

/**
* Replaces the current set of request header providers with the given list.
* Providers are invoked during the initial request construction.
*
* <p><strong>Note:</strong> If the underlying {@code HttpClient} is configured with
* automatic retries, the request (including these headers) may be reused across
* multiple attempts. To guarantee fresh header values (e.g., new nonces) on retry,
* implementers must ensure that automatic transport-level retries are disabled or
* that the client is configured to rebuild requests on retry.</p>
*
* @param providers list of providers used to generate request-specific headers
* @return fluent builder
*/
public TrolieClientBuilder requestHeaderProviders(List<RequestHeaderProvider> providers) {
this.providers.clear();
this.providers.addAll(providers);
return this;
}

/**
* Sets the period length assumed for forecast ratings. Defaults to 60 minutes.
* @param periodLengthMinutes new assumed period length.
Expand Down Expand Up @@ -192,6 +234,21 @@ public TrolieClientBuilder seasonalRatingsPollMs(int seasonalRatingsPollMs) {
return this;
}

/**
* Configures this client to authenticate with an SPP system
* using the SPP Two-Factor Authentication (TFA) protocol.
*
* @param screenName the SPP-assigned screen name
* @param apiKey the Base64-encoded API key assigned by SPP
* @return this builder
* @see SppApiTokenHeaderProvider
*/
public TrolieClientBuilder withSppAuthentication(String screenName, String apiKey) {
return addRequestHeaderProvider(
new SppApiTokenHeaderProvider(screenName, apiKey, Clock.systemUTC())
);
}

/**
* Construct a new client
* @return new client
Expand All @@ -216,7 +273,7 @@ public TrolieClient build() {
}

return new TrolieClientImpl(httpClient, host, requestConfig, bufferSize,
objectMapper, eTagStore, httpHeaders, periodLengthMinutes,
objectMapper, eTagStore, httpHeaders, providers, periodLengthMinutes,
realTimeRatingsPollMs,
forecastRatingsPollMs, monitoringSetPollMs, seasonalRatingsPollMs);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package energy.trolie.client;

import java.net.URI;

public record TrolieRequestContext(String method, URI uri, String contentType) {
}


Loading
Loading