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.

Project Setup

tip

If you're creating a new app, use the graph template. Skip this if you're adding auth to an existing app.

Use your terminal to run the following command:

teams project new python oauth-app --template graph

This command:

  1. Creates a new directory called oauth-app.
  2. Bootstraps the graph agent template files into it under oauth-app/src.

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

Register the connections you plan to use. add_oauth_flow returns the object that owns one.

from microsoft_teams.apps import App

app = App()

graph = app.add_oauth_flow(
"graph",
oauth_card_text="Sign in to your account",
sign_in_button_text="Sign in",
)

Both card options are optional. Use app.get_oauth_flow("graph") to get the flow again from another module.

note

Registering a flow enables per-turn state unless you set state yourself, so sign-in callbacks that don't name a connection can be traced back to the one that started them.

tip

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

note

In many templates, graph is the default name of the OAuth connection, but you can register the connection under any name — it just has to match the OAuth connection configured on the Azure Bot.

Signing In

Start sign-in from inside a route handler, for example when receiving the /signin message:

note

This uses the Single Sign-On (SSO) authentication flow. To learn more about all the available flows and their differences see the official documentation.

@app.on_message_pattern("/signin")
async def handle_signin_message(ctx: ActivityContext[MessageActivity]):
"""Handle message activities for signing in."""
token = await graph.sign_in(ctx)
if token:
await ctx.send("You are already signed in.")

Handling sign-in completion

The sign-in method returns a token when the user already has one, and otherwise sends an OAuth card and returns nothing. The flow finishes on a later turn, so completion is delivered through a callback.

The handler is scoped to its connection, so there's no need to branch on the connection name.

@graph.on_signin
async def on_graph_signin(event: SignInEvent):
"""Only fires for the `graph` connection."""
await event.activity_ctx.send(
f"Signed in on {event.connection_name}. "
"Type **/whoami** to see your profile or **/signout** to sign out."
)
note

A flow can have more than one handler. They run in registration order and are isolated — if one raises, the error is logged and the rest still run. The app-wide sign_in event also still fires for every connection.

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.

From this point, you can query graph for the signed-in user, for example to reply to the /whoami message, or in any other route.

Build the client from the flow's token, so it always talks to the connection that owns it.

from microsoft_teams.graph import get_graph_client

@app.on_message_pattern("/whoami")
async def handle_whoami_message(ctx: ActivityContext[MessageActivity]):
"""Handle messages to show user information from Microsoft Graph."""
token = await graph.sign_in(ctx)
if token is None:
return # OAuth card sent — resumes on the callback turn

client = get_graph_client(token)
me = await client.me.get()
await ctx.send(f"Hello {me.display_name}! Your email is {me.mail or me.user_principal_name}")

Signing Out

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

@app.on_message_pattern("/signout")
async def handle_signout_message(ctx: ActivityContext[MessageActivity]):
"""Handle sign out requests."""
await graph.sign_out(ctx)
await ctx.send("You have been signed out!")

Using multiple OAuth connections

An agent may need more than one identity — for example Graph for the user's profile and GitHub for their repos. Each connection is registered separately, so tokens, callbacks and sign-out are scoped to the connection that owns them.

app = App()

graph = app.add_oauth_flow("graph", oauth_card_text="Sign in with Microsoft")
github = app.add_oauth_flow("github", oauth_card_text="Sign in with GitHub")

@app.on_message_pattern("/graph")
async def handle_graph(ctx: ActivityContext[MessageActivity]):
token = await graph.sign_in(ctx)
if token:
await send_graph_profile(ctx, token)

@app.on_message_pattern("/github")
async def handle_github(ctx: ActivityContext[MessageActivity]):
token = await github.sign_in(ctx)
if token:
await send_github_profile(ctx, token)

@graph.on_signin
async def on_graph_signin(event: SignInEvent):
await send_graph_profile(event.activity_ctx, event.token_response.token)

@github.on_signin
async def on_github_signin(event: SignInEvent):
await send_github_profile(event.activity_ctx, event.token_response.token)

@app.on_message_pattern("/signout github")
async def handle_signout_github(ctx: ActivityContext[MessageActivity]):
await github.sign_out(ctx)
await ctx.send("Signed out of GitHub.")

Signing out of one connection leaves the other signed in.

info

Each connection must exist on the Azure Bot resource under the name you register it with. Follow the User Authentication Setup guide once per connection.

Checking connection status

One call returns the status of every registered connection, so you don't have to probe them individually.

@app.on_message_pattern("/status")
async def handle_status(ctx: ActivityContext[MessageActivity]):
statuses = await ctx.get_connection_status()
lines = [
f"- `{status.connection_name}`: {'signed in' if status.has_token else 'signed out'}"
for status in statuses
]
await ctx.send("\n".join(lines))

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:

from microsoft_teams.apps import App, ActivityContext, SignInEvent
from microsoft_teams.apps.routing import SignInOptions
from microsoft_teams.api import MessageActivity

app = App()
graph = app.add_oauth_flow("graph")

pending_messages: dict[str, str] = {}

@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
# sign_in() returns the token if already signed in, or None if an OAuth card was sent
token = await graph.sign_in(ctx, SignInOptions(
oauth_card_text="To help with that, I need to sign you in first."
))

if token is None:
pending_messages[ctx.activity.from_.id] = ctx.activity.text
return

await process_message(ctx.activity.text, ctx, token)

@graph.on_signin
async def on_graph_signin(event: SignInEvent):
user_id = event.activity_ctx.activity.from_.id
pending = pending_messages.pop(user_id, None)

if pending:
await event.activity_ctx.send("Successfully signed in! Processing your original request...")
await process_message(pending, event.activity_ctx, event.token_response.token)
else:
await event.activity_ctx.send("You are now signed in!")
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:

from microsoft_teams.apps import SignInFailureEvent

@graph.on_signin_failure
async def on_graph_signin_failure(event: SignInFailureEvent):
ctx = event.activity_ctx
ctx.logger.error(f"Graph sign-in failed: {event.code} - {event.message}")
await ctx.send("Graph sign-in failed.")

A flow can have more than one failure handler; they run in registration order and are isolated from each other.

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.

warning

A signin/failure activity doesn't name its connection. The SDK attributes it from the pending sign-in recorded when the flow started; if that can't be resolved, every registered connection's failure handler is notified. Check the connection first if your handler does something destructive.

Registering a flow also turns on per-turn state unless you configure state yourself, defaulting to local, in-process storage. Configure shared state storage before running more than one instance — otherwise pending sign-in attribution and token-exchange dedup don't cross processes, and sign-in fails intermittently.

Regional Configs

You may be building a regional bot that is deployed in a specific Azure region (such as West Europe, East US, etc.) rather than global. This is important for organizations that have data residency requirements or want to reduce latency by keeping data and authentication flows within a specific area.

These examples use West Europe, but follow the equivalent for other regions.

To configure a new regional bot in Azure, you must setup your resoures in the desired region. Your resource group must also be in the same region.

  1. Deploy a new App Registration in westeurope.
  2. Deploy and link a new Enterprise Application (Service Principal) on Microsoft Entra in westeurope.
  3. Deploy and link a new Azure Bot in westeurope.
  4. In your App Registration, in the Authentication (Preview) tab, add a Redirect URI for the Platform Type Web to your regional endpoint (e.g., https://europe.token.botframework.com/.auth/web/redirect)

Authentication Tab

  1. In your .env file (or wherever you set your environment variables), add your OAUTH_URL. For example: OAUTH_URL=https://europe.token.botframework.com

Resources

User Authentication Basics