diff --git a/examples/graph/src/index.ts b/examples/graph/src/index.ts index 5588c2fab..274b19b3f 100644 --- a/examples/graph/src/index.ts +++ b/examples/graph/src/index.ts @@ -10,6 +10,8 @@ const app = new App({ */ defaultConnectionName: 'graph' }, + // Instead of setting in ConsoleLogger like below, you can also + // set LOG_LEVEL=debug or LOG_LEVEL=trace env var for verbose SDK logging logger: new ConsoleLogger('@tests/auth', { level: 'debug' }), // This is an example of overriding the token URL for a specific region (e.g., Europe). // Uncomment this block if needed. @@ -43,4 +45,10 @@ app.event('signin', async ({ send, userGraph, token }) => { await send(`user "${me.displayName}" signed in. Here's the token: ${JSON.stringify(token)}`); }); +app.on('signin.failure', async ({ activity, log, send }) => { + const { code, message } = activity.value; + log.error(`sign-in failed: ${code} - ${message}`); + await send('sign-in failed. please contact your admin.'); +}); + app.start().catch(console.error); diff --git a/packages/apps/src/app.oauth.ts b/packages/apps/src/app.oauth.ts index ca525a31f..b5bd3189c 100644 --- a/packages/apps/src/app.oauth.ts +++ b/packages/apps/src/app.oauth.ts @@ -1,6 +1,7 @@ import { AxiosError } from 'axios'; import { + ISignInFailureInvokeActivity, ISignInTokenExchangeInvokeActivity, ISignInVerifyStateInvokeActivity, TokenExchangeInvokeResponse, @@ -103,3 +104,47 @@ export async function onVerifyState( return { status: 412 }; } } + +/** + * Default handler for signin/failure invoke activities. + * + * Teams sends a signin/failure invoke when SSO token exchange fails + * (e.g., due to a misconfigured Entra app registration). This handler + * logs the failure details and emits an error event so developers are + * notified rather than having the failure silently swallowed. + * + * Known failure codes (sent by the Teams client): + * - `installappfailed`: Failed to install the app in the user's personal scope (non-silent). + * - `authrequestfailed`: The SSO auth request failed after app installation (non-silent). + * - `installedappnotfound`: The bot app is not installed for the user or group chat. + * - `invokeerror`: A generic error occurred during the SSO invoke flow. + * - `resourcematchfailed`: The token exchange resource URI on the OAuthCard does not + * match the Application ID URI in the Entra app registration's "Expose an API" section. + * - `oauthcardnotvalid`: The bot's OAuthCard could not be parsed. + * - `tokenmissing`: AAD token acquisition failed. + * - `userconsentrequired`: The user needs to consent (handled via OAuth card fallback, + * does not typically reach the bot). + * - `interactionrequired`: User interaction is required (handled via OAuth card fallback, + * does not typically reach the bot). + */ +export async function onSignInFailure( + this: App, + ctx: contexts.IActivityContext> +) { + const { log, activity, next } = ctx; + const { code, message } = activity.value; + + log.warn( + `sign-in failed for user "${activity.from.id}" in conversation "${activity.conversation.id}": ${code} — ${message}. ` + + 'If the code is \'resourcematchfailed\', verify that your Entra app registration has \'Expose an API\' configured ' + + 'with the correct Application ID URI matching your OAuth connection\'s Token Exchange URL.' + ); + + this.events.emit('error', { + error: new Error(`Sign-in failure: ${code} — ${message}`), + activity, + }); + + next(ctx); + return { status: 200 }; +} diff --git a/packages/apps/src/app.process.spec.ts b/packages/apps/src/app.process.spec.ts index 215cc2ea5..f63e8d4f5 100644 --- a/packages/apps/src/app.process.spec.ts +++ b/packages/apps/src/app.process.spec.ts @@ -1,4 +1,4 @@ -import { IMessageActivity, InvokeResponse, ITaskFetchInvokeActivity, IToken, MessageActivity, TaskModuleResponse } from '@microsoft/teams.api'; +import { IMessageActivity, InvokeResponse, ISignInFailureInvokeActivity, ITaskFetchInvokeActivity, IToken, MessageActivity, TaskModuleResponse } from '@microsoft/teams.api'; import { App } from './app'; import { IActivityEvent } from './events/activity'; @@ -108,5 +108,29 @@ describe('App', () => { expect(response.status).toBe(500); expect(response.body).toBeUndefined(); }); + + it('should handle signin/failure invoke with default handler', async () => { + const signinFailureActivity = { + type: 'invoke', + name: 'signin/failure', + channelId: 'msteams', + from: { id: 'user-1', name: 'Test User' }, + conversation: { id: 'conv-1' }, + recipient: { id: 'bot-1', name: 'Test Bot' }, + value: { + code: 'resourcematchfailed', + message: 'Resource match failed', + }, + } as unknown as ISignInFailureInvokeActivity; + + const event: IActivityEvent = { + token: token, + activity: signinFailureActivity, + sender: senderPlugin, + }; + + const response = await app.process(senderPlugin, event); + expect(response.status).toBe(200); + }); }); }); diff --git a/packages/apps/src/app.ts b/packages/apps/src/app.ts index 4a71efd74..e023b0861 100644 --- a/packages/apps/src/app.ts +++ b/packages/apps/src/app.ts @@ -27,8 +27,9 @@ import { onError, } from './app.events'; import { + onSignInFailure, onTokenExchange, - onVerifyState + onVerifyState, } from './app.oauth'; import { getMetadata, getPlugin, inject, plugin } from './app.plugins'; import { $process } from './app.process'; @@ -347,6 +348,13 @@ export class App { callback: ctx => this.onVerifyState(ctx), }); + this.router.register({ + name: 'signin.failure', + type: 'system', + select: activity => activity.type === 'invoke' && activity.name === 'signin/failure', + callback: ctx => this.onSignInFailure(ctx), + }); + this.event('error', ({ error }) => { this.log.error(error.message); @@ -518,6 +526,7 @@ export class App { protected onTokenExchange = onTokenExchange; // eslint-disable-line @typescript-eslint/member-ordering protected onVerifyState = onVerifyState; // eslint-disable-line @typescript-eslint/member-ordering + protected onSignInFailure = onSignInFailure; // eslint-disable-line @typescript-eslint/member-ordering /// /// Events diff --git a/packages/cli/templates/typescript/graph/src/index.ts b/packages/cli/templates/typescript/graph/src/index.ts index 67ff5c281..1ab188211 100644 --- a/packages/cli/templates/typescript/graph/src/index.ts +++ b/packages/cli/templates/typescript/graph/src/index.ts @@ -42,4 +42,10 @@ app.event('signin', async ({ send, userGraph }) => { ); }); +app.on('signin.failure', async ({ activity, log, send }) => { + const { code, message } = activity.value; + log.error(`sign-in failed: ${code} - ${message}`); + await send('sign-in failed. please contact your admin.'); +}); + app.start(process.env.PORT || 3978).catch(console.error);