Customer API / Authentication

OIDC OAuth 2.0

Use the client-credentials grant for unattended integrations and validate tokens through standard OpenID Connect metadata.

In this article

Configure the client

  1. Create or select an OAuth authentication method for the Customer API integration.
  2. Use the issuer supplied by Wavac and read its /.well-known/openid-configuration document to find token_endpoint.
  3. Request only customer.discovery.read and customer.data.read. The integration policy can further restrict sites and lines.
  4. Use grant_type=client_credentials. This flow has no interactive user or refresh token.

A client secret is a machine credential. Store it in a secret manager, never in source, Postman collection variables, command history, or logs.

Request and use a token

Both examples discover the token endpoint, request a client-credentials token, and call discovery. Use a secret manager for unattended production clients.

Linux: Bash, curl, and jq

# Set non-secret configuration only for this shell session.
export OIDC_ISSUER='https://your-issuer.example/application/o/customer-api/'
export OIDC_CLIENT_ID='your-client-id'

# Prompt without echoing the client secret or placing it in shell history.
read -rsp 'Client secret: ' OIDC_CLIENT_SECRET
echo
export OIDC_CLIENT_SECRET

# Read the standards-based token endpoint from the issuer metadata.
TOKEN_ENDPOINT=$(curl --silent --show-error --fail \
  "${OIDC_ISSUER%/}/.well-known/openid-configuration" \
  | jq -r '.token_endpoint')

# Exchange the machine credential for a short-lived access token.
# Supplying curl configuration on stdin keeps the expanded secret out of argv.
ACCESS_TOKEN=$(
  curl --silent --show-error --fail --config - <<EOF | jq -r '.access_token'
user = "$OIDC_CLIENT_ID:$OIDC_CLIENT_SECRET"
url = "$TOKEN_ENDPOINT"
request = "POST"
data-urlencode = "grant_type=client_credentials"
data-urlencode = "scope=customer.discovery.read customer.data.read"
EOF
)

# Send the access token in the Authorization header, never in the URL.
curl --fail-with-body \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  https://api.wavac.io/api/customer/v1/discovery

# Remove secret values from the shell when the tutorial is complete.
unset ACCESS_TOKEN OIDC_CLIENT_SECRET

Windows: PowerShell

# Set non-secret configuration only for this PowerShell process.
$env:OIDC_ISSUER = 'https://your-issuer.example/application/o/customer-api/'
$env:OIDC_CLIENT_ID = 'your-client-id'

# Prompt without echoing the client secret or placing it in command history.
$secureSecret = Read-Host 'Client secret' -AsSecureString
$clientSecret = [System.Net.NetworkCredential]::new('', $secureSecret).Password

# Read the standards-based token endpoint from the issuer metadata.
$issuer = $env:OIDC_ISSUER.TrimEnd('/')
$metadata = Invoke-RestMethod -Uri "$issuer/.well-known/openid-configuration"

# Encode the client credential for OAuth HTTP Basic authentication.
$credentialBytes = [System.Text.Encoding]::UTF8.GetBytes(
  "$($env:OIDC_CLIENT_ID):$clientSecret")
$basicCredential = [Convert]::ToBase64String($credentialBytes)

# Exchange the machine credential for a short-lived access token.
$token = Invoke-RestMethod -Method Post -Uri $metadata.token_endpoint `
  -Headers @{ Authorization = "Basic $basicCredential" } `
  -ContentType 'application/x-www-form-urlencoded' `
  -Body @{
    grant_type = 'client_credentials'
    scope = 'customer.discovery.read customer.data.read'
  }

# Send the access token in the Authorization header, never in the URL.
Invoke-RestMethod -Uri 'https://api.wavac.io/api/customer/v1/discovery' `
  -Headers @{ Authorization = "Bearer $($token.access_token)" }

# Remove secret values from memory when the tutorial is complete.
$clientSecret = $null
$basicCredential = $null
$credentialBytes = $null
$token = $null
$secureSecret.Dispose()

Runnable .NET example

The C# client is cross-platform. Set its environment variables using the Linux or Windows instructions below, then place this commented example in Program.cs.

// Import HTTP authentication and JSON helpers from the .NET base class library.
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

// Read configuration from the process environment instead of source code.
var issuer = Required("OIDC_ISSUER").TrimEnd('/');
using var client = new HttpClient();

// Discover the token endpoint advertised by the configured OIDC issuer.
var metadata = await client.GetFromJsonAsync<OidcMetadata>(
    $"{issuer}/.well-known/openid-configuration");

// Request only the scopes needed by this machine-to-machine client.
using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, metadata!.TokenEndpoint) {
    Content = new FormUrlEncodedContent(new Dictionary<string, string> {
        ["grant_type"] = "client_credentials",
        ["client_id"] = Required("OIDC_CLIENT_ID"),
        ["client_secret"] = Required("OIDC_CLIENT_SECRET"),
        ["scope"] = "customer.discovery.read customer.data.read"
    })
};

// Reject an unsuccessful token response before attempting to read a token.
var tokenResponse = await client.SendAsync(tokenRequest);
tokenResponse.EnsureSuccessStatusCode();
var token = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();

// Attach the short-lived token to the API request and call discovery first.
client.DefaultRequestHeaders.Authorization = new("Bearer", token!.AccessToken);
Console.WriteLine(await client.GetStringAsync(
    "https://api.wavac.io/api/customer/v1/discovery"));

// Fail clearly when required process configuration is missing.
static string Required(string name) => Environment.GetEnvironmentVariable(name)
    ?? throw new InvalidOperationException($"Set {name}.");

// Map only the JSON properties this small tutorial client consumes.
sealed record OidcMetadata([property: JsonPropertyName("token_endpoint")] string TokenEndpoint);
sealed record TokenResponse([property: JsonPropertyName("access_token")] string AccessToken);

Linux: Run with Bash

# Create the console project once and replace Program.cs with the example above.
dotnet new console --name CustomerApiOAuth
cd CustomerApiOAuth

# Set credentials in this shell, then compile and run the tutorial.
export OIDC_ISSUER='https://your-issuer.example/application/o/customer-api/'
export OIDC_CLIENT_ID='your-client-id'
read -rsp 'Client secret: ' OIDC_CLIENT_SECRET && echo
export OIDC_CLIENT_SECRET
dotnet run

Windows: Run with PowerShell

# Create the console project once and replace Program.cs with the example above.
dotnet new console --name CustomerApiOAuth
Set-Location CustomerApiOAuth

# Set non-secret values for this process and securely prompt for the secret.
$env:OIDC_ISSUER = 'https://your-issuer.example/application/o/customer-api/'
$env:OIDC_CLIENT_ID = 'your-client-id'
$secret = Read-Host 'Client secret' -AsSecureString
$env:OIDC_CLIENT_SECRET = [System.Net.NetworkCredential]::new('', $secret).Password

# Compile and run, then clear the process-local plaintext value.
dotnet run
$env:OIDC_CLIENT_SECRET = $null
$secret.Dispose()

Runnable Python example

This client discovers the token endpoint, requests a client-credentials token, and calls discovery using only Python's standard library. Save it as customer_api_oauth.py.

# Import only Python standard-library modules; no package installation is required.
import base64
import json
import os
import urllib.parse
import urllib.request


# Fail clearly when required process configuration is missing.
def required(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"Set {name}.")
    return value


# Read machine configuration from the process environment instead of source.
issuer = required("OIDC_ISSUER").rstrip("/")
client_id = required("OIDC_CLIENT_ID")
client_secret = required("OIDC_CLIENT_SECRET")

# Discover the token endpoint advertised by the configured OIDC issuer.
with urllib.request.urlopen(
    f"{issuer}/.well-known/openid-configuration", timeout=30
) as response:
    metadata = json.load(response)

# Encode the client credential for OAuth HTTP Basic authentication.
basic_credential = base64.b64encode(
    f"{client_id}:{client_secret}".encode("utf-8")
).decode("ascii")

# Request only the scopes needed by this machine-to-machine client.
token_body = urllib.parse.urlencode(
    {
        "grant_type": "client_credentials",
        "scope": "customer.discovery.read customer.data.read",
    }
).encode("utf-8")
token_request = urllib.request.Request(
    metadata["token_endpoint"],
    data=token_body,
    headers={
        "Authorization": f"Basic {basic_credential}",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json",
    },
    method="POST",
)

# Exchange the machine credential for a short-lived access token.
with urllib.request.urlopen(token_request, timeout=30) as response:
    access_token = json.load(response)["access_token"]

# Send the access token in the Authorization header, never in the URL.
discovery_request = urllib.request.Request(
    "https://api.wavac.io/api/customer/v1/discovery",
    headers={
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json",
    },
)
with urllib.request.urlopen(discovery_request, timeout=30) as response:
    print(response.read().decode("utf-8"))

Linux: Run Python with Bash

# Set non-secret values for this shell and prompt without echoing the secret.
export OIDC_ISSUER='https://your-issuer.example/application/o/customer-api/'
export OIDC_CLIENT_ID='your-client-id'
read -rsp 'Client secret: ' OIDC_CLIENT_SECRET && echo
export OIDC_CLIENT_SECRET

# Run the client, then remove the secret from this shell.
python3 customer_api_oauth.py
unset OIDC_CLIENT_SECRET

Windows: Run Python with PowerShell

# Set non-secret values for this process and securely prompt for the secret.
$env:OIDC_ISSUER = 'https://your-issuer.example/application/o/customer-api/'
$env:OIDC_CLIENT_ID = 'your-client-id'
$secret = Read-Host 'Client secret' -AsSecureString
$env:OIDC_CLIENT_SECRET = [System.Net.NetworkCredential]::new('', $secret).Password

# Run the client, then clear the process-local plaintext value.
py customer_api_oauth.py
$env:OIDC_CLIENT_SECRET = $null
$secret.Dispose()

Token lifecycle

Cache the access token in memory until shortly before its expiry, then request a new token. On 401, discard the cached token and retry authentication once. Do not repeatedly request tokens for every data page.