Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion Libraries/Microsoft.Teams.Api/SignIn/StateVerifyQuery.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,9 +14,49 @@ public class StateVerifyQuery
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("state")]
[JsonPropertyOrder(0)]
[JsonConverter(typeof(StringOrObjectConverter))]
public string? State { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This is a change that will break existing customers. Can you help me understand if it's possible to keep this as a string, and deserialize values (even if they're json objects) into a string as well. How will that look like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the implementation to use a custom JsonConverter that keeps the State property as string?, maintaining full backward compatibility. The converter automatically handles both cases:

  • When the value is a string, it returns it directly
  • When the value is a JSON object (Android/iOS), it serializes it to a JSON string

This approach is non-breaking - existing code that reads State as a string will continue to work without any changes. See commit 415b665.


/// <summary>
/// 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.
/// </summary>
private class StringOrObjectConverter : JsonConverter<string?>
{
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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifyStateActivity>(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<SignInActivity>(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<Activity>(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);
}
}