From 432189b26c9757e4ba7f39c62b14e2c075abfe1e Mon Sep 17 00:00:00 2001 From: Kailash B Date: Wed, 8 Jul 2026 15:51:21 +0530 Subject: [PATCH 1/5] feat: [Core] Phase out Newtonsoft.Json --- .../FlexibleDateTimeConverter.cs | 55 ++--- .../HttpClientAuthenticationConnection.cs | 21 +- .../Models/AccessTokenResponse.cs | 8 +- .../Models/ChangePasswordRequest.cs | 4 +- ...itiatedBackchannelAuthorizationResponse.cs | 8 +- ...edBackchannelAuthorizationTokenResponse.cs | 20 +- .../Models/Ciba/LoginHint.cs | 19 +- .../Models/DeviceCodeResponse.cs | 14 +- .../Mfa/AssociateMfaAuthenticatorRequest.cs | 16 +- .../Mfa/AssociateMfaAuthenticatorResponse.cs | 16 +- .../Models/Mfa/MfaAuthenticator.cs | 12 +- .../Models/Mfa/MfaChallengeResponse.cs | 8 +- .../Models/Mfa/MfaOobTokenResponse.cs | 8 +- .../Models/Mfa/MfaOtpTokenResponse.cs | 4 +- .../Models/Mfa/MfaRecoveryCodeResponse.cs | 6 +- .../Models/PasswordlessEmailRequest.cs | 12 +- .../Models/PasswordlessEmailResponse.cs | 8 +- .../Models/PasswordlessSmsRequest.cs | 8 +- .../Models/PasswordlessSmsResponse.cs | 8 +- .../PushedAuthorizationRequestResponse.cs | 6 +- .../Models/SignupUserRequest.cs | 20 +- .../Models/SignupUserResponse.cs | 34 +-- .../Models/TokenBase.cs | 6 +- .../Models/UserInfo.cs | 209 ++++++----------- .../Models/UserInfoAddress.cs | 16 +- .../Models/UserMaintenanceRequestBase.cs | 8 +- src/Auth0.Core/Auth0.Core.csproj | 1 + src/Auth0.Core/Exceptions/ApiError.cs | 21 +- .../Serialization/ApiErrorConverter.cs | 86 +++---- .../Auth0JsonSerializerOptions.cs | 23 ++ .../StringOrObjectAsStringConverter.cs | 53 ++--- .../StringOrStringArrayJsonConverter.cs | 46 ++-- .../ApiErrorDeserializationTests.cs | 210 ++++-------------- .../Auth0JsonSerializerOptionsTests.cs | 39 ++++ .../StringOrObjectAsStringConverterTests.cs | 183 ++------------- .../StringOrStringArrayConverterTests.cs | 115 ++-------- 36 files changed, 483 insertions(+), 848 deletions(-) create mode 100644 src/Auth0.Core/Serialization/Auth0JsonSerializerOptions.cs create mode 100644 tests/Auth0.Core.UnitTests/Auth0JsonSerializerOptionsTests.cs diff --git a/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs b/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs index b2533e1d7..95379fe6b 100644 --- a/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs +++ b/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs @@ -1,41 +1,36 @@ -using System; - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi; /// -/// A JSON date converter that reads both ISO 8601 and epoch dates. +/// A JSON date converter that reads both ISO 8601 and epoch (seconds) dates. /// -internal class FlexibleDateTimeConverter : IsoDateTimeConverter +internal class FlexibleDateTimeConverter : JsonConverter { private static readonly DateTime Epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + case JsonTokenType.Number: + return Add(Epoch, TimeSpan.FromSeconds(reader.GetInt64())); + case JsonTokenType.String: + return reader.GetDateTime(); + default: + return null; + } } - public override bool CanWrite => false; - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { - if (reader.Value == null) return null; - - return reader.TokenType == JsonToken.Integer - ? Add(Epoch, TimeSpan.FromSeconds((long)reader.Value)) - : reader.Value; + throw new NotImplementedException(); } - /// - /// Add a DateTime and a TimeSpan. - /// The maximum time is DateTime.MaxTime. It is not an error if time + timespan > MaxTime. - /// Just return MaxTime. - /// - /// Initial value. - /// to add. - /// as the sum of time and timespan. private static DateTime Add(DateTime time, TimeSpan timespan) { if (timespan == TimeSpan.Zero) @@ -50,25 +45,15 @@ private static DateTime Add(DateTime time, TimeSpan timespan) return time + timespan; } - /// - /// Gets the Maximum value for a DateTime specifying kind. - /// - /// DateTimeKind to use. - /// DateTime of specified kind. private static DateTime GetMaxValue(DateTimeKind kind) { return new DateTime(DateTime.MaxValue.Ticks, kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : kind); } - /// - /// Gets the Minimum value for a DateTime specifying kind. - /// - /// DateTimeKind to use. - /// DateTime of specified kind. private static DateTime GetMinValue(DateTimeKind kind) { return new DateTime(DateTime.MinValue.Ticks, kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : kind); } -} \ No newline at end of file +} diff --git a/src/Auth0.AuthenticationApi/HttpClientAuthenticationConnection.cs b/src/Auth0.AuthenticationApi/HttpClientAuthenticationConnection.cs index 37474a78e..ef52d6c9d 100644 --- a/src/Auth0.AuthenticationApi/HttpClientAuthenticationConnection.cs +++ b/src/Auth0.AuthenticationApi/HttpClientAuthenticationConnection.cs @@ -7,7 +7,8 @@ using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; +using System.Text.Json; +using Auth0.Core.Serialization; using Auth0.Core.Exceptions; using Auth0.Core.Http; @@ -20,8 +21,6 @@ namespace Auth0.AuthenticationApi; /// public class HttpClientAuthenticationConnection : IAuthenticationConnection, IDisposable { - private static readonly JsonSerializerSettings jsonSerializerSettings = new() { NullValueHandling = NullValueHandling.Ignore, DateParseHandling = DateParseHandling.DateTime }; - private readonly HttpClient httpClient; private readonly string agentString; private bool ownHttpClient; @@ -98,9 +97,13 @@ private async Task SendRequest(HttpRequestMessage request, CancellationTok internal T DeserializeContent(string content) { - return typeof(T) == typeof(string) - ? (T)(object)content - : JsonConvert.DeserializeObject(content, jsonSerializerSettings); + if (typeof(T) == typeof(string)) + return (T)(object)content; + + if (string.IsNullOrWhiteSpace(content)) + return default; + + return JsonSerializer.Deserialize(content, Auth0JsonSerializerOptions.Default); } private void ApplyHeaders(HttpRequestMessage request, IDictionary headers) @@ -125,7 +128,7 @@ private HttpContent BuildMessageContent(object body) private static HttpContent CreateJsonStringContent(object body) { - var json = JsonConvert.SerializeObject(body, jsonSerializerSettings); + var json = JsonSerializer.Serialize(body, body.GetType(), Auth0JsonSerializerOptions.Default); return new StringContent(json, Encoding.UTF8, "application/json"); } @@ -157,12 +160,12 @@ private static string CreateAgentString() #endif var sdkVersion = typeof(HttpClientAuthenticationConnection).GetTypeInfo().Assembly.GetName().Version; - var agentJson = JsonConvert.SerializeObject(new + var agentJson = JsonSerializer.Serialize(new { name = "Auth0.Net", version = sdkVersion.Major + "." + sdkVersion.Minor + "." + sdkVersion.Revision, env = new { target } - }, Formatting.None); + }); return Utils.Base64UrlEncode(Encoding.UTF8.GetBytes(agentJson)); } diff --git a/src/Auth0.AuthenticationApi/Models/AccessTokenResponse.cs b/src/Auth0.AuthenticationApi/Models/AccessTokenResponse.cs index 38ebd7ef9..deb054fdd 100644 --- a/src/Auth0.AuthenticationApi/Models/AccessTokenResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/AccessTokenResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -11,19 +11,19 @@ public class AccessTokenResponse : TokenBase /// /// Identifier token. /// - [JsonProperty("id_token")] + [JsonPropertyName("id_token")] public string IdToken { get; set; } /// /// Expiration time in seconds. /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } /// /// Refresh token. /// - [JsonProperty("refresh_token")] + [JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } public IDictionary> Headers { get; set; } diff --git a/src/Auth0.AuthenticationApi/Models/ChangePasswordRequest.cs b/src/Auth0.AuthenticationApi/Models/ChangePasswordRequest.cs index 35791e1f7..79f34c7c5 100644 --- a/src/Auth0.AuthenticationApi/Models/ChangePasswordRequest.cs +++ b/src/Auth0.AuthenticationApi/Models/ChangePasswordRequest.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,6 +10,6 @@ public class ChangePasswordRequest : UserMaintenanceRequestBase /// /// The organization_id of the Organization associated with the user. /// - [JsonProperty("organization")] + [JsonPropertyName("organization")] public string Organization { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationResponse.cs b/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationResponse.cs index 848d40bea..cd3a152d8 100644 --- a/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Ciba; @@ -10,18 +10,18 @@ public class ClientInitiatedBackchannelAuthorizationResponse /// /// Unique id of the authorization request. Can be used further to poll for /oauth/token. /// - [JsonProperty("auth_req_id")] + [JsonPropertyName("auth_req_id")] public string AuthRequestId { get; set; } /// /// Tells you how many seconds we have until the authentication request expires. /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } /// /// Tells how many seconds you must leave between poll requests. /// - [JsonProperty("interval")] + [JsonPropertyName("interval")] public int Interval { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationTokenResponse.cs b/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationTokenResponse.cs index 59c0e0ee8..71c250522 100644 --- a/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationTokenResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Ciba/ClientInitiatedBackchannelAuthorizationTokenResponse.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; - -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using Auth0.AuthenticationApi.Models; using Auth0.Core.Serialization; @@ -10,25 +10,25 @@ namespace Auth0.AuthenticationApi.Models.Ciba; public class ClientInitiatedBackchannelAuthorizationTokenResponse : AccessTokenResponse { - [JsonProperty("scope")] + [JsonPropertyName("scope")] public string Scope { get; set; } /// /// Raw authorization_details JSON returned by the token endpoint as part of a /// Rich Authorization Requests (RAR) flow. Use for a strongly-typed view. /// - [JsonProperty("authorization_details")] + [JsonPropertyName("authorization_details")] [JsonConverter(typeof(StringOrObjectAsStringConverter))] public string AuthorizationDetailsRaw { get; set; } + private bool _authorizationDetailsParsed; + private IReadOnlyList _authorizationDetails; + /// /// Strongly-typed view of the authorization_details returned by the token endpoint. /// Returns when no authorization details were returned. /// - private bool _authorizationDetailsParsed; - private IReadOnlyList _authorizationDetails; - - [Newtonsoft.Json.JsonIgnore] + [JsonIgnore] public IReadOnlyList AuthorizationDetails { get @@ -41,11 +41,11 @@ public IReadOnlyList AuthorizationDetails try { - _authorizationDetails = System.Text.Json.JsonSerializer.Deserialize>(AuthorizationDetailsRaw); + _authorizationDetails = JsonSerializer.Deserialize>(AuthorizationDetailsRaw); _authorizationDetailsParsed = true; return _authorizationDetails; } - catch (System.Text.Json.JsonException ex) + catch (JsonException ex) { throw new InvalidOperationException( "Failed to deserialize the 'authorization_details' returned by the token endpoint.", ex); diff --git a/src/Auth0.AuthenticationApi/Models/Ciba/LoginHint.cs b/src/Auth0.AuthenticationApi/Models/Ciba/LoginHint.cs index 579700c01..7ad497274 100644 --- a/src/Auth0.AuthenticationApi/Models/Ciba/LoginHint.cs +++ b/src/Auth0.AuthenticationApi/Models/Ciba/LoginHint.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Ciba; @@ -7,21 +8,21 @@ namespace Auth0.AuthenticationApi.Models.Ciba; /// public class LoginHint { - [JsonProperty("format")] + [JsonPropertyName("format")] public string Format { get; set; } - + /// - /// Issuer of the ID Token. + /// Issuer of the ID Token. /// This value should match the 'Issuer' value configured in the well-known configuration. /// - [JsonProperty("iss")] + [JsonPropertyName("iss")] public string Issuer { get; set; } - - [JsonProperty("sub")] + + [JsonPropertyName("sub")] public string Subject { get; set; } public override string ToString() { - return JsonConvert.SerializeObject(this); + return JsonSerializer.Serialize(this, Auth0.Core.Serialization.Auth0JsonSerializerOptions.Default); } -} \ No newline at end of file +} diff --git a/src/Auth0.AuthenticationApi/Models/DeviceCodeResponse.cs b/src/Auth0.AuthenticationApi/Models/DeviceCodeResponse.cs index 45fed9df5..e8e757783 100644 --- a/src/Auth0.AuthenticationApi/Models/DeviceCodeResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/DeviceCodeResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,36 +10,36 @@ public class DeviceCodeResponse /// /// The unique code for the device. When the user goes to the in their browser-based device, this code will be bound to their session. /// - [JsonProperty("device_code")] + [JsonPropertyName("device_code")] public string DeviceCode { get; set; } /// /// The code that should be input at the to authorize the device. /// - [JsonProperty("user_code")] + [JsonPropertyName("user_code")] public string UserCode { get; set; } /// /// The URL the user should visit to authorize the device. /// - [JsonProperty("verification_uri")] + [JsonPropertyName("verification_uri")] public string VerificationUri { get; set; } /// /// The lifetime (in seconds) of the and . /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } /// /// The interval (in seconds) at which the app should poll the token URL to request a token. /// - [JsonProperty("interval")] + [JsonPropertyName("interval")] public int Interval { get; set; } /// /// The complete URL the user should visit to authorize the device. This allows embedding the in the URL. /// - [JsonProperty("verification_uri_complete")] + [JsonPropertyName("verification_uri_complete")] public string VerificationUriComplete { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorRequest.cs b/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorRequest.cs index 540cfb812..97b973047 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorRequest.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorRequest.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; @@ -17,48 +17,48 @@ public class AssociateMfaAuthenticatorRequest /// /// Your application's Client ID. /// - [JsonProperty("client_id")] + [JsonPropertyName("client_id")] public string ClientId { get; set; } /// /// A JWT containing a signed assertion with your application credentials. /// Required when Private Key JWT is your application authentication method. /// - [JsonProperty("client_assertion")] + [JsonPropertyName("client_assertion")] public string ClientAssertion { get; set; } /// /// The value is urn:ietf:params:oauth:client-assertion-type:jwt-bearer. /// Required when Private Key JWT is the application authentication method. /// - [JsonProperty("client_assertion_type")] + [JsonPropertyName("client_assertion_type")] public string ClientAssertionType { get; set; } /// /// Your application's Client Secret. /// Required when the Token Endpoint Authentication Method field in your Application Settings is Post or Basic. /// - [JsonProperty("client_secret")] + [JsonPropertyName("client_secret")] public string ClientSecret { get; set; } /// /// The type of authenticators supported by the client. /// Value is an array with values "otp" or "oob". /// - [JsonProperty("authenticator_types")] + [JsonPropertyName("authenticator_types")] public string[] AuthenticatorTypes { get; set; } /// /// The type of OOB channels supported by the client. /// Required if authenticator_types include oob. /// - [JsonProperty("oob_channels")] + [JsonPropertyName("oob_channels")] public List OobChannels { get; set; } /// /// The phone number to use for SMS or Voice. /// Required if oob_channels includes sms or voice. /// - [JsonProperty("phone_number")] + [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorResponse.cs b/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorResponse.cs index 77701241e..dbab778bb 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/AssociateMfaAuthenticatorResponse.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; @@ -9,42 +9,42 @@ public class AssociateMfaAuthenticatorResponse /// /// The code used for out-of-band authentication. /// - [JsonProperty("oob_code")] + [JsonPropertyName("oob_code")] public string OobCode { get; set; } /// /// Indicates the binding method used. /// - [JsonProperty("binding_method")] + [JsonPropertyName("binding_method")] public string BindingMethod { get; set; } /// /// Type of authenticator added. /// - [JsonProperty("authenticator_type")] + [JsonPropertyName("authenticator_type")] public string AuthenticatorType { get; set; } /// /// The OOB channels used. /// - [JsonProperty("oob_channel")] + [JsonPropertyName("oob_channel")] public string OobChannel { get; set; } /// /// An array of recovery codes generated for the user. /// - [JsonProperty("recovery_codes")] + [JsonPropertyName("recovery_codes")] public List RecoveryCodes { get; set; } /// /// The URI to generate a QR code for the authenticator. /// - [JsonProperty("barcode_uri")] + [JsonPropertyName("barcode_uri")] public string BarcodeUri { get; set; } /// /// The secret to use for the OTP. /// - [JsonProperty("secret")] + [JsonPropertyName("secret")] public string Secret { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/MfaAuthenticator.cs b/src/Auth0.AuthenticationApi/Models/Mfa/MfaAuthenticator.cs index 1b3639fb0..5c2d017e7 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/MfaAuthenticator.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/MfaAuthenticator.cs @@ -1,21 +1,21 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; public class Authenticator { - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; set; } - [JsonProperty("authenticator_type")] + [JsonPropertyName("authenticator_type")] public string AuthenticatorType { get; set; } - [JsonProperty("oob_channel")] + [JsonPropertyName("oob_channel")] public string OobChannel { get; set; } - [JsonProperty("name")] + [JsonPropertyName("name")] public string Name { get; set; } - [JsonProperty("active")] + [JsonPropertyName("active")] public bool Active { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/MfaChallengeResponse.cs b/src/Auth0.AuthenticationApi/Models/Mfa/MfaChallengeResponse.cs index fbae775d7..eaf1b3601 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/MfaChallengeResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/MfaChallengeResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; @@ -7,18 +7,18 @@ public class MfaChallengeResponse /// /// Type of challenge issued. /// - [JsonProperty("challenge_type")] + [JsonPropertyName("challenge_type")] public string ChallengeType { get; set; } /// /// Code for out-of-band challenge (only if applicable). /// - [JsonProperty("oob_code")] + [JsonPropertyName("oob_code")] public string OobCode { get; set; } /// /// Method used for binding (only if applicable). /// - [JsonProperty("binding_method")] + [JsonPropertyName("binding_method")] public string BindingMethod { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/MfaOobTokenResponse.cs b/src/Auth0.AuthenticationApi/Models/Mfa/MfaOobTokenResponse.cs index 21e0a98f6..dbce89e2d 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/MfaOobTokenResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/MfaOobTokenResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; @@ -7,18 +7,18 @@ public class MfaOobTokenResponse : TokenBase /// /// The duration in seconds that the access token is valid. /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } /// /// Error code returned in case of failure. /// - [JsonProperty("error")] + [JsonPropertyName("error")] public string Error { get; set; } /// /// Description of the error. /// - [JsonProperty("error_description")] + [JsonPropertyName("error_description")] public string ErrorDescription { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/MfaOtpTokenResponse.cs b/src/Auth0.AuthenticationApi/Models/Mfa/MfaOtpTokenResponse.cs index 1d751523d..8a4634e4e 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/MfaOtpTokenResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/MfaOtpTokenResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; @@ -7,6 +7,6 @@ public class MfaOtpTokenResponse : TokenBase /// /// The duration in seconds that the access token is valid. /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/Mfa/MfaRecoveryCodeResponse.cs b/src/Auth0.AuthenticationApi/Models/Mfa/MfaRecoveryCodeResponse.cs index 3cec05f39..df21439f6 100644 --- a/src/Auth0.AuthenticationApi/Models/Mfa/MfaRecoveryCodeResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/Mfa/MfaRecoveryCodeResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models.Mfa; @@ -7,12 +7,12 @@ public class MfaRecoveryCodeResponse : TokenBase /// /// The duration in seconds that the access token is valid. /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } /// /// Recovery code to be stored securely for future use. /// - [JsonProperty("recovery_code")] + [JsonPropertyName("recovery_code")] public string RecoveryCode { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/PasswordlessEmailRequest.cs b/src/Auth0.AuthenticationApi/Models/PasswordlessEmailRequest.cs index 0899f8ef3..f7a3993f6 100644 --- a/src/Auth0.AuthenticationApi/Models/PasswordlessEmailRequest.cs +++ b/src/Auth0.AuthenticationApi/Models/PasswordlessEmailRequest.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -13,13 +13,13 @@ public class PasswordlessEmailRequest /// /// Client ID of the application. /// - [JsonProperty("client_id")] + [JsonPropertyName("client_id")] public string ClientId { get; set; } /// /// Client Secret of the application. /// - [JsonProperty("client_secret")] + [JsonPropertyName("client_secret")] public string ClientSecret { get; set; } /// @@ -35,18 +35,18 @@ public class PasswordlessEmailRequest /// /// Email to which the link or code must be sent. /// - [JsonProperty("email")] + [JsonPropertyName("email")] public string Email { get; set; } /// /// of the request. /// - [JsonProperty("send")] + [JsonPropertyName("send")] public PasswordlessEmailRequestType Type { get; set; } /// /// Additional authentication parameters. /// - [JsonProperty("authParams")] + [JsonPropertyName("authParams")] public IDictionary AuthenticationParameters { get; set; } = new Dictionary(); } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/PasswordlessEmailResponse.cs b/src/Auth0.AuthenticationApi/Models/PasswordlessEmailResponse.cs index 3a17bd750..7bc5d456f 100644 --- a/src/Auth0.AuthenticationApi/Models/PasswordlessEmailResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/PasswordlessEmailResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,18 +10,18 @@ public class PasswordlessEmailResponse /// /// Identifier of this request. /// - [JsonProperty("_id")] + [JsonPropertyName("_id")] public string Id { get; set; } /// /// Email to which the request was sent. /// - [JsonProperty("email")] + [JsonPropertyName("email")] public string Email { get; set; } /// /// Whether the email address has been verified (true) or has not been verified (false). /// - [JsonProperty("email_verified")] + [JsonPropertyName("email_verified")] public bool? EmailVerified { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/PasswordlessSmsRequest.cs b/src/Auth0.AuthenticationApi/Models/PasswordlessSmsRequest.cs index d21b0cb96..fcd4167cf 100644 --- a/src/Auth0.AuthenticationApi/Models/PasswordlessSmsRequest.cs +++ b/src/Auth0.AuthenticationApi/Models/PasswordlessSmsRequest.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,19 +10,19 @@ public class PasswordlessSmsRequest /// /// Client ID of the application. /// - [JsonProperty("client_id")] + [JsonPropertyName("client_id")] public string ClientId { get; set; } /// /// Client Secret of the application. /// - [JsonProperty("client_secret")] + [JsonPropertyName("client_secret")] public string ClientSecret { get; set; } /// /// Phone number to which the code must be sent. /// - [JsonProperty("phone_number")] + [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } /// diff --git a/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs b/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs index b8bc6fea0..e1ae8f773 100644 --- a/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,18 +10,18 @@ public class PasswordlessSmsResponse /// /// Unique identifier of the request. /// - [JsonProperty("_id")] + [JsonPropertyName("_id")] public string Id { get; set; } /// /// Phone number to which the code was sent. /// - [JsonProperty("phone_number")] + [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } /// /// Language the message sent was written in. /// - [JsonProperty("request_language")] + [JsonPropertyName("request_language")] public object RequestLanguage { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/PushedAuthorizationRequestResponse.cs b/src/Auth0.AuthenticationApi/Models/PushedAuthorizationRequestResponse.cs index d281e0dfe..45b568055 100644 --- a/src/Auth0.AuthenticationApi/Models/PushedAuthorizationRequestResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/PushedAuthorizationRequestResponse.cs @@ -1,6 +1,6 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -15,13 +15,13 @@ public class PushedAuthorizationRequestResponse /// /// This URI is a single-use reference to the respective request data in the subsequent authorization request. /// - [JsonProperty("request_uri")] + [JsonPropertyName("request_uri")] public Uri RequestUri { get; set; } /// /// A number that represents the lifetime of the request URI in seconds as a positive integer. /// - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/SignupUserRequest.cs b/src/Auth0.AuthenticationApi/Models/SignupUserRequest.cs index 1fbbb7928..4c4999f60 100644 --- a/src/Auth0.AuthenticationApi/Models/SignupUserRequest.cs +++ b/src/Auth0.AuthenticationApi/Models/SignupUserRequest.cs @@ -1,6 +1,6 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -12,54 +12,54 @@ public class SignupUserRequest : UserMaintenanceRequestBase /// /// Initial password for this user. /// - [JsonProperty("password")] + [JsonPropertyName("password")] public string Password { get; set; } /// /// Username of this user. Only valid if the connection requires a username. /// - [JsonProperty("username")] + [JsonPropertyName("username")] public string Username { get; set; } /// /// Given name for this user. /// - [JsonProperty("given_name")] + [JsonPropertyName("given_name")] public string GivenName { get; set; } /// /// Family name for this user. /// - [JsonProperty("family_name")] + [JsonPropertyName("family_name")] public string FamilyName { get; set; } /// /// Name of this user. /// - [JsonProperty("name")] + [JsonPropertyName("name")] public string Name { get; set; } /// /// Nickname of this user. /// - [JsonProperty("nickname")] + [JsonPropertyName("nickname")] public string Nickname { get; set; } /// /// URL to a picture of this user. /// - [JsonProperty("picture")] + [JsonPropertyName("picture")] public Uri Picture { get; set; } /// /// Metadata the user has read/write access to. /// - [JsonProperty("user_metadata")] + [JsonPropertyName("user_metadata")] public dynamic UserMetadata { get; set; } /// /// The user's phone number. /// - [JsonProperty("phone_number")] + [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs b/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs index 26713f000..9b1ae614b 100644 --- a/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs @@ -1,6 +1,6 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -12,14 +12,14 @@ public class SignupUserResponse /// /// Email address of the new user. /// - [JsonProperty("email")] + [JsonPropertyName("email")] public string Email { get; set; } /// /// Indicates whether the email has been verified or not. /// /// true if the email is verified; otherwise, false. - [JsonProperty("email_verified")] + [JsonPropertyName("email_verified")] public bool EmailVerified { get; set; } /// @@ -29,68 +29,72 @@ public class SignupUserResponse /// The server can return `_id`, `id` or `user_id` depending on various factors. /// For convenience we expose it here as just one. /// + [JsonIgnore] public string Id { get { return _id ?? id ?? user_id; } set { _id = value; } } - [JsonProperty("_id")] + [JsonInclude] + [JsonPropertyName("_id")] private string _id; // Standard connection #pragma warning disable 0649 - [JsonProperty("id")] + [JsonInclude] + [JsonPropertyName("id")] private string id; // Custom connection - [JsonProperty("user_id")] - private readonly string user_id; // Custom connection external + [JsonInclude] + [JsonPropertyName("user_id")] + private string user_id; // Custom connection external #pragma warning restore 0649 /// /// Username of this user. /// - [JsonProperty("username")] + [JsonPropertyName("username")] public string Username { get; set; } /// /// Given name of this user. /// - [JsonProperty("given_name")] + [JsonPropertyName("given_name")] public string GivenName { get; set; } /// /// Family name of this user. /// - [JsonProperty("family_name")] + [JsonPropertyName("family_name")] public string FamilyName { get; set; } /// /// Name of this user. /// - [JsonProperty("name")] + [JsonPropertyName("name")] public string Name { get; set; } /// /// Nickname of this user. /// - [JsonProperty("nickname")] + [JsonPropertyName("nickname")] public string Nickname { get; set; } /// /// Url to a picture of this user. /// - [JsonProperty("picture")] + [JsonPropertyName("picture")] public Uri Picture { get; set; } /// /// Metadata the user has read/write access to. /// - [JsonProperty("user_metadata")] + [JsonPropertyName("user_metadata")] public dynamic UserMetadata { get; set; } /// /// The user's phone number. /// - [JsonProperty("phone_number")] + [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/TokenBase.cs b/src/Auth0.AuthenticationApi/Models/TokenBase.cs index 1374c55a5..deb778fc5 100644 --- a/src/Auth0.AuthenticationApi/Models/TokenBase.cs +++ b/src/Auth0.AuthenticationApi/Models/TokenBase.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,12 +10,12 @@ public abstract class TokenBase /// /// Access token. /// - [JsonProperty("access_token")] + [JsonPropertyName("access_token")] public string AccessToken { get; set; } /// /// Type of token. /// - [JsonProperty("token_type")] + [JsonPropertyName("token_type")] public string TokenType { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/UserInfo.cs b/src/Auth0.AuthenticationApi/Models/UserInfo.cs index 93d68f84a..3dfcd58c1 100644 --- a/src/Auth0.AuthenticationApi/Models/UserInfo.cs +++ b/src/Auth0.AuthenticationApi/Models/UserInfo.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Auth0.Core.Serialization; @@ -16,193 +18,106 @@ namespace Auth0.AuthenticationApi.Models; /// public class UserInfo { - /// - /// Subject-Identifier for the user at the issuer. A unique value to identify the user. - /// - [JsonProperty("sub")] + /// Subject-Identifier for the user at the issuer. A unique value to identify the user. + [JsonPropertyName("sub")] public string UserId { get; set; } - /// - /// Full name of the user in displayable form including all name parts, - /// possibly including titles and suffixes, ordered according to the user's locale and preferences. - /// - [JsonProperty("name")] + /// Full name of the user in displayable form. + [JsonPropertyName("name")] public string FullName { get; set; } - /// - /// Given name(s) or first name(s) of the user. - /// - /// - /// May contain multiple given names separated by space characters. - /// - [JsonProperty("given_name")] + /// Given name(s) or first name(s) of the user. + [JsonPropertyName("given_name")] public string FirstName { get; set; } - /// - /// Surname(s) or last name(s) of the user. - /// - /// - /// May contain multiple given names separated by space characters or no family name at all. - /// - [JsonProperty("family_name")] + /// Surname(s) or last name(s) of the user. + [JsonPropertyName("family_name")] public string LastName { get; set; } - /// - /// Middle name(s) of the user. - /// - /// - /// May contain multiple middle names separated by space characters. - /// - [JsonProperty("middle_name")] + /// Middle name(s) of the user. + [JsonPropertyName("middle_name")] public string MiddleName { get; set; } - /// - /// Casual name of the user that may or may not be the same as . - /// - /// - /// Nickname of 'Mike' might be returned for a of `Michael`. - /// - [JsonProperty("nickname")] + /// Casual name of the user. + [JsonPropertyName("nickname")] public string NickName { get; set; } - /// - /// Shorthand name by which the user wishes to be referred to at the RP, such as janedoe or j.doe. - /// - /// - /// MAY be any valid JSON string including special characters such as @, /, or whitespace. - /// You MUST NOT rely upon this value being unique. - /// - [JsonProperty("preferred_username")] + /// Shorthand name by which the user wishes to be referred to at the RP. + [JsonPropertyName("preferred_username")] public string PreferredUsername { get; set; } - /// - /// URL of the user's profile page. - /// - /// - /// The contents of this Web page SHOULD be about the End-User. - /// - [JsonProperty("profile")] + /// URL of the user's profile page. + [JsonPropertyName("profile")] public string Profile { get; set; } - /// - /// URL of the user's profile picture. - /// - /// - /// Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for - /// displaying when describing the user, rather than an arbitrary photo taken by the user. - /// - [JsonProperty("picture")] + /// URL of the user's profile picture. + [JsonPropertyName("picture")] public string Picture { get; set; } - /// - /// URL of the user's Web page or blog. - /// - /// - /// This Web page SHOULD contain information published by the End-User or an organization that - /// the End-User is affiliated with. - /// - [JsonProperty("website")] + /// URL of the user's Web page or blog. + [JsonPropertyName("website")] public string Website { get; set; } - /// - /// User's preferred e-mail address. - /// - /// - /// You MUST NOT rely upon this value being unique. - /// - [JsonProperty("email")] + /// User's preferred e-mail address. + [JsonPropertyName("email")] public string Email { get; set; } - /// - /// Whether the user's email address has been verified or not. - /// - /// - /// When this Claim Value is true, this means that the OP took affirmative steps to ensure that this e-mail - /// address was controlled by the user at the time the verification was performed. The means by which an - /// e-mail address is verified is context-specific, and dependent upon the trust framework or contractual - /// agreements within which the parties are operating. - /// - [JsonProperty("email_verified")] + /// Whether the user's email address has been verified or not. + [JsonPropertyName("email_verified")] public bool? EmailVerified { get; set; } - /// - /// User's gender. - /// - /// - /// Values defined by the specification include `female` and `male`. - /// Other values MAY be used when neither of the defined values are applicable. - /// - [JsonProperty("gender")] + /// User's gender. + [JsonPropertyName("gender")] public string Gender { get; set; } - /// - /// Gets or sets the user's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format. - /// The year MAY be 0000, indicating that it is omitted. To represent only the year, YYYY format is allowed. - /// - [JsonProperty("birthdate")] + /// User's birthday, ISO 8601:2004 YYYY-MM-DD format. + [JsonPropertyName("birthdate")] public string Birthdate { get; set; } - /// - /// User's time zone as a "tz database name". - /// - /// Europe/Paris or America/Los_Angeles. - /// - /// See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for more details. - /// - [JsonProperty("zoneinfo")] + /// User's time zone as a "tz database name". + [JsonPropertyName("zoneinfo")] public string ZoneInformation { get; set; } - /// - /// User's locale, represented as a BCP47 language tag. - /// - /// en-US or fr-CA - /// - /// Typically an ISO 639-1 Alpha-2 language code in lowercase and an - /// ISO 3166-1 Alpha-2 country code in uppercase, separated by a dash. - /// - [JsonProperty("locale")] + /// User's locale, represented as a BCP47 language tag. + [JsonPropertyName("locale")] [JsonConverter(typeof(StringOrObjectAsStringConverter))] public string Locale { get; set; } - /// - /// User's preferred telephone number. - /// - /// +1 (425) 555-1212 or +1 (604) 555-1234;ext=5678. - /// - /// E.164 is the RECOMMENDED format. - /// If the phone number contains an extension, it is RECOMMENDED that the - /// extension be represented using the RFC 3966 extension syntax. - /// - [JsonProperty("phone_number")] + /// User's preferred telephone number. + [JsonPropertyName("phone_number")] public string PhoneNumber { get; set; } - /// - /// Whether the user's phone number has been verified or not. - /// - /// - /// When this Claim Value is true, this means that the OP took affirmative steps to ensure that this - /// phone number was controlled by the user at the time the verification was performed. - /// When true, the phone_number Claim MUST be in E.164 format and any extensions MUST be represented in RFC 3966 format. - /// - [JsonProperty("phone_number_verified")] + /// Whether the user's phone number has been verified or not. + [JsonPropertyName("phone_number_verified")] public bool? PhoneNumberVerified { get; set; } - /// - /// User's preferred postal/mailing address. - /// - [JsonProperty("address")] + /// User's preferred postal/mailing address. + [JsonPropertyName("address")] public UserInfoAddress Address { get; set; } + /// Time and date the user's information was last updated. + [JsonPropertyName("updated_at")] + [JsonConverter(typeof(Auth0.AuthenticationApi.FlexibleDateTimeConverter))] + public DateTime? UpdatedAt { get; set; } + /// - /// Time and date the user's information was last updated. + /// Additional claims about the user, captured as raw JSON elements. /// - [JsonProperty("updated_at")] - [JsonConverter(typeof(FlexibleDateTimeConverter))] - public DateTime? UpdatedAt { get; set; } + [JsonExtensionData] + public IDictionary AdditionalClaimsJson { get; set; } + = new Dictionary(); /// /// Additional claims about the user. /// - [JsonExtensionData] - public IDictionary AdditionalClaims { get; set; } -} \ No newline at end of file + /// + /// Computed on each access from ; the returned dictionary is a + /// snapshot, so mutating it does not persist back onto the instance. + /// + [Obsolete("Use AdditionalClaimsJson. AdditionalClaims will be removed in a future major version.")] + [JsonIgnore] + public IDictionary AdditionalClaims => + AdditionalClaimsJson.ToDictionary( + kv => kv.Key, + kv => JToken.Parse(kv.Value.GetRawText())); +} diff --git a/src/Auth0.AuthenticationApi/Models/UserInfoAddress.cs b/src/Auth0.AuthenticationApi/Models/UserInfoAddress.cs index a2822b81e..07d84af00 100644 --- a/src/Auth0.AuthenticationApi/Models/UserInfoAddress.cs +++ b/src/Auth0.AuthenticationApi/Models/UserInfoAddress.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -13,40 +13,40 @@ public class UserInfoAddress /// /// MAY contain multiple lines, separated by newlines as either CRLF (Windows) `\r\n` or LF (Unix) `\n`. /// - [JsonProperty("formatted")] + [JsonPropertyName("formatted")] public string Formatted { get; set; } /// - /// Full street address component, which MAY include house number, + /// Full street address component, which MAY include house number, /// street name, Post Office Box, and multi-line extended street address information. /// /// /// MAY contain multiple lines, separated by newlines as either CRLF (Windows) `\r\n` or LF (Unix) `\n`. /// - [JsonProperty("street_address")] + [JsonPropertyName("street_address")] public string StreetAddress { get; set; } /// /// City or locality component of the address. /// - [JsonProperty("locality")] + [JsonPropertyName("locality")] public string Locality { get; set; } /// /// State, province, prefecture, or region component of the address. /// - [JsonProperty("region")] + [JsonPropertyName("region")] public string Region { get; set; } /// /// Zip or postal code component of the address. /// - [JsonProperty("postal_code")] + [JsonPropertyName("postal_code")] public string PostalCode { get; set; } /// /// Country component of the address. /// - [JsonProperty("country")] + [JsonPropertyName("country")] public string Country { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/UserMaintenanceRequestBase.cs b/src/Auth0.AuthenticationApi/Models/UserMaintenanceRequestBase.cs index dc041766f..b6494c97e 100644 --- a/src/Auth0.AuthenticationApi/Models/UserMaintenanceRequestBase.cs +++ b/src/Auth0.AuthenticationApi/Models/UserMaintenanceRequestBase.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Auth0.AuthenticationApi.Models; @@ -10,18 +10,18 @@ public class UserMaintenanceRequestBase /// /// Client ID of the application. /// - [JsonProperty("client_id")] + [JsonPropertyName("client_id")] public string ClientId { get; set; } /// /// Name of the connection. /// - [JsonProperty("connection")] + [JsonPropertyName("connection")] public string Connection { get; set; } /// /// Email address of the user. /// - [JsonProperty("email")] + [JsonPropertyName("email")] public string Email { get; set; } } \ No newline at end of file diff --git a/src/Auth0.Core/Auth0.Core.csproj b/src/Auth0.Core/Auth0.Core.csproj index 5b471cf0d..da9e97a07 100644 --- a/src/Auth0.Core/Auth0.Core.csproj +++ b/src/Auth0.Core/Auth0.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Auth0.Core/Exceptions/ApiError.cs b/src/Auth0.Core/Exceptions/ApiError.cs index 8b344f4a1..4821334e4 100644 --- a/src/Auth0.Core/Exceptions/ApiError.cs +++ b/src/Auth0.Core/Exceptions/ApiError.cs @@ -1,8 +1,9 @@ -using Auth0.Core.Serialization; -using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; +using Auth0.Core.Serialization; namespace Auth0.Core.Exceptions; @@ -15,25 +16,25 @@ public class ApiError /// /// Description of the failing HTTP Status Code. /// - [JsonProperty("error")] + [JsonPropertyName("error")] public string? Error { get; set; } /// /// Error code returned by the API. /// - [JsonProperty("errorCode")] + [JsonPropertyName("errorCode")] public string? ErrorCode { get; set; } /// /// Description of the error. /// - [JsonProperty("message")] + [JsonPropertyName("message")] public string? Message { get; set; } /// /// Additional key/values that might be returned by the error such as `mfa_required`. /// - [JsonProperty("extraData")] + [JsonPropertyName("extraData")] public Dictionary ExtraData { get; set; } = new(); /// @@ -54,14 +55,14 @@ public class ApiError internal static ApiError? Parse(string? content) { - if (content is null) + if (string.IsNullOrWhiteSpace(content)) { return null; } - + try { - return JsonConvert.DeserializeObject(content); + return JsonSerializer.Deserialize(content!, Auth0JsonSerializerOptions.Default); } catch (JsonException) { @@ -72,4 +73,4 @@ public class ApiError }; } } -} \ No newline at end of file +} diff --git a/src/Auth0.Core/Serialization/ApiErrorConverter.cs b/src/Auth0.Core/Serialization/ApiErrorConverter.cs index 055fd49a5..6da20df80 100644 --- a/src/Auth0.Core/Serialization/ApiErrorConverter.cs +++ b/src/Auth0.Core/Serialization/ApiErrorConverter.cs @@ -1,59 +1,63 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Auth0.Core.Exceptions; namespace Auth0.Core.Serialization; -internal class ApiErrorConverter : JsonConverter +internal class ApiErrorConverter : JsonConverter { - private readonly Dictionary _propertyMappings = new() + private static readonly Dictionary PropertyMappings = new(StringComparer.OrdinalIgnoreCase) { - {"code", "errorCode"}, - {"name", "error"}, - {"description", "message"}, - {"error_description", "message"} + { "code", "errorCode" }, + { "name", "error" }, + { "description", "message" }, + { "error_description", "message" }, }; - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override ApiError Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); - } + using var doc = JsonDocument.ParseValue(ref reader); + var error = new ApiError(); - public override bool CanConvert(Type objectType) - { - return objectType.GetTypeInfo().IsClass; - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var instance = Activator.CreateInstance(objectType); - var props = objectType.GetTypeInfo().DeclaredProperties.Where(p => p.CanWrite).ToList(); - var extraDataProp = props.FirstOrDefault(p => p.PropertyType == typeof(Dictionary)); - - foreach (var jp in JObject.Load(reader).Properties()) + foreach (var property in doc.RootElement.EnumerateObject()) { - if (!_propertyMappings.TryGetValue(jp.Name, out var name)) - name = jp.Name; - - var prop = props.FirstOrDefault(pi => string.Equals(pi.Name, name, StringComparison.OrdinalIgnoreCase)); - - if (prop != null) + if (!PropertyMappings.TryGetValue(property.Name, out var name)) + name = property.Name; + + if (string.Equals(name, "errorCode", StringComparison.OrdinalIgnoreCase)) + error.ErrorCode = ReadString(property.Value); + else if (string.Equals(name, "error", StringComparison.OrdinalIgnoreCase)) + error.Error = ReadString(property.Value); + else if (string.Equals(name, "message", StringComparison.OrdinalIgnoreCase)) + error.Message = ReadString(property.Value); + else if (string.Equals(name, "extraData", StringComparison.OrdinalIgnoreCase) + && property.Value.ValueKind == JsonValueKind.Object) { - prop.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer)); + foreach (var inner in property.Value.EnumerateObject()) + error.ExtraData[inner.Name] = ReadString(inner.Value) ?? ""; } - else if (extraDataProp != null && extraDataProp.PropertyType == typeof(Dictionary)) + else { - var extraData = (IDictionary)extraDataProp.GetValue(instance) ?? new Dictionary(); - extraData[name] = jp.Value.ToString(); - extraDataProp.SetValue(instance, extraData); + error.ExtraData[name] = ReadString(property.Value) ?? ""; } } - return instance; + return error; + } + + // Mirrors Newtonsoft's ToObject/ToString coercion: JSON null → null, + // strings unwrapped, other scalars kept as their raw text (never throws on non-strings). + private static string? ReadString(JsonElement value) => value.ValueKind switch + { + JsonValueKind.Null => null, + JsonValueKind.String => value.GetString(), + _ => value.GetRawText(), + }; + + public override void Write(Utf8JsonWriter writer, ApiError value, JsonSerializerOptions options) + { + throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/src/Auth0.Core/Serialization/Auth0JsonSerializerOptions.cs b/src/Auth0.Core/Serialization/Auth0JsonSerializerOptions.cs new file mode 100644 index 000000000..e9c5210eb --- /dev/null +++ b/src/Auth0.Core/Serialization/Auth0JsonSerializerOptions.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Auth0.Core.Serialization; + +/// +/// Shared System.Text.Json options that reproduce the serialization behavior the SDK +/// historically provided via Newtonsoft.Json (case-insensitive matching, null omission +/// on write). This is the single source of truth for serialization configuration. +/// +internal static class Auth0JsonSerializerOptions +{ + public static readonly JsonSerializerOptions Default = Create(); + + private static JsonSerializerOptions Create() + { + return new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + } +} diff --git a/src/Auth0.Core/Serialization/StringOrObjectAsStringConverter.cs b/src/Auth0.Core/Serialization/StringOrObjectAsStringConverter.cs index 816c83b73..132ec9a1f 100644 --- a/src/Auth0.Core/Serialization/StringOrObjectAsStringConverter.cs +++ b/src/Auth0.Core/Serialization/StringOrObjectAsStringConverter.cs @@ -1,40 +1,35 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Auth0.Core.Serialization; -internal class StringOrObjectAsStringConverter : JsonConverter +internal class StringOrObjectAsStringConverter : JsonConverter { - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override bool CanConvert(Type objectType) + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return true; - } + if (reader.TokenType == JsonTokenType.String) + return reader.GetString(); - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var instance = ""; - - if (reader.TokenType == JsonToken.String) + if (reader.TokenType == JsonTokenType.StartObject || reader.TokenType == JsonTokenType.StartArray) { - instance = reader.Value?.ToString(); - } - else if (reader.TokenType == JsonToken.StartObject) - { - instance = JObject.Load(reader).ToString(); - } - else if (reader.TokenType == JsonToken.StartArray) - { - instance = JArray.Load(reader).ToString(); + using var doc = JsonDocument.ParseValue(ref reader); + // Re-serialize to compact JSON using Utf8JsonWriter + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false })) + { + doc.RootElement.WriteTo(writer); + } + return Encoding.UTF8.GetString(stream.ToArray()); } - return instance; + return ""; + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs b/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs index 1b3ec2b3b..6b73f68ec 100644 --- a/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs +++ b/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs @@ -1,39 +1,33 @@ using System; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Auth0.Core.Serialization; -internal class StringOrStringArrayJsonConverter : JsonConverter +internal class StringOrStringArrayJsonConverter : JsonConverter { - public override bool CanWrite => false; - public override bool CanConvert(Type objectType) - { - return true; - } - - public override object? ReadJson( - JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.StartArray) + switch (reader.TokenType) { - // Deserialize as string array and return; - var array = JArray.Load(reader); - return array.ToObject(); + case JsonTokenType.String: + return reader.GetString(); + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + list.Add(reader.GetString()); + } + return list.ToArray(); + default: + reader.Skip(); + return null; } - - if (reader.TokenType == JsonToken.String) - { - // Deserialize as a single string - return reader.Value?.ToString(); - } - - return null; } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs b/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs index 3749dce89..5083a115c 100644 --- a/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs +++ b/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs @@ -1,12 +1,8 @@ -using System; using System.Collections; -using FluentAssertions; -using Newtonsoft.Json; -using Xunit; using System.Collections.Generic; -using System.IO; using Auth0.Core.Exceptions; -using Auth0.Core.Serialization; +using FluentAssertions; +using Xunit; namespace Auth0.Core.UnitTests; @@ -16,168 +12,86 @@ public class ApiErrorDeserializationTests public void Should_deserialize_extra_properties_like_mfa_response() { var content = - "{\"error\": \"mfa_required\", \"error_description\": \"Multifactor authentication required\", \"mfa_token\": \"2x4b-r2d2-c3po-bb8-ig88\"}"; + """{"error": "mfa_required", "error_description": "Multifactor authentication required", "mfa_token": "2x4b-r2d2-c3po-bb8-ig88"}"""; var parsed = ApiError.Parse(content); - Assert.Equal("mfa_required", parsed.Error); - Assert.Equal("Multifactor authentication required", parsed.Message); - Assert.Contains(parsed.ExtraData, - (d) => d.Key == "mfa_token" && string.Equals(d.Value, "2x4b-r2d2-c3po-bb8-ig88")); + parsed.Error.Should().Be("mfa_required"); + parsed.Message.Should().Be("Multifactor authentication required"); + parsed.ExtraData.Should().ContainKey("mfa_token") + .WhichValue.Should().Be("2x4b-r2d2-c3po-bb8-ig88"); } [Theory] [ClassData(typeof(ApiErrorDeserializationData))] public void Should_deserialize_all_error_structures_correctly(string content, ApiError expected) { - var error = JsonConvert.DeserializeObject(content); + var error = ApiError.Parse(content); error.Should().BeEquivalentTo(expected); } [Fact] - public void CanConvert_WithClassType_ReturnsTrue() + public void Maps_aliased_names_to_canonical_properties() { - var converter = new ApiErrorConverter(); - var result = converter.CanConvert(typeof(string)); - result.Should().BeTrue(); - } + var parsed = ApiError.Parse( + """{"code": "test_code", "name": "test_error", "description": "test_message"}"""); - [Fact] - public void CanConvert_WithValueType_ReturnsFalse() - { - var converter = new ApiErrorConverter(); - var result = converter.CanConvert(typeof(int)); - result.Should().BeFalse(); + parsed.ErrorCode.Should().Be("test_code"); + parsed.Error.Should().Be("test_error"); + parsed.Message.Should().Be("test_message"); } [Fact] - public void CanWrite_ReturnsFalse() + public void Maps_error_description_to_message() { - var converter = new ApiErrorConverter(); - converter.CanWrite.Should().BeFalse(); - } + var parsed = ApiError.Parse("""{"error_description": "test_description"}"""); - [Fact] - public void WriteJson_ThrowsNotImplementedException() - { - var converter = new ApiErrorConverter(); - var writer = new JsonTextWriter(new StringWriter()); - - Action act = () => converter.WriteJson(writer, new object(), new JsonSerializer()); - - act.Should().Throw(); + parsed.Message.Should().Be("test_description"); } [Fact] - public void ReadJson_WithMappedPropertyNames_MapsToCorrectProperties() + public void Matches_keys_case_insensitively() { - var converter = new ApiErrorConverter(); - var json = """{"code": "test_code", "name": "test_error", "description": "test_message"}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); - - var result = converter.ReadJson(reader, typeof(TestApiError), null, serializer) as TestApiError; + var parsed = ApiError.Parse("""{"ERRORCODE": "test_code", "ERROR": "test_error"}"""); - result.Should().NotBeNull(); - result.ErrorCode.Should().Be("test_code"); - result.Error.Should().Be("test_error"); - result.Message.Should().Be("test_message"); + parsed.ErrorCode.Should().Be("test_code"); + parsed.Error.Should().Be("test_error"); } [Fact] - public void ReadJson_WithErrorDescriptionProperty_MapsToMessage() + public void Empty_json_yields_all_null_fields() { - var converter = new ApiErrorConverter(); - var json = """{"error_description": "test_description"}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); + var parsed = ApiError.Parse("{}"); - var result = converter.ReadJson(reader, typeof(TestApiError), null, serializer) as TestApiError; - - result.Should().NotBeNull(); - result.Message.Should().Be("test_description"); + parsed.ErrorCode.Should().BeNull(); + parsed.Error.Should().BeNull(); + parsed.Message.Should().BeNull(); + parsed.ExtraData.Should().BeEmpty(); } [Fact] - public void ReadJson_WithUnmappedProperties_SetsPropertiesDirectly() + public void Null_mapped_field_stays_null() { - var converter = new ApiErrorConverter(); - var json = """{"customProperty": "custom_value"}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); - - var result = - converter.ReadJson(reader, typeof(TestApiErrorWithCustomProperty), null, serializer) as - TestApiErrorWithCustomProperty; + var parsed = ApiError.Parse("""{"code": null}"""); - result.Should().NotBeNull(); - result.CustomProperty.Should().Be("custom_value"); - } - - [Fact] - public void ReadJson_WithUnmappedPropertiesAndExtraDataDictionary_AddsToExtraData() - { - var converter = new ApiErrorConverter(); - var json = """{"unmappedProperty": "unmapped_value"}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); - - var result = - converter.ReadJson(reader, typeof(TestApiErrorWithExtraData), null, - serializer) as TestApiErrorWithExtraData; - - result.Should().NotBeNull(); - result.ExtraData.Should().ContainKey("unmappedProperty"); - result.ExtraData["unmappedProperty"].Should().Be("unmapped_value"); + parsed.ErrorCode.Should().BeNull(); } [Fact] - public void ReadJson_WithCaseInsensitivePropertyNames_MapsCorrectly() + public void Non_string_mapped_field_kept_as_raw_text() { - var converter = new ApiErrorConverter(); - var json = """{"ERRORCODE": "test_code", "ERROR": "test_error"}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); + var parsed = ApiError.Parse("""{"code": 123}"""); - var result = converter.ReadJson(reader, typeof(TestApiError), null, serializer) as TestApiError; - - result.Should().NotBeNull(); - result.ErrorCode.Should().Be("test_code"); - result.Error.Should().Be("test_error"); + parsed.ErrorCode.Should().Be("123"); } [Fact] - public void ReadJson_WithEmptyJson_ReturnsInstanceWithDefaultValues() + public void Null_unmapped_field_becomes_empty_string() { - var converter = new ApiErrorConverter(); - var json = """{}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); - - var result = converter.ReadJson(reader, typeof(TestApiError), null, serializer) as TestApiError; + var parsed = ApiError.Parse("""{"custom": null}"""); - result.Should().NotBeNull(); - result.ErrorCode.Should().BeNull(); - result.Error.Should().BeNull(); - result.Message.Should().BeNull(); - } - - [Fact] - public void ReadJson_WithReadOnlyProperties_IgnoresReadOnlyProperties() - { - var converter = new ApiErrorConverter(); - var json = """{"readOnlyProperty": "should_be_ignored", "errorCode": "test_code"}"""; - var reader = new JsonTextReader(new StringReader(json)); - var serializer = new JsonSerializer(); - - var result = - converter.ReadJson(reader, typeof(TestApiErrorWithReadOnlyProperty), null, serializer) as - TestApiErrorWithReadOnlyProperty; - - result.Should().NotBeNull(); - result.ErrorCode.Should().Be("test_code"); - result.ReadOnlyProperty.Should().Be("default_value"); + parsed.ExtraData.Should().ContainKey("custom").WhichValue.Should().Be(""); } } @@ -187,58 +101,20 @@ public IEnumerator GetEnumerator() { yield return new object[] { - "{\"name\":\"Error\",\"code\":\"error_code\",\"description\":\"The Message\"}", new ApiError - { - Error = "Error", - ErrorCode = "error_code", - Message = "The Message" - } + """{"name":"Error","code":"error_code","description":"The Message"}""", + new ApiError { Error = "Error", ErrorCode = "error_code", Message = "The Message" } }; yield return new object[] { - "{\"error\": \"Error\",\"error_description\": \"The Message\"}", new ApiError - { - Error = "Error", - ErrorCode = null, - Message = "The Message" - } + """{"error": "Error","error_description": "The Message"}""", + new ApiError { Error = "Error", ErrorCode = null, Message = "The Message" } }; yield return new object[] { - "{\"error\": \"Error\", \"message\": \"The Message\", \"errorCode\": \"error_code\"}", new ApiError - { - Error = "Error", - ErrorCode = "error_code", - Message = "The Message" - } + """{"error": "Error", "message": "The Message", "errorCode": "error_code"}""", + new ApiError { Error = "Error", ErrorCode = "error_code", Message = "The Message" } }; } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - -public class TestApiError -{ - public string ErrorCode { get; set; } - public string Error { get; set; } - public string Message { get; set; } -} - -public class TestApiErrorWithCustomProperty -{ - public string CustomProperty { get; set; } -} - -public class TestApiErrorWithExtraData -{ - public Dictionary ExtraData { get; set; } = new(); -} - -public class TestApiErrorWithReadOnlyProperty -{ - public string ErrorCode { get; set; } - public string ReadOnlyProperty { get; } = "default_value"; -} \ No newline at end of file diff --git a/tests/Auth0.Core.UnitTests/Auth0JsonSerializerOptionsTests.cs b/tests/Auth0.Core.UnitTests/Auth0JsonSerializerOptionsTests.cs new file mode 100644 index 000000000..bb14128ac --- /dev/null +++ b/tests/Auth0.Core.UnitTests/Auth0JsonSerializerOptionsTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Auth0.Core.Serialization; +using FluentAssertions; +using Xunit; + +namespace Auth0.Core.UnitTests; + +public class Auth0JsonSerializerOptionsTests +{ + private class Sample + { + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string LastName { get; set; } + } + + [Fact] + public void Omits_null_properties_on_serialize() + { + var json = JsonSerializer.Serialize( + new Sample { FirstName = "Bob", LastName = null }, + Auth0JsonSerializerOptions.Default); + + json.Should().Contain("first_name"); + json.Should().NotContain("last_name"); + } + + [Fact] + public void Matches_property_names_case_insensitively_on_deserialize() + { + var result = JsonSerializer.Deserialize( + """{"FIRST_NAME":"Bob"}""", Auth0JsonSerializerOptions.Default); + + result.FirstName.Should().Be("Bob"); + } +} diff --git a/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs b/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs index 97bd83239..a217ec3a9 100644 --- a/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs +++ b/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs @@ -1,194 +1,59 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using Xunit; -using FluentAssertions; +using System.Text.Json; +using System.Text.Json.Serialization; using Auth0.Core.Serialization; +using FluentAssertions; +using Xunit; namespace Auth0.Core.UnitTests; public class StringOrObjectAsStringConverterTests { - internal class StringOrObjectAsStringConverterData + private class Data { - [JsonProperty("value")] + [JsonPropertyName("value")] [JsonConverter(typeof(StringOrObjectAsStringConverter))] public string Value { get; set; } } - [Fact] - public void Should_deserialize_string() - { - var content = "{ 'value': 'test' }"; - - var parsed = JsonConvert.DeserializeObject(content); - - Assert.Equal("test", parsed.Value); - } + private static Data Parse(string json) => + JsonSerializer.Deserialize(json, Auth0JsonSerializerOptions.Default); [Fact] - public void Should_deserialize_object_as_string() + public void Deserializes_string() { - var content = "{ 'value': { 'innerValue': 'test' } }"; - - var parsed = JsonConvert.DeserializeObject(content); - - Assert.Equal("{\n \"innerValue\": \"test\"\n}", parsed.Value); + Parse("""{ "value": "test" }""").Value.Should().Be("test"); } [Fact] - public void Should_deserialize_when_omitted() + public void Deserializes_object_as_compact_string() { - var content = "{}"; - - var parsed = JsonConvert.DeserializeObject(content); - - Assert.Null(parsed.Value); + // Intentional change from Newtonsoft: compact JSON, not pretty-printed. + Parse("""{ "value": { "innerValue": "test" } }""") + .Value.Should().Be("""{"innerValue":"test"}"""); } [Fact] - public void ReadJson_WithStringToken_ReturnsStringValue() + public void Deserializes_array_as_compact_string() { - var converter = new StringOrObjectAsStringConverter(); - var json = "\"test string\""; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().Be("test string"); + Parse("""{ "value": [{"type":"x"}] }""") + .Value.Should().Be("""[{"type":"x"}]"""); } [Fact] - public void ReadJson_WithObjectToken_ReturnsJsonObjectAsString() + public void Deserializes_empty_object() { - var converter = new StringOrObjectAsStringConverter(); - var json = "{\"key\":\"value\",\"number\":42}"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().NotBeNull(); - result.ToString().Should().Contain("key"); - result.ToString().Should().Contain("value"); - result.ToString().Should().Contain("number"); - result.ToString().Should().Contain("42"); - } - - [Fact] - public void ReadJson_WithEmptyStringToken_ReturnsEmptyString() - { - var converter = new StringOrObjectAsStringConverter(); - var json = "\"\""; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().Be(""); + Parse("""{ "value": {} }""").Value.Should().Be("{}"); } [Fact] - public void ReadJson_WithNullStringToken_ReturnsEmptyString() + public void Deserializes_empty_array() { - var converter = new StringOrObjectAsStringConverter(); - var json = "null"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().Be(""); - } - - [Fact] - public void ReadJson_WithEmptyObjectToken_ReturnsEmptyObjectString() - { - var converter = new StringOrObjectAsStringConverter(); - var json = "{}"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().NotBeNull(); - result.ToString().Should().Be("{}"); + Parse("""{ "value": [] }""").Value.Should().Be("[]"); } [Fact] - public void ReadJson_WithNestedObjectToken_ReturnsNestedObjectAsString() + public void Returns_null_when_omitted() { - var converter = new StringOrObjectAsStringConverter(); - var json = "{\"outer\":{\"inner\":\"value\"}}"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().NotBeNull(); - result.ToString().Should().Contain("outer"); - result.ToString().Should().Contain("inner"); - result.ToString().Should().Contain("value"); - } - - [Fact] - public void ReadJson_WithArrayToken_ReturnsJsonArrayAsString() - { - var converter = new StringOrObjectAsStringConverter(); - var json = "[{\"type\":\"payment_initiation\",\"amount\":\"100\"}]"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().NotBeNull(); - result.ToString().Should().Contain("type"); - result.ToString().Should().Contain("payment_initiation"); - result.ToString().Should().Contain("amount"); - result.ToString().Should().Contain("100"); - } - - [Fact] - public void ReadJson_WithEmptyArrayToken_ReturnsEmptyArrayString() - { - var converter = new StringOrObjectAsStringConverter(); - var json = "[]"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - result.Should().NotBeNull(); - result.ToString().Should().Be("[]"); - } - - [Fact] - public void CanConvert_WithAnyType_ReturnsTrue() - { - var converter = new StringOrObjectAsStringConverter(); - - converter.CanConvert(typeof(string)).Should().BeTrue(); - converter.CanConvert(typeof(object)).Should().BeTrue(); - converter.CanConvert(typeof(int)).Should().BeTrue(); - } - - [Fact] - public void CanWrite_ReturnsAlwaysFalse() - { - var converter = new StringOrObjectAsStringConverter(); - - converter.CanWrite.Should().BeFalse(); - } - - [Fact] - public void WriteJson_ThrowsNotImplementedException() - { - var converter = new StringOrObjectAsStringConverter(); - var writer = new JsonTextWriter(new StringWriter()); - - var action = () => converter.WriteJson(writer, "test", new JsonSerializer()); - - action.Should().Throw(); + Parse("{}").Value.Should().BeNull(); } -} \ No newline at end of file +} diff --git a/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs b/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs index c5ebc8a60..0490a964e 100644 --- a/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs +++ b/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs @@ -1,125 +1,54 @@ -using System; -using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using Auth0.Core.Serialization; using FluentAssertions; -using Newtonsoft.Json; using Xunit; namespace Auth0.Core.UnitTests; public class StringOrStringArrayJsonConverterTests { - internal class StringOrStringArrayJsonConverterData + private class Data { - [JsonProperty("value")] + [JsonPropertyName("value")] [JsonConverter(typeof(StringOrStringArrayJsonConverter))] - public dynamic Value { get; set; } + public object Value { get; set; } } - - [Fact] - public void CanConvert_ShouldReturnTrue_ForAnyType() - { - // Act - var converter = new StringOrStringArrayJsonConverter(); - var result = converter.CanConvert(typeof(object)); - // Assert - Assert.True(result); - } - - [Fact] - public void CanWrite_ShouldReturnFalse_Always() - { - // Act - var converter = new StringOrStringArrayJsonConverter(); - Assert.False(converter.CanWrite); - } - - [Fact] - public void WriteJson_ShouldThrow_ForAnyInputs() - { - // Act - var converter = new StringOrStringArrayJsonConverter(); - Assert.Throws( () => converter.WriteJson(null!, null, new JsonSerializer())); - } + private static Data Parse(string json) => + JsonSerializer.Deserialize(json, Auth0JsonSerializerOptions.Default); [Fact] - public void ReadJson_ShouldDeserializeStringArray_WhenJsonIsArray() + public void Deserializes_single_string() { - // Arrange - var json = "[\"value1\", \"value2\"]"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - var converter = new StringOrStringArrayJsonConverter(); - - // Act - var result = converter.ReadJson(reader, typeof(string[]), null, new JsonSerializer()); - - // Assert - Assert.IsType(result); - Assert.Equal(new[] { "value1", "value2" }, result); + Parse("""{ "value": "test" }""").Value.Should().Be("test"); } [Fact] - public void ReadJson_ShouldDeserializeString_WhenJsonIsString() + public void Deserializes_array_as_string_array() { - // Arrange - var json = "\"value\""; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - var converter = new StringOrStringArrayJsonConverter(); - - // Act - var result = converter.ReadJson(reader, typeof(string), null, new JsonSerializer()); - - // Assert - Assert.IsType(result); - Assert.Equal("value", result); + var value = Parse("""{ "value": ["value1", "value2"] }""").Value; + value.Should().BeOfType(); + ((string[])value).Should().Equal("value1", "value2"); } [Fact] - public void ReadJson_ShouldReturnNull_WhenJsonIsNotStringOrArray() + public void Returns_null_for_non_string_non_array() { - // Arrange - var json = "123"; - var reader = new JsonTextReader(new StringReader(json)); - reader.Read(); - var converter = new StringOrStringArrayJsonConverter(); - - // Act - var result = converter.ReadJson(reader, typeof(object), null, new JsonSerializer()); - - // Assert - Assert.Null(result); + Parse("""{ "value": 123 }""").Value.Should().BeNull(); } - - [Fact] - public void Should_deserialize_string() - { - var content = "{ 'value': 'test' }"; - - var parsed = JsonConvert.DeserializeObject(content); - Assert.Equal("test", parsed.Value); - } - [Fact] - public void Should_deserialize_object_as_string_array() + public void Returns_null_when_omitted() { - var content = "{ 'value': [\"value1\", \"value2\"] }"; - - var parsed = JsonConvert.DeserializeObject(content); - - new[] { "value1", "value2" }.Should().BeEquivalentTo(parsed.Value); + Parse("{}").Value.Should().BeNull(); } - + [Fact] - public void Should_deserialize_when_omitted() + public void Preserves_null_elements_in_array() { - var content = "{}"; - - var parsed = JsonConvert.DeserializeObject(content); - - Assert.Null(parsed.Value); + var value = Parse("""{ "value": ["a", null, "b"] }""").Value; + value.Should().BeOfType(); + ((string[])value).Should().Equal("a", null, "b"); } } From 316d581bf75f13b07ccd8332b06a9d3ffbce28c8 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Wed, 8 Jul 2026 15:52:39 +0530 Subject: [PATCH 2/5] feat: [AuthenticationApi] Phase out Newtonsoft.Json --- ...tInitiatedBackchannelAuthorizationTests.cs | 28 +-- .../DeviceCodeTests.cs | 6 +- .../FlexibleDateTimeConverterTests.cs | 50 ++++-- .../PasswordlessTests.cs | 6 +- .../PushedAuthorizationRequestTests.cs | 18 +- .../UserInfoDeserializationTests.cs | 170 +++++++++--------- 6 files changed, 149 insertions(+), 129 deletions(-) diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/ClientInitiatedBackchannelAuthorizationTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/ClientInitiatedBackchannelAuthorizationTests.cs index be1b07243..ca8defab7 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/ClientInitiatedBackchannelAuthorizationTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/ClientInitiatedBackchannelAuthorizationTests.cs @@ -3,12 +3,12 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Web; using Moq; using Moq.Protected; -using Newtonsoft.Json; using FluentAssertions; using Auth0.AuthenticationApi.Models; using Auth0.AuthenticationApi.Models.Ciba; @@ -38,8 +38,8 @@ public async void Ciba_request_should_succeed() }; var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL"); - SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse)); - SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonConvert.SerializeObject(mockTokenResponse)); + SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonSerializer.Serialize(mockResponse, mockResponse.GetType())); + SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonSerializer.Serialize(mockTokenResponse, mockTokenResponse.GetType())); var httpClient = new HttpClient(mockHandler.Object); var authenticationApiClient = new TestAuthenticationApiClient(domain, new TestHttpClientAuthenticationConnection(httpClient)); @@ -87,11 +87,11 @@ public async void Ciba_request_in_pending_state() var mockTokenResponse = "{\"error\": \"authorization_pending\",\n\"error_description\": \"The end-user authorization is pending\"\n}"; - + var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL"); - - SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse)); - SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonConvert.SerializeObject(mockTokenResponse), HttpStatusCode.BadRequest); + + SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonSerializer.Serialize(mockResponse, mockResponse.GetType())); + SetupMockWith(mockHandler,$"https://{domain}/oauth/token", mockTokenResponse, HttpStatusCode.BadRequest); var httpClient = new HttpClient(mockHandler.Object); var authenticationApiClient = new TestAuthenticationApiClient(domain, new TestHttpClientAuthenticationConnection(httpClient)); @@ -138,11 +138,11 @@ public async void Ciba_request_in_expired_or_rejected_state() var mockTokenResponse = "{\n\"error\": \"access_denied\",\n\"error_description\": \"The end-user denied the authorization request or it\nhas been expired\"\n}"; - + var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL"); - - SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse)); - SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonConvert.SerializeObject(mockTokenResponse), HttpStatusCode.BadRequest); + + SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonSerializer.Serialize(mockResponse, mockResponse.GetType())); + SetupMockWith(mockHandler,$"https://{domain}/oauth/token", mockTokenResponse, HttpStatusCode.BadRequest); var httpClient = new HttpClient(mockHandler.Object); var authenticationApiClient = new TestAuthenticationApiClient(domain, new TestHttpClientAuthenticationConnection(httpClient)); @@ -203,7 +203,7 @@ public async void Ciba_request_should_send_authorization_details_and_requested_e .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(mockResponse), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(mockResponse, mockResponse.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -250,7 +250,7 @@ public async void Ciba_token_response_should_expose_typed_authorization_details( var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL"); - SetupMockWith(mockHandler, $"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse)); + SetupMockWith(mockHandler, $"https://{domain}/bc-authorize", JsonSerializer.Serialize(mockResponse, mockResponse.GetType())); SetupMockWith(mockHandler, $"https://{domain}/oauth/token", mockTokenResponse); var httpClient = new HttpClient(mockHandler.Object); @@ -304,7 +304,7 @@ public async void Ciba_token_response_typed_authorization_details_should_handle_ var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL"); - SetupMockWith(mockHandler, $"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse)); + SetupMockWith(mockHandler, $"https://{domain}/bc-authorize", JsonSerializer.Serialize(mockResponse, mockResponse.GetType())); SetupMockWith(mockHandler, $"https://{domain}/oauth/token", mockTokenResponse); var httpClient = new HttpClient(mockHandler.Object); diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/DeviceCodeTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/DeviceCodeTests.cs index 85e5498c4..11e9b26aa 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/DeviceCodeTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/DeviceCodeTests.cs @@ -6,13 +6,13 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Web; using Xunit; using Moq; using Moq.Protected; -using Newtonsoft.Json; using System.Diagnostics; namespace Auth0.AuthenticationApi.IntegrationTests; @@ -73,7 +73,7 @@ public async Task Can_start_device_flow() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -114,7 +114,7 @@ public async Task Can_request_token_using_device_code() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/FlexibleDateTimeConverterTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/FlexibleDateTimeConverterTests.cs index 26adca7f1..b8a33bde4 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/FlexibleDateTimeConverterTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/FlexibleDateTimeConverterTests.cs @@ -1,41 +1,53 @@ -using FluentAssertions; -using Newtonsoft.Json; using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Auth0.AuthenticationApi; +using FluentAssertions; using Xunit; namespace Auth0.AuthenticationApi.IntegrationTests; public class FlexibleDateTimeConverterTests { - [Fact] - public void HandlesIsoStringDates() + private class TestClass { + [JsonConverter(typeof(FlexibleDateTimeConverter))] + public DateTime? DateValue { get; set; } + } - var json = "{ \"DateValue\": \"2017-01-03T19:23:03.807Z\"}"; - var result = JsonConvert.DeserializeObject(json); + private static TestClass Parse(string json) => + JsonSerializer.Deserialize(json); - result.DateValue.Should().Be(new DateTime(2017, 1, 3, 19, 23, 3, 807)); + [Fact] + public void Handles_iso_string_with_z() + { + Parse("""{ "DateValue": "2017-01-03T19:23:03.807Z" }""") + .DateValue.Should().Be(new DateTime(2017, 1, 3, 19, 23, 3, 807, DateTimeKind.Utc)); } [Fact] - public void HandlesEpochDates() + public void Handles_iso_string_without_z() { - var json = "{ \"DateValue\": 1483650127 }"; - var result = JsonConvert.DeserializeObject(json); - result.DateValue.Should().Be(new DateTime(2017, 1, 5, 21, 2, 7)); + Parse("""{ "DateValue": "2017-01-03T19:23:03" }""") + .DateValue.Value.Year.Should().Be(2017); } [Fact] - public void HandlesMissingValues() + public void Handles_epoch_seconds() { - var json = "{}"; - var result = JsonConvert.DeserializeObject(json); - result.DateValue.Should().Be(null); + Parse("""{ "DateValue": 1483650127 }""") + .DateValue.Should().Be(new DateTime(2017, 1, 5, 21, 2, 7, DateTimeKind.Utc)); } - internal class TestClass + [Fact] + public void Handles_missing_value() { - [JsonConverter(typeof(FlexibleDateTimeConverter))] - public DateTime? DateValue { get; set; } + Parse("{}").DateValue.Should().BeNull(); + } + + [Fact] + public void Handles_null_value() + { + Parse("""{ "DateValue": null }""").DateValue.Should().BeNull(); } -} \ No newline at end of file +} diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/PasswordlessTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/PasswordlessTests.cs index 7e4972780..f66e41612 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/PasswordlessTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/PasswordlessTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Web; @@ -11,7 +12,6 @@ using Xunit; using Moq; using Moq.Protected; -using Newtonsoft.Json; using Auth0.AuthenticationApi.Models; using Auth0.Tests.Shared; @@ -191,7 +191,7 @@ public async Task Should_Request_a_token_using_sms_code() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -247,7 +247,7 @@ public async Task Should_Request_a_token_using_email_code() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/PushedAuthorizationRequestTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/PushedAuthorizationRequestTests.cs index 69dd22b9c..a0cf5728c 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/PushedAuthorizationRequestTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/PushedAuthorizationRequestTests.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Web; @@ -14,7 +15,6 @@ using Microsoft.IdentityModel.Tokens; using Moq; using Moq.Protected; -using Newtonsoft.Json; using Xunit; namespace Auth0.AuthenticationApi.IntegrationTests; @@ -43,7 +43,7 @@ public async void Should_Call_OAuth_Par_With_Client_Secret() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -81,7 +81,7 @@ public async void Should_Call_OAuth_Par_With_Client_Assertion() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -171,7 +171,7 @@ public async void Should_Call_OAuth_Par_With_Arguments() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -219,7 +219,7 @@ public async void Should_Call_OAuth_Par_With_Structured_AuthorizationDetails() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -262,7 +262,7 @@ public async void Should_Prefer_Structured_AuthorizationDetails_Over_String() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -306,7 +306,7 @@ public async void Should_Call_OAuth_Par_With_AuthorizationDetails_AdditionalData .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -356,7 +356,7 @@ public async void Should_Fall_Back_To_Raw_AuthorizationDetails_When_Objects_List .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); @@ -397,7 +397,7 @@ public async void Should_Call_OAuth_Par_With_AdditionalProperties() .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json"), + Content = new StringContent(JsonSerializer.Serialize(response, response.GetType()), Encoding.UTF8, "application/json"), }); var httpClient = new HttpClient(mockHandler.Object); diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs index c1d7a8c34..8dbbd89b3 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs @@ -1,48 +1,35 @@ -using Auth0.AuthenticationApi.Models; +using System; +using System.Text.Json; +using Auth0.AuthenticationApi.Models; using FluentAssertions; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System; using Xunit; namespace Auth0.AuthenticationApi.IntegrationTests; public class UserInfoDeserializationTests { + private static UserInfo Parse(string json) => + JsonSerializer.Deserialize(json); [Fact] public void Can_read_standard_claims() { - var jsonPayload = @"{ - 'sub': '123456', - 'name': 'Robert Smith', - 'given_name': 'Robert', - 'family_name': 'Smith', - 'middle_name': 'Franklin', - 'nickname': 'Bob', - 'preferred_username': 'b.smith', - 'profile': 'http://profiles.com/bsmith', - 'picture': 'http://mycompany.com/bob_photo.jpg', - 'website': 'http://mycompany.com/users/bob', - 'email': 'bob@mycompany.com', - 'email_verified': true, - 'gender': 'male', - 'birthdate': '0000-05-22', - 'zoneinfo': 'Europe/Paris', - 'locale': 'en-US', - 'phone_number': '+1 (604) 555-1234;ext5678', - 'phone_number_verified': false, - 'address': { - 'formatted' : '123 Main St., Anytown, TX 77777', - 'street_address': '123 Main St.', - 'locality': 'Anytown', - 'region': 'Texas', - 'postal_code': '77777', - 'country': 'US' - }, - 'updated_at': 1233233333 -}"; - var userInfo = GetUserInfoFromJsonPayload(jsonPayload); + var json = """ + { + "sub": "123456", "name": "Robert Smith", "given_name": "Robert", + "family_name": "Smith", "middle_name": "Franklin", "nickname": "Bob", + "preferred_username": "b.smith", "profile": "http://profiles.com/bsmith", + "picture": "http://mycompany.com/bob_photo.jpg", "website": "http://mycompany.com/users/bob", + "email": "bob@mycompany.com", "email_verified": true, "gender": "male", + "birthdate": "0000-05-22", "zoneinfo": "Europe/Paris", "locale": "en-US", + "phone_number": "+1 (604) 555-1234;ext5678", "phone_number_verified": false, + "address": { "formatted": "123 Main St., Anytown, TX 77777", "street_address": "123 Main St.", + "locality": "Anytown", "region": "Texas", "postal_code": "77777", "country": "US" }, + "updated_at": 1233233333 + } + """; + var userInfo = Parse(json); userInfo.UserId.Should().Be("123456"); userInfo.FullName.Should().Be("Robert Smith"); @@ -51,81 +38,102 @@ public void Can_read_standard_claims() userInfo.MiddleName.Should().Be("Franklin"); userInfo.NickName.Should().Be("Bob"); userInfo.PreferredUsername.Should().Be("b.smith"); - userInfo.Profile.Should().Be("http://profiles.com/bsmith"); - userInfo.Picture.Should().Be("http://mycompany.com/bob_photo.jpg"); - userInfo.Website.Should().Be("http://mycompany.com/users/bob"); userInfo.Email.Should().Be("bob@mycompany.com"); userInfo.EmailVerified.Should().Be(true); - userInfo.Gender.Should().Be("male"); userInfo.Birthdate.Should().Be("0000-05-22"); - userInfo.ZoneInformation.Should().Be("Europe/Paris"); userInfo.Locale.Should().Be("en-US"); - userInfo.PhoneNumber.Should().Be("+1 (604) 555-1234;ext5678"); userInfo.PhoneNumberVerified.Should().Be(false); - userInfo.Address.Formatted.Should().Be("123 Main St., Anytown, TX 77777"); - userInfo.Address.StreetAddress.Should().Be("123 Main St."); - userInfo.Address.Locality.Should().Be("Anytown"); - userInfo.Address.Region.Should().Be("Texas"); - userInfo.Address.PostalCode.Should().Be("77777"); userInfo.Address.Country.Should().Be("US"); - userInfo.UpdatedAt.Should().Be(new DateTime(2009, 1, 29, 12, 48, 53)); + userInfo.UpdatedAt.Should().Be(new DateTime(2009, 1, 29, 12, 48, 53, DateTimeKind.Utc)); } [Fact] - public void Can_read_custom_locale_claim() + public void Can_read_custom_locale_claim_as_compact_string() { - var jsonPayload = @"{ - 'sub': '123456', - 'locale': { - 'country': 'US', - 'language': 'en' - }, - 'updated_at': 1233233333 -}"; - var userInfo = GetUserInfoFromJsonPayload(jsonPayload); + var userInfo = Parse("""{ "sub": "123456", "locale": { "country": "US", "language": "en" } }"""); - userInfo.UserId.Should().Be("123456"); userInfo.Locale.Should().NotBeNull(); - JObject.Parse(userInfo.Locale).GetValue("country")!.Value().Should().Be("US"); - JObject.Parse(userInfo.Locale).GetValue("language")!.Value().Should().Be("en"); + var locale = JsonDocument.Parse(userInfo.Locale).RootElement; + locale.GetProperty("country").GetString().Should().Be("US"); + locale.GetProperty("language").GetString().Should().Be("en"); } - + [Fact] public void Missing_values_are_null() { - var jsonPayload = @"{ 'sub': '123456'}"; - var userInfo = GetUserInfoFromJsonPayload(jsonPayload); - Assert.Null(userInfo.FullName); - Assert.Null(userInfo.EmailVerified); - Assert.Null(userInfo.PhoneNumberVerified); - Assert.Null(userInfo.Address); - Assert.Null(userInfo.UpdatedAt); + var userInfo = Parse("""{ "sub": "123456" }"""); + + userInfo.FullName.Should().BeNull(); + userInfo.EmailVerified.Should().BeNull(); + userInfo.PhoneNumberVerified.Should().BeNull(); + userInfo.Address.Should().BeNull(); + userInfo.UpdatedAt.Should().BeNull(); } [Fact] - public void Can_read_additional_claims() + public void Can_read_additional_claims_via_new_json_property() { - var jsonPayload = @"{ - 'sub': '123456', - 'http://acme.com/claims/groupIds': [ 'bobsdepartment','administrators' ], - 'http://acme.com/claims/manager': { 'name' : 'John Doe' }, - 'http://acme.com/claims/office': 'building 125' -}"; - var userInfo = GetUserInfoFromJsonPayload(jsonPayload); + var userInfo = Parse(""" + { + "sub": "123456", + "http://acme.com/claims/groupIds": [ "bobsdepartment", "administrators" ], + "http://acme.com/claims/manager": { "name": "John Doe" } + } + """); - var groups = (JArray)userInfo.AdditionalClaims["http://acme.com/claims/groupIds"]; + var groups = userInfo.AdditionalClaimsJson["http://acme.com/claims/groupIds"]; + groups.ValueKind.Should().Be(JsonValueKind.Array); + groups[0].GetString().Should().Be("bobsdepartment"); + groups[1].GetString().Should().Be("administrators"); - Assert.Equal(2, groups.Count); - Assert.Equal("bobsdepartment", (string)groups[0]); - Assert.Equal("administrators", (string)groups[1]); + userInfo.AdditionalClaimsJson["http://acme.com/claims/manager"] + .GetProperty("name").GetString().Should().Be("John Doe"); + } + + [Fact] + public void Obsolete_AdditionalClaims_shim_still_returns_jtokens() + { +#pragma warning disable CS0618 // testing the obsolete member on purpose + var userInfo = Parse(""" + { + "sub": "123456", + "http://acme.com/claims/groupIds": [ "bobsdepartment", "administrators" ], + "http://acme.com/claims/manager": { "name": "John Doe" } + } + """); + + var groups = (JArray)userInfo.AdditionalClaims["http://acme.com/claims/groupIds"]; + groups.Count.Should().Be(2); + ((string)groups[0]).Should().Be("bobsdepartment"); dynamic manager = userInfo.AdditionalClaims["http://acme.com/claims/manager"]; string managerName = manager.name; managerName.Should().Be("John Doe"); +#pragma warning restore CS0618 + } + + [Fact] + public void Scalar_additional_claim_is_captured() + { + var userInfo = Parse("""{ "sub": "123456", "login_count": 42 }"""); + + userInfo.AdditionalClaimsJson["login_count"].GetInt32().Should().Be(42); + +#pragma warning disable CS0618 // testing the obsolete member on purpose + ((int)userInfo.AdditionalClaims["login_count"]).Should().Be(42); +#pragma warning restore CS0618 } - private UserInfo GetUserInfoFromJsonPayload(string jsonPayload) + [Fact] + public void Null_additional_claim_is_captured() { - return JsonConvert.DeserializeObject(jsonPayload); + var userInfo = Parse("""{ "sub": "123456", "optional_claim": null }"""); + + userInfo.AdditionalClaimsJson.Should().ContainKey("optional_claim"); + userInfo.AdditionalClaimsJson["optional_claim"].ValueKind.Should().Be(JsonValueKind.Null); + +#pragma warning disable CS0618 // testing the obsolete member on purpose + userInfo.AdditionalClaims["optional_claim"].Type.Should().Be(JTokenType.Null); +#pragma warning restore CS0618 } -} \ No newline at end of file +} From 774e4fa882cbcb383bc53a24b57da95b99770496 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Wed, 8 Jul 2026 17:03:19 +0530 Subject: [PATCH 3/5] chore: Improve test coverage --- .../ApiErrorDeserializationTests.cs | 38 ++++--------------- .../StringOrObjectAsStringConverterTests.cs | 6 +++ 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs b/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs index 5083a115c..335c0cf74 100644 --- a/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs +++ b/tests/Auth0.Core.UnitTests/ApiErrorDeserializationTests.cs @@ -1,5 +1,3 @@ -using System.Collections; -using System.Collections.Generic; using Auth0.Core.Exceptions; using FluentAssertions; using Xunit; @@ -22,13 +20,15 @@ public void Should_deserialize_extra_properties_like_mfa_response() .WhichValue.Should().Be("2x4b-r2d2-c3po-bb8-ig88"); } - [Theory] - [ClassData(typeof(ApiErrorDeserializationData))] - public void Should_deserialize_all_error_structures_correctly(string content, ApiError expected) + [Fact] + public void Maps_camelcase_errorCode_alias_to_error_code() { - var error = ApiError.Parse(content); + var parsed = ApiError.Parse( + """{"error": "Error", "message": "The Message", "errorCode": "error_code"}"""); - error.Should().BeEquivalentTo(expected); + parsed.Error.Should().Be("Error"); + parsed.Message.Should().Be("The Message"); + parsed.ErrorCode.Should().Be("error_code"); } [Fact] @@ -94,27 +94,3 @@ public void Null_unmapped_field_becomes_empty_string() parsed.ExtraData.Should().ContainKey("custom").WhichValue.Should().Be(""); } } - -public class ApiErrorDeserializationData : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new object[] - { - """{"name":"Error","code":"error_code","description":"The Message"}""", - new ApiError { Error = "Error", ErrorCode = "error_code", Message = "The Message" } - }; - yield return new object[] - { - """{"error": "Error","error_description": "The Message"}""", - new ApiError { Error = "Error", ErrorCode = null, Message = "The Message" } - }; - yield return new object[] - { - """{"error": "Error", "message": "The Message", "errorCode": "error_code"}""", - new ApiError { Error = "Error", ErrorCode = "error_code", Message = "The Message" } - }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs b/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs index a217ec3a9..bcea18c5a 100644 --- a/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs +++ b/tests/Auth0.Core.UnitTests/StringOrObjectAsStringConverterTests.cs @@ -56,4 +56,10 @@ public void Returns_null_when_omitted() { Parse("{}").Value.Should().BeNull(); } + + [Fact] + public void Returns_empty_string_for_scalar_non_string() + { + Parse("""{ "value": 123 }""").Value.Should().Be(""); + } } From 7af98df7927c30465e90fd5630de8e9a8ee89210 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Thu, 9 Jul 2026 14:58:45 +0530 Subject: [PATCH 4/5] fix: Use custom serialiser for SignUpUserResponse --- .../Models/SignupUserResponse.cs | 22 +---- .../SignupUserResponseConverter.cs | 90 +++++++++++++++++++ .../SignupUserResponseDeserializationTests.cs | 68 ++++++++++++++ 3 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 src/Auth0.AuthenticationApi/Serialization/SignupUserResponseConverter.cs create mode 100644 tests/Auth0.AuthenticationApi.IntegrationTests/SignupUserResponseDeserializationTests.cs diff --git a/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs b/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs index 9b1ae614b..6739d0e44 100644 --- a/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs @@ -1,12 +1,14 @@ using System; using System.Text.Json.Serialization; +using Auth0.AuthenticationApi.Serialization; namespace Auth0.AuthenticationApi.Models; /// /// Represents the response from signing up a new user. /// +[JsonConverter(typeof(SignupUserResponseConverter))] public class SignupUserResponse { /// @@ -30,25 +32,7 @@ public class SignupUserResponse /// For convenience we expose it here as just one. /// [JsonIgnore] - public string Id - { - get { return _id ?? id ?? user_id; } - set { _id = value; } - } - - [JsonInclude] - [JsonPropertyName("_id")] - private string _id; // Standard connection - -#pragma warning disable 0649 - [JsonInclude] - [JsonPropertyName("id")] - private string id; // Custom connection - - [JsonInclude] - [JsonPropertyName("user_id")] - private string user_id; // Custom connection external -#pragma warning restore 0649 + public string Id { get; set; } /// /// Username of this user. diff --git a/src/Auth0.AuthenticationApi/Serialization/SignupUserResponseConverter.cs b/src/Auth0.AuthenticationApi/Serialization/SignupUserResponseConverter.cs new file mode 100644 index 000000000..f7ec9ba24 --- /dev/null +++ b/src/Auth0.AuthenticationApi/Serialization/SignupUserResponseConverter.cs @@ -0,0 +1,90 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Auth0.AuthenticationApi.Models; + +namespace Auth0.AuthenticationApi.Serialization; + +/// +/// Deserializes , resolving the user identifier from +/// whichever of _id, id or user_id the server returned. +/// +/// +/// System.Text.Json only reliably honours [JsonInclude] on non-public +/// properties, not on private fields; the latter silently fail to bind on +/// .NET Framework. This converter reads through a public-property surrogate so the mapping +/// works identically on every target framework. +/// +internal class SignupUserResponseConverter : JsonConverter +{ + // Public-property mirror of SignupUserResponse used purely as the deserialization + // target. It carries no converter attribute, so binding it does not recurse. + private class Surrogate + { + [JsonPropertyName("_id")] + public string UnderscoreId { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("email_verified")] + public bool EmailVerified { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("given_name")] + public string GivenName { get; set; } + + [JsonPropertyName("family_name")] + public string FamilyName { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("nickname")] + public string Nickname { get; set; } + + [JsonPropertyName("picture")] + public Uri Picture { get; set; } + + [JsonPropertyName("user_metadata")] + public JsonElement? UserMetadata { get; set; } + + [JsonPropertyName("phone_number")] + public string PhoneNumber { get; set; } + } + + public override SignupUserResponse Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var surrogate = JsonSerializer.Deserialize(ref reader, options); + if (surrogate == null) + return null; + + return new SignupUserResponse + { + Id = surrogate.UnderscoreId ?? surrogate.Id ?? surrogate.UserId, + Email = surrogate.Email, + EmailVerified = surrogate.EmailVerified, + Username = surrogate.Username, + GivenName = surrogate.GivenName, + FamilyName = surrogate.FamilyName, + Name = surrogate.Name, + Nickname = surrogate.Nickname, + Picture = surrogate.Picture, + UserMetadata = surrogate.UserMetadata, + PhoneNumber = surrogate.PhoneNumber, + }; + } + + public override void Write(Utf8JsonWriter writer, SignupUserResponse value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } +} diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/SignupUserResponseDeserializationTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/SignupUserResponseDeserializationTests.cs new file mode 100644 index 000000000..a119ff6b8 --- /dev/null +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/SignupUserResponseDeserializationTests.cs @@ -0,0 +1,68 @@ +using System.Text.Json; +using Auth0.AuthenticationApi.Models; +using FluentAssertions; +using Xunit; + +namespace Auth0.AuthenticationApi.IntegrationTests; + +public class SignupUserResponseDeserializationTests +{ + private static SignupUserResponse Parse(string json) => + JsonSerializer.Deserialize(json); + + [Theory] + [InlineData("""{ "_id": "abc123" }""")] // Standard connection + [InlineData("""{ "id": "abc123" }""")] // Custom connection + [InlineData("""{ "user_id": "abc123" }""")] // Custom connection external + public void Resolves_Id_from_each_server_field(string json) + { + Parse(json).Id.Should().Be("abc123"); + } + + [Fact] + public void Id_prefers_underscore_id_over_aliases() + { + var response = Parse("""{ "_id": "primary", "id": "secondary", "user_id": "tertiary" }"""); + + response.Id.Should().Be("primary"); + } + + [Fact] + public void Id_falls_back_to_id_when_underscore_id_absent() + { + var response = Parse("""{ "id": "secondary", "user_id": "tertiary" }"""); + + response.Id.Should().Be("secondary"); + } + + [Fact] + public void Id_is_null_when_no_identifier_present() + { + Parse("""{ "email": "user@example.com" }""").Id.Should().BeNull(); + } + + [Fact] + public void Maps_all_fields_through_the_converter() + { + var response = Parse(""" + { + "_id": "abc123", "email": "user@example.com", "email_verified": true, + "username": "bsmith", "given_name": "Bob", "family_name": "Smith", + "name": "Bob Smith", "nickname": "Bob", "picture": "https://example.com/p.png", + "phone_number": "+15551234", "user_metadata": { "plan": "gold" } + } + """); + + response.Id.Should().Be("abc123"); + response.Email.Should().Be("user@example.com"); + response.EmailVerified.Should().BeTrue(); + response.Username.Should().Be("bsmith"); + response.GivenName.Should().Be("Bob"); + response.FamilyName.Should().Be("Smith"); + response.Name.Should().Be("Bob Smith"); + response.Nickname.Should().Be("Bob"); + response.Picture.Should().Be(new System.Uri("https://example.com/p.png")); + response.PhoneNumber.Should().Be("+15551234"); + ((string)response.UserMetadata.GetProperty("plan").GetString()).Should().Be("gold"); + } +} From 7ba9de99f99360172befb72a9f58a051f167b454 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Thu, 9 Jul 2026 15:27:37 +0530 Subject: [PATCH 5/5] chore: Address review comments --- .../FlexibleDateTimeConverter.cs | 9 +++- .../Models/PasswordlessSmsResponse.cs | 5 ++ .../Models/SignupUserResponse.cs | 7 ++- src/Auth0.Core/Auth0.Core.csproj | 2 +- src/Auth0.Core/Exceptions/ApiError.cs | 5 +- .../StringOrStringArrayJsonConverter.cs | 33 ------------ .../UserInfoDeserializationTests.cs | 19 +++++++ tests/Auth0.Core.UnitTests/ApiErrorTests.cs | 14 +++++ .../StringOrStringArrayConverterTests.cs | 54 ------------------- 9 files changed, 57 insertions(+), 91 deletions(-) delete mode 100644 src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs delete mode 100644 tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs diff --git a/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs b/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs index 95379fe6b..4c1814111 100644 --- a/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs +++ b/src/Auth0.AuthenticationApi/FlexibleDateTimeConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -20,7 +21,13 @@ internal class FlexibleDateTimeConverter : JsonConverter case JsonTokenType.Number: return Add(Epoch, TimeSpan.FromSeconds(reader.GetInt64())); case JsonTokenType.String: - return reader.GetDateTime(); + // Prefer STJ's ISO 8601 reader; fall back to a lenient parse for other + // formats. An unparseable value yields null rather than throwing and + // failing the entire enclosing object's deserialization. + if (reader.TryGetDateTime(out var iso)) + return iso; + return DateTime.TryParse(reader.GetString(), CultureInfo.InvariantCulture, + DateTimeStyles.RoundtripKind, out var parsed) ? parsed : null; default: return null; } diff --git a/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs b/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs index e1ae8f773..b68178a1a 100644 --- a/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/PasswordlessSmsResponse.cs @@ -22,6 +22,11 @@ public class PasswordlessSmsResponse /// /// Language the message sent was written in. /// + /// + /// On deserialization this is a (previously a + /// Newtonsoft JToken). Read the value via the JsonElement API, e.g. + /// ((JsonElement)response.RequestLanguage).GetString(). + /// [JsonPropertyName("request_language")] public object RequestLanguage { get; set; } } \ No newline at end of file diff --git a/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs b/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs index 6739d0e44..48ce90c35 100644 --- a/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs +++ b/src/Auth0.AuthenticationApi/Models/SignupUserResponse.cs @@ -71,8 +71,13 @@ public class SignupUserResponse public Uri Picture { get; set; } /// - /// Metadata the user has read/write access to. + /// Metadata the user has read/write access to. /// + /// + /// On deserialization this is a (previously a + /// Newtonsoft JObject). Read values via the JsonElement API, e.g. + /// ((JsonElement)response.UserMetadata).GetProperty("plan").GetString(). + /// [JsonPropertyName("user_metadata")] public dynamic UserMetadata { get; set; } diff --git a/src/Auth0.Core/Auth0.Core.csproj b/src/Auth0.Core/Auth0.Core.csproj index da9e97a07..77368b671 100644 --- a/src/Auth0.Core/Auth0.Core.csproj +++ b/src/Auth0.Core/Auth0.Core.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Auth0.Core/Exceptions/ApiError.cs b/src/Auth0.Core/Exceptions/ApiError.cs index 4821334e4..2568349fb 100644 --- a/src/Auth0.Core/Exceptions/ApiError.cs +++ b/src/Auth0.Core/Exceptions/ApiError.cs @@ -64,7 +64,10 @@ public class ApiError { return JsonSerializer.Deserialize(content!, Auth0JsonSerializerOptions.Default); } - catch (JsonException) + // JsonException: malformed JSON. InvalidOperationException: valid JSON whose root is + // not an object (e.g. a bare string or array), which the converter cannot enumerate. + // Both fall back to surfacing the raw body. + catch (System.Exception ex) when (ex is JsonException or System.InvalidOperationException) { return new ApiError { diff --git a/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs b/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs deleted file mode 100644 index 6b73f68ec..000000000 --- a/src/Auth0.Core/Serialization/StringOrStringArrayJsonConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Auth0.Core.Serialization; - -internal class StringOrStringArrayJsonConverter : JsonConverter -{ - public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - switch (reader.TokenType) - { - case JsonTokenType.String: - return reader.GetString(); - case JsonTokenType.StartArray: - var list = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - list.Add(reader.GetString()); - } - return list.ToArray(); - default: - reader.Skip(); - return null; - } - } - - public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } -} diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs index 8dbbd89b3..e91e8cd1c 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/UserInfoDeserializationTests.cs @@ -70,6 +70,25 @@ public void Missing_values_are_null() userInfo.UpdatedAt.Should().BeNull(); } + [Fact] + public void UpdatedAt_reads_iso8601_string() + { + var userInfo = Parse("""{ "sub": "123456", "updated_at": "2009-01-29T12:48:53Z" }"""); + + userInfo.UpdatedAt.Should().Be(new DateTime(2009, 1, 29, 12, 48, 53, DateTimeKind.Utc)); + } + + [Fact] + public void UpdatedAt_falls_back_to_null_for_unparseable_string() + { + // A non-ISO / malformed updated_at must not fail the whole UserInfo deserialization. + var userInfo = Parse("""{ "sub": "123456", "email": "bob@mycompany.com", "updated_at": "not-a-date" }"""); + + userInfo.UserId.Should().Be("123456"); + userInfo.Email.Should().Be("bob@mycompany.com"); + userInfo.UpdatedAt.Should().BeNull(); + } + [Fact] public void Can_read_additional_claims_via_new_json_property() { diff --git a/tests/Auth0.Core.UnitTests/ApiErrorTests.cs b/tests/Auth0.Core.UnitTests/ApiErrorTests.cs index a6784e59e..2800b6bd9 100644 --- a/tests/Auth0.Core.UnitTests/ApiErrorTests.cs +++ b/tests/Auth0.Core.UnitTests/ApiErrorTests.cs @@ -108,6 +108,20 @@ public void ParseFromString_WithInvalidJson_ReturnsApiErrorWithContentAsErrorAnd result.ExtraData.Should().BeEmpty(); } + [Theory] + [InlineData(""" "Unauthorized" """)] // bare JSON string + [InlineData("123")] // bare JSON number + [InlineData("[1,2,3]")] // JSON array + public void ParseFromString_WithNonObjectJson_FallsBackToContent(string body) + { + var result = ApiError.Parse(body); + + result.Should().NotBeNull(); + result.Error.Should().Be(body); + result.Message.Should().Be(body); + result.ExtraData.Should().BeEmpty(); + } + [Fact] public void ParseFromString_WithEmptyString_ReturnsNull() { diff --git a/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs b/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs deleted file mode 100644 index 0490a964e..000000000 --- a/tests/Auth0.Core.UnitTests/StringOrStringArrayConverterTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Auth0.Core.Serialization; -using FluentAssertions; -using Xunit; - -namespace Auth0.Core.UnitTests; - -public class StringOrStringArrayJsonConverterTests -{ - private class Data - { - [JsonPropertyName("value")] - [JsonConverter(typeof(StringOrStringArrayJsonConverter))] - public object Value { get; set; } - } - - private static Data Parse(string json) => - JsonSerializer.Deserialize(json, Auth0JsonSerializerOptions.Default); - - [Fact] - public void Deserializes_single_string() - { - Parse("""{ "value": "test" }""").Value.Should().Be("test"); - } - - [Fact] - public void Deserializes_array_as_string_array() - { - var value = Parse("""{ "value": ["value1", "value2"] }""").Value; - value.Should().BeOfType(); - ((string[])value).Should().Equal("value1", "value2"); - } - - [Fact] - public void Returns_null_for_non_string_non_array() - { - Parse("""{ "value": 123 }""").Value.Should().BeNull(); - } - - [Fact] - public void Returns_null_when_omitted() - { - Parse("{}").Value.Should().BeNull(); - } - - [Fact] - public void Preserves_null_elements_in_array() - { - var value = Parse("""{ "value": ["a", null, "b"] }""").Value; - value.Should().BeOfType(); - ((string[])value).Should().Equal("a", null, "b"); - } -}