Skip to content

Conversation

@heyitsaamir
Copy link
Collaborator

@heyitsaamir heyitsaamir commented Oct 25, 2025

Context

This fixes #184 which stated that the token wasn't being refreshed after an hour. This was because we weren't refreshing it on use.
In typescript, we refresh the token on every app.process. But this actually still leaves proactive scenarios to used an cached, potentially expired, tokens. To remediate this, the token that gets passed to the API is a factory which refreshes the token if the cached token is expired.
For the record C# sets the token value to be refreshable (just as this PR is attempting to do).

Changes

This PR includes several changes

  1. No more graph token manager. Instead we now have a TokenManager which manages all tokens. Soon, this might change to msal doing the changes
  2. The app no longer refreshes token on start. But it does it the first time the token is being used. Because of this, the id field is now from the credentials, and the "name" field had to be removed. I don't think this should cause a big deal because name is honestly, not a very well used (or documented) field. This is a breaking change though.
  3. The token that's being passed around now is an async function that either gets the token from the cache, or refreshes it if it's expired.

Testing

  1. Unit tests
  2. Sanity tests to make sure we can send messages etc normally.
  3. Tested to make sure app token refreshes automatically after an hour
  4. Tested user graph tokens
  5. Tested app graph tokens

PR Dependency Tree

This tree was auto-generated by Charcoal

@heyitsaamir heyitsaamir requested review from MehakBindra, Copilot and lilyydu and removed request for Copilot and lilyydu October 25, 2025 18:46
Copilot AI review requested due to automatic review settings October 25, 2025 18:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements refreshable app tokens to fix issue #184 where tokens weren't being refreshed after an hour. The implementation follows TypeScript's approach by providing a token factory that automatically refreshes expired tokens on use, extending to proactive scenarios as well.

Key Changes:

  • Consolidated token management from separate bot and graph token managers into a unified TokenManager
  • Changed app.id to use credentials instead of tokens, removing app.name property (breaking change)
  • Token passing now uses async factory functions that check expiration and refresh as needed

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/apps/src/microsoft/teams/apps/token_manager.py New unified token manager handling bot, graph, and user tokens with automatic refresh logic
packages/apps/src/microsoft/teams/apps/routing/activity_context.py Updated to use Token type and removed null check for app_token since it's now always a factory function
packages/apps/src/microsoft/teams/apps/http_plugin.py Updated type annotations to use Token and removed separate graph_token parameter
packages/apps/src/microsoft/teams/apps/graph_token_manager.py Removed in favor of unified TokenManager
packages/apps/src/microsoft/teams/apps/app_tokens.py Removed dataclass in favor of TokenManager-based approach
packages/apps/src/microsoft/teams/apps/app_process.py Refactored to use TokenManager methods and pass token factories to context
packages/apps/src/microsoft/teams/apps/app.py Major refactoring removing token refresh logic, app.name property, and integrating TokenManager throughout

Copy link
Collaborator Author

@heyitsaamir heyitsaamir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments for the reviwer

self.container.set_provider("name", providers.Object(self.name))
self.container.set_provider("credentials", providers.Object(self.credentials))
self.container.set_provider("bot_token", providers.Callable(lambda: self.tokens.bot))
self.container.set_provider("graph_token", providers.Callable(lambda: self.tokens.graph))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wasn't being used anywhere, so removing until we need it

self.container.set_provider("credentials", providers.Object(self.credentials))
self.container.set_provider("bot_token", providers.Callable(lambda: self.tokens.bot))
self.container.set_provider("graph_token", providers.Callable(lambda: self.tokens.graph))
self.container.set_provider("bot_token", providers.Factory(self._get_or_refresh_bot_token))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer using a cached token

self.api = ApiClient(
"https://smba.trafficmanager.net/teams",
self.http_client.clone(ClientOptions(token=lambda: self.tokens.bot)),
self.http_client.clone(ClientOptions(token=self._get_or_refresh_bot_token)),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer using a cached token

"""The app's ID from credentials."""
if not self.credentials:
return None
return self.credentials.client_id
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the credentials now, now that we are no longer refreshing the token on app-start

channel_id="msteams",
service_url=self.api.service_url,
bot=Account(id=self.id, name=self.name, role="bot"),
bot=Account(id=self.id, role="bot"),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't have access to "name", and I don't think it's actually useful


return None

async def _refresh_tokens(self, force: bool = False) -> None:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh token logic is all in token manager now

)
api_client = ApiClient(service_url, self.http_client.clone(ClientOptions(token=self.tokens.bot)))
api_client = ApiClient(
service_url, self.http_client.clone(ClientOptions(token=self.token_manager.refresh_bot_token))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing in a factory for the token which can be refreshed if needed.

Copy link

@corinagum corinagum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw copilot left one unresolved comment regarding async function, could you take a look at that? Thanks for doing this work plus extensive testing.

@heyitsaamir heyitsaamir mentioned this pull request Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bot token is not refreshed after an hour and a 401 is received upon ctx.send

4 participants