Intercepting and Responding to Connector Consent Cards via the Agents SDK
Detect a connector consent (OBO) Adaptive Card, summarize it, and submit Allow or Cancel.
When a conversational agent invokes a connector that is configured with end user authentication, the runtime surfaces a consent card asking the end user to grant permission for the agent to create a connection on their behalf.
This experience is only available for connectors that natively support Entra ID authentication (excluding custom connectors). Other connectors trigger the (more disruptive) connection manager experience.
Why Intercept the Card via the M365 Agents SDK?
Agents SDK is often used to communicate with Copilot Studio Agents in headless scenarios or when developers bring their own custom UI. In these scenarios, it is up to the developer to take care of rendering the consent card, and send the user’s choice (Allow
or Cancel
) back to Copilot Studio. Without user consent, Copilot Studio won’t invoke connectors that use end user authentication.
Detection Heuristics
Unfortunately, requests for consent aren’t being sent as specific event types. Instead, we need to use heuristics to detect the consent adaptive card based on its payload.
A consent adaptive card seem to contain:
- A TextBlock containing “Connect to continue”.
- Two
Action.Submit
buttons titled Allow and Cancel. - Capability lines describing what the connection can do.
- A security / privacy warning section.
Here’s a minimal consent adaptive card (only fields needed for detection):
Show minimal consent Adaptive Card JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"type": "AdaptiveCard",
"version": "1.5",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"body": [
{ "type": "TextBlock", "text": "Connect to continue", "size": "Medium", "weight": "Bolder", "wrap": true },
{ "type": "TextBlock", "text": "I'll use your credentials to connect and get the information you need.", "wrap": true },
{ "type": "ColumnSet", "columns": [
{ "type": "Column", "width": "auto" },
{ "type": "Column", "width": "stretch", "items": [
{ "type": "TextBlock", "text": "Office 365 Users", "weight": "Bolder", "wrap": true }
]}
]},
{ "type": "TextBlock", "text": "This connection can:", "wrap": true },
{ "type": "TextBlock", "id": "capability0", "text": "- Get my profile (V2)", "wrap": true },
{ "type": "TextBlock", "id": "securityWarning", "isSubtle": true, "text": "Connecting to other services with your credentials may expose your data to privacy and security risks.", "wrap": true },
{ "type": "ActionSet", "actions": [
{ "type": "Action.Submit", "title": "Allow", "data": { "action": "Allow" } },
{ "type": "Action.Submit", "title": "Cancel", "data": { "action": "Cancel" } }
]}
]
}
Response payload
In order to respond to the consent request, your custom client needs to send the following activity payload, with either Allow
or Cancel
– based on the user’s choice.
1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "message",
"channelData": {
"postBack": true,
"enableDiagnostics": true
},
"value": {
"action": "Allow",
"id": "submit",
"shouldAwaitUserInput": true
}
}
Implementation Walkthrough
This implementation walkthrough extends the Agents SDK C# console app sample, however it applies to any application or service using Agents SDK to communicate with Copilot Studio.
Detect the consent card in PrintActivity
We’ll start by adding the logic that detects the consent adaptive card to PrintActivity
in ChatConsoleService.cs
. The IsConsentCard
helper function assumes that a card is a consent card if it has the textBlock Connect to continue and has Allow
and Cancel
buttons. You can create your own logic as long as it successfully captures the consent card payload.
Message handling in PrintActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Inside your message handling (act is an IActivity)
if (act.Type == "message")
{
var adaptiveCards = act.Attachments?
.Where(a => a.ContentType == "application/vnd.microsoft.card.adaptive");
if (adaptiveCards != null)
{
foreach (var cardAttachment in adaptiveCards)
{
if (cardAttachment.Content != null &&
TryParseAdaptiveCard(cardAttachment.Content, out var cardJson) &&
IsConsentCard(cardJson))
{
// Print card details (service name, capabilities, warning, etc.)
// (omitted here for brevity)
}
}
}
}
// Heuristic: headline AND both Allow + Cancel buttons.
static bool IsConsentCard(JToken card)
{
bool hasConnectPhrase = card
.SelectTokens("$.body[?(@.type == 'TextBlock')].text")
.Any(t => t?.ToString().Contains("Connect to continue", StringComparison.OrdinalIgnoreCase) == true);
var actionTitles = card
.SelectTokens("$.body..actions[?(@.type == 'Action.Submit')].title")
.Select(t => t.ToString())
.ToList();
bool hasAllowCancel =
actionTitles.Any(t => t.Equals("Allow", StringComparison.OrdinalIgnoreCase)) &&
actionTitles.Any(t => t.Equals("Cancel", StringComparison.OrdinalIgnoreCase));
return hasConnectPhrase && hasAllowCancel;
}
// Normalizes attachment content to JToken
static bool TryParseAdaptiveCard(object content, out JToken json)
{
try
{
json = content switch
{
string s => JToken.Parse(s),
JToken jt => jt,
System.Text.Json.JsonElement je => JToken.Parse(je.GetRawText()),
_ => JToken.FromObject(content)
};
return true;
}
catch
{
json = null!;
return false;
}
}
Respond to the consent card
Once the user chooses Allow or Cancel, we send a Message Activity that emulates the Adaptive Card button click. AskQuestionAsync
has an overload that takes a raw activity object; this allows sending any activity back to the agent.
To send the user’s choice back to Copilot Studio, we create a message activity object:
- Set
channelData.postBack = true
(marks it as a button action) - Build the
value
payload with the user’s choice (“Allow” or “Cancel”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Assume userChoice is "Allow" or "Cancel" from user input
var consentActivity = new Activity
{
Type = ActivityTypes.Message,
ChannelData = new
{
postBack = true // Required: tells service this is a button click
},
Value = new
{
action = userChoice, // "Allow" or "Cancel"
id = "submit",
shouldAwaitUserInput = true
}
};
// Send the consent response
await foreach (var response in copilotClient.AskQuestionAsync(consentActivity, cancellationToken))
{
// Handle response
}