>_ The Manifest

Building an API plugin for a declarative agent is one thing; securing it is another. Authentication determines whether your agent gets a token, an API key, or nothing at all when calling your backend. Let’s go deep on all three options.

The Three Auth Types: A Mental Model

Every API plugin declares its authentication strategy in the auth object of the plugin manifest’s runtimes array. There are exactly three valid types:

TypeWhen to UseToken Delivery
OAuthPluginVaultEnterprise APIs with user context (Entra ID, third-party OAuth)Bearer token in Authorization header
ApiKeyPluginVaultService-to-service, third-party APIs, prototypingKey in header or query parameter
NonePublic APIs with no auth requiredNothing, anonymous calls

Here’s the quick mental model: OAuth for user-specific data, API keys for shared access, None for public endpoints. Most enterprise scenarios land on OAuth. If you’re building an internal agent that reads employee data from a backend (like our Zava Insurance onboarding API), OAuth is almost certainly what you want.

OAuth with Entra ID (OAuthPluginVault)

This is the most common pattern for enterprise declarative agents. The user authenticates with their Microsoft Entra ID credentials, Copilot handles the token exchange, and your API receives a bearer token with the user’s identity. Your backend reads the token, knows who is asking, and returns their data.

Here’s how to wire it up end-to-end.

Step 1: Register an App in Microsoft Entra ID

Go to the Microsoft Entra admin center and register a new application (or use an existing one). This is the app that represents your API: it defines what permissions exist and who can request them.

Under Authentication, add the following redirect URI to the Web platform:

https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect

This is the redirect URI that the Copilot token service uses during the OAuth authorization code flow. Get this wrong and the entire flow breaks silently.

Step 2: Define Scopes

Under Expose an API, create the scopes your API needs. For our Zava Insurance onboarding API, we define:

  • Onboarding.Read: Read onboarding status and task details
  • Onboarding.ReadWrite: Read and update onboarding tasks

These scopes show up in the consent prompt when a user first triggers the plugin. Be specific: broad scopes like “access everything” erode trust and may not pass admin consent review.

Step 3: Register in Teams Developer Portal

Open the Teams Developer Portal and navigate to Tools > OAuth client registration. Fill in:

  • Registration name: Something descriptive: Zava Onboarding API OAuth
  • Base URL: Your API’s base URL (must match the servers array in your OpenAPI spec)
  • Client ID: The application ID from your Entra app registration
  • Client secret: A secret you generated for that app
  • Authorization endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
  • Token endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
  • Refresh endpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
  • Scope: The scopes you defined (e.g., api://{app-id}/Onboarding.Read)

Save the registration. You’ll get an OAuth client registration ID: this is your reference_id.

💡 Tip

When using Microsoft 365 Agents Toolkit, you can skip this manual step entirely. Define your securitySchemes in your OpenAPI spec and configure the credentials in your m365agents.yml file. During provisioning (atk provision), ATK creates the OAuth client registration in the Teams Developer Portal, injects the generated reference_id into your plugin manifest, and stores your client credentials securely.

Your OpenAPI spec needs the securitySchemes block so Copilot knows how to apply the token. For OAuth with Entra ID:

securitySchemes:
  OAuth2AuthCode:
    type: oauth2
    flows:
      authorizationCode:
        authorizationUrl: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
        tokenUrl: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
        scopes:
          Onboarding.Read: Read onboarding status

And in your m365agents.yml, configure the OAuth registration so ATK can provision it:

oauth2AuthCode:
  name: OnboardingOAuth
  isPKCEEnabled: false
  flow: authorizationCode
  authorizationEndpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
  tokenEndpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
  refreshEndpoint: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
  scope: api://{app-id}/Onboarding.Read
  clientId: ${{OAUTH2AUTHCODE_CLIENT_ID}}
  clientSecret: ${{OAUTH2AUTHCODE_CLIENT_SECRET}}

Step 4: Configure the Plugin Manifest

In your apiPlugin.json, set the auth block in the runtimes array:

{
  "schema_version": "v2.4",
  "name_for_human": "Onboarding Status",
  "namespace": "OnboardingStatus",
  "description_for_human": "Check your onboarding progress and task details.",
  "description_for_model": "Use this plugin when the user asks about their onboarding progress, task checklist, completed items, or pending onboarding steps.",
  "runtimes": [
    {
      "type": "OpenApi",
      "auth": {
        "type": "OAuthPluginVault",
        "reference_id": "${{OAUTH2AUTHCODE_CONFIGURATION_ID}}"
      },
      "spec": {
        "url": "apiSpecificationFiles/openapi.yaml"
      }
    }
  ]
}

The reference_id ties your manifest to the OAuth registration you created in the Teams Developer Portal. Copilot uses this to look up the client credentials, endpoints, and scopes at runtime. Never hardcode this value: use the ${{OAUTH2AUTHCODE_CONFIGURATION_ID}} environment variable so ATK injects the correct ID during provisioning.

📝 Note

The naming matters: ATK maps the securitySchemes name in your OpenAPI spec to the environment variable. OAuth2AuthCode becomes ${{OAUTH2AUTHCODE_CONFIGURATION_ID}}. Keep these in sync or provisioning won’t wire things up correctly.

API Key Authentication (ApiKeyPluginVault)

Not every API uses OAuth. Some third-party services hand you an API key and call it a day. For these, ApiKeyPluginVault is your auth type.

Registering the Key

In the Teams Developer Portal, go to Tools > API key registration and create a new key:

  • API key name: Zava External Training API
  • Secret: Paste the actual API key
  • Base URL: The API’s base URL
  • Target tenant: Restrict to your organization or allow any

Save it and grab the API key registration ID.

💡 Tip

When using Microsoft 365 Agents Toolkit, you can skip this manual step too. Define your apiKey security scheme in your OpenAPI spec and provide the key value in your m365agents.yml file. ATK registers the key in the Teams Developer Portal during provisioning and wires the reference_id into your plugin manifest automatically.

OpenAPI Security Scheme

Tell Copilot how to send the key by defining the scheme in your OpenAPI spec. Three options:

Bearer token in Authorization header:

securitySchemes:
  BearerAuth:
    type: http
    scheme: bearer

Custom header:

securitySchemes:
  ApiKey:
    type: apiKey
    in: header
    name: X-API-KEY

Query parameter:

securitySchemes:
  ApiKey:
    type: apiKey
    in: query
    name: api_key

Plugin Manifest Config

{
  "runtimes": [
    {
      "type": "OpenApi",
      "auth": {
        "type": "ApiKeyPluginVault",
        "reference_id": "${{APIKEY_REGISTRATION_ID}}"
      },
      "spec": {
        "url": "apiSpecificationFiles/openapi.yaml"
      }
    }
  ]
}

Copilot reads the securitySchemes from your OpenAPI spec to determine where to attach the key (header, query param, or bearer token), then sends it on every request.

📝 Note

API key auth is a good fit for third-party integrations, prototyping, or server-to-server calls where user context isn’t needed. For anything involving user-specific data in an enterprise setting, prefer OAuth.

No Authentication (None)

For public APIs that require no credentials at all: weather data, public news feeds, open datasets: set the auth type to None:

{
  "runtimes": [
    {
      "type": "OpenApi",
      "auth": {
        "type": "None"
      },
      "spec": {
        "url": "apiSpecificationFiles/openapi.yaml"
      }
    }
  ]
}

No registration needed. No portal configuration. Copilot calls the API directly with no auth header. This is the fastest path to a working plugin: great for prototyping or when you’re wrapping a genuinely public endpoint.

⚠️ Warning

Don’t use None as a shortcut during development if your production API requires auth. The auth flow affects the entire user experience (consent prompts, token refresh, error handling), so test with real auth as early as possible.

The Runtime Auth Flow

Understanding what happens at runtime helps you debug when things go wrong. Here’s the sequence when a user triggers an API plugin with OAuth authentication:

  1. User asks a question: “What’s left on my onboarding checklist?”
  2. Copilot selects the plugin: Based on description_for_model, the orchestrator decides to call the onboarding API.
  3. Copilot checks auth config: It reads the auth block from the plugin manifest and looks up the OAuth registration.
  4. First-time consent: If the user hasn’t consented before, Copilot presents a consent card in the chat. The user clicks to approve. On subsequent calls, this step is skipped.

When a user first triggers the plugin, Copilot displays an OAuth consent card showing the app name, requested permissions, and an Accept button. The user approves once, and subsequent calls use the cached token silently.

  1. Token exchange: Copilot’s token service uses the authorization code flow to obtain an access token from your identity provider (Entra ID in our case).
  2. API call with bearer token: Copilot calls your API endpoint with the access token in the Authorization: Bearer {token} header.
  3. Response returned: Your API validates the token, reads the user identity, fetches the relevant data, and returns it. Copilot renders the response conversationally.

For ApiKeyPluginVault, the flow is simpler: steps 4 and 5 are replaced by Copilot retrieving the stored key from the vault and attaching it to the request. For None, steps 3 to 5 are skipped entirely.

💡 Tip

The consent prompt only appears the first time a user interacts with a plugin that requires OAuth. After that, the token service handles refresh silently. If users report being prompted repeatedly, check that your refresh endpoint is configured correctly in the Teams Developer Portal.

Common Gotchas

After helping teams configure auth for their plugins, these are the issues I see most often:

Wrong redirect URI. The redirect URI in your Entra app registration must be exactly https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect. A trailing slash, a typo, or using the old Bot Framework redirect will cause the auth flow to fail silently. No error message: just no token.

Missing or mismatched scopes. The scopes in your Teams Developer Portal OAuth registration must match what your API expects. If your API validates for api://{app-id}/Onboarding.Read but you registered Onboarding.Read without the full URI, the token won’t have the right claims.

Token not refreshing. If you left the refresh endpoint blank in the OAuth registration, tokens expire and users get re-prompted for consent. Always fill in the refresh URL: for Entra ID, it’s the same as the token endpoint.

Base URL mismatch. The base URL in your Teams Developer Portal registration must match a servers entry in your OpenAPI spec. If they don’t match, Copilot won’t associate the auth config with the right API calls.

Testing auth in sideloaded apps. When sideloading during development, OAuth consent can behave differently than in production. Test with a real tenant and real user accounts before shipping. The Teams Developer Portal’s Restrict usage by app setting should be set to Any Teams app during development.

307 redirects from token endpoint. Copilot’s token service does not follow 307 Temporary Redirect responses from OAuth token endpoints. If your identity provider returns a 307, you’ll need to use the final URL directly.

Choosing the Right Auth Type

If you’re still deciding which auth type fits your scenario:

  • Your API serves user-specific data (employee records, personal dashboards, user preferences): use OAuthPluginVault
  • Your API uses a shared key (third-party SaaS, internal microservice, prototype): use ApiKeyPluginVault
  • Your API is public (weather, news, open data): use None

For the Zava Insurance HR Onboarding Buddy, OAuth is the clear choice: the onboarding API needs to know which employee is asking to return their checklist. An API key would give every user the same data. None wouldn’t work at all for employee records.

The Value You Just Unlocked

  • OAuth for user-specific access: You can wire up Entra ID so Copilot handles the full token lifecycle, consent prompts included, and your API always knows who is asking.
  • API keys for shared access: Third-party integrations and prototypes work with a simple key registration, no user consent flow required.
  • Anonymous for public endpoints: Zero configuration gets you calling public APIs immediately.
  • Runtime flow clarity: You now understand what happens at each step when Copilot calls your API, making auth failures far easier to debug.
  • Common gotcha awareness: Redirect URIs, scope mismatches, and 307 redirects are the issues that silently break auth. Now you know where to look first.

Every declarative agent that calls a real API needs one of these three auth types configured correctly. With this knowledge, you can pick the right one, wire it up confidently, and debug it when something goes wrong.

Resources

Have questions or want to share what you're building? Connect with me on LinkedIn or check out more on The Manifest.