Skip to main content

Provider Access Token Acquisition Guide

Audience: External provider teams integrating with Kontrolle+ Provider API.
Scope: How to obtain an access token. Does not cover payload schemas or submission rules.

:::info Machine-to-machine only Token flow is OAuth 2.0 client credentials — no user login is involved. :::


1. What You Need

Request the following values from the Kontrolle+ team before starting:

ValueDescription
Authority base URLMicrosoft Entra tenant endpoint
OAuth token endpointFull token URL
Client IDYour provider application ID
Client secretCredential for your application
Scope<provider-api-app-id-uri>/.default
Expected aud claimAudience expected by the API runtime

2. Request Format

POST <token-endpoint>
Content-Type: application/x-www-form-urlencoded

client_id=<provider-client-id>
client_secret=<provider-client-secret>
grant_type=client_credentials
scope=<provider-api-app-id-uri>/.default

3. curl — Request Token

curl -sS -X POST "https://<tenant-domain>/<tenant-id>/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=<provider-client-id>" \
--data-urlencode "client_secret=<provider-client-secret>" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=<provider-api-app-id-uri>/.default"

Successful response includes token_type, expires_in, and access_token.


4. curl — Extract Token to Variable

ACCESS_TOKEN=$(curl -sS -X POST "https://<tenant-domain>/<tenant-id>/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=<provider-client-id>" \
--data-urlencode "client_secret=<provider-client-secret>" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=<provider-api-app-id-uri>/.default" | jq -r '.access_token')

echo "$ACCESS_TOKEN"

5. Validate Token Claims

Before the first API call, check the token contains:

ClaimExpected value
audMust match the API runtime audience
rolesMust include ProviderApi.Access
expMust be in the future

Quick decode (no signature verification):

echo "$ACCESS_TOKEN" | awk -F '.' '{print $2}' | tr '_-' '/+' | base64 -d 2>/dev/null

6. Common Errors

ErrorCauseFix
400 invalid_clientWrong client_id/client_secret, disabled credential, or expired secretVerify credential pair and secret validity window
400 invalid_scopeIncorrect scope valueUse exact value <provider-api-app-id-uri>/.default
401 from APIIssuer/audience mismatch or expired tokenVerify authority, aud claim, and token lifetime
403 from APIMissing ProviderApi.Access role or client not allow-listedEnsure app role assignment is complete

7. Production Recommendations

  • Cache the token until shortly before expiry — do not request one per API call.
  • Rotate client secrets/certificates before expiration.
  • Store credentials only in a secure secret management system.
  • Never log full access tokens.

8. Integration Template (per environment)

Use this block as a runbook input when onboarding a new environment:

Environment: <ENV_NAME>
Authority: <AUTHORITY>
Token endpoint: <TOKEN_ENDPOINT>
Client ID: <CLIENT_ID>
Client secret delivery: <SECURE_CHANNEL_REFERENCE>
Scope: <PROVIDER_API_APP_ID_URI>/.default
Expected aud: <API_AUDIENCE>
Required role: ProviderApi.Access

See Also