diff --git a/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs b/Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs index 66f877f9..c9cbf3c2 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; @@ -13,9 +14,49 @@ public class StateVerifyQuery /// /// 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) + /// 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)] + [JsonConverter(typeof(StringOrObjectConverter))] public string? State { get; set; } + + /// + /// 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. + /// + private class StringOrObjectConverter : JsonConverter + { + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + 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); + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value); + } + } + } } \ No newline at end of file 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..6164590d 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Invokes/SignIn/VerifyStateSignInActivityTests.cs @@ -147,4 +147,129 @@ public void setupSignInVerifyStateActivity_JsonDeserialize_Derived_Activity_Inte Assert.NotNull(activity); Assert.Equal(expected.ToString(), activity.ToString()); } + + [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); + + // 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] + 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); + + // Verify the state was serialized to a JSON string + Assert.Contains("sessionId", verifyStateActivity.Value.State); + Assert.Contains("session-456", verifyStateActivity.Value.State); + } + + [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.Contains("code", verifyStateActivity.Value.State); + Assert.Contains("auth-code-789", verifyStateActivity.Value.State); + } } \ No newline at end of file