Provider API Authentication Onboarding Guide
This guide explains how to onboard an external provider for machine-to-machine access to the Provider API. It focuses on Microsoft Entra registration, credential handling, and the exact integration package that must be shared with the provider.
Scope
- API surface:
POST /api/provider/v1/controls - Runtime: Data Service (
services/data-service) - Auth model: OAuth 2.0 client credentials with JWT bearer tokens
- Identity platform: Microsoft Entra External ID / Entra-compatible issuer
Current Implementation Summary
The Provider API is protected by a dedicated JWT authentication scheme named ProviderJwt and a dedicated authorization policy named ProviderApiPolicy.
Provider routes are selected onto that scheme explicitly and are not authenticated with the default internal user-token scheme.
The access token is accepted only when all of the following are true:
- The token issuer matches
ProviderJwt:Authority - The audience matches
ProviderJwt:Audience - The token is not expired
- The token contains
roles=ProviderApi.Access
Implementation references:
services/data-service/Program.csservices/data-service/Controllers/ProviderControlsController.csservices/data-service/appsettings.jsonopenapi/provider-controls.openapi.yaml
Recommended Operating Model
Use one confidential client application registration per provider.
Why this is the recommended model:
- Each provider gets isolated credentials
- Credentials can be rotated independently
- Access can be revoked per provider without affecting others
- Audit logs can be tied to a specific client application
For provider integrations, prefer application roles over delegated scopes.
Recommended standard:
- Primary authorization model:
roles=ProviderApi.Access - Token flow: OAuth 2.0 client credentials
- Client authentication: certificate preferred, client secret acceptable for lower environments
Registration Model
There are two application objects involved.
1. Provider API application
This is the application that represents the protected API.
It defines:
- The Application ID URI used during token acquisition
- The API identity whose issued
audclaim must match runtime validation - The app role exposed to calling clients
- The token issuer/tenant that signs valid tokens
Current audience rule in this repository:
ProviderJwt:Audiencemust match theaudclaim issued in provider access tokens- The chosen audience model is the API app client ID GUID when Entra emits that GUID in
aud - The Application ID URI remains the token acquisition identifier used in
scope=<app-id-uri>/.default
Current configured role expected by policy:
ProviderApi.Access
2. Provider client application
This is the confidential client used by one external provider.
It holds:
client_id- A client secret or certificate credential
- Permission to call the Provider API
Each provider should receive a separate client application.
What You Configure Internally
For each new provider, create or assign the following:
- A dedicated client application registration for that provider
- A client secret or certificate credential
- The Provider API application permission assignment
- The
ProviderApi.Accessapp role assignment to that client - Environment-specific base URL information for each target environment
You should also record internally:
- Provider name
- Owning organization
- Environment access granted
- Credential expiry date
- Rotation owner
- Technical contact
What You Send To The Provider
Do not send “the app registration” as an abstract concept. Send a concrete integration package.
The provider needs the following values:
Required integration values
Authorityor issuer base URL- OAuth 2.0 token endpoint
Client ID- Client credential material:
- client secret, or
- certificate onboarding instructions
- API audience / resource identifier
- Required permission name
- API base URL
- Reference documentation for endpoints and payloads
Recommended provider onboarding package
Use a template like this:
Provider API Integration Package
Environment: <target-environment>
Authority:
https://<tenant>.ciamlogin.com/<tenant>/v2.0
Token endpoint:
https://<tenant>.ciamlogin.com/<tenant>/oauth2/v2.0/token
Client ID:
<provider-client-id>
Client authentication:
Client secret provided through secure channel
API audience:
Use the API app client ID GUID (the value emitted in the token `aud` claim).
Token scope:
Use the Provider API Application ID URI with `/.default`.
Required application permission:
ProviderApi.Access
API base URL:
https://<data-service-host>/api/provider/v1
Endpoints:
POST /controls
Documentation:
documentation/v2/api/16_PROVIDER_API_NOVA_DATA_MAPPING.md
documentation/v2/api/09_PROVIDER_AUTH_ONBOARDING_GUIDE.md
What The Provider Must Do
The provider system must:
- Store the client credential securely
- Request an access token from the token endpoint
- Ask for a token for the configured audience
- Call the Provider API with
Authorization: Bearer <access_token>
Conceptually, the flow is:
- Provider client authenticates to Microsoft Entra
- Microsoft Entra issues an access token for the Provider API
- Provider sends the token to the Data Service
- Data Service validates issuer, audience, lifetime, and permission claims
- Request is accepted or rejected
Client Credentials Flow Example
Example token request shape:
POST /<tenant>/oauth2/v2.0/token
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
Notes:
- In client credentials flow, using
/.defaultis the usual pattern - The effective claims in the token are derived from app role / application permission assignment
- The provider should not request user-delegated permissions for this integration
- Microsoft Entra can issue the API application's client ID as the
audclaim even when the token was requested using the Application ID URI inscope. - That means the API app client ID GUID is the intended runtime audience value.
Request Example Against The API
curl -X POST "https://<data-service-host>/api/provider/v1/controls" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <provider_access_token>" \
-d @provider-payload.json
Identity Mapping In The Service
The current implementation resolves the provider identity in this order:
azpappidclient_idX-Provider-Idunknown-provider
This means:
- The real authenticated identity should come from token claims
X-Provider-Idmust be treated as tracing metadata only- It must not be treated as proof of identity
Validation Checklist Before Hand-Off
Before sending credentials to the provider, validate the following:
- The Provider API app exposes the expected audience
- The provider client has the correct app role assignment
- A token can be acquired with client credentials
- The token contains the expected
roles=ProviderApi.Accessclaim - The token
audmatchesProviderJwt:Audience - The provider client ID is present in
ProviderJwt:AllowedClientIdswhen allowlisting is enabled - A test call to
POST /api/provider/v1/controlsreturns201or200for duplicates
Verified local development example:
- Token acquisition scope:
api://kontrolleplus-provider-api-dev/.default - Issued
audclaim:747deab7-cdf4-4c36-9d77-2ab600fa8743 - Required role:
ProviderApi.Access
This same emitted-audience pattern is the model to apply in each target environment.
Repository validation helper:
scripts/test-provider-ingest.shscripts/validate-provider-jwt.shscripts/get-and-validate-provider-jwt.sh
Example claim validation before allowlisting:
export PROVIDER_JWT_TOKEN="<provider-access-token>"
export EXPECTED_PROVIDER_AUDIENCE="<provider-api-client-id-guid>"
export EXPECTED_PROVIDER_CLIENT_ID="<provider-client-app-id-guid>"
export EXPECTED_PROVIDER_ROLE="ProviderApi.Access"
./scripts/validate-provider-jwt.sh
One-command token acquisition + validation:
export PROVIDER_TENANT_ID="<tenant-guid-or-domain>"
export PROVIDER_CLIENT_ID="<provider-client-app-id-guid>"
export PROVIDER_CLIENT_SECRET="<provider-client-secret>"
export PROVIDER_TOKEN_SCOPE="api://<provider-api-app-id-uri>/.default"
export PROVIDER_TOKEN_ENDPOINT="https://<tenant>.ciamlogin.com/<tenant>/oauth2/v2.0/token"
export EXPECTED_PROVIDER_AUDIENCE="<provider-api-client-id-guid>"
export EXPECTED_PROVIDER_CLIENT_ID="<provider-client-app-id-guid>"
export EXPECTED_PROVIDER_ROLE="ProviderApi.Access"
./scripts/get-and-validate-provider-jwt.sh
Security Requirements
Use these controls by default:
- Prefer certificates over shared client secrets in production
- Deliver secrets through a secure channel only
- Never send secrets in email or chat
- Set explicit secret expiry dates
- Track credential owners and rotation dates
- Use one client application per provider
- Revoke access by disabling the client or removing app role assignment when needed
Operational Recommendations
Recommended
- One provider = one client application
- One environment = separate credential set
- Application role based authorization
- Certificate-based authentication in production
Avoid
- Shared credentials across multiple providers
- Using
X-Provider-Idas an identity mechanism - Mixing user tokens with provider machine tokens
- Reusing credentials across environments
Current Authorization Standard
Provider integrations must use application-role based authorization.
Required token claim:
roles=ProviderApi.Access
This repository no longer accepts scope-based authorization for provider access.
Provider Hand-Off Template
Use the following short-form message when sending onboarding details to a provider.
Subject: Provider API Access Details
Below are the credentials and endpoints required to integrate with the Kontrolle+ Provider API.
Environment:
<target-environment>
Authority:
<authority-url>
Token endpoint:
<token-endpoint>
Client ID:
<client-id>
Credential delivery:
Provided separately through secure channel
API audience:
<provider-api-client-id-guid>
Token scope:
<provider-api-app-id-uri>/.default
Required permission:
ProviderApi.Access
API base URL:
<provider-api-base-url>
Required header:
Authorization: Bearer <access_token>
Reference documentation:
- documentation/v2/api/16_PROVIDER_API_NOVA_DATA_MAPPING.md
- documentation/v2/api/09_PROVIDER_AUTH_ONBOARDING_GUIDE.md
Provider Token Request Example (Copy/Paste)
Share this exact pattern with providers so they can obtain an access token using client credentials.
TOKEN_RESPONSE=$(curl -sS -X POST "<token-endpoint>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=<provider-client-id>" \
-d "client_secret=<provider-client-secret>" \
-d "grant_type=client_credentials" \
-d "scope=<provider-api-app-id-uri>/.default")
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
echo "$TOKEN_RESPONSE" | jq '{token_type, expires_in, error, error_description}'
Expected token checks:
audequals<provider-api-client-id-guid>rolescontainsProviderApi.Accessazp(orappid/client_id) equals<provider-client-id>
Example API call with the token:
curl -X POST "<provider-api-base-url>/controls" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d @provider-payload.json
Related Documents
documentation/v2/api/16_PROVIDER_API_NOVA_DATA_MAPPING.mdopenapi/provider-controls.openapi.yamlservices/data-service/Program.csscripts/test-provider-ingest.sh
Extension: Future Security Hardening Roadmap (ACA)
This section extends the current Provider API Authentication approach with a forward-looking security roadmap. Current behavior remains unchanged unless explicitly implemented.
Current Baseline (Already Implemented)
- OAuth 2.0 client credentials via Microsoft Entra
- Provider JWT validation in Data Service (issuer, audience, lifetime, role)
- Client application allowlisting (
ProviderJwt:AllowedClientId/ProviderJwt:AllowedClientIds)
This baseline is valid, but secret theft remains a residual risk.
Target Security Model
- Azure API Management (APIM) in front of provider endpoints
- Gateway IP allowlisting for provider egress ranges
- Mutual TLS (mTLS) for transport-level caller identity
- Certificate-based client authentication in Entra (replace shared secret)
- Keep backend JWT checks for defense in depth
Phase Plan
- Phase 0: Stabilize current provider auth
- Keep current JWT checks active.
- Rotate provider client secrets periodically.
- Add negative tests: wrong audience, wrong role, wrong client ID, expired token.
- Phase 1: Introduce APIM gateway
- Publish provider routes through APIM only.
- Add JWT validation policy in APIM.
- Add rate limiting and quota policy.
- Phase 2: Restrict source networks
- Collect provider outbound IP ranges.
- Enforce APIM IP allowlist policy.
- Block all other source IPs.
- Phase 3: Add mTLS
- Require client certificate at APIM custom domain.
- Validate trusted issuer/thumbprint policy.
- Map cert identity to provider onboarding record.
- Phase 4: Replace secret-based auth
- Migrate provider clients from secret to certificate credentials.
- Remove production secret usage from onboarding templates.
- Enforce credential rotation policy and expiry monitoring.
- Phase 5: Operational hardening
- Add monitoring and alerts for auth failures, IP blocks, mTLS failures, and rate limits.
- Maintain an incident playbook for credential compromise and rapid revocation.
- Perform periodic provider access reviews.
Design Principles
- Keep business code in
services/platform-agnostic. - Implement transport and edge controls in infrastructure/gateway layers.
- Preserve backend claim validation even after gateway controls are introduced.
Suggested Adoption Order
- APIM gateway + JWT policy
- IP allowlisting
- mTLS
- Certificate-based Entra client authentication
Exit Criteria Per Phase
- Phase 1: Provider traffic is routed via APIM and JWT policy is enforced.
- Phase 2: Only allowlisted provider egress IPs can reach the gateway.
- Phase 3: Calls require valid client certificate plus JWT.
- Phase 4: Production providers no longer use client secrets.
- Phase 5: Monitoring and incident response are tested and documented.