Microsoft Agent Framework + Copilot Studio Delegated Auth Demo

July 7, 2026 ยท View on GitHub

This demo shows a local, customer-runnable Microsoft Agent Framework (MAF) application that calls an existing Microsoft Copilot Studio agent with the signed-in user's delegated identity.

The app is:

  • Frontend: React + Vite using MSAL (@azure/msal-browser, @azure/msal-react) for browser sign-in.
  • Backend: FastAPI on Python 3.13 using Microsoft Agent Framework (agent-framework, agent-framework-copilotstudio) and microsoft-agents-copilotstudio-client.
  • Auth: Two single-tenant Microsoft Entra app registrations: a public SPA app and a confidential backend/API app. The backend validates the browser API access token and uses On-Behalf-Of (OBO) to get a Power Platform token for Copilot Studio.
  • Protocol: AG-UI over Server-Sent Events (SSE) from POST /api/agent.
  • Orchestration: .NET Aspire from src/apphost.mts, with Redis cache support and the Aspire dashboard.

Architecture

Current pinned local ports from src/apphost.mts are:

  • Frontend: http://localhost:5173
  • Backend API health endpoint: https://localhost:8080/health
  • Backend route: POST /api/agent returns text/event-stream
  • Browser API calls use the frontend's relative /api proxy; users interact with http://localhost:5173.
  • Redis resource: Aspire resource name cache, injected into the backend as CACHE_URI
flowchart LR
    Browser[Browser / React + MSAL\nhttp://localhost:5173]
    Entra[Microsoft Entra ID]
    Api[FastAPI + MAF\nhttps://localhost:8080]
    Redis[(Redis cache\nAspire resource: cache)]
    PP[Power Platform API\nhttps://api.powerplatform.com/.default]
    Agent[Copilot Studio published agent]
    Dash[Aspire dashboard\nlogs + traces]

    Browser -- MSAL sign-in + PKCE --> Entra
    Browser -- delegated access token via relative /api/agent proxy --> Api
    Api -- validate issuer/audience/scope via JWKS --> Entra
    Api -- confidential-client OBO --> PP
    PP -- delegated token --> Api
    Api -- CopilotClient + CopilotStudioAgent --> Agent
    Agent -- streamed activities --> Api
    Api -- AG-UI SSE TEXT_MESSAGE_* --> Browser
    Api <-- CACHE_URI --> Redis
    Api -- OTLP traces/logs --> Dash

Delegated-auth flow

  1. The browser loads the React app from http://localhost:5173.
  2. MSAL signs the user in through the SPA app registration using PKCE and no client secret.
  3. MSAL requests the backend API scope from VITE_ENTRA_API_SCOPE, for example api://<backend-client-id>/admin-consent.
  4. The React AG-UI client calls the backend through Vite's relative /api proxy with an Authorization header containing the API access token. Aspire injects APP_HTTPS, so Vite reaches the TLS backend endpoint.
  5. FastAPI validates the token in src/app/auth.py: issuer, signature, audience, nbf, expiration, and required delegated scope.
  6. The backend confidential client uses MSAL OBO to request ENTRA_OBO_DOWNSTREAM_SCOPE=https://api.powerplatform.com/.default.
  7. src/app/agent.py builds ConnectionSettings with COPILOTSTUDIOAGENT__ENVIRONMENTID and COPILOTSTUDIOAGENT__SCHEMANAME, then creates CopilotClient and CopilotStudioAgent.
  8. Copilot Studio returns streamed activities. The backend maps text to AG-UI TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, and TEXT_MESSAGE_END SSE events.
  9. React renders the assistant response.

App sign-in App agent response This screenshot was captured after the demo agent hit a temporary usage limit. A correctly configured and published agent returns a substantive answer.

Prerequisites

Install or confirm these before configuring Entra and Copilot Studio:

  • Docker Desktop, running locally. Aspire uses it for Redis.
  • .NET SDK with the Aspire CLI.
  • Node.js matching src/frontend/package.json: ^20.19.0 || >=22.12.0.
  • Python 3.13 or newer.
  • uv for Python dependency management.
  • A Microsoft Entra tenant where you can create app registrations and grant admin consent.
  • An existing, published Copilot Studio agent in a Power Platform environment.
  • Admin access to share that Copilot Studio agent with end users.

Install the Aspire CLI with the .NET tool command:

dotnet tool install -g Aspire.Cli --prerelease
# If it is already installed:
dotnet tool update -g Aspire.Cli --prerelease
aspire --version

Microsoft Entra app registrations

Create two single-tenant app registrations: one confidential backend/API app and one public SPA/frontend app.

1. Backend/API app registration

Create this app first. It represents the FastAPI backend, exposes the delegated API scope consumed by the SPA, and holds the confidential client secret used for OBO.

  1. In the Microsoft Entra admin center, go to Identity > Applications > App registrations > New registration.
  2. Name it something like maf-copilot-studio-demo-api.
  3. Set Supported account types to Accounts in this organizational directory only.
  4. Do not add a redirect URI for this backend app.
  5. Save these values:
Entra valuePut it here
Directory (tenant) IDENTRA_TENANT_ID, COPILOTSTUDIOAGENT__TENANTID, and part of VITE_ENTRA_AUTHORITY
Application (client) IDENTRA_BACKEND_CLIENT_ID and COPILOTSTUDIOAGENT__AGENTAPPID; do not substitute the Copilot Studio Metadata page's different Agent app ID

Expose an API

  1. Open the backend app registration.
  2. Go to Expose an API.
  3. Set the Application ID URI to api://<backend-client-id>.
  4. Add a delegated scope.
  5. This demo currently expects the scope name admin-consent because ENTRA_BACKEND_REQUIRED_SCOPE defaults to admin-consent and src/frontend/.env.example uses api://<backend-client-id>/admin-consent.
  6. If you choose a different scope name, update both:
    • ENTRA_BACKEND_REQUIRED_SCOPE=<your-scope-name> in src/app/.env
    • VITE_ENTRA_API_SCOPE=api://<backend-client-id>/<your-scope-name> in src/frontend/.env.local
Expose API valuePut it here
Application ID URI api://<backend-client-id>ENTRA_BACKEND_EXPECTED_AUDIENCE
Scope name admin-consentENTRA_BACKEND_REQUIRED_SCOPE
Full scope api://<backend-client-id>/admin-consentVITE_ENTRA_API_SCOPE

Backend Expose an API

Create a client secret

  1. Go to Certificates & secrets.
  2. Create a new client secret.
  3. Copy the Value immediately.
  4. Put it in ENTRA_BACKEND_CLIENT_SECRET in src/app/.env only.

Never put this secret in frontend config and never commit it.

Backend client secret

  1. Go to API permissions on the backend app registration.
  2. Choose Add a permission.
  3. Find Power Platform API. If searching by name is ambiguous, use app ID 8578e004-a5c6-46e7-913e-12f58912df43.
  4. Choose Delegated permissions.
  5. Add CopilotStudio.Copilots.Invoke.
  6. Click Grant admin consent for the tenant.

Admin consent is required for the downstream OBO call to Copilot Studio, even if the portal UI suggests consent is not required for the permission.

The backend OBO scope remains:

ENTRA_OBO_DOWNSTREAM_SCOPE=https://api.powerplatform.com/.default

Backend API permissions and admin consent

2. Frontend/SPA app registration

Create this app for the React browser client. It is a public client that uses PKCE and has no secret.

  1. Go to Identity > Applications > App registrations > New registration.
  2. Name it something like maf-copilot-studio-demo-spa.
  3. Set Supported account types to Accounts in this organizational directory only.
  4. After creation, copy the Application (client) ID to VITE_ENTRA_SPA_CLIENT_ID.
  5. Set VITE_ENTRA_AUTHORITY=https://login.microsoftonline.com/<tenant-id>.

Add the SPA redirect URI

  1. Open the SPA app registration.
  2. Go to Authentication.
  3. Add a Single-page application platform.
  4. Add this redirect URI exactly:
http://localhost:5173
  1. Put the same value in VITE_ENTRA_REDIRECT_URI.

SPA redirect URI

Add delegated permission to the backend API

  1. Go to API permissions on the SPA app registration.
  2. Choose Add a permission > My APIs.
  3. Select the backend/API app registration you created above.
  4. Add the delegated scope, normally admin-consent.
  5. Grant tenant consent if your tenant requires it.

The full scope goes in VITE_ENTRA_API_SCOPE, for example:

VITE_ENTRA_API_SCOPE=api://<backend-client-id>/admin-consent

SPA backend API permission

Copilot Studio configuration

Use an existing Copilot Studio agent and collect the values below.

Environment ID

Find the Power Platform environment ID on the Copilot Studio agent metadata page:

  1. Open Copilot Studio.
  2. Open your agent.
  3. Go to Settings > Advanced > Metadata.
  4. Copy Environment ID, which looks like Default-<tenant-guid>.
  5. Put it in COPILOTSTUDIOAGENT__ENVIRONMENTID.

The same Metadata panel also shows Tenant ID, Agent app ID, and Schema name.

Copilot Studio metadata environment ID

Agent schema name

The MAF ConnectionSettings uses the Copilot Studio schema name, not the display name and not a GUID.

  1. Open Copilot Studio.
  2. Open your agent.
  3. Go to Settings > Advanced > Metadata.
  4. Copy Schema name, for example crf1e_productAdvisor.
  5. Put it in COPILOTSTUDIOAGENT__SCHEMANAME.

The schema name is the publisher-prefix plus agent name, like crf1e_productAdvisor; it is not the display name and not a GUID. Using the display name or an unrelated GUID can produce a Copilot Studio 404 from the backend.

Copilot Studio metadata schema name

Publish and share the agent

  1. Publish the agent before running the demo. The backend uses AgentType.PUBLISHED.
  2. Share the agent with the users who will sign in to the React app.
  3. Grant End user access so delegated users can talk to the bot.

If the app signs in and the chat response says You don't have access to talk to this bot, contact the owner., the OBO path worked but the agent has not been shared with that user.

Copilot Studio publish dialog Copilot Studio share users

Streaming behavior

There is no explicit Copilot Studio streaming toggle for this agent. Native token streaming depends on the agent's generative orchestration and model behavior under Settings > Generative AI. If Copilot Studio sends only a single final message, src/app/agent.py uses a chunked fallback so the React UI still renders progressively.

Environment files

Copy the examples and fill in your tenant-specific values. These local files are ignored by git and must not be committed.

From the repo root:

Copy-Item .\src\app\.env.example .\src\app\.env
Copy-Item .\src\frontend\.env.example .\src\frontend\.env.local

Backend: src/app/.env

KeySource
ENTRA_TENANT_IDEntra tenant overview: Directory (tenant) ID
ENTRA_BACKEND_CLIENT_IDBackend/API app registration: Application (client) ID
ENTRA_BACKEND_CLIENT_SECRETBackend/API app registration: Certificates & secrets secret Value
ENTRA_BACKEND_EXPECTED_AUDIENCEBackend/API app registration: Application ID URI, normally api://<backend-client-id>
ENTRA_BACKEND_REQUIRED_SCOPEBackend/API app registration: exposed delegated scope name, default admin-consent
ENTRA_OBO_DOWNSTREAM_SCOPELiteral Power Platform downstream scope: https://api.powerplatform.com/.default
COPILOTSTUDIOAGENT__ENVIRONMENTIDCopilot Studio agent: Settings > Advanced > Metadata > Environment ID
COPILOTSTUDIOAGENT__SCHEMANAMECopilot Studio agent: Settings > Advanced > Metadata > Schema name, for example crf1e_productAdvisor
COPILOTSTUDIOAGENT__AGENTAPPIDBackend/API app registration Application (client) ID. Do not use the different Agent app ID shown on the Copilot Studio Metadata page for this demo.
COPILOTSTUDIOAGENT__TENANTIDSame tenant GUID as ENTRA_TENANT_ID

Frontend: src/frontend/.env.local

KeySource
VITE_ENTRA_SPA_CLIENT_IDSPA/frontend app registration: Application (client) ID
VITE_ENTRA_AUTHORITYhttps://login.microsoftonline.com/<tenant-id>
VITE_ENTRA_REDIRECT_URISPA app registration redirect URI: http://localhost:5173
VITE_ENTRA_API_SCOPEBackend/API app exposed delegated scope, normally api://<backend-client-id>/admin-consent

Run it

  1. Start Docker Desktop.
  2. From the repo root, go to the canonical Aspire project:
cd .\src
aspire start

Aspire starts:

  • cache: Redis container/resource.
  • app: Python FastAPI/Uvicorn app from src/app/main.py, pinned to https://localhost:8080 with /health.
  • frontend: Vite React app from src/frontend, pinned to http://localhost:5173.
  • Aspire dashboard: resources, logs, metrics, and traces.

Open http://localhost:5173, sign in, and send a chat message. The browser stays on the frontend origin and reaches the API through Vite's relative /api proxy, which Aspire wires to the backend through APP_HTTPS.

Aspire dashboard traces

Tests

Backend tests live under src/app/tests and are run with uv:

cd .\src\app
uv run pytest

The current backend suite has 10 tests.

The frontend Playwright E2E test is in src/frontend/e2e and uses the e2e script from src/frontend/package.json:

cd .\src\frontend
npm install
npm run e2e

Run E2E with the Aspire stack running and valid local env files.

Observability

The backend configures OpenTelemetry in src/app/telemetry.py and exports through Aspire-injected OTLP settings. In the Aspire dashboard, inspect traces for the app resource.

Useful spans include:

  • auth.acquire_obo_token for the confidential-client OBO exchange.
  • create_agent CopilotStudioAgent for MAF/Copilot Studio client construction.
  • copilot_studio.start_conversation for conversation setup.
  • chat CopilotStudioAgent for the Copilot Studio stream.
  • invoke_agent CopilotStudioAgent for the overall AG-UI request.

Telemetry toggles:

Env varBehavior
ENABLE_INSTRUMENTATION=falseDisables Microsoft Agent Framework instrumentation
ENABLE_SENSITIVE_DATA=falseDisables message-content capture in MAF/GenAI span attributes

The code does not record bearer tokens, OBO tokens, or client secrets in telemetry.

Troubleshooting

SymptomLikely causeFix
401 Invalid bearer tokenThe browser sent the wrong token, the audience does not match the backend API, or tenant/issuer values are wrong. The backend accepts same-tenant Entra v2 issuer https://login.microsoftonline.com/<tenant>/v2.0 and v1 issuer https://sts.windows.net/<tenant>/.Ensure MSAL requests VITE_ENTRA_API_SCOPE, not only an ID token. Confirm ENTRA_TENANT_ID, ENTRA_BACKEND_EXPECTED_AUDIENCE=api://<backend-client-id>, and the backend app Application ID URI.
403 insufficient scopeToken is valid but does not contain the required scp.Match ENTRA_BACKEND_REQUIRED_SCOPE to the exposed backend scope name and set VITE_ENTRA_API_SCOPE to the full scope. This repo defaults to admin-consent.
502 with consent_required during agent callThe OBO exchange cannot get a Power Platform token for the delegated user.On the backend/API app registration, add Power Platform API delegated permission CopilotStudio.Copilots.Invoke and click Grant admin consent.
Copilot Studio 404Wrong agent identifier in COPILOTSTUDIOAGENT__SCHEMANAME.Use Copilot Studio Settings > Advanced > Metadata > Schema name. Do not use the display name or a random GUID.
Copilot Studio auth/config still fails after using Metadata valuesThe Copilot Studio Metadata page's Agent app ID was copied into COPILOTSTUDIOAGENT__AGENTAPPID. That value is different from the backend/API app registration client ID.For this demo, set COPILOTSTUDIOAGENT__AGENTAPPID to the backend/API app registration Application (client) ID, the same value as ENTRA_BACKEND_CLIENT_ID.
Chat returns You don't have access to talk to this bot, contact the owner.Delegated auth reached Copilot Studio, but the signed-in user lacks end-user access to the agent.Share the agent with that user and grant End user access.
Response arrives as one chunk or backend logs path=chunked-fallbackCopilot Studio sent only a final message activity instead of multiple streamed text updates.There is no explicit streaming toggle for this agent. Review Settings > Generative AI and model/orchestration behavior; the backend fallback still renders progressively.