From c1dc59746f87ec2ed23a8b47cb81b21cde540ef7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Feb 2026 17:53:46 +0000
Subject: [PATCH 1/6] Initial plan
From e4326739f44c00e007daa7708830519697467a9b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Feb 2026 17:59:18 +0000
Subject: [PATCH 2/6] Fix SignIn VerifyState state deserialization to support
both string and object types
Co-authored-by: singhk97 <115390646+singhk97@users.noreply.github.com>
---
.../SignIn/StateVerifyQuery.cs | 56 ++++++-
Libraries/Microsoft.Teams.Apps/AppRouting.cs | 2 +-
.../SignIn/VerifyStateSignInActivityTests.cs | 146 +++++++++++++++++-
3 files changed, 199 insertions(+), 5 deletions(-)
diff --git a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
index 66f877f9..9814bc9a 100644
--- a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
+++ b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Teams.Api.SignIn;
@@ -11,11 +12,60 @@ namespace Microsoft.Teams.Api.SignIn;
public class StateVerifyQuery
{
///
- /// The state string originally received when the
+ /// The state value originally received when the
/// signin web flow is finished with a state posted back to client via tab SDK
- /// microsoftTeams.authentication.notifySuccess(state)
+ /// microsoftTeams.authentication.notifySuccess(state).
+ /// Can be either a string or a JSON object depending on the platform (Android/iOS may send objects).
///
[JsonPropertyName("state")]
[JsonPropertyOrder(0)]
- public string? State { get; set; }
+ public JsonElement? State { get; set; }
+
+ ///
+ /// Gets the state as a string if it is a string value, otherwise returns the JSON representation.
+ ///
+ /// The state as a string, or null if State is null.
+ public string? GetStateAsString()
+ {
+ if (State == null)
+ {
+ return null;
+ }
+
+ var element = State.Value;
+
+ // If it's a string, return the string value
+ if (element.ValueKind == JsonValueKind.String)
+ {
+ return element.GetString();
+ }
+
+ // Otherwise, return the JSON representation
+ return element.ToString();
+ }
+
+ ///
+ /// Tries to get the state as a string value.
+ ///
+ /// The state as a string if it is a string value.
+ /// True if the state is a string value, false otherwise.
+ public bool TryGetStateAsString(out string? stateString)
+ {
+ stateString = null;
+
+ if (State == null)
+ {
+ return false;
+ }
+
+ var element = State.Value;
+
+ if (element.ValueKind == JsonValueKind.String)
+ {
+ stateString = element.GetString();
+ return true;
+ }
+
+ return false;
+ }
}
\ No newline at end of file
diff --git a/Libraries/Microsoft.Teams.Apps/AppRouting.cs b/Libraries/Microsoft.Teams.Apps/AppRouting.cs
index 664da0d5..d70417f3 100644
--- a/Libraries/Microsoft.Teams.Apps/AppRouting.cs
+++ b/Libraries/Microsoft.Teams.Apps/AppRouting.cs
@@ -141,7 +141,7 @@ await Events.Emit(
ChannelId = context.Activity.ChannelId,
UserId = context.Activity.From.Id,
ConnectionName = OAuth.DefaultConnectionName,
- Code = context.Activity.Value.State
+ Code = context.Activity.Value.GetStateAsString()
});
context.UserGraphToken = new JsonWebToken(res);
diff --git a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
index 44d3a9a1..e7d2ee6a 100644
--- a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
+++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
@@ -16,7 +16,7 @@ private VerifyStateActivity SetupSignInValidStateActivity()
{
Value = new StateVerifyQuery()
{
- State = "success"
+ State = JsonSerializer.SerializeToElement("success")
},
Conversation = new Api.Conversation()
{
@@ -147,4 +147,148 @@ public void setupSignInVerifyStateActivity_JsonDeserialize_Derived_Activity_Inte
Assert.NotNull(activity);
Assert.Equal(expected.ToString(), activity.ToString());
}
+
+ [Fact]
+ public void setupSignInVerifyStateActivity_StateAsString()
+ {
+ var activity = SetupSignInValidStateActivity();
+
+ Assert.NotNull(activity.Value.State);
+ Assert.Equal(JsonValueKind.String, activity.Value.State.Value.ValueKind);
+ Assert.Equal("success", activity.Value.GetStateAsString());
+
+ Assert.True(activity.Value.TryGetStateAsString(out var stateString));
+ Assert.Equal("success", stateString);
+ }
+
+ [Fact]
+ public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject()
+ {
+ // Test JSON with state as an object (Android/iOS scenario)
+ var jsonWithObjectState = @"{
+ ""type"": ""invoke"",
+ ""channelId"": ""msteams"",
+ ""name"": ""signin/verifyState"",
+ ""value"": {
+ ""state"": {
+ ""token"": ""abc123"",
+ ""userId"": ""user123""
+ }
+ },
+ ""from"": {
+ ""id"": ""botId"",
+ ""aadObjectId"": ""aadObjectId"",
+ ""name"": ""User Name""
+ },
+ ""recipient"": {
+ ""id"": ""recipientId"",
+ ""name"": ""Recipient Name""
+ },
+ ""conversation"": {
+ ""id"": ""conversationId"",
+ ""conversationType"": ""groupChat""
+ }
+}";
+
+ var activity = JsonSerializer.Deserialize(jsonWithObjectState);
+
+ Assert.NotNull(activity);
+ Assert.NotNull(activity.Value);
+ Assert.NotNull(activity.Value.State);
+ Assert.Equal(JsonValueKind.Object, activity.Value.State.Value.ValueKind);
+
+ // Verify we can get the state as a string (JSON representation)
+ var stateString = activity.Value.GetStateAsString();
+ Assert.NotNull(stateString);
+ Assert.Contains("token", stateString);
+ Assert.Contains("abc123", stateString);
+
+ // Verify TryGetStateAsString returns false for object state
+ Assert.False(activity.Value.TryGetStateAsString(out _));
+ }
+
+ [Fact]
+ public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject_ViaSignInActivity()
+ {
+ // Test JSON with state as an object through SignInActivity
+ var jsonWithObjectState = @"{
+ ""type"": ""invoke"",
+ ""channelId"": ""msteams"",
+ ""name"": ""signin/verifyState"",
+ ""value"": {
+ ""state"": {
+ ""sessionId"": ""session-456"",
+ ""redirectUrl"": ""https://example.com/callback""
+ }
+ },
+ ""from"": {
+ ""id"": ""botId"",
+ ""aadObjectId"": ""aadObjectId"",
+ ""name"": ""User Name""
+ },
+ ""recipient"": {
+ ""id"": ""recipientId"",
+ ""name"": ""Recipient Name""
+ },
+ ""conversation"": {
+ ""id"": ""conversationId"",
+ ""conversationType"": ""groupChat""
+ }
+}";
+
+ var activity = JsonSerializer.Deserialize(jsonWithObjectState);
+
+ Assert.NotNull(activity);
+ var verifyStateActivity = activity.ToVerifyState();
+ Assert.NotNull(verifyStateActivity);
+ Assert.NotNull(verifyStateActivity.Value.State);
+ Assert.Equal(JsonValueKind.Object, verifyStateActivity.Value.State.Value.ValueKind);
+
+ // Verify we can access the state
+ var stateString = verifyStateActivity.Value.GetStateAsString();
+ Assert.NotNull(stateString);
+ Assert.Contains("sessionId", stateString);
+ Assert.Contains("session-456", stateString);
+ }
+
+ [Fact]
+ public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject_ViaActivity()
+ {
+ // Test JSON with state as an object through Activity
+ var jsonWithObjectState = @"{
+ ""type"": ""invoke"",
+ ""channelId"": ""msteams"",
+ ""name"": ""signin/verifyState"",
+ ""value"": {
+ ""state"": {
+ ""code"": ""auth-code-789""
+ }
+ },
+ ""from"": {
+ ""id"": ""botId"",
+ ""aadObjectId"": ""aadObjectId"",
+ ""name"": ""User Name""
+ },
+ ""recipient"": {
+ ""id"": ""recipientId"",
+ ""name"": ""Recipient Name""
+ },
+ ""conversation"": {
+ ""id"": ""conversationId"",
+ ""conversationType"": ""groupChat""
+ }
+}";
+
+ var activity = JsonSerializer.Deserialize(jsonWithObjectState);
+
+ Assert.NotNull(activity);
+ Assert.True(activity is InvokeActivity);
+ var invokeActivity = (InvokeActivity)activity;
+ Assert.True(invokeActivity is SignInActivity);
+ var signInActivity = (SignInActivity)invokeActivity;
+ var verifyStateActivity = signInActivity.ToVerifyState();
+
+ Assert.NotNull(verifyStateActivity.Value.State);
+ Assert.Equal(JsonValueKind.Object, verifyStateActivity.Value.State.Value.ValueKind);
+ }
}
\ No newline at end of file
From e8db7831ceae84705ca20faa2d5e02e22efdd004 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Feb 2026 18:01:11 +0000
Subject: [PATCH 3/6] Use JsonSerializer.Serialize for proper JSON formatting
Co-authored-by: singhk97 <115390646+singhk97@users.noreply.github.com>
---
Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
index 9814bc9a..7ac99d8a 100644
--- a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
+++ b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
@@ -41,7 +41,7 @@ public class StateVerifyQuery
}
// Otherwise, return the JSON representation
- return element.ToString();
+ return JsonSerializer.Serialize(element);
}
///
From 415b665b3430afcc722103d45c69b2f509c25aa8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 18:01:12 +0000
Subject: [PATCH 4/6] Use custom JsonConverter to keep State as string
(non-breaking)
Co-authored-by: singhk97 <115390646+singhk97@users.noreply.github.com>
---
.../SignIn/StateVerifyQuery.cs | 71 ++++++++-----------
Libraries/Microsoft.Teams.Apps/AppRouting.cs | 2 +-
.../SignIn/VerifyStateSignInActivityTests.cs | 34 ++++-----
3 files changed, 44 insertions(+), 63 deletions(-)
diff --git a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
index 7ac99d8a..e1a88f31 100644
--- a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
+++ b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
@@ -12,60 +12,51 @@ namespace Microsoft.Teams.Api.SignIn;
public class StateVerifyQuery
{
///
- /// The state value originally received when the
+ /// The state string originally received when the
/// signin web flow is finished with a state posted back to client via tab SDK
/// microsoftTeams.authentication.notifySuccess(state).
/// Can be either a string or a JSON object depending on the platform (Android/iOS may send objects).
+ /// When a JSON object is received, it is automatically serialized to a JSON string.
///
[JsonPropertyName("state")]
[JsonPropertyOrder(0)]
- public JsonElement? State { get; set; }
+ [JsonConverter(typeof(StringOrObjectConverter))]
+ public string? State { get; set; }
///
- /// Gets the state as a string if it is a string value, otherwise returns the JSON representation.
+ /// Custom JSON converter that handles both string and object values for the State property.
+ /// When deserializing, if the value is a string, it returns the string value.
+ /// If the value is a JSON object (or any other type), it serializes it to a JSON string.
///
- /// The state as a string, or null if State is null.
- public string? GetStateAsString()
+ private class StringOrObjectConverter : JsonConverter
{
- if (State == null)
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- return null;
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return reader.GetString();
+ }
+
+ // For any other token type (object, array, number, etc.), read as JsonElement and serialize
+ using var doc = JsonDocument.ParseValue(ref reader);
+ return JsonSerializer.Serialize(doc.RootElement, options);
}
- var element = State.Value;
-
- // If it's a string, return the string value
- if (element.ValueKind == JsonValueKind.String)
+ public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
- return element.GetString();
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ }
+ else
+ {
+ writer.WriteStringValue(value);
+ }
}
-
- // Otherwise, return the JSON representation
- return JsonSerializer.Serialize(element);
- }
-
- ///
- /// Tries to get the state as a string value.
- ///
- /// The state as a string if it is a string value.
- /// True if the state is a string value, false otherwise.
- public bool TryGetStateAsString(out string? stateString)
- {
- stateString = null;
-
- if (State == null)
- {
- return false;
- }
-
- var element = State.Value;
-
- if (element.ValueKind == JsonValueKind.String)
- {
- stateString = element.GetString();
- return true;
- }
-
- return false;
}
}
\ No newline at end of file
diff --git a/Libraries/Microsoft.Teams.Apps/AppRouting.cs b/Libraries/Microsoft.Teams.Apps/AppRouting.cs
index d70417f3..664da0d5 100644
--- a/Libraries/Microsoft.Teams.Apps/AppRouting.cs
+++ b/Libraries/Microsoft.Teams.Apps/AppRouting.cs
@@ -141,7 +141,7 @@ await Events.Emit(
ChannelId = context.Activity.ChannelId,
UserId = context.Activity.From.Id,
ConnectionName = OAuth.DefaultConnectionName,
- Code = context.Activity.Value.GetStateAsString()
+ Code = context.Activity.Value.State
});
context.UserGraphToken = new JsonWebToken(res);
diff --git a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
index e7d2ee6a..9ede5faf 100644
--- a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
+++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
@@ -16,7 +16,7 @@ private VerifyStateActivity SetupSignInValidStateActivity()
{
Value = new StateVerifyQuery()
{
- State = JsonSerializer.SerializeToElement("success")
+ State = "success"
},
Conversation = new Api.Conversation()
{
@@ -154,11 +154,7 @@ public void setupSignInVerifyStateActivity_StateAsString()
var activity = SetupSignInValidStateActivity();
Assert.NotNull(activity.Value.State);
- Assert.Equal(JsonValueKind.String, activity.Value.State.Value.ValueKind);
- Assert.Equal("success", activity.Value.GetStateAsString());
-
- Assert.True(activity.Value.TryGetStateAsString(out var stateString));
- Assert.Equal("success", stateString);
+ Assert.Equal("success", activity.Value.State);
}
[Fact]
@@ -195,16 +191,12 @@ public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject()
Assert.NotNull(activity);
Assert.NotNull(activity.Value);
Assert.NotNull(activity.Value.State);
- Assert.Equal(JsonValueKind.Object, activity.Value.State.Value.ValueKind);
-
- // Verify we can get the state as a string (JSON representation)
- var stateString = activity.Value.GetStateAsString();
- Assert.NotNull(stateString);
- Assert.Contains("token", stateString);
- Assert.Contains("abc123", stateString);
- // Verify TryGetStateAsString returns false for object state
- Assert.False(activity.Value.TryGetStateAsString(out _));
+ // Verify the state was serialized to a JSON string
+ Assert.Contains("token", activity.Value.State);
+ Assert.Contains("abc123", activity.Value.State);
+ Assert.Contains("userId", activity.Value.State);
+ Assert.Contains("user123", activity.Value.State);
}
[Fact]
@@ -242,13 +234,10 @@ public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject_ViaSign
var verifyStateActivity = activity.ToVerifyState();
Assert.NotNull(verifyStateActivity);
Assert.NotNull(verifyStateActivity.Value.State);
- Assert.Equal(JsonValueKind.Object, verifyStateActivity.Value.State.Value.ValueKind);
- // Verify we can access the state
- var stateString = verifyStateActivity.Value.GetStateAsString();
- Assert.NotNull(stateString);
- Assert.Contains("sessionId", stateString);
- Assert.Contains("session-456", stateString);
+ // Verify the state was serialized to a JSON string
+ Assert.Contains("sessionId", verifyStateActivity.Value.State);
+ Assert.Contains("session-456", verifyStateActivity.Value.State);
}
[Fact]
@@ -289,6 +278,7 @@ public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject_ViaActi
var verifyStateActivity = signInActivity.ToVerifyState();
Assert.NotNull(verifyStateActivity.Value.State);
- Assert.Equal(JsonValueKind.Object, verifyStateActivity.Value.State.Value.ValueKind);
+ Assert.Contains("code", verifyStateActivity.Value.State);
+ Assert.Contains("auth-code-789", verifyStateActivity.Value.State);
}
}
\ No newline at end of file
From 8381831a2772f3d48ac1cc61e93f7bc24e488d1f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 18:04:56 +0000
Subject: [PATCH 5/6] Prevent potential recursion in JsonSerializer.Serialize
call
Co-authored-by: singhk97 <115390646+singhk97@users.noreply.github.com>
---
Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
index e1a88f31..c9cbf3c2 100644
--- a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
+++ b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
@@ -44,7 +44,7 @@ private class StringOrObjectConverter : JsonConverter
// For any other token type (object, array, number, etc.), read as JsonElement and serialize
using var doc = JsonDocument.ParseValue(ref reader);
- return JsonSerializer.Serialize(doc.RootElement, options);
+ return JsonSerializer.Serialize(doc.RootElement);
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
From 641a08ce2c54a08e4c78624733f3e5047b5d5da3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 18:29:34 +0000
Subject: [PATCH 6/6] Remove redundant test case
setupSignInVerifyStateActivity_StateAsString
Co-authored-by: singhk97 <115390646+singhk97@users.noreply.github.com>
---
.../Invokes/SignIn/VerifyStateSignInActivityTests.cs | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
index 9ede5faf..6164590d 100644
--- a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
+++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs
@@ -148,15 +148,6 @@ public void setupSignInVerifyStateActivity_JsonDeserialize_Derived_Activity_Inte
Assert.Equal(expected.ToString(), activity.ToString());
}
- [Fact]
- public void setupSignInVerifyStateActivity_StateAsString()
- {
- var activity = SetupSignInValidStateActivity();
-
- Assert.NotNull(activity.Value.State);
- Assert.Equal("success", activity.Value.State);
- }
-
[Fact]
public void setupSignInVerifyStateActivity_JsonDeserialize_StateAsObject()
{