diff --git a/examples/graph/README.md b/examples/graph/README.md index 5372d3589..7a0d6ec06 100644 --- a/examples/graph/README.md +++ b/examples/graph/README.md @@ -1,70 +1,51 @@ -# 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) | -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 +``` diff --git a/examples/graph/src/index.ts b/examples/graph/src/index.ts index 50a4c1b45..9a2d792cd 100644 --- a/examples/graph/src/index.ts +++ b/examples/graph/src/index.ts @@ -26,6 +26,28 @@ app.message('/signout', async ({ send, signout, isSignedIn }) => { await send('you have been signed out!'); }); +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${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({ diff --git a/packages/apps/src/app.ts b/packages/apps/src/app.ts index b3529614d..7d0d799d7 100644 --- a/packages/apps/src/app.ts +++ b/packages/apps/src/app.ts @@ -187,7 +187,6 @@ export type AppActivityOptions = { export class App { readonly api: ApiClient; readonly cloud: CloudEnvironment; - readonly graph: GraphClient; readonly log: ILogger; readonly server: HttpServer; readonly http?: HttpPlugin; @@ -203,6 +202,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 */ @@ -309,10 +316,6 @@ export class App { this.cloud ); - this.graph = new GraphClient( - this.client.clone({ token: () => this.getAppGraphToken() }) - ); - // initialize TokenManager with credentials this.tokenManager = new TokenManager({ clientId: this.options.clientId, @@ -545,6 +548,22 @@ export class App { return res; } + /** + * Get a Microsoft Graph client configured with the app's token. + * + * @remarks + * This client can be used for app-only operations that don't require user context. + * For multi-tenant apps, pass a tenantId to get a tenant-specific token. + * + * @param tenantId - Optional tenant ID. If not provided, uses the app's default tenant. + * @returns A GraphClient for app-only Graph operations. + */ + getAppGraph(tenantId?: string) { + return new GraphClient( + this.client.clone({ token: () => this.getAppGraphToken(tenantId) }) + ); + } + /** * subscribe to an event * @param name event to subscribe to 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;