Skip to main content

How to set up integration client credentials

Overview

The integration services (soap-hub, inrest, modbus) authenticate against the Energyworx platform API with the OAuth2 client credentials grant (RFC 6749 §4.4) on the identity service. This replaces the earlier jwt-config mechanism, where each service signed its own platform JWT with a pre-registered private key.

One client credential (a client_id/client_secret pair) is shared by all integration services per target environment. At runtime a service exchanges the credentials for a short-lived access token at the identity service token endpoint and caches it until it (nearly) expires.

1. Provision the client credentials

Credentials are created by the intelligence bootstrap, gated behind an opt-in flag so environments without integration services are unaffected:

python super_bootstrap.py --full-provision \
--datastore-project-id <gcp-project> \
--with-integrations-credentials \
...

This creates the integrations ClientCredentials entity in the identity service, whitelists the credential's account, and seeds the per-namespace users the legacy authorizer requires. The generated client_id and client_secret are printed exactly once at the end of the run and are not stored anywhere — capture them immediately for step 2. To rotate or re-run with known values, pass --integrations-client-id / --integrations-client-secret.

2. Store the secret (SOPS in ewx-config)

The credentials are committed SOPS-encrypted to the ewx-config repository — never in plaintext in config, source, or Terraform state. From the ewx-config repo root:

mkdir -p secrets/<TF_WORKSPACE>
cp secrets/integrations.enc.yaml.example secrets/<TF_WORKSPACE>/integrations.enc.yaml
# fill in the printed values, then encrypt in place:
sops --encrypt --in-place secrets/<TF_WORKSPACE>/integrations.enc.yaml

The file maps target GCP project ids to credentials, because one deployment can call the APIs of several environments (inrest in particular):

integration_credentials:
ewx-example-project:
client_id: ...
client_secret: ...
# token_url is optional; defaults to
# https://identity.<gcp_project>.ewxapis.com/oauth2/v1/token

See secrets/README.md in ewx-config for the SOPS/KMS runbook and the pre-commit and CI guards that reject unencrypted files.

3. Deploy

Deploy the integrations scope from the ewx-root monorepo:

mise run tf-apply-integrations -p <env>

Each service's terragrunt.hcl decrypts the environment's integrations.enc.yaml and Terraform injects the map into the pods as a Kubernetes Secret. The secret feeds the DYNACONF_INTEGRATION_CREDENTIALS environment variable (all integration services share the DYNACONF_ Dynaconf prefix), carrying the JSON map with Dynaconf's @json cast prefix so it lands in settings as the INTEGRATION_CREDENTIALS mapping. An environment without the file still plans and applies — the services then fail authentication only if they actually need a token.

4. Runtime token flow

The shared OAuth2ClientCredentialsService (in the ewx-integrations common library) implements the token lifecycle:

  1. On each API request the service asks the token service for a bearer token for the target GCP project.
  2. A cached token that is still valid (with a 60-second early-refresh margin) is reused.
  3. Otherwise the service POSTs {"grant_type": "client_credentials", "client_id": ..., "client_secret": ...} to https://identity.<target-project>.ewxapis.com/oauth2/v1/token and caches the returned access_token together with its expiry (from expires_in).

There is no refresh token for this grant: refreshing simply means exchanging the client credentials again.

The token service itself is configuration-agnostic: each service resolves the INTEGRATION_CREDENTIALS mapping from its own Dynaconf settings and passes it in at construction, along with the target project id. The token URL template, expiry skew, and request timeouts are constructor parameters with sensible defaults.

Troubleshooting

  • No client credentials configured for target project ... — the environment's integrations.enc.yaml is missing an entry for that target project (or the prefixed *_INTEGRATION_CREDENTIALS environment variable didn't reach the pod). Add the entry and redeploy.
  • 401 unauthorized_client from the token endpoint — the credentials don't match the ClientCredentials entity (rotated? typo?), or the entity expired. Re-run the bootstrap with --with-integrations-credentials and known values to rotate.
  • API calls return 403/500 after tokens are issued — the credential's account is likely missing whitelisting or per-namespace users; re-run the bootstrap, which seeds both.