From 3030e5dacdbeb9200a21bc1de51b5738a4db7c2a Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Mon, 23 Feb 2026 17:16:35 -0800 Subject: [PATCH 1/8] Feat: add signin/failure invoke handling --- .../Invokes/SignIn/FailureActivity.cs | 33 ++++ .../Activities/Invokes/SignInActivity.cs | 9 ++ .../Microsoft.Teams.Api/SignIn/Failure.cs | 26 +++ .../Invokes/SignIn/FailureActivity.cs | 124 +++++++++++++++ Libraries/Microsoft.Teams.Apps/App.cs | 1 + Libraries/Microsoft.Teams.Apps/AppRouting.cs | 32 ++++ .../SignIn/FailureSignInActivityTests.cs | 149 ++++++++++++++++++ .../Invokes/SignInFailureActivity.json | 22 +++ 8 files changed, 396 insertions(+) create mode 100644 Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs create mode 100644 Libraries/Microsoft.Teams.Api/SignIn/Failure.cs create mode 100644 Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs create mode 100644 Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs create mode 100644 Tests/Microsoft.Teams.Api.Tests/Json/Activity/Invokes/SignInFailureActivity.json diff --git a/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs b/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs new file mode 100644 index 00000000..de8ad726 --- /dev/null +++ b/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +using Microsoft.Teams.Common; + +namespace Microsoft.Teams.Api.Activities.Invokes; + +public partial class Name : StringEnum +{ + public partial class SignIn : StringEnum + { + public static readonly SignIn Failure = new("signin/failure"); + public bool IsFailure => Failure.Equals(Value); + } +} + +public static partial class SignIn +{ + /// + /// Represents a signin/failure invoke activity sent by Teams when SSO token exchange fails. + /// + public class FailureActivity() : SignInActivity(Name.SignIn.Failure) + { + /// + /// A value that is associated with the activity. + /// + [JsonPropertyName("value")] + [JsonPropertyOrder(32)] + public new required Api.SignIn.Failure Value { get; set; } + } +} diff --git a/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignInActivity.cs b/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignInActivity.cs index 6f068fe0..78768308 100644 --- a/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignInActivity.cs +++ b/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignInActivity.cs @@ -21,11 +21,13 @@ public abstract class SignInActivity(Name.SignIn name) : InvokeActivity(new(name { public SignIn.TokenExchangeActivity ToTokenExchange() => (SignIn.TokenExchangeActivity)this; public SignIn.VerifyStateActivity ToVerifyState() => (SignIn.VerifyStateActivity)this; + public SignIn.FailureActivity ToFailure() => (SignIn.FailureActivity)this; public override object ToType(Type type, IFormatProvider? provider) { if (type == typeof(SignIn.TokenExchangeActivity)) return ToTokenExchange(); if (type == typeof(SignIn.VerifyStateActivity)) return ToVerifyState(); + if (type == typeof(SignIn.FailureActivity)) return ToFailure(); return this; } @@ -56,6 +58,7 @@ public override bool CanConvert(Type typeToConvert) { "signin/tokenExchange" => JsonSerializer.Deserialize(element.ToString(), options), "signin/verifyState" => JsonSerializer.Deserialize(element.ToString(), options), + "signin/failure" => JsonSerializer.Deserialize(element.ToString(), options), _ => throw new JsonException($"failed to deserialize signin activity '{name}' doesn't match any known types.") }; } @@ -74,6 +77,12 @@ public override void Write(Utf8JsonWriter writer, SignInActivity value, JsonSeri return; } + if (value is SignIn.FailureActivity failure) + { + JsonSerializer.Serialize(writer, failure, options); + return; + } + JsonSerializer.Serialize(writer, value, options); } } diff --git a/Libraries/Microsoft.Teams.Api/SignIn/Failure.cs b/Libraries/Microsoft.Teams.Api/SignIn/Failure.cs new file mode 100644 index 00000000..320926da --- /dev/null +++ b/Libraries/Microsoft.Teams.Api/SignIn/Failure.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Microsoft.Teams.Api.SignIn; + +/// +/// Sign-in failure information sent by Teams when SSO token exchange fails. +/// +public class Failure +{ + /// + /// The error code for the sign-in failure (e.g., "resourcematchfailed"). + /// + [JsonPropertyName("code")] + [JsonPropertyOrder(0)] + public string? Code { get; set; } + + /// + /// The error message for the sign-in failure. + /// + [JsonPropertyName("message")] + [JsonPropertyOrder(1)] + public string? Message { get; set; } +} diff --git a/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs b/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs new file mode 100644 index 00000000..bebfa45b --- /dev/null +++ b/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Api.Activities.Invokes; +using Microsoft.Teams.Apps.Routing; + +namespace Microsoft.Teams.Apps.Activities.Invokes; + +/// +/// Attribute for handling signin/failure invoke activities sent when SSO token exchange fails. +/// +[AttributeUsage(AttributeTargets.Method, Inherited = true)] +public class FailureAttribute() : InvokeAttribute(Api.Activities.Invokes.Name.SignIn.Failure, typeof(SignIn.FailureActivity)) +{ + public override object Coerce(IContext context) => context.ToActivityType(); +} + +public static partial class AppInvokeActivityExtensions +{ + /// + /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. + /// + public static App OnFailure(this App app, Func, Task> handler) + { + app.Router.Register(new Route() + { + Name = string.Join("/", [ActivityType.Invoke, Name.SignIn.Failure]), + Type = app.Status is null ? RouteType.System : RouteType.User, + Handler = async context => + { + await handler(context.ToActivityType()); + return null; + }, + Selector = activity => activity is SignIn.FailureActivity + }); + + return app; + } + + /// + /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. + /// + public static App OnFailure(this App app, Func, Task> handler) + { + app.Router.Register(new Route() + { + Name = string.Join("/", [ActivityType.Invoke, Name.SignIn.Failure]), + Type = app.Status is null ? RouteType.System : RouteType.User, + Handler = context => handler(context.ToActivityType()), + Selector = activity => activity is SignIn.FailureActivity + }); + + return app; + } + + /// + /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. + /// + public static App OnFailure(this App app, Func, Task> handler) + { + app.Router.Register(new Route() + { + Name = string.Join("/", [ActivityType.Invoke, Name.SignIn.Failure]), + Type = app.Status is null ? RouteType.System : RouteType.User, + Handler = async context => await handler(context.ToActivityType()), + Selector = activity => activity is SignIn.FailureActivity + }); + + return app; + } + + /// + /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. + /// + public static App OnFailure(this App app, Func, CancellationToken, Task> handler) + { + app.Router.Register(new Route() + { + Name = string.Join("/", [ActivityType.Invoke, Name.SignIn.Failure]), + Type = app.Status is null ? RouteType.System : RouteType.User, + Handler = async context => + { + await handler(context.ToActivityType(), context.CancellationToken); + return null; + }, + Selector = activity => activity is SignIn.FailureActivity + }); + + return app; + } + + /// + /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. + /// + public static App OnFailure(this App app, Func, CancellationToken, Task> handler) + { + app.Router.Register(new Route() + { + Name = string.Join("/", [ActivityType.Invoke, Name.SignIn.Failure]), + Type = app.Status is null ? RouteType.System : RouteType.User, + Handler = context => handler(context.ToActivityType(), context.CancellationToken), + Selector = activity => activity is SignIn.FailureActivity + }); + + return app; + } + + /// + /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. + /// + public static App OnFailure(this App app, Func, CancellationToken, Task> handler) + { + app.Router.Register(new Route() + { + Name = string.Join("/", [ActivityType.Invoke, Name.SignIn.Failure]), + Type = app.Status is null ? RouteType.System : RouteType.User, + Handler = async context => await handler(context.ToActivityType(), context.CancellationToken), + Selector = activity => activity is SignIn.FailureActivity + }); + + return app; + } +} diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 5f7768bc..529c0100 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -102,6 +102,7 @@ public App(AppOptions? options = null) this.OnTokenExchange(OnTokenExchangeActivity); this.OnVerifyState(OnVerifyStateActivity); + this.OnFailure(OnFailureActivity); this.OnError(OnErrorEvent); this.OnActivitySent(OnActivitySentEvent); this.OnActivityResponse(OnActivityResponseEvent); diff --git a/Libraries/Microsoft.Teams.Apps/AppRouting.cs b/Libraries/Microsoft.Teams.Apps/AppRouting.cs index 664da0d5..8fd43a9d 100644 --- a/Libraries/Microsoft.Teams.Apps/AppRouting.cs +++ b/Libraries/Microsoft.Teams.Apps/AppRouting.cs @@ -179,6 +179,38 @@ await Events.Emit( } } + /// + /// Default handler for signin/failure invoke activities. + /// Teams sends this when SSO token exchange fails (e.g., due to a + /// misconfigured Entra app registration). Logs the failure details + /// and emits an error event. + /// + protected async Task OnFailureActivity(IContext context) + { + var failure = context.Activity.Value; + + Logger.Warn( + $"sign-in failed for user \"{context.Activity.From.Id}\" in conversation " + + $"\"{context.Ref.Conversation.Id}\": {failure.Code} — {failure.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." + ); + + await Events.Emit( + context.Sender, + EventType.Error, + new ErrorEvent() + { + Exception = new Exception($"Sign-in failure: {failure.Code} — {failure.Message}"), + Context = context.ToActivityType() + }, + context.CancellationToken + ); + + return new Response(HttpStatusCode.OK); + } + /// /// Register a middleware. /// diff --git a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs new file mode 100644 index 00000000..0f7f3d1a --- /dev/null +++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs @@ -0,0 +1,149 @@ +using System.Text.Json; + +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Api.Activities.Invokes; + +using static Microsoft.Teams.Api.Activities.Invokes.SignIn; + +namespace Microsoft.Teams.Api.Tests.Activities.Invokes; + +public class FailureSignInActivityTests +{ + private FailureActivity SetupSignInFailureActivity() + { + return new FailureActivity() + { + Value = new Api.SignIn.Failure() + { + Code = "resourcematchfailed", + Message = "Resource match failed" + }, + Conversation = new Api.Conversation() + { + Id = "conversationId", + Type = ConversationType.GroupChat + }, + From = new Account() + { + Id = "botId", + Name = "User Name", + AadObjectId = "aadObjectId" + }, + Recipient = new Account() + { + Id = "recipientId", + Name = "Recipient Name", + }, + }; + } + + [Fact] + public void SignInFailureActivity_JsonSerialize() + { + var activity = SetupSignInFailureActivity(); + + var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() + { + WriteIndented = true, + IndentSize = 2, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + string expectedPath = "Activity.Invoke.Signin/failure"; + Assert.Equal(expectedPath, activity.GetPath()); + Assert.NotNull(activity.ToFailure()); + var expectedSubmitException = "Unable to cast object of type 'FailureActivity' to type 'TokenExchangeActivity'."; + var ex = Assert.Throws(() => activity.ToTokenExchange()); + Assert.Equal(expectedSubmitException, ex.Message); + Assert.Equal(File.ReadAllText( + @"../../../Json/Activity/Invokes/SignInFailureActivity.json" + ), json); + } + + [Fact] + public void SignInFailureActivity_JsonSerialize_Derived() + { + SignInActivity activity = SetupSignInFailureActivity(); + + var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() + { + WriteIndented = true, + IndentSize = 2, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + string expectedPath = "Activity.Invoke.Signin/failure"; + Assert.Equal(expectedPath, activity.GetPath()); + Assert.Equal(File.ReadAllText( + @"../../../Json/Activity/Invokes/SignInFailureActivity.json" + ), json); + } + + [Fact] + public void SignInFailureActivity_JsonSerialize_Derived_Interface() + { + InvokeActivity activity = SetupSignInFailureActivity(); + + var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() + { + WriteIndented = true, + IndentSize = 2, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + string expectedPath = "Activity.Invoke.Signin/failure"; + Assert.Equal(expectedPath, activity.GetPath()); + Assert.Equal(File.ReadAllText( + @"../../../Json/Activity/Invokes/SignInFailureActivity.json" + ), json); + } + + [Fact] + public void SignInFailureActivity_JsonDeserialize() + { + var json = File.ReadAllText(@"../../../Json/Activity/Invokes/SignInFailureActivity.json"); + var activity = JsonSerializer.Deserialize(json); + var expected = SetupSignInFailureActivity(); + + Assert.Equal(expected.ToString(), activity!.ToString()); + Assert.NotNull(activity.ToFailure()); + + var expectedSubmitException = "Unable to cast object of type 'FailureActivity' to type 'TokenExchangeActivity'."; + var ex = Assert.Throws(() => activity.ToTokenExchange()); + Assert.Equal(expectedSubmitException, ex.Message); + } + + [Fact] + public void SignInFailureActivity_JsonDeserialize_Derived() + { + var json = File.ReadAllText(@"../../../Json/Activity/Invokes/SignInFailureActivity.json"); + var activity = JsonSerializer.Deserialize(json); + var expected = SetupSignInFailureActivity(); + + Assert.NotNull(activity); + Assert.Equal(expected.ToString(), activity.ToString()); + Assert.NotNull(activity.ToSignIn()); + } + + [Fact] + public void SignInFailureActivity_JsonDeserialize_Derived_Interface() + { + var json = File.ReadAllText(@"../../../Json/Activity/Invokes/SignInFailureActivity.json"); + var activity = JsonSerializer.Deserialize(json); + var expected = SetupSignInFailureActivity(); + + Assert.NotNull(activity); + Assert.Equal(expected.ToString(), activity.ToString()); + } + + [Fact] + public void SignInFailureActivity_JsonDeserialize_Derived_Activity_Interface() + { + var json = File.ReadAllText(@"../../../Json/Activity/Invokes/SignInFailureActivity.json"); + var activity = JsonSerializer.Deserialize(json); + var expected = SetupSignInFailureActivity(); + + Assert.NotNull(activity); + Assert.Equal(expected.ToString(), activity.ToString()); + } +} diff --git a/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Invokes/SignInFailureActivity.json b/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Invokes/SignInFailureActivity.json new file mode 100644 index 00000000..3db31ab8 --- /dev/null +++ b/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Invokes/SignInFailureActivity.json @@ -0,0 +1,22 @@ +{ + "type": "invoke", + "channelId": "msteams", + "name": "signin/failure", + "value": { + "code": "resourcematchfailed", + "message": "Resource match failed" + }, + "from": { + "id": "botId", + "aadObjectId": "aadObjectId", + "name": "User Name" + }, + "recipient": { + "id": "recipientId", + "name": "Recipient Name" + }, + "conversation": { + "id": "conversationId", + "conversationType": "groupChat" + } +} \ No newline at end of file From cf46d70c8c949d2652714dc774ba92e144f161a0 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Tue, 3 Mar 2026 08:59:34 -0800 Subject: [PATCH 2/8] Apply PR feedback --- .../Activities/Invokes/SignIn/FailureActivity.cs | 12 ++++++------ Libraries/Microsoft.Teams.Apps/App.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs b/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs index bebfa45b..1fff7314 100644 --- a/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs +++ b/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs @@ -21,7 +21,7 @@ public static partial class AppInvokeActivityExtensions /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. /// - public static App OnFailure(this App app, Func, Task> handler) + public static App OnSigninFailure(this App app, Func, Task> handler) { app.Router.Register(new Route() { @@ -41,7 +41,7 @@ public static App OnFailure(this App app, Func, /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. /// - public static App OnFailure(this App app, Func, Task> handler) + public static App OnSigninFailure(this App app, Func, Task> handler) { app.Router.Register(new Route() { @@ -57,7 +57,7 @@ public static App OnFailure(this App app, Func, /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. /// - public static App OnFailure(this App app, Func, Task> handler) + public static App OnSigninFailure(this App app, Func, Task> handler) { app.Router.Register(new Route() { @@ -73,7 +73,7 @@ public static App OnFailure(this App app, Func, /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. /// - public static App OnFailure(this App app, Func, CancellationToken, Task> handler) + public static App OnSigninFailure(this App app, Func, CancellationToken, Task> handler) { app.Router.Register(new Route() { @@ -93,7 +93,7 @@ public static App OnFailure(this App app, Func, /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. /// - public static App OnFailure(this App app, Func, CancellationToken, Task> handler) + public static App OnSigninFailure(this App app, Func, CancellationToken, Task> handler) { app.Router.Register(new Route() { @@ -109,7 +109,7 @@ public static App OnFailure(this App app, Func, /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. /// - public static App OnFailure(this App app, Func, CancellationToken, Task> handler) + public static App OnSigninFailure(this App app, Func, CancellationToken, Task> handler) { app.Router.Register(new Route() { diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 529c0100..6864fe83 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -102,7 +102,7 @@ public App(AppOptions? options = null) this.OnTokenExchange(OnTokenExchangeActivity); this.OnVerifyState(OnVerifyStateActivity); - this.OnFailure(OnFailureActivity); + this.OnSigninFailure(OnFailureActivity); this.OnError(OnErrorEvent); this.OnActivitySent(OnActivitySentEvent); this.OnActivityResponse(OnActivityResponseEvent); From 9a7c41f2f47b78a6d59799f9007773a9d64553ee Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Tue, 3 Mar 2026 09:31:09 -0800 Subject: [PATCH 3/8] Apply PR feedback --- .../Activities/Invokes/SignIn/FailureActivity.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs b/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs index de8ad726..bec8f919 100644 --- a/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs +++ b/Libraries/Microsoft.Teams.Api/Activities/Invokes/SignIn/FailureActivity.cs @@ -28,6 +28,10 @@ public class FailureActivity() : SignInActivity(Name.SignIn.Failure) /// [JsonPropertyName("value")] [JsonPropertyOrder(32)] - public new required Api.SignIn.Failure Value { get; set; } + public new required Api.SignIn.Failure Value + { + get => (Api.SignIn.Failure)base.Value!; + set => base.Value = value; + } } } From f80c9396dd2405ab4d22563204942c0102a556cf Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 10:46:34 -0800 Subject: [PATCH 4/8] Add example to graph sample --- Samples/Samples.Graph/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Samples/Samples.Graph/Program.cs b/Samples/Samples.Graph/Program.cs index 28ee108c..cadf6ee6 100644 --- a/Samples/Samples.Graph/Program.cs +++ b/Samples/Samples.Graph/Program.cs @@ -74,4 +74,11 @@ await context.SignIn(new OAuthOptions() await context.Send($"user \"{me!.DisplayName}\" signed in. Here's the token: {token.Token}", cancellationToken); }); +teams.OnSigninFailure(async (context, cancellationToken) => +{ + var failure = context.Activity.Value; + Console.WriteLine($"sign-in failed: {failure?.Code} - {failure?.Message}"); + await context.Send("sign-in failed. please contact your admin.", cancellationToken); +}); + app.Run(); \ No newline at end of file From e919a9ea46c61b55405185f9af970f3c265e6751 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 11:24:18 -0800 Subject: [PATCH 5/8] Update with list of failure codes --- Libraries/Microsoft.Teams.Apps/AppRouting.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Libraries/Microsoft.Teams.Apps/AppRouting.cs b/Libraries/Microsoft.Teams.Apps/AppRouting.cs index 8fd43a9d..d17bbefb 100644 --- a/Libraries/Microsoft.Teams.Apps/AppRouting.cs +++ b/Libraries/Microsoft.Teams.Apps/AppRouting.cs @@ -184,6 +184,19 @@ await Events.Emit( /// Teams sends this when SSO token exchange fails (e.g., due to a /// misconfigured Entra app registration). Logs the failure details /// and emits an error event. + /// + /// Known failure codes (sent by the Teams client): + /// + /// installappfailedFailed to install the app in the user's personal scope (non-silent). + /// authrequestfailedThe SSO auth request failed after app installation (non-silent). + /// installedappnotfoundThe bot app is not installed for the user or group chat. + /// invokeerrorA generic error occurred during the SSO invoke flow. + /// resourcematchfailedThe token exchange resource URI on the OAuthCard does not match the Application ID URI in the Entra app registration's "Expose an API" section. + /// oauthcardnotvalidThe bot's OAuthCard could not be parsed. + /// tokenmissingAAD token acquisition failed. + /// userconsentrequiredThe user needs to consent (handled via OAuth card fallback, does not typically reach the bot). + /// interactionrequiredUser interaction is required (handled via OAuth card fallback, does not typically reach the bot). + /// /// protected async Task OnFailureActivity(IContext context) { From a8f95e943f30f8b3fda9ac4fc35c82eeb16428ec Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 13:10:22 -0800 Subject: [PATCH 6/8] Fix capitalization --- .../Activities/Invokes/SignIn/FailureActivity.cs | 12 ++++++------ Libraries/Microsoft.Teams.Apps/App.cs | 2 +- Samples/Samples.Graph/Program.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs b/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs index 1fff7314..5fed8f77 100644 --- a/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs +++ b/Libraries/Microsoft.Teams.Apps/Activities/Invokes/SignIn/FailureActivity.cs @@ -21,7 +21,7 @@ public static partial class AppInvokeActivityExtensions /// /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. /// - public static App OnSigninFailure(this App app, Func, Task> handler) + public static App OnSignInFailure(this App app, Func, Task> handler) { app.Router.Register(new Route() { @@ -41,7 +41,7 @@ public static App OnSigninFailure(this App app, Func /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. /// - public static App OnSigninFailure(this App app, Func, Task> handler) + public static App OnSignInFailure(this App app, Func, Task> handler) { app.Router.Register(new Route() { @@ -57,7 +57,7 @@ public static App OnSigninFailure(this App app, Func /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails. /// - public static App OnSigninFailure(this App app, Func, Task> handler) + public static App OnSignInFailure(this App app, Func, Task> handler) { app.Router.Register(new Route() { @@ -73,7 +73,7 @@ public static App OnSigninFailure(this App app, Func /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. /// - public static App OnSigninFailure(this App app, Func, CancellationToken, Task> handler) + public static App OnSignInFailure(this App app, Func, CancellationToken, Task> handler) { app.Router.Register(new Route() { @@ -93,7 +93,7 @@ public static App OnSigninFailure(this App app, Func /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. /// - public static App OnSigninFailure(this App app, Func, CancellationToken, Task> handler) + public static App OnSignInFailure(this App app, Func, CancellationToken, Task> handler) { app.Router.Register(new Route() { @@ -109,7 +109,7 @@ public static App OnSigninFailure(this App app, Func /// Registers a handler for signin/failure invoke activities sent when SSO token exchange fails, with cancellation token support. /// - public static App OnSigninFailure(this App app, Func, CancellationToken, Task> handler) + public static App OnSignInFailure(this App app, Func, CancellationToken, Task> handler) { app.Router.Register(new Route() { diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 6864fe83..37b27a85 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -102,7 +102,7 @@ public App(AppOptions? options = null) this.OnTokenExchange(OnTokenExchangeActivity); this.OnVerifyState(OnVerifyStateActivity); - this.OnSigninFailure(OnFailureActivity); + this.OnSignInFailure(OnFailureActivity); this.OnError(OnErrorEvent); this.OnActivitySent(OnActivitySentEvent); this.OnActivityResponse(OnActivityResponseEvent); diff --git a/Samples/Samples.Graph/Program.cs b/Samples/Samples.Graph/Program.cs index cadf6ee6..83fe7eba 100644 --- a/Samples/Samples.Graph/Program.cs +++ b/Samples/Samples.Graph/Program.cs @@ -74,10 +74,10 @@ await context.SignIn(new OAuthOptions() await context.Send($"user \"{me!.DisplayName}\" signed in. Here's the token: {token.Token}", cancellationToken); }); -teams.OnSigninFailure(async (context, cancellationToken) => +teams.OnSignInFailure(async (context, cancellationToken) => { var failure = context.Activity.Value; - Console.WriteLine($"sign-in failed: {failure?.Code} - {failure?.Message}"); + context.Log.Error($"sign-in failed: {failure?.Code} - {failure?.Message}"); await context.Send("sign-in failed. please contact your admin.", cancellationToken); }); From 8b083ce9789ab867bc6a7bdfa1533e10baebf134 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 13:19:41 -0800 Subject: [PATCH 7/8] Fix samples build --- Samples/Samples.Graph/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Samples/Samples.Graph/Program.cs b/Samples/Samples.Graph/Program.cs index 83fe7eba..c89f83f8 100644 --- a/Samples/Samples.Graph/Program.cs +++ b/Samples/Samples.Graph/Program.cs @@ -1,5 +1,6 @@ using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Events; using Microsoft.Teams.Apps.Extensions; using Microsoft.Teams.Common.Logging; From 8276a846fc5da9528d3215ec44e9d8b0fc32d3d9 Mon Sep 17 00:00:00 2001 From: Corina Gum <> Date: Wed, 4 Mar 2026 13:30:50 -0800 Subject: [PATCH 8/8] Apply copilot feedback --- .../Invokes/SignIn/FailureSignInActivityTests.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs index 0f7f3d1a..78ec1101 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/FailureSignInActivityTests.cs @@ -45,16 +45,15 @@ public void SignInFailureActivity_JsonSerialize() var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() { WriteIndented = true, - IndentSize = 2, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); string expectedPath = "Activity.Invoke.Signin/failure"; Assert.Equal(expectedPath, activity.GetPath()); Assert.NotNull(activity.ToFailure()); - var expectedSubmitException = "Unable to cast object of type 'FailureActivity' to type 'TokenExchangeActivity'."; + var expectedCastExceptionMessage = "Unable to cast object of type 'FailureActivity' to type 'TokenExchangeActivity'."; var ex = Assert.Throws(() => activity.ToTokenExchange()); - Assert.Equal(expectedSubmitException, ex.Message); + Assert.Equal(expectedCastExceptionMessage, ex.Message); Assert.Equal(File.ReadAllText( @"../../../Json/Activity/Invokes/SignInFailureActivity.json" ), json); @@ -68,7 +67,6 @@ public void SignInFailureActivity_JsonSerialize_Derived() var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() { WriteIndented = true, - IndentSize = 2, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); @@ -87,7 +85,6 @@ public void SignInFailureActivity_JsonSerialize_Derived_Interface() var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() { WriteIndented = true, - IndentSize = 2, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); @@ -108,9 +105,9 @@ public void SignInFailureActivity_JsonDeserialize() Assert.Equal(expected.ToString(), activity!.ToString()); Assert.NotNull(activity.ToFailure()); - var expectedSubmitException = "Unable to cast object of type 'FailureActivity' to type 'TokenExchangeActivity'."; + var expectedCastExceptionMessage = "Unable to cast object of type 'FailureActivity' to type 'TokenExchangeActivity'."; var ex = Assert.Throws(() => activity.ToTokenExchange()); - Assert.Equal(expectedSubmitException, ex.Message); + Assert.Equal(expectedCastExceptionMessage, ex.Message); } [Fact]