Skip to main content

🔒 User Authentication

At times agents must access secured online resources on behalf of the user, such as checking email, checking on flight status, or placing an order. To enable this, the user must authenticate their identity and grant consent for the application to access these resources. This process results in the application receiving a token, which the application can then use to access the permitted resources on the user's behalf.

info

This is an advanced guide. It is highly recommended that you are familiar with Teams Core Concepts before attempting this guide.

warning

User authentication does not work with the developer tools setup. You have to run the app in Teams. Follow Quickstart: Register your app to register and sideload your bot.

info

It is possible to authenticate the user into other auth providers like Facebook, Github, Google, Dropbox, and so on.

Set up the OAuth connection

User authentication requires an Azure-managed bot (Teams-managed bots don't support OAuth connections). If you registered with --teams-managed, migrate first:

teams app bot migrate <appId> --subscription <id> --resource-group <your-resource-group>

Then follow the User Authentication Setup guide to configure the AAD app, create the Azure Bot OAuth connection, and update the manifest. The guide covers both SSO (silent token exchange) and generic OAuth.

tip

If you'd rather have an AI coding assistant run the setup, install the teams-dev skill and ask it to "set up SSO for my Teams bot".

Configure the OAuth connection

info

In SDK 2.1, you can register and use multiple OAuth connections in one bot (for example, Graph + GitHub) by adding multiple AddOAuthFlow(...) entries.

using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.OAuth;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTeamsBotApplication(options =>
{
options.AddOAuthFlow("graph", oauth =>
{
oauth.OAuthCardText = "Sign in to Microsoft Graph";
oauth.SignInButtonText = "Sign in to Graph";
});
options.AddOAuthFlow("github", oauth =>
{
oauth.OAuthCardText = "Sign in to GitHub";
oauth.SignInButtonText = "Sign in to GitHub";
});
});

var app = builder.Build();
var teams = app.UseTeamsBotApplication();

OAuthFlow graphAuth = teams.GetOAuthFlow("graph");
OAuthFlow githubAuth = teams.GetOAuthFlow("github");
tip

Make sure you use the same name you used when creating the OAuth connection in the Azure Bot Service resource.

Signing In

You must call the signin method inside your route handler, for example: to signin when receiving the /signin message:

Call SignInAsync on the flow directly — each flow manages its own sign-in state independently.

note

The graph connection uses the Single Sign-On (SSO) authentication flow whereas the Github connection uses the oauth flow. To learn more about all the available flows and their differences see the official documentation.

teams.OnMessage("/signin graph", async (context, cancellationToken) =>
{
string? token = await graphAuth.SignInAsync(context, cancellationToken);
if (token is not null)
{
await context.SendAsync("you are already signed in to Graph!", cancellationToken);
}
});

teams.OnMessage("/signin github", async (context, cancellationToken) =>
{
string? token = await githubAuth.SignInAsync(context, cancellationToken);
if (token is not null)
{
await context.SendAsync("you are already signed in to GitHub!", cancellationToken);
}
});

Subscribe to the SignIn event

You can subscribe to the signin event, that will be triggered once the OAuth flow completes.

Success and failure callbacks are scoped per flow — no branching on connection name needed.

graphAuth.OnSignInComplete(async (context, tokenResponse, cancellationToken) =>
{
await context.SendAsync(
$"Signed in to Graph ({tokenResponse.ConnectionName}). Type **/whoami** to continue.",
cancellationToken);
});

githubAuth.OnSignInComplete(async (context, tokenResponse, cancellationToken) =>
{
await context.SendAsync(
$"Signed in to GitHub ({tokenResponse.ConnectionName}).",
cancellationToken);
});

Start using the client

note

The default graph configuration requests the User.ReadBasic.All permission. It is possible to request other permissions by modifying the App Registration for the bot on Azure.

using System.Net.Http.Headers;

teams.OnMessage("/whoami", async (context, cancellationToken) =>
{
string? token = await graphAuth.SignInAsync(context, cancellationToken);
if (token is null) return; // OAuth card sent / waiting for callback turn

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

string meJson = await http.GetStringAsync(
"https://graph.microsoft.com/v1.0/me?$select=displayName,mail,userPrincipalName",
cancellationToken);

await context.SendAsync(meJson, cancellationToken);
});

teams.OnMessage("(?i)^my gh user$", async (context, cancellationToken) =>
{
string? token = await githubAuth.SignInAsync(context, cancellationToken);
if (token is null) return;

using HttpClient http = new();
http.DefaultRequestHeaders.Authorization = new("Bearer", token);
http.DefaultRequestHeaders.UserAgent.ParseAdd("TeamsBot/1.0");

string response = await http.GetStringAsync(
"https://api.github.com/user", ct);
await context.SendAsync($"Your GitHub user :\n```json\n{response}\n```", ct);
});

Signing Out

You can signout by calling the signout method, this will remove the token from the User Token service cache

teams.OnMessage("/signout graph", async (context, cancellationToken) =>
{
await graphAuth.SignOutAsync(context, cancellationToken);
await context.SendAsync("you have been signed out from Graph!", cancellationToken);
});

teams.OnMessage("/signout github", async (context, cancellationToken) =>
{
await githubAuth.SignOutAsync(context, cancellationToken);
await context.SendAsync("you have been signed out from GitHub!", cancellationToken);
});

Resuming Pending Messages After Sign-In

When a user isn't signed in and your message handler calls the sign-in method, an OAuth card is sent and the current turn ends. The sign-in completes on a separate turn — meaning the original message text is not available in the sign-in success context.

To avoid ignoring what the user originally asked, store the pending message before initiating sign-in, then retrieve and process it once sign-in succeeds:

using System.Collections.Concurrent;
using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.OAuth;

var pendingMessages = new ConcurrentDictionary<string, string>();

teams.OnMessage("(?i)^my ad user$", async (context, cancellationToken) =>
{
var userId = context.Activity.From?.Id ?? string.Empty;
string? token = await graphAuth.SignInAsync(context, cancellationToken);
if (token is null)
{
pendingMessages[userId] = context.Activity.Text ?? string.Empty;
return;
}

await ProcessGraphMessage(context.Activity.Text, context, token, cancellationToken);
});

graphAuth.OnSignInComplete(async (context, tokenResponse, cancellationToken) =>
{
var userId = context.Activity.From?.Id ?? string.Empty;
if (pendingMessages.TryRemove(userId, out var text))
{
await context.SendAsync("Successfully signed in to Graph! Processing your request...", cancellationToken);
await ProcessGraphMessage(text, context, tokenResponse.Token, cancellationToken);
}
});
tip

For production apps, consider using a persistent store (database, Redis, etc.) instead of an in-memory map so pending messages survive restarts. You should also implement expiration or cleanup logic (e.g., a TTL) to discard stale entries when sign-in is cancelled, times out, or fails.

Handling Sign-In Failures

When using SSO, if the token exchange fails Teams sends a signin/failure invoke activity to your app. The SDK includes a built-in default handler that logs a warning with actionable troubleshooting guidance. You can optionally register your own handler to customize the behavior:

graphAuth.OnSignInFailure(async (context, failure, cancellationToken) =>
{
await context.SendAsync(
$"Graph sign-in failed: {failure?.Code} - {failure?.Message}",
cancellationToken);
});

githubAuth.OnSignInFailure(async (context, failure, cancellationToken) =>
{
await context.SendAsync(
$"GitHub sign-in failed: {failure?.Code} - {failure?.Message}",
cancellationToken);
});
tip

The most common failure codes are installedappnotfound (bot app not installed for the user) and resourcematchfailed (Token Exchange URL doesn't match the Application ID URI). See SSO Setup - Troubleshooting for a full list of failure codes and troubleshooting steps.

Resources

User Authentication Basics