Skip to content

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:

  1. Infrastructure: Be listed as an authorized app in the downstream API's configuration
  2. 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:

  1. Add a Kiota-generated client to your project
  2. Configure the client with the target API's project ID and required scopes
  3. 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 test environment 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 IncludePath to 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.

builder.ConfigureHttpClient<ApiClient, ClientCredentialsTokenProvider>(
    "it-api-sys-downstream",
    options => options.Scopes = [".default"]);

The .default scope is an Entra ID pattern that requests all static permissions configured for the app registration.

builder.ConfigureHttpClient<ApiClient, ClientCredentialsTokenProvider>(
    "it-api-sys-downstream",
    options => options.Scopes = ["App.Read"]);

Okta requires explicit scopesβ€”.default is not supported. Specify the app roles your app needs.

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_roles granted in the downstream API's authorized_apps config

🎯 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-groups automatically
  • Corporate (Entra): Adds user_impersonation automatically

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:

  1. Your app is not authorized: Check that your project ID is in the downstream API's authorized_apps configuration
  2. Scopes don't match: Ensure the scopes in your code match those granted in infrastructure
  3. Missing delegation scope: Verify user_impersonation (corp) or user-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:

  1. Insufficient scopes: You're authenticated but lack the required scope
  2. App role required: The endpoint requires an app role, not a scope

Check:

  • Review the downstream API's @useAuth decorator in TypeSpec
  • Verify you're using the correct token provider

Token Not Being Acquired

Possible causes:

  1. Downstream API not deployed: The downstream API must be deployed to the environment before your app can call it
  2. 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:

  1. Your app's connection to App Configuration is set up automatically by Terraform:
  2. AppConfigurationEndpoint is injected from shared services
  3. ProjectId identifies your app for key selection
  4. Managed identity (DefaultAzureCredential) handles authentication

  5. 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:

  1. Verify the downstream API is deployed to your target environment (check Azure DevOps pipeline)
  2. Confirm both apps are deployed to the same environment (test, qa, uat, production)
  3. Check the downstream API's deployment logs for any Terraform errors