Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dialogtoken validator return claims #1931

Closed
wants to merge 7 commits into from

Conversation

Fargekritt
Copy link
Contributor

Description

Related Issue(s)

  • #{issue number}

Verification

  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)

Documentation

  • Documentation is updated (either in docs-directory, Altinnpedia or a separate linked PR in altinn-studio-docs., if applicable)

Copy link
Contributor

coderabbitai bot commented Feb 21, 2025

📝 Walkthrough

Walkthrough

The changes update the Dialogporten WebApiClient by modifying the dialog management and token validation components. The API now documents a new HTTP 410 status for removed dialogs and adds optional non-sensitive properties to several dialog content classes. In token validation, the interfaces and classes have been enhanced with a new Claims property and updated annotations to reflect non-null claims when valid. Additionally, the token decoding logic now uses a new JsonTokenParts structure, and corresponding tests assert the presence of claims for valid tokens.

Changes

Files Change Summary
src/.../RefitterInterface.cs Updated the IServiceownerApi interface to include a 410 status code and added optional properties (NonSensitiveTitle, NonSensitiveSummary) with XML documentation to dialog-related content classes.
src/.../IDialogTokenValidator.cs,
src/.../DefaultValidationResult.cs
Enhanced validation interfaces and classes by adding a Claims property and applying the [MemberNotNullWhen] attribute on the IsValid property for better claim handling.
src/.../DialogTokenValidator.cs Revised token validation methods to use the new JsonTokenParts structure, extract claims from the token, and update signature and expiration verification methods.
tests/.../DialogTokenValidatorTests.cs Added an assertion in the valid token test to verify that the Claims property is not null.

Possibly related PRs

Suggested reviewers

  • oskogstad
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Fargekritt Fargekritt marked this pull request as ready for review February 21, 2025 11:40
@Fargekritt Fargekritt requested a review from a team as a code owner February 21, 2025 11:40
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs (3)

51-65: Handle missing or malformed properties gracefully.
Using body.GetProperty(...) may throw if the properties are missing or malformed in the token. For robustness, consider using TryGetProperty(...) and validating any conversions (e.g., the expected numeric fields).

- new(DialogTokenClaimTypes.C, body.GetProperty(DialogTokenClaimTypes.C).GetString() ?? string.Empty),
+ if (body.TryGetProperty(DialogTokenClaimTypes.C, out var cProp) && cProp.ValueKind == JsonValueKind.String)
+ {
+     claims.Add(new Claim(DialogTokenClaimTypes.C, cProp.GetString() ?? string.Empty));
+ }
+ else
+ {
+     // Optionally, add validation error or default claim
+ }

186-195: Expiration checks are straightforward, but consider additional grace handling.
This is correct for basic expiration checks. Some systems might require clock skew allowance. If needed, you could incorporate a small margin for differences in server time.


277-289: Claim type constants are self-documenting.
These short property names map well to your token’s structure. No concerns here, though more descriptive naming might assist unfamiliar developers.

tests/Digdir.Library.Dialogporten.WebApiClient.Unit.Tests/DialogTokenValidatorTests.cs (2)

36-37: Enhance claims validation assertions.

While the new assertion verifies that claims are present, consider these improvements:

  1. Use Assert.NotNull(result.Claims) for more specific assertion
  2. Add assertions to verify the expected claims from the token (e.g., 'jti', 'c', 'l', etc.)
 Assert.True(result.IsValid);
-Assert.True(result.Claims != null);
+Assert.NotNull(result.Claims);
+Assert.Contains(result.Claims.Claims, c => c.Type == "jti" && c.Value == "38cfdcb9-388b-47b8-a1bf-9f15b2819894");
+Assert.Contains(result.Claims.Claims, c => c.Type == "c" && c.Value == "urn:altinn:person:identifier-no:14886498226");
+Assert.Contains(result.Claims.Claims, c => c.Type == "l" && c.Value == "3");

52-226: Add Claims assertions to invalid token scenarios.

For completeness, consider adding assertions to verify that Claims is null when the token is invalid. This aligns with the [MemberNotNullWhen(true, nameof(Claims))] attribute contract.

Add the following assertion to each test method that expects invalid tokens:

 Assert.False(result.IsValid);
 Assert.True(result.Errors.ContainsKey("token"));
 Assert.Contains("<error_message>", result.Errors["token"]);
+Assert.Null(result.Claims);
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f3607c and c147d96.

📒 Files selected for processing (5)
  • src/Digdir.Library.Dialogporten.WebApiClient/Features/V1/RefitterInterface.cs (6 hunks)
  • src/Digdir.Library.Dialogporten.WebApiClient/IDialogTokenValidator.cs (2 hunks)
  • src/Digdir.Library.Dialogporten.WebApiClient/Services/DefaultValidationResult.cs (1 hunks)
  • src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs (6 hunks)
  • tests/Digdir.Library.Dialogporten.WebApiClient.Unit.Tests/DialogTokenValidatorTests.cs (1 hunks)
🔇 Additional comments (15)
src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs (7)

5-6: Imports look appropriate.
No issues found with adding System.Globalization and System.Security.Claims references.


46-49: Early exit on invalid state.
Returning immediately if the validation result is already marked invalid helps avoid unnecessary processing. This is a good guard clause.


69-92: Decoding token into JSON is well-structured.
The TryDecodeToken approach and usage of JsonTokenParts neatly separate concerns for decoding and parsing. Good job returning early if any step fails.


93-100: JSON parsing and exception handling look fine.
Catching the JsonException and returning false is a clean way to handle malformed JSON.


157-159: Extended signature-verification contract.
Adjusting VerifySignature to accept JsonTokenParts for header/body access is consistent with the new structure. Implementation remains straightforward.


221-232: Kid property retrieval with [NotNullWhen(true)].
Leverages modern C# nullability features well. The property-based approach is clear, though you might consider adding explicit error context if the kid is missing or unrecognized.


259-275: Ref struct usage for JSON parsing is efficient.
The immutable JsonTokenParts structure cleanly bundles the header, body, and signature. This is a good design for performance and clarity.

src/Digdir.Library.Dialogporten.WebApiClient/IDialogTokenValidator.cs (2)

1-3: New imports align with claims-based logic.
References to System.Diagnostics.CodeAnalysis and System.Security.Claims are consistent with the updated token validation approach.


14-15: IsValid and Claims relationship is well defined.
Using [MemberNotNullWhen(true, nameof(Claims))] clarifies that Claims is guaranteed to be non-null when the validation has succeeded. This is a nice usage of compiler annotations.

src/Digdir.Library.Dialogporten.WebApiClient/Services/DefaultValidationResult.cs (2)

1-2: Imports align with the new claims property.
Pulling in System.Security.Claims and System.Diagnostics.CodeAnalysis for the annotation is consistent.


9-10: Implementation of IsValid and Claims.
The addition of [MemberNotNullWhen(true, nameof(Claims))] and a ClaimsPrincipal? property ensures consistency with the contract in IValidationResult, simplifying usage in the validator logic.

src/Digdir.Library.Dialogporten.WebApiClient/Features/V1/RefitterInterface.cs (4)

541-543: LGTM! Documentation for HTTP 410 status code is clear and consistent.

The added documentation for HTTP 410 status code properly indicates that the entity with the given key(s) is removed, which aligns with REST API best practices for handling deleted resources.


1729-1751: LGTM! Well-designed security enhancement for handling sensitive content.

The addition of NonSensitiveTitle and NonSensitiveSummary properties provides a robust mechanism for displaying appropriate content based on the user's eIDAS authentication level. This is a good security practice that helps prevent accidental exposure of sensitive information.


2600-2622: LGTM! Consistent implementation of non-sensitive content properties.

The implementation of NonSensitiveTitle and NonSensitiveSummary in the search content model maintains consistency with the update content model, ensuring a uniform approach to handling sensitive content across different operations.


2910-2932: LGTM! Completes the consistent implementation of non-sensitive content properties.

The implementation of NonSensitiveTitle and NonSensitiveSummary in the get content model maintains consistency with other content models, ensuring a uniform approach to handling sensitive content across all dialog operations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs (5)

95-106: Add logging for JSON parsing failures.

Silent exception handling might mask specific issues during JSON parsing. Consider adding logging to help with debugging.

+    private readonly ILogger<DialogTokenValidator> _logger;
+
+    public DialogTokenValidator(
+        IEdDsaSecurityKeysCache publicKeysCache, 
+        IClock clock,
+        ILogger<DialogTokenValidator> logger)
+    {
+        _publicKeysCache = publicKeysCache;
+        _clock = clock;
+        _logger = logger;
+    }

     private static bool TryParseJson(ReadOnlySpan<byte> span, out JsonElement jsonElement)
     {
         jsonElement = default;
         var reader = new Utf8JsonReader(span);
         try
         {
             jsonElement = JsonElement.ParseValue(ref reader);
             return true;
         }
-        catch (JsonException) { }
+        catch (JsonException ex)
+        {
+            _logger.LogDebug(ex, "Failed to parse JSON token part");
+        }
         return false;
     }

158-185: Improve error handling and logging in signature verification.

Consider these improvements:

  1. Move the large error message to a constant
  2. Add logging for signature verification failures
+    private const string NO_PUBLIC_KEYS_ERROR = 
+        "No public keys available. Most likely due to an error when fetching the " +
+        "public keys from the dialogporten well-known endpoint. Please check the " +
+        "logs for more information. Alternatively, set " +
+        "DialogportenSettings.ThrowOnPublicKeyFetchInit=true to ensure public " +
+        "keys are fetched before starting up the application.";

     private bool VerifySignature(
         JwksTokenParts<char> tokenParts,
         JsonTokenParts decodedTokenParts)
     {
         var publicKeys = _publicKeysCache.PublicKeys;
         if (publicKeys.Count == 0)
         {
-            throw new InvalidOperationException(
-                "No public keys available. Most likely due to an error when fetching the " +
-                "public keys from the dialogporten well-known endpoint. Please check the " +
-                "logs for more information. Alternatively, set " +
-                "DialogportenSettings.ThrowOnPublicKeyFetchInit=true to ensure public " +
-                "keys are fetched before starting up the application.");
+            throw new InvalidOperationException(NO_PUBLIC_KEYS_ERROR);
         }

         // ... existing code ...

-        return TryGetPublicKey(publicKeys, decodedTokenParts.Header, out var publicKey)
-               && SignatureAlgorithm.Ed25519.Verify(publicKey, signedPart, decodedTokenParts.Signature);
+        if (!TryGetPublicKey(publicKeys, decodedTokenParts.Header, out var publicKey))
+        {
+            _logger.LogWarning("Failed to find matching public key for token");
+            return false;
+        }
+
+        var isValid = SignatureAlgorithm.Ed25519.Verify(
+            publicKey, signedPart, decodedTokenParts.Signature);
+
+        if (!isValid)
+        {
+            _logger.LogWarning("Token signature verification failed");
+        }
+
+        return isValid;

187-202: Use constant from DialogTokenClaimTypes for consistency.

Replace the magic string "exp" with the constant from DialogTokenClaimTypes for better maintainability.

     private bool VerifyExpiration(JsonTokenParts decodedTokenParts)
     {
-        const string expiresPropertyName = "exp";
-        if (!decodedTokenParts.Body.TryGetProperty(expiresPropertyName, out var expiresElement))
+        if (!decodedTokenParts.Body.TryGetProperty(DialogTokenClaimTypes.Expire, out var expiresElement))
         {
             return false;
         }

222-239: Improve public key lookup implementation.

Consider these improvements:

  1. Move the magic string "kid" to a constant
  2. Use LINQ for more concise key lookup
+    private const string KID_PROPERTY_NAME = "kid";
+
     private static bool TryGetPublicKey(
         ReadOnlyCollection<PublicKeyPair> keyPairs,
         JsonElement header,
         [NotNullWhen(true)] out PublicKey? publicKey)
     {
-        const string kidPropertyName = "kid";
         publicKey = null;
-        if (!header.TryGetProperty(kidPropertyName, out var tokenKid))
+        if (!header.TryGetProperty(KID_PROPERTY_NAME, out var tokenKid))
         {
             return false;
         }
 
-        foreach (var (kid, key) in keyPairs)
-        {
-            if (!kid.AsSpan().SequenceEqual(tokenKid.GetString())) continue;
-            publicKey = key;
-            return true;
-        }
+        var tokenKidString = tokenKid.GetString();
+        var keyPair = keyPairs.FirstOrDefault(
+            pair => pair.Kid.AsSpan().SequenceEqual(tokenKidString));
+
+        publicKey = keyPair?.Key;
+        return publicKey != null;

278-290: Add XML documentation for better IntelliSense support.

Consider adding XML documentation to describe each claim type's purpose and format.

     internal static class DialogTokenClaimTypes
     {
+        /// <summary>
+        /// The 'c' claim represents the client identifier.
+        /// </summary>
         public const string C = "c";
 
+        /// <summary>
+        /// The 'l' claim represents the security level.
+        /// </summary>
         public const string Level = "l";
 
         // Add similar documentation for other claims
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c147d96 and 1ecf9b5.

📒 Files selected for processing (1)
  • src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs (6 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Dry run deploy apps / Deploy job sync-resource-policy-information-job to test
  • GitHub Check: Dry run deploy apps / Deploy job sync-subject-resource-mappings-job to test
  • GitHub Check: build / build-and-test
🔇 Additional comments (2)
src/Digdir.Library.Dialogporten.WebApiClient/Services/DialogTokenValidator.cs (2)

108-156: Implementation looks good!

The token parts handling implementation is robust with proper boundary checks, clear error handling, and efficient buffer usage.


241-275: Well-designed data structures!

The implementation of both JwksTokenParts<T> and JsonTokenParts structs is efficient and well-designed:

  • Use of readonly ref structs for performance
  • Clear property definitions
  • Proper constructor implementation

Comment on lines +51 to +66
var body = decodedTokenParts.Body;
validationResult.Claims = new ClaimsPrincipal(
new ClaimsIdentity(
new List<Claim>
{
new(DialogTokenClaimTypes.C, body.GetProperty(DialogTokenClaimTypes.C).GetString() ?? string.Empty),
new(DialogTokenClaimTypes.Level, body.GetProperty(DialogTokenClaimTypes.Level).GetInt64().ToString(CultureInfo.InvariantCulture)),
new(DialogTokenClaimTypes.Party, body.GetProperty(DialogTokenClaimTypes.Party).GetString() ?? string.Empty),
new(DialogTokenClaimTypes.Scope, body.GetProperty(DialogTokenClaimTypes.Scope).GetString() ?? string.Empty),
new(DialogTokenClaimTypes.DialogId, body.GetProperty(DialogTokenClaimTypes.DialogId).GetString() ?? string.Empty),
new(DialogTokenClaimTypes.A, body.GetProperty(DialogTokenClaimTypes.A).GetString() ?? string.Empty),
new(DialogTokenClaimTypes.Issuer, body.GetProperty(DialogTokenClaimTypes.Issuer).GetString() ?? string.Empty),
new(DialogTokenClaimTypes.Expire, body.GetProperty(DialogTokenClaimTypes.Expire).GetInt64().ToString(CultureInfo.InvariantCulture)),
new(DialogTokenClaimTypes.IssuedAt, body.GetProperty(DialogTokenClaimTypes.IssuedAt).GetInt64().ToString(CultureInfo.InvariantCulture)),
new(DialogTokenClaimTypes.NotVisibleBefore, body.GetProperty(DialogTokenClaimTypes.NotVisibleBefore).GetInt64().ToString(CultureInfo.InvariantCulture))
}, "DialogToken"));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null checks and validation for required claims.

The current implementation has potential issues:

  1. Missing null checks for required JSON properties could lead to runtime exceptions
  2. Using empty string fallback for missing claims might mask validation issues
  3. No validation of claim values

Consider this safer implementation:

         var body = decodedTokenParts.Body;
+        // Validate required properties exist
+        var requiredProperties = new[] 
+        { 
+            DialogTokenClaimTypes.C,
+            DialogTokenClaimTypes.Level,
+            DialogTokenClaimTypes.Party,
+            // Add other required properties
+        };
+        
+        foreach (var prop in requiredProperties)
+        {
+            if (!body.TryGetProperty(prop, out var _))
+            {
+                validationResult.AddError("token", $"Missing required claim: {prop}");
+                return validationResult;
+            }
+        }
+
         validationResult.Claims = new ClaimsPrincipal(
             new ClaimsIdentity(
                 new List<Claim>
                 {
-                    new(DialogTokenClaimTypes.C, body.GetProperty(DialogTokenClaimTypes.C).GetString() ?? string.Empty),
+                    new(DialogTokenClaimTypes.C, body.GetProperty(DialogTokenClaimTypes.C).GetString() 
+                        ?? throw new JsonException($"Required claim {DialogTokenClaimTypes.C} is null")),
                     // Apply similar changes to other required claims

@oskogstad
Copy link
Collaborator

Dupe of #1933 ?

@Fargekritt Fargekritt closed this Feb 21, 2025
@Fargekritt Fargekritt deleted the feat/dialogtoken-validator-return-claims branch February 24, 2025 08:36
This was referenced Feb 26, 2025
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