Skip to content

Commit 2017eda

Browse files
authored
Merge pull request #232 from petrsnd/feature/225-stj-conversion-for-aot
Migrate from Newtonsoft.Json to System.Text.Json
2 parents f029d74 + bb7512b commit 2017eda

61 files changed

Lines changed: 1481 additions & 367 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/testing-guide/SKILL.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,28 @@ correctly.
286286
5. **Test both modes when applicable.** The PKCE test suite supports two modes:
287287
- **Standard mode** (no `TotpSeed`): Runs login success + error tests
288288
- **MFA mode** (`TotpSeed` provided): Runs only the TOTP success test
289+
290+
## Suite interactivity classification
291+
292+
All suites are designed to run **non-interactively** (no human input during execution).
293+
Suites that test browser or device-code flows use timeouts and error paths to validate
294+
the flow starts correctly without requiring a human to complete authentication.
295+
296+
| Suite | Requires | Notes |
297+
|---|---|---|
298+
| SpsIntegration | SPS appliance (`-SpsAppliance`, `-SpsPassword`) | Only suite needing separate infrastructure |
299+
| BrowserAuthentication | Nothing extra | Tests error paths + verifies listener starts (timeout expected) |
300+
| DeviceCodeAuthentication | Nothing extra | Tests grant toggle + verifies device code issued (timeout expected) |
301+
| A2A* suites | Nothing extra | Creates own certs, users, registrations via setup |
302+
| CertificateAuth | Nothing extra | Uses embedded test certificates |
303+
| Streaming | Nothing extra | Tests streaming download paths |
304+
| All others | Nothing extra | Standard password/PKCE auth against appliance |
305+
306+
**When running a full regression**, exclude only `SpsIntegration` (unless an SPS appliance
307+
is available):
308+
309+
```powershell
310+
pwsh -File Test\TestFramework\Invoke-SafeguardTests.ps1 `
311+
-Appliance <address> -AdminPassword <password> -SkipBuild `
312+
-ExcludeSuite SpsIntegration
313+
```

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<PropertyGroup>
33
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
44
<GenerateDocumentationFile>true</GenerateDocumentationFile>
5+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
56
</PropertyGroup>
67
<ItemGroup>
78
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />

SafeguardDotNet.Core.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipeline-templates", "pipel
5454
EndProject
5555
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeguardDotNetUnitTest", "Test\SafeguardDotNetUnitTest\SafeguardDotNetUnitTest.csproj", "{1B11B367-2F76-49EE-A820-2A0A8B1721B1}"
5656
EndProject
57+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeguardDotNet.SerializationTests", "Test\SafeguardDotNet.SerializationTests\SafeguardDotNet.SerializationTests.csproj", "{F5591287-9C28-4778-951A-31F6A6766B63}"
58+
EndProject
5759
Global
5860
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5961
Debug|Any CPU = Debug|Any CPU
@@ -112,6 +114,10 @@ Global
112114
{1B11B367-2F76-49EE-A820-2A0A8B1721B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
113115
{1B11B367-2F76-49EE-A820-2A0A8B1721B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
114116
{1B11B367-2F76-49EE-A820-2A0A8B1721B1}.Release|Any CPU.Build.0 = Release|Any CPU
117+
{F5591287-9C28-4778-951A-31F6A6766B63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
118+
{F5591287-9C28-4778-951A-31F6A6766B63}.Debug|Any CPU.Build.0 = Debug|Any CPU
119+
{F5591287-9C28-4778-951A-31F6A6766B63}.Release|Any CPU.ActiveCfg = Release|Any CPU
120+
{F5591287-9C28-4778-951A-31F6A6766B63}.Release|Any CPU.Build.0 = Release|Any CPU
115121
{6802F7A9-CD3D-4608-9EEC-C98636DA2708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
116122
{6802F7A9-CD3D-4608-9EEC-C98636DA2708}.Debug|Any CPU.Build.0 = Debug|Any CPU
117123
{6802F7A9-CD3D-4608-9EEC-C98636DA2708}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -137,6 +143,7 @@ Global
137143
{20D9ED51-6852-44FC-A413-5EC7631139F9} = {DD89D86B-68DA-4EB0-8EBC-60DE3DC2B084}
138144
{1EB6D64D-7AFB-41DC-B11B-58934D053684} = {9249E337-656D-4970-B45C-7A077C56FA44}
139145
{1B11B367-2F76-49EE-A820-2A0A8B1721B1} = {DD89D86B-68DA-4EB0-8EBC-60DE3DC2B084}
146+
{F5591287-9C28-4778-951A-31F6A6766B63} = {DD89D86B-68DA-4EB0-8EBC-60DE3DC2B084}
140147
EndGlobalSection
141148
GlobalSection(ExtensibilityGlobals) = postSolution
142149
SolutionGuid = {5A3B2C1D-4E6F-7A8B-9C0D-1E2F3A4B5C6D}

SafeguardDotNet.DeviceCodeLogin/DeviceCodeLogin.cs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ namespace OneIdentity.SafeguardDotNet.DeviceCodeLogin;
66
using System.Net.Http;
77
using System.Security;
88
using System.Text;
9+
using System.Text.Json;
910
using System.Threading;
1011
using System.Threading.Tasks;
1112

12-
using Newtonsoft.Json;
13-
using Newtonsoft.Json.Linq;
14-
1513
using Serilog;
1614

1715
/// <summary>
@@ -81,7 +79,7 @@ public static async Task<ISafeguardConnection> ConnectAsync(
8179
Log.Debug("Requesting device authorization from {Appliance}", appliance);
8280

8381
var deviceAuthUrl = $"https://{appliance}/RSTS/oauth2/DeviceLogin";
84-
var requestBody = JsonConvert.SerializeObject(new { client_id = clientId, scope });
82+
var requestBody = JsonSerializer.Serialize(new { client_id = clientId, scope });
8583
var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
8684

8785
HttpResponseMessage response;
@@ -105,12 +103,13 @@ public static async Task<ISafeguardConnection> ConnectAsync(
105103
responseBody);
106104
}
107105

108-
var deviceResponse = JObject.Parse(responseBody);
109-
var deviceCode = deviceResponse["device_code"]?.ToString();
110-
var userCode = deviceResponse["user_code"]?.ToString();
111-
var verificationUri = deviceResponse["verification_uri"]?.ToString();
112-
var verificationUriComplete = deviceResponse["verification_uri_complete"]?.ToString();
113-
var expiresIn = deviceResponse["expires_in"]?.Value<int>() ?? 300;
106+
using var deviceResponse = JsonDocument.Parse(responseBody);
107+
var deviceRoot = deviceResponse.RootElement;
108+
var deviceCode = deviceRoot.TryGetProperty("device_code", out var dcEl) ? dcEl.GetString() : null;
109+
var userCode = deviceRoot.TryGetProperty("user_code", out var ucEl) ? ucEl.GetString() : null;
110+
var verificationUri = deviceRoot.TryGetProperty("verification_uri", out var vuEl) ? vuEl.GetString() : null;
111+
var verificationUriComplete = deviceRoot.TryGetProperty("verification_uri_complete", out var vucEl) ? vucEl.GetString() : null;
112+
var expiresIn = deviceRoot.TryGetProperty("expires_in", out var eiEl) && eiEl.TryGetInt32(out var eiVal) ? eiVal : 300;
114113

115114
// Step 2: Display to user via callback
116115
parameters.DisplayCallback(new DeviceCodeInfo
@@ -135,7 +134,7 @@ public static async Task<ISafeguardConnection> ConnectAsync(
135134

136135
await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), cancellationToken).ConfigureAwait(false);
137136

138-
var pollBody = JsonConvert.SerializeObject(new
137+
var pollBody = JsonSerializer.Serialize(new
139138
{
140139
grant_type = "urn:ietf:params:oauth:grant-type:device_code",
141140
device_code = deviceCode,
@@ -144,15 +143,18 @@ public static async Task<ISafeguardConnection> ConnectAsync(
144143
var pollContent = new StringContent(pollBody, Encoding.UTF8, "application/json");
145144
var pollResponse = await http.PostAsync(tokenUrl, pollContent, cancellationToken).ConfigureAwait(false);
146145
var pollResponseBody = await pollResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
147-
var pollJson = JObject.Parse(pollResponseBody);
146+
using var pollJson = JsonDocument.Parse(pollResponseBody);
147+
148+
var pollRoot = pollJson.RootElement;
148149

149150
if (pollResponse.IsSuccessStatusCode)
150151
{
151-
rstsAccessToken = pollJson["access_token"]?.ToString().ToSecureString();
152+
var accessTokenValue = pollRoot.TryGetProperty("access_token", out var atEl) ? atEl.GetString() : null;
153+
rstsAccessToken = accessTokenValue?.ToSecureString();
152154
break;
153155
}
154156

155-
var error = pollJson["error"]?.ToString();
157+
var error = pollRoot.TryGetProperty("error", out var errEl) ? errEl.GetString() : null;
156158
switch (error)
157159
{
158160
case "authorization_pending":

SafeguardDotNet.DeviceCodeLogin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ using var connection = await DeviceCodeLogin.ConnectAsync(
9191

9292
- SafeguardDotNet (core SDK)
9393
- Serilog (logging)
94-
- Newtonsoft.Json
94+
- System.Text.Json (via SafeguardDotNet)
9595

9696
## Testing
9797

SafeguardDotNet.DeviceCodeLogin/SafeguardDotNet.DeviceCodeLogin.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838

3939
<ItemGroup>
4040
<PackageReference Include="Serilog" Version="4.3.0" />
41-
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
4241
</ItemGroup>
4342

4443
<ItemGroup>

SafeguardDotNet.GuiLogin/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ The dialog handles all OAuth flow details automatically.
7575
This package uses `.nuspec` packaging to support .NET Framework 4.8.1 dependencies and framework assemblies:
7676
- System.Windows.Forms
7777
- System.Web
78-
- Newtonsoft.Json
7978
- Serilog
8079

8180
## Related Packages

SafeguardDotNet.GuiLogin/SafeguardDotNet.GuiLogin.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@
5959
<PackageReference Include="Microsoft.Web.WebView2">
6060
<Version>1.0.3179.45</Version>
6161
</PackageReference>
62-
<PackageReference Include="Newtonsoft.Json">
63-
<Version>13.0.4</Version>
64-
</PackageReference>
6562
<PackageReference Include="Serilog">
6663
<Version>4.3.0</Version>
6764
</PackageReference>

SafeguardDotNet.GuiLogin/SafeguardDotNet.GuiLogin.nuspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
<tags>safeguard credentials vault sdk</tags>
1818
<dependencies>
1919
<group targetFramework=".NETFramework4.8.1">
20-
<dependency id="Newtonsoft.Json" version="(13.0.3, )" />
2120
<dependency id="Serilog" version="(3.0.1, )" />
2221
</group>
2322
</dependencies>

SafeguardDotNet.PkceNoninteractiveLogin/PkceNoninteractiveLogin.cs

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ namespace OneIdentity.SafeguardDotNet.PkceNoninteractiveLogin;
1010
using System.Net.Http;
1111
using System.Security;
1212
using System.Text;
13+
using System.Text.Json;
1314
using System.Threading;
1415
using System.Threading.Tasks;
1516
using System.Web;
1617

17-
using Newtonsoft.Json.Linq;
18-
1918
using Serilog;
2019

2120
/// <summary>
@@ -217,30 +216,34 @@ private static async Task HandleSecondaryAuthenticationAsync(
217216
SecureString secondaryPassword,
218217
CancellationToken cancellationToken)
219218
{
220-
JObject primaryResponse;
219+
JsonDocument primaryResponse;
221220
try
222221
{
223-
primaryResponse = JObject.Parse(primaryAuthBody);
222+
primaryResponse = JsonDocument.Parse(primaryAuthBody);
224223
}
225224
catch
226225
{
227226
return; // Non-JSON response means no secondary auth info
228227
}
229228

230-
var secondaryProviderId = primaryResponse["SecondaryProviderID"]?.ToString();
231-
232-
if (string.IsNullOrEmpty(secondaryProviderId))
229+
using (primaryResponse)
233230
{
234-
return; // No MFA required
235-
}
231+
var root = primaryResponse.RootElement;
232+
var secondaryProviderId = root.TryGetProperty("SecondaryProviderID", out var spId) ? spId.GetString() : null;
236233

237-
Log.Debug("Secondary authentication required, provider: {SecondaryProviderId}", secondaryProviderId);
234+
if (string.IsNullOrEmpty(secondaryProviderId))
235+
{
236+
return; // No MFA required
237+
}
238238

239-
if (secondaryPassword == null)
240-
{
241-
throw new SafeguardDotNetException(
242-
$"Multi-factor authentication is required (provider: {secondaryProviderId}) " +
243-
"but no secondary password was provided. Use the secondaryPassword parameter to supply the one-time code.");
239+
Log.Debug("Secondary authentication required, provider: {SecondaryProviderId}", secondaryProviderId);
240+
241+
if (secondaryPassword == null)
242+
{
243+
throw new SafeguardDotNetException(
244+
$"Multi-factor authentication is required (provider: {secondaryProviderId}) " +
245+
"but no secondary password was provided. Use the secondaryPassword parameter to supply the one-time code.");
246+
}
244247
}
245248

246249
cancellationToken.ThrowIfCancellationRequested();
@@ -255,9 +258,10 @@ private static async Task HandleSecondaryAuthenticationAsync(
255258
{
256259
try
257260
{
258-
var initResponse = JObject.Parse(initBody);
259-
mfaState = initResponse["State"]?.ToString() ?? string.Empty;
260-
var mfaMessage = initResponse["Message"]?.ToString();
261+
using var initResponse = JsonDocument.Parse(initBody);
262+
var initRoot = initResponse.RootElement;
263+
mfaState = initRoot.TryGetProperty("State", out var stateEl) ? stateEl.GetString() ?? string.Empty : string.Empty;
264+
var mfaMessage = initRoot.TryGetProperty("Message", out var msgEl) ? msgEl.GetString() : null;
261265
if (!string.IsNullOrEmpty(mfaMessage))
262266
{
263267
Log.Debug("MFA prompt: {Message}", mfaMessage);
@@ -286,8 +290,8 @@ private static async Task HandleSecondaryAuthenticationAsync(
286290
var errorMessage = "Secondary authentication failed.";
287291
try
288292
{
289-
var mfaResponse = JObject.Parse(mfaBody);
290-
errorMessage = mfaResponse["Message"]?.ToString() ?? errorMessage;
293+
using var mfaResponse = JsonDocument.Parse(mfaBody);
294+
errorMessage = mfaResponse.RootElement.TryGetProperty("Message", out var mEl) ? mEl.GetString() ?? errorMessage : errorMessage;
291295
}
292296
catch
293297
{
@@ -315,8 +319,8 @@ private static string ExtractAuthorizationCode(string response)
315319
string authorizationCode;
316320
try
317321
{
318-
var jsonObject = JObject.Parse(response);
319-
var relyingPartyUrl = jsonObject["RelyingPartyUrl"]?.ToString();
322+
using var jsonDoc = JsonDocument.Parse(response);
323+
var relyingPartyUrl = jsonDoc.RootElement.TryGetProperty("RelyingPartyUrl", out var rpEl) ? rpEl.GetString() : null;
320324

321325
if (string.IsNullOrEmpty(relyingPartyUrl))
322326
{
@@ -364,11 +368,16 @@ private static async Task<string> ResolveIdentityProviderAsync(
364368
cancellationToken)
365369
.ConfigureAwait(false);
366370

367-
var jProviders = JArray.Parse(response);
368371
var knownScopes = new List<(string RstsProviderId, string Name, string RstsProviderScope)>();
369-
if (jProviders != null)
372+
using (var jProviders = JsonDocument.Parse(response))
370373
{
371-
knownScopes = jProviders.Select(s => (Id: s["RstsProviderId"].ToString(), Name: s["Name"].ToString(), Scope: s["RstsProviderScope"].ToString())).ToList();
374+
foreach (var item in jProviders.RootElement.EnumerateArray())
375+
{
376+
var id = item.TryGetProperty("RstsProviderId", out var idEl) ? idEl.GetString() : null;
377+
var name = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
378+
var providerScope = item.TryGetProperty("RstsProviderScope", out var scopeEl) ? scopeEl.GetString() : null;
379+
knownScopes.Add((id, name, providerScope));
380+
}
372381
}
373382

374383
// try to match what the user typed for provider to an rSTS ID

0 commit comments

Comments
 (0)