Skip to content

Conversation

@mikaelweave
Copy link
Contributor

@mikaelweave mikaelweave commented Dec 3, 2025

Description

Implements RFC 7662 token introspection endpoint at /connect/introspect for SMART on FHIR server with swapple support for the introspection endpoint for alternate SMART configurations.

Key Features:

  • RFC 7662 compliant introspection endpoint supporting both OpenIddict (development) and external IdP tokens
  • Service abstraction pattern (ITokenIntrospectionService) enabling alternate authentication patterns
  • SMART on FHIR v1/v2 claim support (patient, fhirUser, raw_scope)
  • Bearer token authentication required via [Authorize] attribute
  • Integrated with existing audit logging infrastructure
  • Handles issuer variations (with/without trailing slash) for OpenIddict compatibility

Related issues

Addresses AB#174822

Testing

  • Unit Tests (12 tests - all passing):
  • Manual Testing

Test Coverage:

  • Controller layer: Parameter validation, HTTP status codes
  • Service layer: Token parsing, validation, claim extraction
  • Integration: Audit logging, authentication filters

FHIR Team Checklist

  • Update the title of the PR to be succinct and less than 65 characters
  • Add a milestone to the PR for the sprint that it is merged (i.e. add S47)
  • Tag the PR with the type of update: Bug, Build, Dependencies, Enhancement, New-Feature or Documentation
  • Tag the PR with Open source, Azure API for FHIR (CosmosDB or common code) or Azure Healthcare APIs (SQL or common code) to specify where this change is intended to be released.
  • Tag the PR with Schema Version backward compatible or Schema Version backward incompatible or Schema Version unchanged if this adds or updates Sql script which is/is not backward compatible with the code.
  • When changing or adding behavior, if your code modifies the system design or changes design assumptions, please create and include an ADR.
  • CI is green before merge Build Status
  • Review squash-merge requirements

Semver Change (docs)

Patch|Skip|Feature|Breaking (reason)

@mikaelweave mikaelweave changed the title Personal/mikaelw/smart token introspection endpoint SMART on FHIR Token Introspection Endpoint Dec 3, 2025
public void GivenInvalidSignatureToken_WhenIntrospect_ThenReturnsInactive()
{
// Arrange - Create token with different signing key
var differentRsa = RSA.Create(2048);

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning

Disposable 'RSA' is created but not disposed.

Copilot Autofix

AI 1 day ago

To fix the problem, we should ensure that the RSA instance created at line 144 is properly disposed after use. The best approach is to use a using statement (or in modern C# a using declaration) to wrap the lifetime of differentRsa so it will be disposed of automatically after we're finished with it. This requires wrapping the block of code where differentRsa is used (lines 144–162) inside a using block or using declaration. This change will be localized to the test method GivenInvalidSignatureToken_WhenIntrospect_ThenReturnsInactive() and does not affect the functionality of the test.


Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs
--- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/TokenIntrospectionControllerTests.cs
@@ -141,26 +141,36 @@
         public void GivenInvalidSignatureToken_WhenIntrospect_ThenReturnsInactive()
         {
             // Arrange - Create token with different signing key
-            var differentRsa = RSA.Create(2048);
-            var differentKey = new RsaSecurityKey(differentRsa);
-            var differentCredentials = new SigningCredentials(differentKey, SecurityAlgorithms.RsaSha256);
-
-            var tokenHandler = new JwtSecurityTokenHandler();
-            var tokenDescriptor = new SecurityTokenDescriptor
+            using (var differentRsa = RSA.Create(2048))
             {
-                Subject = new ClaimsIdentity(new[]
+                var differentKey = new RsaSecurityKey(differentRsa);
+                var differentCredentials = new SigningCredentials(differentKey, SecurityAlgorithms.RsaSha256);
+
+                var tokenHandler = new JwtSecurityTokenHandler();
+                var tokenDescriptor = new SecurityTokenDescriptor
                 {
-                    new Claim("sub", "test-user"),
-                }),
-                Expires = DateTime.UtcNow.AddHours(1),
-                Issuer = _issuer,
-                Audience = _audience,
-                SigningCredentials = differentCredentials, // Wrong key
-            };
+                    Subject = new ClaimsIdentity(new[]
+                    {
+                        new Claim("sub", "test-user"),
+                    }),
+                    Expires = DateTime.UtcNow.AddHours(1),
+                    Issuer = _issuer,
+                    Audience = _audience,
+                    SigningCredentials = differentCredentials, // Wrong key
+                };
 
-            var token = tokenHandler.CreateToken(tokenDescriptor);
-            var tokenString = tokenHandler.WriteToken(token);
+                var token = tokenHandler.CreateToken(tokenDescriptor);
+                var tokenString = tokenHandler.WriteToken(token);
 
+                // Act
+                var result = _controller.Introspect(tokenString);
+
+                // Assert
+                var okResult = Assert.IsType<OkObjectResult>(result);
+                var response = Assert.IsType<Dictionary<string, object>>(okResult.Value);
+                Assert.True(response.TryGetValue("active", out var active));
+                Assert.False((bool)active);
+            }
             // Act
             var result = _controller.Introspect(tokenString);
 
EOF
@@ -141,26 +141,36 @@
public void GivenInvalidSignatureToken_WhenIntrospect_ThenReturnsInactive()
{
// Arrange - Create token with different signing key
var differentRsa = RSA.Create(2048);
var differentKey = new RsaSecurityKey(differentRsa);
var differentCredentials = new SigningCredentials(differentKey, SecurityAlgorithms.RsaSha256);

var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
using (var differentRsa = RSA.Create(2048))
{
Subject = new ClaimsIdentity(new[]
var differentKey = new RsaSecurityKey(differentRsa);
var differentCredentials = new SigningCredentials(differentKey, SecurityAlgorithms.RsaSha256);

var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
new Claim("sub", "test-user"),
}),
Expires = DateTime.UtcNow.AddHours(1),
Issuer = _issuer,
Audience = _audience,
SigningCredentials = differentCredentials, // Wrong key
};
Subject = new ClaimsIdentity(new[]
{
new Claim("sub", "test-user"),
}),
Expires = DateTime.UtcNow.AddHours(1),
Issuer = _issuer,
Audience = _audience,
SigningCredentials = differentCredentials, // Wrong key
};

var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);

// Act
var result = _controller.Introspect(tokenString);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var response = Assert.IsType<Dictionary<string, object>>(okResult.Value);
Assert.True(response.TryGetValue("active", out var active));
Assert.False((bool)active);
}
// Act
var result = _controller.Introspect(tokenString);

Copilot is powered by AI and may make mistakes. Always verify output.

_securityConfiguration = securityConfiguration.Value;
_logger = logger;
_tokenHandler = new JwtSecurityTokenHandler();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to look at an implementation here using DI and not typing to new

Comment on lines +178 to +181
var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'HttpRequestMessage' is created but not disposed.

Copilot Autofix

AI about 10 hours ago

The best and most reliable way to fix the missing Dispose call on the local HttpRequestMessage is to wrap its construction and usage in a using block. This ensures that, regardless of exceptions or early returns, the object's resources are correctly disposed of. In the context of async code and C# 8.0 and above, using is supported with local variables even when the body of the using includes await expressions.

Within the method GivenMissingTokenParameter_WhenIntrospecting_ThenReturnsBadRequest, modify lines 177-184 to enclose the declaration of request and the following lines which require it within a using statement. This will involve placing a using declaration for the HttpRequestMessage and adjusting the code so the disposable is cleaned up after the async call, but before assertions that access only the response values.

No new imports are needed, and no method signatures need to be changed.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -175,14 +175,22 @@
 
             // Act - Send request without token parameter
             var content = new FormUrlEncodedContent(new Dictionary<string, string>());
-            var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
+            using (var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
             {
                 Content = content,
-            };
-            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
+            })
+            {
+                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
 
-            var introspectionResponse = await _httpClient.SendAsync(request);
+                var introspectionResponse = await _httpClient.SendAsync(request);
 
+                // Assert
+                Assert.Equal(HttpStatusCode.BadRequest, introspectionResponse.StatusCode);
+
+                var responseJson = await introspectionResponse.Content.ReadAsStringAsync();
+                Assert.Contains("token parameter is required", responseJson, StringComparison.OrdinalIgnoreCase);
+            }
+
             // Assert
             Assert.Equal(HttpStatusCode.BadRequest, introspectionResponse.StatusCode);
 
EOF
@@ -175,14 +175,22 @@

// Act - Send request without token parameter
var content = new FormUrlEncodedContent(new Dictionary<string, string>());
var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
using (var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
})
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

var introspectionResponse = await _httpClient.SendAsync(request);
var introspectionResponse = await _httpClient.SendAsync(request);

// Assert
Assert.Equal(HttpStatusCode.BadRequest, introspectionResponse.StatusCode);

var responseJson = await introspectionResponse.Content.ReadAsStringAsync();
Assert.Contains("token parameter is required", responseJson, StringComparison.OrdinalIgnoreCase);
}

// Assert
Assert.Equal(HttpStatusCode.BadRequest, introspectionResponse.StatusCode);

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Comment on lines +205 to +208
var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'HttpRequestMessage' is created but not disposed.
Comment on lines +226 to +229
var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'HttpRequestMessage' is created but not disposed.

Copilot Autofix

AI about 10 hours ago

To fix the issue, we need to ensure that the HttpRequestMessage created on line 226, as well as its associated content (the StringContent object created on line 225), are properly disposed after use. The best and least intrusive way to do this is to wrap the creation and usage of both objects in a using statement. Since the request and content are both disposable and their scope is limited to this method, we can use nested or a single combined using declaration (C# 8.0 and above). This can be achieved by replacing the variable declarations and usage block with a using block that ensures disposal after the request has been sent and the assertion is performed. This approach maintains existing functionality and only affects the resource management pattern within the relevant test method.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -222,17 +222,19 @@
             var accessToken = await GetAccessTokenAsync(TestApplications.GlobalAdminServicePrincipal);
 
             // Act - Send with wrong content type
-            var content = new StringContent("{\"token\": \"test\"}", System.Text.Encoding.UTF8, "application/json");
-            var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
+            using (var content = new StringContent("{\"token\": \"test\"}", System.Text.Encoding.UTF8, "application/json"))
+            using (var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
             {
                 Content = content,
-            };
-            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
+            })
+            {
+                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
 
-            var introspectionResponse = await _httpClient.SendAsync(request);
+                var introspectionResponse = await _httpClient.SendAsync(request);
 
-            // Assert - RFC 7662 requires application/x-www-form-urlencoded
-            Assert.Equal(HttpStatusCode.UnsupportedMediaType, introspectionResponse.StatusCode);
+                // Assert - RFC 7662 requires application/x-www-form-urlencoded
+                Assert.Equal(HttpStatusCode.UnsupportedMediaType, introspectionResponse.StatusCode);
+            }
         }
 
         [Fact]
EOF
@@ -222,17 +222,19 @@
var accessToken = await GetAccessTokenAsync(TestApplications.GlobalAdminServicePrincipal);

// Act - Send with wrong content type
var content = new StringContent("{\"token\": \"test\"}", System.Text.Encoding.UTF8, "application/json");
var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
using (var content = new StringContent("{\"token\": \"test\"}", System.Text.Encoding.UTF8, "application/json"))
using (var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
})
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

var introspectionResponse = await _httpClient.SendAsync(request);
var introspectionResponse = await _httpClient.SendAsync(request);

// Assert - RFC 7662 requires application/x-www-form-urlencoded
Assert.Equal(HttpStatusCode.UnsupportedMediaType, introspectionResponse.StatusCode);
// Assert - RFC 7662 requires application/x-www-form-urlencoded
Assert.Equal(HttpStatusCode.UnsupportedMediaType, introspectionResponse.StatusCode);
}
}

[Fact]
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Comment on lines +269 to +275
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", testApplication.ClientId },
{ "client_secret", testApplication.ClientSecret },
{ "scope", "fhir-api" },
});

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'FormUrlEncodedContent' is created but not disposed.

Copilot Autofix

AI about 10 hours ago

To fix this problem, we need to ensure that the FormUrlEncodedContent instance created in GetAccessTokenAsync is disposed of properly—i.e., its Dispose method is invoked when it is no longer needed. The most concise and idiomatic way to do this is to wrap its usage in a using statement.

Specifically, in GetAccessTokenAsync, wrap the section of code that uses content from its creation at line 269 to the point where it is passed to _httpClient.PostAsync (and ensure that we do not use content beyond that point). Since PostAsync does not require content to be alive after invocation, we can make the using block encompass the call to PostAsync. In async code, this means using the C# 8.0 using declaration or otherwise nesting a regular using block. Since all usages are within the method and are awaited before further usage, this is straightforward.

No new imports or other changes are required beyond converting the code to use a using block for FormUrlEncodedContent inside GetAccessTokenAsync.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -266,21 +266,22 @@
         /// </summary>
         private async Task<string> GetAccessTokenAsync(TestApplication testApplication)
         {
-            var content = new FormUrlEncodedContent(new Dictionary<string, string>
+            using (var content = new FormUrlEncodedContent(new Dictionary<string, string>
             {
                 { "grant_type", "client_credentials" },
                 { "client_id", testApplication.ClientId },
                 { "client_secret", testApplication.ClientSecret },
                 { "scope", "fhir-api" },
-            });
+            }))
+            {
+                var response = await _httpClient.PostAsync(_tokenUri, content);
+                response.EnsureSuccessStatusCode();
 
-            var response = await _httpClient.PostAsync(_tokenUri, content);
-            response.EnsureSuccessStatusCode();
+                var responseJson = await response.Content.ReadAsStringAsync();
+                var tokenResponse = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);
 
-            var responseJson = await response.Content.ReadAsStringAsync();
-            var tokenResponse = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);
-
-            return tokenResponse["access_token"].GetString();
+                return tokenResponse["access_token"].GetString();
+            }
         }
 
         /// <summary>
EOF
@@ -266,21 +266,22 @@
/// </summary>
private async Task<string> GetAccessTokenAsync(TestApplication testApplication)
{
var content = new FormUrlEncodedContent(new Dictionary<string, string>
using (var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", testApplication.ClientId },
{ "client_secret", testApplication.ClientSecret },
{ "scope", "fhir-api" },
});
}))
{
var response = await _httpClient.PostAsync(_tokenUri, content);
response.EnsureSuccessStatusCode();

var response = await _httpClient.PostAsync(_tokenUri, content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);

var responseJson = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);

return tokenResponse["access_token"].GetString();
return tokenResponse["access_token"].GetString();
}
}

/// <summary>
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Comment on lines +296 to +299
var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'HttpRequestMessage' is created but not disposed.

Copilot Autofix

AI about 10 hours ago

The best way to fix this issue is to wrap the creation and use of the HttpRequestMessage object in a using statement. This ensures that it will be disposed of as soon as it is no longer needed, even if exceptions occur. In this context, since the code creates the request, sends it via HttpClient.SendAsync, and returns the corresponding HttpResponseMessage, we must ensure that disposal happens after the send. However, since the method returns the HttpResponseMessage, not the request, using an async using block will suffice.

To implement the fix, in IntrospectTokenAsync, lines 295–302, replace the direct creation and sending of the request with a using block. This change does not require new imports, as using and IDisposable are built-in to .NET.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -293,13 +293,14 @@
                 { "token", tokenToIntrospect },
             });
 
-            var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
+            await using (var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
             {
                 Content = content,
-            };
-            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authToken);
-
-            return await _httpClient.SendAsync(request);
+            })
+            {
+                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authToken);
+                return await _httpClient.SendAsync(request);
+            }
         }
     }
 }
EOF
@@ -293,13 +293,14 @@
{ "token", tokenToIntrospect },
});

var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
await using (var request = new HttpRequestMessage(HttpMethod.Post, _introspectionUri)
{
Content = content,
};
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authToken);

return await _httpClient.SendAsync(request);
})
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authToken);
return await _httpClient.SendAsync(request);
}
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Assert.True(response.ContainsKey("sub"));
Assert.True(response.ContainsKey("iss"));
Assert.True(response.ContainsKey("aud"));
Assert.True(response.ContainsKey("exp"));

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note test

Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI about 10 hours ago

The best way to fix this issue is to replace the check for response.ContainsKey("exp") and subsequent access to response["exp"] with a single call to response.TryGetValue("exp", out JsonElement expElement). This only performs one dictionary lookup and is more efficient. The assertion should then use expElement.GetInt64(). The change should be made in the method GivenValidToken_WhenIntrospecting_ThenReturnsActiveWithStandardClaims in lines 63 and 67 of test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs. You may need to add a local variable for expElement. No new imports or external methods are required.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -64,7 +64,7 @@
             Assert.True(response.ContainsKey("client_id"));
 
             // Verify timestamps are Unix timestamps (positive numbers)
-            Assert.True(response["exp"].GetInt64() > 0);
+            Assert.True(response.TryGetValue("exp", out var expElement) && expElement.GetInt64() > 0);
             if (response.ContainsKey("iat"))
             {
                 Assert.True(response["iat"].GetInt64() > 0);
EOF
@@ -64,7 +64,7 @@
Assert.True(response.ContainsKey("client_id"));

// Verify timestamps are Unix timestamps (positive numbers)
Assert.True(response["exp"].GetInt64() > 0);
Assert.True(response.TryGetValue("exp", out var expElement) && expElement.GetInt64() > 0);
if (response.ContainsKey("iat"))
{
Assert.True(response["iat"].GetInt64() > 0);
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated

// Verify timestamps are Unix timestamps (positive numbers)
Assert.True(response["exp"].GetInt64() > 0);
if (response.ContainsKey("iat"))

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note test

Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI about 10 hours ago

To fix this performance issue, replace the existing check:

if (response.ContainsKey("iat"))
{
    Assert.True(response["iat"].GetInt64() > 0);
}

with a TryGetValue-based idiom, which combines both the existence check and the access into a single operation. Specifically, declare a variable for the out value, use response.TryGetValue("iat", out var iatValue), and in the body use iatValue.GetInt64(). This change should be made directly in the test method, at the exact code location, keeping naming conventions and context identical.

No additional imports or helper methods are necessary, as TryGetValue is a method on Dictionary<TKey, TValue>.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -65,9 +65,9 @@
 
             // Verify timestamps are Unix timestamps (positive numbers)
             Assert.True(response["exp"].GetInt64() > 0);
-            if (response.ContainsKey("iat"))
+            if (response.TryGetValue("iat", out var iatValue))
             {
-                Assert.True(response["iat"].GetInt64() > 0);
+                Assert.True(iatValue.GetInt64() > 0);
             }
         }
 
EOF
@@ -65,9 +65,9 @@

// Verify timestamps are Unix timestamps (positive numbers)
Assert.True(response["exp"].GetInt64() > 0);
if (response.ContainsKey("iat"))
if (response.TryGetValue("iat", out var iatValue))
{
Assert.True(response["iat"].GetInt64() > 0);
Assert.True(iatValue.GetInt64() > 0);
}
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Assert.True(response["active"].GetBoolean());

// Scope should be a space-separated string, not an array
if (response.ContainsKey("scope"))

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note test

Inefficient use of 'ContainsKey' and
indexer
.
Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI about 10 hours ago

To fix the problem, replace the use of response.ContainsKey("scope") followed by repeated dictionary lookups with a single call to response.TryGetValue("scope", out var scopeElement). Specifically, in the affected test method, replace the if block:

if (response.ContainsKey("scope"))
{
    Assert.Equal(JsonValueKind.String, response["scope"].ValueKind);
    var scope = response["scope"].GetString();
    Assert.NotEmpty(scope);
}

with:

if (response.TryGetValue("scope", out var scopeElement))
{
    Assert.Equal(JsonValueKind.String, scopeElement.ValueKind);
    var scope = scopeElement.GetString();
    Assert.NotEmpty(scope);
}

This change ensures that the key existence check and retrieval are combined efficiently, without changing functionality. No new methods or imports are necessary, as all required functionality is part of the standard library.

Make the edit only in the relevant section of the file test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs, and ensure any usages of response["scope"] in this scope are changed to use the local variable for correctness and efficiency.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -87,10 +87,10 @@
             Assert.True(response["active"].GetBoolean());
 
             // Scope should be a space-separated string, not an array
-            if (response.ContainsKey("scope"))
+            if (response.TryGetValue("scope", out var scopeElement))
             {
-                Assert.Equal(JsonValueKind.String, response["scope"].ValueKind);
-                var scope = response["scope"].GetString();
+                Assert.Equal(JsonValueKind.String, scopeElement.ValueKind);
+                var scope = scopeElement.GetString();
                 Assert.NotEmpty(scope);
             }
         }
EOF
@@ -87,10 +87,10 @@
Assert.True(response["active"].GetBoolean());

// Scope should be a space-separated string, not an array
if (response.ContainsKey("scope"))
if (response.TryGetValue("scope", out var scopeElement))
{
Assert.Equal(JsonValueKind.String, response["scope"].ValueKind);
var scope = response["scope"].GetString();
Assert.Equal(JsonValueKind.String, scopeElement.ValueKind);
var scope = scopeElement.GetString();
Assert.NotEmpty(scope);
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Assert.True(response["active"].GetBoolean());

// SMART clients should have fhirUser claim
if (response.ContainsKey("fhirUser"))

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note test

Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI about 10 hours ago

To fix the problem, on lines 114-116 in TokenIntrospectionTests.cs, replace the use of ContainsKey followed by accessing via the indexer with a single call to TryGetValue. This involves declaring a local JsonElement variable to store the value, and then using it if the key existed. The scope of the fix only encompasses these lines, and no additional imports or helper methods are needed.


Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -111,9 +111,9 @@
             Assert.True(response["active"].GetBoolean());
 
             // SMART clients should have fhirUser claim
-            if (response.ContainsKey("fhirUser"))
+            if (response.TryGetValue("fhirUser", out var fhirUserElement))
             {
-                var fhirUser = response["fhirUser"].GetString();
+                var fhirUser = fhirUserElement.GetString();
                 Assert.NotEmpty(fhirUser);
 
                 // fhirUser should be a full URL to a Practitioner, Patient, Person, or RelatedPerson
EOF
@@ -111,9 +111,9 @@
Assert.True(response["active"].GetBoolean());

// SMART clients should have fhirUser claim
if (response.ContainsKey("fhirUser"))
if (response.TryGetValue("fhirUser", out var fhirUserElement))
{
var fhirUser = response["fhirUser"].GetString();
var fhirUser = fhirUserElement.GetString();
Assert.NotEmpty(fhirUser);

// fhirUser should be a full URL to a Practitioner, Patient, Person, or RelatedPerson
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
var response = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);

// Verify inactive status
Assert.True(response.ContainsKey("active"));

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note test

Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI about 10 hours ago

To fix the inefficiency, we replace the two operations on the dictionary (response.ContainsKey("active") and then accessing the value via response["active"]) in the test method with a single TryGetValue call. Specifically, we should use:

Assert.True(response.TryGetValue("active", out JsonElement activeValue));
Assert.False(activeValue.GetBoolean());

This maintains the two assertions, but avoids the double lookup, combining them for efficiency. No other changes to functionality are needed. The only change is in the assertion region (lines 141 and 142) of the file test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs. All necessary types are already imported, so no imports or additional methods are required.

Suggested changeset 1
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
--- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
+++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/TokenIntrospectionTests.cs
@@ -138,8 +138,8 @@
             var response = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);
 
             // Verify inactive status
-            Assert.True(response.ContainsKey("active"));
-            Assert.False(response["active"].GetBoolean());
+            Assert.True(response.TryGetValue("active", out JsonElement activeValue));
+            Assert.False(activeValue.GetBoolean());
 
             // Per RFC 7662 section 2.2: "If the introspection call is properly authorized
             // but the token is not active, the authorization server MUST return ... {"active": false}"
EOF
@@ -138,8 +138,8 @@
var response = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseJson);

// Verify inactive status
Assert.True(response.ContainsKey("active"));
Assert.False(response["active"].GetBoolean());
Assert.True(response.TryGetValue("active", out JsonElement activeValue));
Assert.False(activeValue.GetBoolean());

// Per RFC 7662 section 2.2: "If the introspection call is properly authorized
// but the token is not active, the authorization server MUST return ... {"active": false}"
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants