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