From 43721f0ee4b69900a59914d6f312a24c7b6ffbe2 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 1 Aug 2025 17:56:10 -0700 Subject: [PATCH 1/8] Add getTenantGraph --- packages/apps/src/app.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/apps/src/app.ts b/packages/apps/src/app.ts index f23174db7..26e4af0f6 100644 --- a/packages/apps/src/app.ts +++ b/packages/apps/src/app.ts @@ -400,6 +400,22 @@ export class App { return res; } + /** + * get a tenant specific graph client + * @remarks + * This will use the tenant id to get a token for the graph client. + * @param tenantId the tenant id to get the graph client for + * @returns + */ + getTenantGraph(tenantId: string) { + const getTenantSpecificGraph = async () => { + return this.getOrRefreshTenantToken(tenantId); + }; + return new GraphClient( + this.client.clone({ token: getTenantSpecificGraph }) + ); + } + /** * subscribe to an event * @param name event to subscribe to From f33bc8af1d477741f8955bddbc49a35be2448ae8 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 2 Aug 2025 08:11:36 -0700 Subject: [PATCH 2/8] add logging --- packages/apps/src/app.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/apps/src/app.ts b/packages/apps/src/app.ts index 26e4af0f6..518b4dd4c 100644 --- a/packages/apps/src/app.ts +++ b/packages/apps/src/app.ts @@ -239,6 +239,11 @@ export class App { const tenantId = ('tenantId' in this.options ? this.options.tenantId : undefined) || process.env.TENANT_ID; + if (tenantId) { + this.log.info(`Using tenantId: ${tenantId}. Assuming single-tenant app.`); + } else { + this.log.debug('No tenantId provided. Assuming multi-tenant app.'); + } const token = 'token' in this.options ? this.options.token : undefined; if (clientId && clientSecret) { From a66c58343705f0c0881065b82664d4377d3d1297 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Tue, 24 Mar 2026 23:48:23 -0700 Subject: [PATCH 3/8] Add e2e test spec for echo bot and repo-wide e2e instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These two markdown files are all a human needs to write — the e2e testing skill generates and manages the actual Playwright test code from here. Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e-instructions.md | 9 +++++++++ examples/echo/e2e.spec.md | 12 ++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 e2e-instructions.md create mode 100644 examples/echo/e2e.spec.md diff --git a/e2e-instructions.md b/e2e-instructions.md new file mode 100644 index 000000000..b4f161009 --- /dev/null +++ b/e2e-instructions.md @@ -0,0 +1,9 @@ +# E2E Instructions + +## Environment +- env: `e2e-test/.env` +- devtunnel: `devtunnel host $DEVTUNNEL_NAME` + +## Bot Start +- command: `DOTENV_CONFIG_PATH=../../e2e-test/.env npm run dev` +- ready: `listening on port` diff --git a/examples/echo/e2e.spec.md b/examples/echo/e2e.spec.md new file mode 100644 index 000000000..0962945d0 --- /dev/null +++ b/examples/echo/e2e.spec.md @@ -0,0 +1,12 @@ +# Echo Bot E2E Tests + +## Tests + +- act: send "Hello there" + assert: bot replies with 'you said "Hello there"' + +- act: send "Testing 123" + assert: bot replies with 'you said "Testing 123"' + +- act: send "🎉" + assert: bot replies with 'you said "🎉"' From 7a0e46a1b2dd514f4ce9f06a77a1b03f89242b2f Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Wed, 8 Apr 2026 13:09:53 -0700 Subject: [PATCH 4/8] feat: add App.getAppGraph() and app-users example commands - Rename getTenantGraph to getAppGraph with optional tenantId - Add /app-users and /app-users ctx commands to graph example demonstrating app-only Graph access patterns Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/graph/src/index.ts | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/examples/graph/src/index.ts b/examples/graph/src/index.ts index 50a4c1b45..55dd498d1 100644 --- a/examples/graph/src/index.ts +++ b/examples/graph/src/index.ts @@ -26,6 +26,49 @@ app.message('/signout', async ({ send, signout, isSignedIn }) => { await send('you have been signed out!'); }); +app.message('/app-users ctx', async ({ appGraph, send }) => { + try { + const users = await appGraph.call(endpoints.users.list); + + if (users?.value?.length) { + const userList = users.value.slice(0, 5).map( + (u, i) => `**${i + 1}.** ${u.displayName ?? 'N/A'} (${u.userPrincipalName ?? 'N/A'})` + ).join('\n\n'); + await send(`**Organization Users**\n\n*Fetched using \`ctx.appGraph\`*\n\n${userList}`); + } else { + await send('No users found.'); + } + } catch (e) { + await send( + `Failed to list users: ${e}\n\n` + + 'Ensure the app has **User.Read.All** application permission granted ' + + 'in Azure Portal > App registrations > API permissions, and that an admin has consented.' + ); + } +}); + +app.message('/app-users', async ({ send }) => { + try { + const graph = app.getAppGraph(); + const users = await graph.call(endpoints.users.list); + + if (users?.value?.length) { + const userList = users.value.slice(0, 5).map( + (u, i) => `**${i + 1}.** ${u.displayName ?? 'N/A'} (${u.userPrincipalName ?? 'N/A'})` + ).join('\n\n'); + await send(`**Organization Users**\n\n*Fetched using \`app.getAppGraph()\`*\n\n${userList}`); + } else { + await send('No users found.'); + } + } catch (e) { + await send( + `Failed to list users: ${e}\n\n` + + 'Ensure the app has **User.Read.All** application permission granted ' + + 'in Azure Portal > App registrations > API permissions, and that an admin has consented.' + ); + } +}); + app.on('message', async ({ log, signin, userGraph, isSignedIn }) => { if (!isSignedIn) { await signin({ From d2063f9749359d0fd10d75465867c774d2ce64a2 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Wed, 8 Apr 2026 13:15:13 -0700 Subject: [PATCH 5/8] Remove stale e2e test files Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e-instructions.md | 9 --------- examples/echo/e2e.spec.md | 12 ------------ 2 files changed, 21 deletions(-) delete mode 100644 e2e-instructions.md delete mode 100644 examples/echo/e2e.spec.md diff --git a/e2e-instructions.md b/e2e-instructions.md deleted file mode 100644 index b4f161009..000000000 --- a/e2e-instructions.md +++ /dev/null @@ -1,9 +0,0 @@ -# E2E Instructions - -## Environment -- env: `e2e-test/.env` -- devtunnel: `devtunnel host $DEVTUNNEL_NAME` - -## Bot Start -- command: `DOTENV_CONFIG_PATH=../../e2e-test/.env npm run dev` -- ready: `listening on port` diff --git a/examples/echo/e2e.spec.md b/examples/echo/e2e.spec.md deleted file mode 100644 index 0962945d0..000000000 --- a/examples/echo/e2e.spec.md +++ /dev/null @@ -1,12 +0,0 @@ -# Echo Bot E2E Tests - -## Tests - -- act: send "Hello there" - assert: bot replies with 'you said "Hello there"' - -- act: send "Testing 123" - assert: bot replies with 'you said "Testing 123"' - -- act: send "🎉" - assert: bot replies with 'you said "🎉"' From d0a3a6f17f4ca3e129c4b28e8d5f30e0ae4fe8d3 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 9 Apr 2026 16:35:30 -0700 Subject: [PATCH 6/8] Update graph example README with new commands and teams CLI setup Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/graph/README.md | 84 ++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/examples/graph/README.md b/examples/graph/README.md index 5372d3589..530aa9b4c 100644 --- a/examples/graph/README.md +++ b/examples/graph/README.md @@ -1,70 +1,52 @@ -# Auth test +# Graph Example -Run this first to get all the config files: +Demonstrates Microsoft Graph access from a Teams bot using both user-delegated and app-only permissions. -``` -teams config add atk.oauth -``` - -Then run via ATK. +## Commands -## Teams Toolkit Configuration: Oauth +| Command | Description | +|---------|-------------| +| Any message | Triggers SSO sign-in, then shows your profile | +| `/signout` | Sign out of your account | +| `/app-users` | List org users via `app.getAppGraph()` (no sign-in needed) | +| `/app-users ctx` | List org users via `ctx.appGraph` (no sign-in needed) | -Use this if you want to enable user authentication in your Teams application. +## Setup -## How to update scopes +1. Create an app with SSO: -1. In the `aad.manifest.json` file, update the `requiredResourceAccess` list to add the required scopes. +``` +teams app create +teams app user-auth sso setup +``` -2. In the `infra/botRegistration/azurebot.bicep` file, under the `botServicesMicrosoftGraphConnection` resource, update the `properties.scopes` string to be a comma-delimited list of the required scopes. +2. Run the bot: -### Example +``` +teams app bot start +``` -If you want to add the `People.Read.All` and `User.ReadBasic.All` scopes. +## Adding Scopes -1. Your `requiredResourceAccess` property should look like: +To request additional Graph permissions, edit the SSO connection: -```json -"requiredResourceAccess": [ - { - "resourceAppId": "Microsoft Graph", - "resourceAccess": [ - { - "id": "People.Read.All", - "type": "Scope" - } - ] - }, - { - "resourceAppId": "Microsoft Graph", - "resourceAccess": [ - { - "id": "User.ReadBasic.All", - "type": "Scope" - } - ] - }, -] +``` +teams app user-auth sso edit --connection-name graph --scopes "User.Read,People.Read.All" ``` -2. Update the `properties.scopes` to be `People.Read.All,User.ReadBasic.All`. +For app-only permissions (e.g., `User.Read.All` for `/app-users`), grant them in Azure Portal > App registrations > API permissions and have an admin consent. -## Configuring a Regional Bot -NOTE: This example uses west europe, but follow the equivalent for other locations. +## Regional Bot -1. In `azurebot.bicep`, replace all `global` occurrences to `westeurope` -2. In `manifest.json`, in `validDomains`, `*.botframework.com` should be replaced by `europe.token.botframework.com` -2. In `aad.manifest.json`, replace `https://token.botframework.com/.auth/web/redirect` with `https://europe.token.botframework.com/.auth/web/redirect` -3. In `index.ts`, update `AppOptions` to include `apiClientSettings` +To use a regional token endpoint (e.g., Europe), update `validDomains` in your manifest to include `europe.token.botframework.com` and set `apiClientSettings` in your app: ```typescript const app = new App({ -oauth: { -defaultConnectionName: 'graph', -}, -logger: new ConsoleLogger('@examples/auth', { level: 'debug' }), -apiClientSettings: { - oauthUrl: "https://europe.token.botframework.com", -} + oauth: { + defaultConnectionName: 'graph', + }, + apiClientSettings: { + oauthUrl: 'https://europe.token.botframework.com', + } }); -``` \ No newline at end of file +``` From d0f16d8990b631bbb1b42df8417be115ce8096b3 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sun, 12 Apr 2026 21:09:16 -0700 Subject: [PATCH 7/8] Deprecate app.graph in favor of getAppGraph() Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/apps/src/app.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/apps/src/app.ts b/packages/apps/src/app.ts index 7b08bc311..1bffd7b74 100644 --- a/packages/apps/src/app.ts +++ b/packages/apps/src/app.ts @@ -175,7 +175,7 @@ export type AppActivityOptions = { */ export class App { readonly api: ApiClient; - readonly graph: GraphClient; + readonly log: ILogger; readonly server: HttpServer; readonly http?: HttpPlugin; @@ -191,6 +191,14 @@ export class App { return this.tokenManager.credentials; } + /** + * A Microsoft Graph client using the app's default tenant. + * @deprecated Use {@link getAppGraph}() instead. This getter always uses the app's default tenant. `getAppGraph(tenantId?)` supports multi-tenant scenarios. + */ + get graph(): GraphClient { + return this.getAppGraph(); + } + /** * the apps id */ @@ -291,10 +299,6 @@ export class App { this.options.apiClientSettings ); - this.graph = new GraphClient( - this.client.clone({ token: () => this.getAppGraphToken() }) - ); - // initialize TokenManager with credentials this.tokenManager = new TokenManager({ clientId: this.options.clientId, From 64569c20c68a82331d22773edd1174fc809992ab Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sun, 12 Apr 2026 22:34:13 -0700 Subject: [PATCH 8/8] Deprecate app.graph and ctx.appGraph in favor of app.getAppGraph() Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/graph/README.md | 1 - examples/graph/src/index.ts | 23 +---------------------- packages/apps/src/contexts/activity.ts | 1 + packages/apps/src/contexts/function.ts | 1 + 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/examples/graph/README.md b/examples/graph/README.md index 530aa9b4c..7a0d6ec06 100644 --- a/examples/graph/README.md +++ b/examples/graph/README.md @@ -9,7 +9,6 @@ Demonstrates Microsoft Graph access from a Teams bot using both user-delegated a | Any message | Triggers SSO sign-in, then shows your profile | | `/signout` | Sign out of your account | | `/app-users` | List org users via `app.getAppGraph()` (no sign-in needed) | -| `/app-users ctx` | List org users via `ctx.appGraph` (no sign-in needed) | ## Setup diff --git a/examples/graph/src/index.ts b/examples/graph/src/index.ts index 55dd498d1..9a2d792cd 100644 --- a/examples/graph/src/index.ts +++ b/examples/graph/src/index.ts @@ -26,27 +26,6 @@ app.message('/signout', async ({ send, signout, isSignedIn }) => { await send('you have been signed out!'); }); -app.message('/app-users ctx', async ({ appGraph, send }) => { - try { - const users = await appGraph.call(endpoints.users.list); - - if (users?.value?.length) { - const userList = users.value.slice(0, 5).map( - (u, i) => `**${i + 1}.** ${u.displayName ?? 'N/A'} (${u.userPrincipalName ?? 'N/A'})` - ).join('\n\n'); - await send(`**Organization Users**\n\n*Fetched using \`ctx.appGraph\`*\n\n${userList}`); - } else { - await send('No users found.'); - } - } catch (e) { - await send( - `Failed to list users: ${e}\n\n` + - 'Ensure the app has **User.Read.All** application permission granted ' + - 'in Azure Portal > App registrations > API permissions, and that an admin has consented.' - ); - } -}); - app.message('/app-users', async ({ send }) => { try { const graph = app.getAppGraph(); @@ -56,7 +35,7 @@ app.message('/app-users', async ({ send }) => { const userList = users.value.slice(0, 5).map( (u, i) => `**${i + 1}.** ${u.displayName ?? 'N/A'} (${u.userPrincipalName ?? 'N/A'})` ).join('\n\n'); - await send(`**Organization Users**\n\n*Fetched using \`app.getAppGraph()\`*\n\n${userList}`); + await send(`**Organization Users**\n\n${userList}`); } else { await send('No users found.'); } diff --git a/packages/apps/src/contexts/activity.ts b/packages/apps/src/contexts/activity.ts index 763041ec8..0083a0955 100644 --- a/packages/apps/src/contexts/activity.ts +++ b/packages/apps/src/contexts/activity.ts @@ -72,6 +72,7 @@ export interface IBaseActivityContextOptions { /** * the app graph client + * @deprecated Use `app.getAppGraph(tenantId?)` instead for tenant-specific Graph access. */ appGraph: GraphClient; diff --git a/packages/apps/src/contexts/function.ts b/packages/apps/src/contexts/function.ts index 3b7d8ca3a..9c529f414 100644 --- a/packages/apps/src/contexts/function.ts +++ b/packages/apps/src/contexts/function.ts @@ -13,6 +13,7 @@ export interface IFunctionContext extends IClientContext { /** * the app graph client + * @deprecated Use `app.getAppGraph(tenantId?)` instead for tenant-specific Graph access. */ appGraph: GraphClient;