Skip to content

Feature/wal 670#1566

Open
philpotisk wants to merge 4 commits intomainfrom
feature/wal-670
Open

Feature/wal 670#1566
philpotisk wants to merge 4 commits intomainfrom
feature/wal-670

Conversation

@philpotisk
Copy link
Contributor

@philpotisk philpotisk commented Feb 25, 2026

  • Documentation

    • Added implementation tracking document for OIDC external role extraction feature.
    • Updated OIDC flow configuration documentation with new externalRoleExtraction option.
  • New Features

    • OIDC now supports optional external role extraction from ID token claims (realm and client roles).
  • Tests

    • Added comprehensive test coverage for external role extraction scenarios and edge cases.

@linear
Copy link

linear bot commented Feb 25, 2026

@philpotisk philpotisk requested a review from waltkb February 25, 2026 19:53
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This pull request implements external role extraction from OIDC ID tokens for the WAL-670 feature. It introduces configuration options, extraction logic that processes role claims from ID token payloads, updated session data structures to store extracted roles, comprehensive tests, and documentation describing the new capability.

Changes

Cohort / File(s) Summary
Core Feature Implementation
src/main/kotlin/id/walt/ktorauthnz/methods/OIDC.kt, src/main/kotlin/id/walt/ktorauthnz/methods/OidcExternalRoleExtractor.kt
New OidcExternalRoleExtractor singleton extracts realm and client roles from ID token claims. OIDC method now calls extractor and propagates results into session data. Handles disabled extraction gracefully as no-op.
Configuration & Data Structures
src/main/kotlin/id/walt/ktorauthnz/methods/config/OidcAuthConfiguration.kt, src/main/kotlin/id/walt/ktorauthnz/methods/sessiondata/OidcSessionData.kt
Introduces OidcExternalRoleExtractionConfiguration with enable flag, claim paths, and optional clientId override. Adds OidcExternalRoles data class. OidcAuthConfiguration and OidcSessionAuthenticatedData updated with new properties.
Tests
src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt
Comprehensive test coverage for extraction disabled state, realm and client role extraction with default and custom configurations, claim path resolution, clientId filtering, and edge cases with missing or malformed claims.
Documentation
docs/oidc.md, docs/WAL-670-IMPLEMENTATION.md
Updated OIDC documentation with externalRoleExtraction configuration block and field descriptions. New implementation tracker documenting scope, progress, and test baseline.
🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description lacks structured format required by the template and is missing key sections like Type of Change checkboxes and Checklist items. Reformat the description to follow the template structure, including Type of Change selection (bug fix/new feature), and complete all checklist items (code cleanup, test coverage, documentation).
Title check ❓ Inconclusive The title 'Feature/wal 670' is vague and lacks specificity; it only references a ticket number without conveying the actual feature or change being implemented. Use a more descriptive title that summarizes the main change, such as 'Add OIDC external role extraction to session metadata' or 'Implement WAL-670: OIDC external role mapping support.'

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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 (1)
waltid-libraries/auth/waltid-ktor-authnz/src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt (1)

109-128: Add one edge-case test for non-object resource_access / client entries.

Consider extending malformed-claims coverage to include cases where resource_access (or a client entry under it) is not a JSON object, to prevent regressions around claim-shape hardening.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@waltid-libraries/auth/waltid-ktor-authnz/src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt`
around lines 109 - 128, Add a second unit test alongside `gracefully handles
missing or malformed role claims` that constructs a payload where
`resource_access` is a non-object (e.g., a string) and another variant where the
client entry under `resource_access` (e.g., `"waltid_ktor_authnz"`) is a
non-object, then call OidcExternalRoleExtractor.extract with that payload (using
the same `baseConfig` with `externalRoleExtraction.enabled = true`) and assert
the extractor returns non-null result with empty realmRoles and empty
clientRoles; this ensures the extractor handles non-object `resource_access` and
client entries without throwing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/OidcExternalRoleExtractor.kt`:
- Around line 29-35: The code assumes JsonElement.jsonObject will always
succeed; guard against non-object top-level and nested claim values by using
safe casts and type checks: replace direct use of resolvePath(...)? .jsonObject
with a safe cast like (resolvePath(idTokenPayload,
extractionConfig.clientRolesClaimPath) as? JsonObject) and in the allClientRoles
mapping ensure each mapped value is treated as a JsonObject (e.g., cast value
as? JsonObject) before accessing ["roles"], then call .asStringSet().orEmpty()
only when the roles element is present and of the expected type; update
references to clientRolesRoot, resolvePath,
extractionConfig.clientRolesClaimPath, and allClientRoles accordingly and add a
unit test where the top-level claim (e.g., resource_access) is a string to
verify graceful degradation.

---

Nitpick comments:
In
`@waltid-libraries/auth/waltid-ktor-authnz/src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt`:
- Around line 109-128: Add a second unit test alongside `gracefully handles
missing or malformed role claims` that constructs a payload where
`resource_access` is a non-object (e.g., a string) and another variant where the
client entry under `resource_access` (e.g., `"waltid_ktor_authnz"`) is a
non-object, then call OidcExternalRoleExtractor.extract with that payload (using
the same `baseConfig` with `externalRoleExtraction.enabled = true`) and assert
the extractor returns non-null result with empty realmRoles and empty
clientRoles; this ensures the extractor handles non-object `resource_access` and
client entries without throwing.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4bad788 and 538ba30.

📒 Files selected for processing (7)
  • waltid-libraries/auth/waltid-ktor-authnz/docs/WAL-670-IMPLEMENTATION.md
  • waltid-libraries/auth/waltid-ktor-authnz/docs/oidc.md
  • waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/OIDC.kt
  • waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/OidcExternalRoleExtractor.kt
  • waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/config/OidcAuthConfiguration.kt
  • waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/sessiondata/OidcSessionData.kt
  • waltid-libraries/auth/waltid-ktor-authnz/src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt

Comment on lines +29 to +35
val clientRolesRoot = resolvePath(idTokenPayload, extractionConfig.clientRolesClaimPath)
?.jsonObject

val allClientRoles = clientRolesRoot
?.mapValues { (_, value) ->
value.jsonObject["roles"]?.asStringSet().orEmpty()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Examining OidcExternalRoleExtractor.kt ==="
cat -n waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/OidcExternalRoleExtractor.kt | head -60

echo
echo "=== Checking for jsonObject usage in the file ==="
rg -n 'jsonObject' waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/OidcExternalRoleExtractor.kt -C3

echo
echo "=== Checking test file for malformed claim tests ==="
if [ -f waltid-libraries/auth/waltid-ktor-authnz/src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt ]; then
  cat -n waltid-libraries/auth/waltid-ktor-authnz/src/test/kotlin/id/walt/OidcExternalRoleExtractorTest.kt
else
  echo "Test file not found, searching for test files..."
  find . -name "*OidcExternalRoleExtractor*Test*" -type f 2>/dev/null | head -10
fi

Repository: walt-id/waltid-identity

Length of output: 8964


🌐 Web query:

JsonElement jsonObject property kotlinx.serialization throws

💡 Result:

In kotlinx.serialization.json, jsonObject is a convenience cast:

val obj: JsonObject = element.jsonObject

It throws IllegalArgumentException when the JsonElement is not actually a JsonObject (e.g., it’s a JsonPrimitive, JsonArray, or JsonNull). [1]

Safer patterns:

val objOrNull = element as? JsonObject
// or
val objOrNull = element.takeIf { it is JsonObject }?.jsonObject

If you share the exact exception text + the JSON snippet (or the JsonElement you’re accessing), I can point to the specific non-object value that’s triggering it.

Sources
[1] Kotlinx Serialization API docs: JsonElement.jsonObject (throws if not a JsonObject) https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/json-object.html


Guard jsonObject access to prevent auth-flow breakage on malformed claims.

Direct jsonObject property access throws IllegalArgumentException when claim values are not JSON objects. The current test only covers malformed nested claims (roles as string when array expected), but not malformed top-level claims (where resource_access itself is a string instead of an object). This leaves the code vulnerable to crashes when processing JWTs with non-object top-level claim values.

💡 Proposed fix
-        val clientRolesRoot = resolvePath(idTokenPayload, extractionConfig.clientRolesClaimPath)
-            ?.jsonObject
+        val clientRolesRoot = (resolvePath(idTokenPayload, extractionConfig.clientRolesClaimPath) as? JsonObject)

         val allClientRoles = clientRolesRoot
             ?.mapValues { (_, value) ->
-                value.jsonObject["roles"]?.asStringSet().orEmpty()
+                (value as? JsonObject)?.get("roles")?.asStringSet().orEmpty()
             }
             .orEmpty()
             .filterValues { it.isNotEmpty() }

Consider also expanding the test to include a case where resource_access is a string rather than an object to ensure graceful degradation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val clientRolesRoot = resolvePath(idTokenPayload, extractionConfig.clientRolesClaimPath)
?.jsonObject
val allClientRoles = clientRolesRoot
?.mapValues { (_, value) ->
value.jsonObject["roles"]?.asStringSet().orEmpty()
}
val clientRolesRoot = (resolvePath(idTokenPayload, extractionConfig.clientRolesClaimPath) as? JsonObject)
val allClientRoles = clientRolesRoot
?.mapValues { (_, value) ->
(value as? JsonObject)?.get("roles")?.asStringSet().orEmpty()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@waltid-libraries/auth/waltid-ktor-authnz/src/main/kotlin/id/walt/ktorauthnz/methods/OidcExternalRoleExtractor.kt`
around lines 29 - 35, The code assumes JsonElement.jsonObject will always
succeed; guard against non-object top-level and nested claim values by using
safe casts and type checks: replace direct use of resolvePath(...)? .jsonObject
with a safe cast like (resolvePath(idTokenPayload,
extractionConfig.clientRolesClaimPath) as? JsonObject) and in the allClientRoles
mapping ensure each mapped value is treated as a JsonObject (e.g., cast value
as? JsonObject) before accessing ["roles"], then call .asStringSet().orEmpty()
only when the roles element is present and of the expected type; update
references to clientRolesRoot, resolvePath,
extractionConfig.clientRolesClaimPath, and allClientRoles accordingly and add a
unit test where the top-level claim (e.g., resource_access) is a string to
verify graceful degradation.

@sonarqubecloud
Copy link

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