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) andmicrosoft-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/agentreturnstext/event-stream - Browser API calls use the frontend's relative
/apiproxy; users interact withhttp://localhost:5173. - Redis resource: Aspire resource name
cache, injected into the backend asCACHE_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
- The browser loads the React app from
http://localhost:5173. - MSAL signs the user in through the SPA app registration using PKCE and no client secret.
- MSAL requests the backend API scope from
VITE_ENTRA_API_SCOPE, for exampleapi://<backend-client-id>/admin-consent. - The React AG-UI client calls the backend through Vite's relative
/apiproxy with an Authorization header containing the API access token. Aspire injectsAPP_HTTPS, so Vite reaches the TLS backend endpoint. - FastAPI validates the token in
src/app/auth.py: issuer, signature, audience,nbf, expiration, and required delegated scope. - The backend confidential client uses MSAL OBO to request
ENTRA_OBO_DOWNSTREAM_SCOPE=https://api.powerplatform.com/.default. src/app/agent.pybuildsConnectionSettingswithCOPILOTSTUDIOAGENT__ENVIRONMENTIDandCOPILOTSTUDIOAGENT__SCHEMANAME, then createsCopilotClientandCopilotStudioAgent.- Copilot Studio returns streamed activities. The backend maps text to AG-UI
TEXT_MESSAGE_START,TEXT_MESSAGE_CONTENT, andTEXT_MESSAGE_ENDSSE events. - React renders the assistant 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.
uvfor 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.
- In the Microsoft Entra admin center, go to Identity > Applications > App registrations > New registration.
- Name it something like
maf-copilot-studio-demo-api. - Set Supported account types to Accounts in this organizational directory only.
- Do not add a redirect URI for this backend app.
- Save these values:
| Entra value | Put it here |
|---|---|
| Directory (tenant) ID | ENTRA_TENANT_ID, COPILOTSTUDIOAGENT__TENANTID, and part of VITE_ENTRA_AUTHORITY |
| Application (client) ID | ENTRA_BACKEND_CLIENT_ID and COPILOTSTUDIOAGENT__AGENTAPPID; do not substitute the Copilot Studio Metadata page's different Agent app ID |
Expose an API
- Open the backend app registration.
- Go to Expose an API.
- Set the Application ID URI to
api://<backend-client-id>. - Add a delegated scope.
- This demo currently expects the scope name
admin-consentbecauseENTRA_BACKEND_REQUIRED_SCOPEdefaults toadmin-consentandsrc/frontend/.env.exampleusesapi://<backend-client-id>/admin-consent. - If you choose a different scope name, update both:
ENTRA_BACKEND_REQUIRED_SCOPE=<your-scope-name>insrc/app/.envVITE_ENTRA_API_SCOPE=api://<backend-client-id>/<your-scope-name>insrc/frontend/.env.local
| Expose API value | Put it here |
|---|---|
Application ID URI api://<backend-client-id> | ENTRA_BACKEND_EXPECTED_AUDIENCE |
Scope name admin-consent | ENTRA_BACKEND_REQUIRED_SCOPE |
Full scope api://<backend-client-id>/admin-consent | VITE_ENTRA_API_SCOPE |

Create a client secret
- Go to Certificates & secrets.
- Create a new client secret.
- Copy the Value immediately.
- Put it in
ENTRA_BACKEND_CLIENT_SECRETinsrc/app/.envonly.
Never put this secret in frontend config and never commit it.

Add Power Platform delegated permission and grant admin consent
- Go to API permissions on the backend app registration.
- Choose Add a permission.
- Find Power Platform API. If searching by name is ambiguous, use app ID
8578e004-a5c6-46e7-913e-12f58912df43. - Choose Delegated permissions.
- Add
CopilotStudio.Copilots.Invoke. - 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

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.
- Go to Identity > Applications > App registrations > New registration.
- Name it something like
maf-copilot-studio-demo-spa. - Set Supported account types to Accounts in this organizational directory only.
- After creation, copy the Application (client) ID to
VITE_ENTRA_SPA_CLIENT_ID. - Set
VITE_ENTRA_AUTHORITY=https://login.microsoftonline.com/<tenant-id>.
Add the SPA redirect URI
- Open the SPA app registration.
- Go to Authentication.
- Add a Single-page application platform.
- Add this redirect URI exactly:
http://localhost:5173
- Put the same value in
VITE_ENTRA_REDIRECT_URI.

Add delegated permission to the backend API
- Go to API permissions on the SPA app registration.
- Choose Add a permission > My APIs.
- Select the backend/API app registration you created above.
- Add the delegated scope, normally
admin-consent. - 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

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:
- Open Copilot Studio.
- Open your agent.
- Go to Settings > Advanced > Metadata.
- Copy Environment ID, which looks like
Default-<tenant-guid>. - Put it in
COPILOTSTUDIOAGENT__ENVIRONMENTID.
The same Metadata panel also shows Tenant ID, Agent app ID, and Schema name.

Agent schema name
The MAF ConnectionSettings uses the Copilot Studio schema name, not the display name and not a GUID.
- Open Copilot Studio.
- Open your agent.
- Go to Settings > Advanced > Metadata.
- Copy Schema name, for example
crf1e_productAdvisor. - 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.

Publish and share the agent
- Publish the agent before running the demo. The backend uses
AgentType.PUBLISHED. - Share the agent with the users who will sign in to the React app.
- 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.

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
| Key | Source |
|---|---|
ENTRA_TENANT_ID | Entra tenant overview: Directory (tenant) ID |
ENTRA_BACKEND_CLIENT_ID | Backend/API app registration: Application (client) ID |
ENTRA_BACKEND_CLIENT_SECRET | Backend/API app registration: Certificates & secrets secret Value |
ENTRA_BACKEND_EXPECTED_AUDIENCE | Backend/API app registration: Application ID URI, normally api://<backend-client-id> |
ENTRA_BACKEND_REQUIRED_SCOPE | Backend/API app registration: exposed delegated scope name, default admin-consent |
ENTRA_OBO_DOWNSTREAM_SCOPE | Literal Power Platform downstream scope: https://api.powerplatform.com/.default |
COPILOTSTUDIOAGENT__ENVIRONMENTID | Copilot Studio agent: Settings > Advanced > Metadata > Environment ID |
COPILOTSTUDIOAGENT__SCHEMANAME | Copilot Studio agent: Settings > Advanced > Metadata > Schema name, for example crf1e_productAdvisor |
COPILOTSTUDIOAGENT__AGENTAPPID | Backend/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__TENANTID | Same tenant GUID as ENTRA_TENANT_ID |
Frontend: src/frontend/.env.local
| Key | Source |
|---|---|
VITE_ENTRA_SPA_CLIENT_ID | SPA/frontend app registration: Application (client) ID |
VITE_ENTRA_AUTHORITY | https://login.microsoftonline.com/<tenant-id> |
VITE_ENTRA_REDIRECT_URI | SPA app registration redirect URI: http://localhost:5173 |
VITE_ENTRA_API_SCOPE | Backend/API app exposed delegated scope, normally api://<backend-client-id>/admin-consent |
Run it
- Start Docker Desktop.
- 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 fromsrc/app/main.py, pinned tohttps://localhost:8080with/health.frontend: Vite React app fromsrc/frontend, pinned tohttp://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.

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_tokenfor the confidential-client OBO exchange.create_agent CopilotStudioAgentfor MAF/Copilot Studio client construction.copilot_studio.start_conversationfor conversation setup.chat CopilotStudioAgentfor the Copilot Studio stream.invoke_agent CopilotStudioAgentfor the overall AG-UI request.
Telemetry toggles:
| Env var | Behavior |
|---|---|
ENABLE_INSTRUMENTATION=false | Disables Microsoft Agent Framework instrumentation |
ENABLE_SENSITIVE_DATA=false | Disables message-content capture in MAF/GenAI span attributes |
The code does not record bearer tokens, OBO tokens, or client secrets in telemetry.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 Invalid bearer token | The 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 scope | Token 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 call | The 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 404 | Wrong 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 values | The 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-fallback | Copilot 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. |