From 67756c82784018aefb164b460f7bd997fa5d6b26 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Mon, 23 Feb 2026 16:39:59 -0800 Subject: [PATCH 1/6] Feat: add signin/failure invoke handling --- packages/apps/src/app.oauth.ts | 36 +++++++++++++++++++++++++++ packages/apps/src/app.process.spec.ts | 26 ++++++++++++++++++- packages/apps/src/app.ts | 11 +++++++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/apps/src/app.oauth.ts b/packages/apps/src/app.oauth.ts index ca525a31f..c653266db 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,38 @@ 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. + * + * Common failure codes: + * - `resourcematchfailed`: The token exchange resource URI on the + * OAuthCard does not match the Application ID URI configured in + * the Entra app registration's "Expose an API" section. + */ +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 From 857ea951af56cc16928b6624f9bde3abe0b39a80 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Mon, 23 Feb 2026 16:54:01 -0800 Subject: [PATCH 2/6] Fix quotes issue in tests --- packages/apps/src/app.oauth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apps/src/app.oauth.ts b/packages/apps/src/app.oauth.ts index c653266db..b34c681e0 100644 --- a/packages/apps/src/app.oauth.ts +++ b/packages/apps/src/app.oauth.ts @@ -127,8 +127,8 @@ export async function onSignInFailure( 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.` + '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', { From 4f286b421b785e9bbb701585ae4e4482b845b962 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 10:45:12 -0800 Subject: [PATCH 3/6] Add example to graph sample --- examples/graph/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/graph/src/index.ts b/examples/graph/src/index.ts index 5588c2fab..91cea59b9 100644 --- a/examples/graph/src/index.ts +++ b/examples/graph/src/index.ts @@ -43,4 +43,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, send }) => { + const { code, message } = activity.value; + console.log(`sign-in failed: ${code} - ${message}`); + await send('sign-in failed. please contact your admin.'); +}); + app.start().catch(console.error); From ac76c83f5b1541b513737c2723ce681651b4cee7 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 10:47:54 -0800 Subject: [PATCH 4/6] Update template --- packages/cli/templates/typescript/graph/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/cli/templates/typescript/graph/src/index.ts b/packages/cli/templates/typescript/graph/src/index.ts index 67ff5c281..0e1568338 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, send }) => { + const { code, message } = activity.value; + console.log(`sign-in failed: ${code} - ${message}`); + await send('sign-in failed. please contact your admin.'); +}); + app.start(process.env.PORT || 3978).catch(console.error); From fba1d174aebb2357056899292d645edb2ffe064f Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 11:29:14 -0800 Subject: [PATCH 5/6] Update with failure codes --- packages/apps/src/app.oauth.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/apps/src/app.oauth.ts b/packages/apps/src/app.oauth.ts index b34c681e0..b5bd3189c 100644 --- a/packages/apps/src/app.oauth.ts +++ b/packages/apps/src/app.oauth.ts @@ -113,10 +113,19 @@ export async function onVerifyState( * logs the failure details and emits an error event so developers are * notified rather than having the failure silently swallowed. * - * Common failure codes: - * - `resourcematchfailed`: The token exchange resource URI on the - * OAuthCard does not match the Application ID URI configured in - * the Entra app registration's "Expose an API" section. + * 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, From d9c64a215d2ce8ab884933b0ea17dca7b3f04867 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 16:02:05 -0800 Subject: [PATCH 6/6] Update logs --- examples/graph/src/index.ts | 6 ++++-- packages/cli/templates/typescript/graph/src/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/graph/src/index.ts b/examples/graph/src/index.ts index 91cea59b9..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,9 +45,9 @@ 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, send }) => { +app.on('signin.failure', async ({ activity, log, send }) => { const { code, message } = activity.value; - console.log(`sign-in failed: ${code} - ${message}`); + log.error(`sign-in failed: ${code} - ${message}`); await send('sign-in failed. please contact your admin.'); }); diff --git a/packages/cli/templates/typescript/graph/src/index.ts b/packages/cli/templates/typescript/graph/src/index.ts index 0e1568338..1ab188211 100644 --- a/packages/cli/templates/typescript/graph/src/index.ts +++ b/packages/cli/templates/typescript/graph/src/index.ts @@ -42,9 +42,9 @@ app.event('signin', async ({ send, userGraph }) => { ); }); -app.on('signin.failure', async ({ activity, send }) => { +app.on('signin.failure', async ({ activity, log, send }) => { const { code, message } = activity.value; - console.log(`sign-in failed: ${code} - ${message}`); + log.error(`sign-in failed: ${code} - ${message}`); await send('sign-in failed. please contact your admin.'); });