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() {