Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/graph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
45 changes: 45 additions & 0 deletions packages/apps/src/app.oauth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AxiosError } from 'axios';

import {
ISignInFailureInvokeActivity,
ISignInTokenExchangeInvokeActivity,
ISignInVerifyStateInvokeActivity,
TokenExchangeInvokeResponse,
Expand Down Expand Up @@ -103,3 +104,47 @@ export async function onVerifyState<TPlugin extends IPlugin>(
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<TPlugin extends IPlugin>(
this: App<TPlugin>,
ctx: contexts.IActivityContext<ISignInFailureInvokeActivity, PluginAdditionalContext<TPlugin>>
) {
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 };
}
26 changes: 25 additions & 1 deletion packages/apps/src/app.process.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});
});
});
11 changes: 10 additions & 1 deletion packages/apps/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -347,6 +348,13 @@ export class App<TPlugin extends IPlugin = IPlugin> {
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);

Expand Down Expand Up @@ -518,6 +526,7 @@ export class App<TPlugin extends IPlugin = IPlugin> {

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
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/templates/typescript/graph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);