Downstream API Calls¶
This guide explains how to configure your Forge application to call other APIs using Kiota-generated clients with proper authentication and scope configuration.
| Property | Value |
|---|---|
| Goal | Call a downstream API from your application |
| Prerequisites | Downstream API deployed, your app authorized in its config |
| Time Estimate | 15-30 minutes |
| Difficulty | Intermediate |
π Overview¶
When your application needs to call another API (a "downstream API"), you need to:
- Infrastructure: Be listed as an authorized app in the downstream API's configuration
- Code: Configure a Kiota client with the correct scopes to request access tokens
This guide focuses on the code sideβhow to configure Kiota clients with scopes. For infrastructure configuration, see the Security Configuration Guide.
How It Works¶
Your App β Requests Token β With Scopes β Calls Downstream API
(from Entra/Okta) (User.Read) (validates token)
The platform handles token acquisition automatically using the SAIF.Platform.Kiota.HttpClientLibrary package. You just need to:
- Add a Kiota-generated client to your project
- Configure the client with the target API's project ID and required scopes
- Inject and use the client in your code
π Quick Start¶
Step 1: Add a KiotaReference to Your Project¶
Add a <KiotaReference> element to your .csproj file that points to the downstream API's OpenAPI specification:
<ItemGroup>
<KiotaReference Include="DownstreamClient" OpenApi="https://openapi.saif.com/it-api-sys-downstream/test/openapi.v1.yaml">
<NamespaceName>YourApp.Clients.Downstream</NamespaceName>
</KiotaReference>
</ItemGroup>
This generates a strongly-typed client at build time. The Include attribute becomes the client class name.
OpenAPI URL pattern: https://openapi.saif.com/{project-id}/{environment}/openapi.v1.yaml
| Environment | URL Path |
|---|---|
| Test | /test/ |
| Production | /prod/ |
π‘ Tip: Use the
testenvironment URL during development. The client code is the same regardless of environmentβonly the runtime configuration changes.
Step 2: Configure the HTTP Client¶
In your Program.cs, configure the Kiota client with the downstream API's project ID and required scopes:
// Configure Kiota client for downstream API
builder.ConfigureHttpClient<DownstreamApiClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-downstream", // Project ID of the downstream API
options =>
{
options.Scopes = ["User.Read"]; // Scopes your app needs
});
Step 3: Use the Client¶
Inject the client into your endpoint or service:
app.MapGet("/data", async (DownstreamApiClient client) =>
{
var result = await client.Resources.GetAsync();
return Results.Ok(result);
});
That's it! The platform automatically:
- Acquires tokens from the correct identity provider (Entra or Okta)
- Adds the required scopes to the token request
- Attaches the token to outgoing requests
π― Scope Naming Best Practices¶
When requesting scopes from downstream APIs, use names that clearly indicate whether you're calling on behalf of a user or directly as a service.
Use when calling APIs on behalf of a user (with user context).
Recommended scope naming: User.*, Profile.*, Account.*
Example configuration:
builder.ConfigureHttpClient<DownstreamClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-downstream",
options => options.Scopes = ["User.Read", "User.Write"]);
When to use: - Frontend applications calling your API - Experience APIs calling Process APIs with user context - Any scenario requiring user identity
Use for direct service-to-service calls with no user context.
Recommended scope naming: Client.*, Service.*, Data.*
Example configuration:
builder.ConfigureHttpClient<DataSyncClient, ClientCredentialsTokenProvider>(
"it-api-sys-datasync",
options => options.Scopes = ["Client.Read", "Client.Write"]);
When to use: - Background jobs or scheduled tasks - System-to-system integrations - No user is involved in the request
Match Downstream API Configuration
The scope names you request must match what the downstream API has defined in their infra/api/config.yml (Entra) or infra/auth/ext/okta-client/scopes.yml (Okta).
βοΈ Configuration Options¶
KiotaReference Options¶
The <KiotaReference> element supports several configuration options:
<KiotaReference Include="ClientName" OpenApi="https://openapi.saif.com/project-id/test/openapi.v1.yaml">
<NamespaceName>YourApp.Clients.ClientName</NamespaceName>
<IncludePath>/pets;/pets/{id}#GET</IncludePath>
<ExcludePath>/admin/**</ExcludePath>
</KiotaReference>
| Property | Description | Example |
|---|---|---|
Include |
Client class name (required) | BrisheClient |
OpenApi |
URL or path to OpenAPI specification (required) | https://openapi.saif.com/.../openapi.v1.yaml |
NamespaceName |
Namespace for generated code | YourApp.Clients.Brishe |
IncludePath |
Limit generation to specific paths (semicolon-separated) | /pets;/pets/{id}#GET |
ExcludePath |
Exclude specific paths from generation | /admin/** |
π‘ Tip: Use
IncludePathto generate only the endpoints you need, reducing build time and code size.
ConfigureHttpClient Method¶
The ConfigureHttpClient<TClient, TAccessTokenProvider>() extension method registers a Kiota client with authentication:
builder.ConfigureHttpClient<TClient, TAccessTokenProvider>(
projectId: "it-api-sys-target",
configure: options =>
{
options.Scopes = ["User.Read", "User.Write"];
options.PrefixScopesWithProjectId = true; // Default: true
options.AllowedSchemes = ["https", "http"]; // Default: ["https", "http"]
});
Parameters¶
| Parameter | Type | Description |
|---|---|---|
TClient |
Type | The Kiota-generated client class |
TAccessTokenProvider |
Type | Token provider (see Token Providers) |
projectId |
string |
The downstream API's project ID |
configure |
Action<HttpClientConfigurationOptions> |
Optional configuration callback |
HttpClientConfigurationOptions¶
| Property | Type | Default | Description |
|---|---|---|---|
Scopes |
string[] |
[] |
Scopes to request from the downstream API |
PrefixScopesWithProjectId |
bool |
true |
Whether to prefix scopes with the project ID |
AllowedSchemes |
string[] |
["https", "http"] |
Allowed URL schemes for service discovery |
π Token Providers¶
Choose the appropriate token provider based on your authentication flow:
TokenExchangeAccessTokenProvider (User Delegation)¶
Use when calling APIs on behalf of the current user. The token includes the user's identity and permissions.
builder.ConfigureHttpClient<ApiClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-downstream",
options => options.Scopes = ["User.Read"]);
When to use:
- β Your API is called by a user (via frontend or another API)
- β The downstream API needs to know who the user is
- β Authorization decisions depend on user identity
Required infrastructure scope:
- Corporate (Entra):
user_impersonation+ your scopes - External (Okta):
user-groups+ your scopes
ClientCredentialsTokenProvider (App-to-App)¶
Use for service-to-service calls where there is no user context. The token represents your application, not a user.
When to use:
- β Background jobs or scheduled tasks
- β System-to-system integrations
- β No user is involved in the request
Required infrastructure:
- Your app must have
app_rolesgranted in the downstream API'sauthorized_appsconfig
π― Scope Configuration¶
Understanding Scope Prefixing¶
By default, scopes are prefixed with the project ID to create fully-qualified scope names. The format differs by identity provider:
| Tenant | Scope Format | Example |
|---|---|---|
| External | {projectId}.{scope} |
it-api-sys-downstream.User.Read |
| Corp | api://{projectId}-{environment}/{scope} |
api://it-api-sys-downstream-production/User.Read |
Automatic Delegation Scopes¶
The platform automatically adds the required delegation scope if not present:
- External (Okta): Adds
user-groupsautomatically - Corporate (Entra): Adds
user_impersonationautomatically
You don't need to include these in your options.Scopes arrayβthey're added for you.
Example: Requesting Multiple Scopes¶
builder.ConfigureHttpClient<ApiClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-downstream",
options =>
{
options.Scopes = ["User.Read", "User.Write", "User.Admin"];
});
This results in tokens with:
- External:
it-api-sys-downstream.User.Read,it-api-sys-downstream.User.Write,it-api-sys-downstream.User.Admin,it-api-sys-downstream.user-groups - Corp:
api://it-api-sys-downstream-{env}/User.Read,api://it-api-sys-downstream-{env}/User.Write,api://it-api-sys-downstream-{env}/User.Admin,api://it-api-sys-downstream-{env}/user_impersonation
Disabling Scope Prefixing¶
For special cases where you need to use raw scope values:
builder.ConfigureHttpClient<ApiClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-downstream",
options =>
{
options.Scopes = ["custom-scope"];
options.PrefixScopesWithProjectId = false; // Use scopes as-is
});
β οΈ Warning: Only disable prefixing if you have a specific reason. The default prefixing ensures scopes are correctly formatted for each identity provider.
π Connecting Code to Infrastructure¶
Your code-side scope configuration must match the infrastructure configuration in the downstream API. Here's how they connect:
Your App (Upstream)¶
Code (Program.cs):
builder.ConfigureHttpClient<DownstreamClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-downstream",
options => options.Scopes = ["User.Read"]);
Downstream API (Infrastructure)¶
Corporate (infra/auth/corp/config.yml):
authorized_apps:
- project_id: your-app-project-id # Your app's project ID
scopes:
- user_impersonation # Required for delegation
- User.Read # Must match your code
External (infra/auth/ext/app/authorized-apps.yml):
authorized_apps:
- project_id: your-app-project-id
scopes:
- user-groups # Required for delegation
- User.Read # Must match your code
Checklist¶
Before calling a downstream API, verify:
- Your app's project ID is in the downstream API's
authorized_apps - The scopes you request in code match those granted in infrastructure
- Delegation scope is included (
user_impersonation/user-groups) - Both corp and ext configurations are in sync (if supporting both tenants)
π‘ Examples¶
Example 1: Calling a System API for User Data¶
Your Experience API needs to call a System API to fetch user profiles:
// Program.cs
builder.ConfigureHttpClient<UserProfileClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-userprofile",
options => options.Scopes = ["Profile.Read"]);
// Endpoint
app.MapGet("/my-profile", async (UserProfileClient client, ClaimsPrincipal user) =>
{
var userId = user.FindFirstValue("sub");
var profile = await client.Users[userId].GetAsync();
return Results.Ok(profile);
});
Example 2: Background Job Calling an API¶
A background service that syncs data without user context:
// Program.cs - For Entra ID (corp) use .default
builder.ConfigureHttpClient<DataSyncClient, ClientCredentialsTokenProvider>(
"it-api-sys-datasync",
options => options.Scopes = [".default"]);
// Program.cs - For Okta (ext) use explicit scopes
builder.ConfigureHttpClient<DataSyncClient, ClientCredentialsTokenProvider>(
"it-api-sys-datasync",
options => options.Scopes = ["Sync.Read", "Sync.Write"]);
// Background service
public class SyncService(DataSyncClient client) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var data = await client.Sync.GetAsync();
// Process data...
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
Example 3: API Calling Multiple Downstream APIs¶
An orchestration API that aggregates data from multiple sources:
// Program.cs
builder
.ConfigureHttpClient<ClaimsClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-claims",
options => options.Scopes = ["Claim.Read"])
.ConfigureHttpClient<PolicyClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-policy",
options => options.Scopes = ["Policy.Read"])
.ConfigureHttpClient<CustomerClient, TokenExchangeAccessTokenProvider>(
"it-api-sys-customer",
options => options.Scopes = ["Customer.Read"]);
// Endpoint
app.MapGet("/dashboard/{customerId}", async (
string customerId,
ClaimsClient claimsClient,
PolicyClient policyClient,
CustomerClient customerClient) =>
{
var customerTask = customerClient.Customers[customerId].GetAsync();
var claimsTask = claimsClient.Claims.GetAsync(q => q.QueryParameters.CustomerId = customerId);
var policiesTask = policyClient.Policies.GetAsync(q => q.QueryParameters.CustomerId = customerId);
await Task.WhenAll(customerTask, claimsTask, policiesTask);
return Results.Ok(new
{
Customer = customerTask.Result,
Claims = claimsTask.Result,
Policies = policiesTask.Result
});
});
π Troubleshooting¶
"401 Unauthorized" When Calling Downstream API¶
Possible causes:
- Your app is not authorized: Check that your project ID is in the downstream API's
authorized_appsconfiguration - Scopes don't match: Ensure the scopes in your code match those granted in infrastructure
- Missing delegation scope: Verify
user_impersonation(corp) oruser-groups(ext) is included
Debug steps:
// Add logging to see what scopes are being requested
builder.Services.AddLogging(logging => logging.SetMinimumLevel(LogLevel.Debug));
"403 Forbidden" After Successful Authentication¶
Possible causes:
- Insufficient scopes: You're authenticated but lack the required scope
- App role required: The endpoint requires an app role, not a scope
Check:
- Review the downstream API's
@useAuthdecorator in TypeSpec - Verify you're using the correct token provider
Token Not Being Acquired¶
Possible causes:
- Downstream API not deployed: The downstream API must be deployed to the environment before your app can call it
- Service discovery failure: The downstream API's service keys don't exist in Azure App Configuration
How service configuration works:
This is fully automated by the platformβyou don't configure anything manually:
- Your app's connection to App Configuration is set up automatically by Terraform:
AppConfigurationEndpointis injected from shared servicesProjectIdidentifies your app for key selection-
Managed identity (
DefaultAzureCredential) handles authentication -
Downstream API service keys are created automatically when each API is deployed. The platform registers keys like
Services:{project-id}:*containing the service URL and auth server details.
Debug steps:
- Verify the downstream API is deployed to your target environment (check Azure DevOps pipeline)
- Confirm both apps are deployed to the same environment (test, qa, uat, production)
- Check the downstream API's deployment logs for any Terraform errors
π Related Documentation¶
- Security Configuration Guide - Infrastructure-side auth configuration
- Kiota Tool Reference - Kiota client generation
- Settings and Secrets - Configuration management
- TypeSpec - API contract definition