Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions utilities/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Ballerina generates this directory during the compilation of a package.
# It contains compiler-generated artifacts and the final executable if this is an application package.
target/

# Ballerina maintains the compiler-generated source code here.
# Remove this if you want to commit generated sources.
generated/

# Contains configuration values used during development time.
# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details.
Config.toml
10 changes: 10 additions & 0 deletions utilities/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
org = "wso2"
name = "utilities"
version = "1.0.0"
distribution = "2201.13.1"
keywords = ["Healthcare", "FHIR", "utilities"]


[build-options]
observabilityIncluded = true
31 changes: 31 additions & 0 deletions utilities/constants.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

const AUTHORIZATION_HEADER = "authorization";

configurable string fhirServerUrl = "";
configurable string asgServerUrl = "";
configurable string orgResolverServiceUrl = "";
Comment on lines +19 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty string defaults for required URLs may cause silent failures.

fhirServerUrl, asgServerUrl, and orgResolverServiceUrl default to empty strings. If not configured, HTTP client initialization (in proxy_service.bal) will fail or behave unexpectedly. Consider adding validation at startup or providing meaningful defaults.

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

In `@utilities/constants.bal` around lines 19 - 21, The three configurable
variables fhirServerUrl, asgServerUrl, and orgResolverServiceUrl currently
default to empty strings which can cause silent failures when used by the HTTP
client (see proxy_service.bal); add startup validation where configuration is
loaded (or in init/start function used by proxy_service.bal) to check these
variables are non-empty and either assign safe defaults or fail-fast: log a
clear error including the variable name (fhirServerUrl, asgServerUrl,
orgResolverServiceUrl) and abort startup (or throw) if any required URL is
missing so downstream HTTP client initialization does not proceed with an empty
value.

configurable int proxyServerPort = 9090;
configurable string[] publicEndpoints = [];
configurable string adminAppClientId = "";
configurable string adminAppClientSecret = "";
configurable string[] audience = [];
configurable string tokenEp = "";

configurable string smart_style_url = "";
configurable boolean need_patient_banner = false;
configurable string patient_id = "";
123 changes: 123 additions & 0 deletions utilities/jwtValidator.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/http;
import ballerina/jwt;
import ballerina/log;

// OAuth Client Authentication Exception
type OAuthClientAuthnException record {|
string message;
string errorCode?;
|};

type ClientAssertionValidationResponse record {|
boolean isValid;
string clientId?;
string clientSecret?;
|};

type AsgAppResponse record {
Application[] applications;
};

type Application record {
string id;
string clientId;
AdvancedConfigurations advancedConfigurations;
};

type AdvancedConfigurations record {
json[] additionalSpProperties;
};

type OIDCInboundProtocolConfig record {
string clientId;
string clientSecret;
};

// JWT validation function - Ballerina equivalent of the Java isValidAssertion method
isolated function isValidAssertion(string? jwtString) returns ClientAssertionValidationResponse|OAuthClientAuthnException {

if jwtString is () {
string errorMessage = "No valid JWT assertion found for JWT Bearer Grant Type";
return logAndThrowException(errorMessage, ());
}

do {
[jwt:Header, jwt:Payload] [_, payload] = check jwt:decode(jwtString);
string jwtSubject = resolveSubject(payload) ?: "";

// get jwks uri from asgardeo
AsgAppResponse|http:ClientError appData =
asgAdminClient->get(string `api/server/v1/applications?filter=clientId+co+${jwtSubject}=&attributes=advancedConfigurations`);
Comment on lines +65 to +66
Copy link
Copy Markdown
Contributor

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
# Search for similar API filter patterns in the codebase
rg -n "api/server/v1/applications" --type-add 'bal:*.bal' --type bal

Repository: wso2/reference-implementation-cms0057f

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Also look for other ASG API calls that might show the expected pattern
rg -n "asgAdminClient" -A 2 -B 2 --type-add 'bal:*.bal' --type bal

Repository: wso2/reference-implementation-cms0057f

Length of output: 1406


🏁 Script executed:

#!/bin/bash
# Search for any filter patterns in the codebase
rg -n "filter=" --type-add 'bal:*.bal' --type bal

Repository: wso2/reference-implementation-cms0057f

Length of output: 249


🏁 Script executed:

#!/bin/bash
# Read the jwtValidator.bal file to understand the context
wc -l utilities/jwtValidator.bal

Repository: wso2/reference-implementation-cms0057f

Length of output: 112


🌐 Web query:

Asgardeo ASG API SCIM filter syntax clientId

💡 Result:

In Asgardeo’s SCIM2 endpoints, filtering is done with the standard SCIM query parameter:

  • GET .../scim2/Users?filter=<SCIM-filter-expression> (wso2.com)

SCIM filter syntax (RFC 7644)

A filter expression is generally:

  • <attrPath> <op> <value>
  • Logical composition: (<expr>) and|or (<expr>), plus not (...) (datatracker.ietf.org)

Supported operators include: eq, ne, co, sw, ew, gt, lt, ge, le, and pr (present). (datatracker.ietf.org)

Examples:

About filtering by clientId

clientId is not a standard SCIM User attribute, so you can only filter by clientId if it exists as an attribute on the SCIM resource you’re querying (e.g., via a custom schema/extension). In that case, you’d filter using its SCIM attribute path (optionally fully-qualified with the schema URN), e.g.:

If you tell me which Asgardeo endpoint you’re calling (e.g., /scim2/Users, /scim2/Groups, Roles, or a non-SCIM “applications” endpoint) and where clientId appears in the response payload, I can give the exact filter string.

Citations:


🌐 Web query:

Asgardeo api/server/v1/applications filter parameter syntax

💡 Result:

For GET .../api/server/v1/applications, the filter query param uses a SCIM-like expression:

  • Shape: <attribute> <operator> <value> [and <attribute> <operator> <value> ...]
  • Example (by name): filter=name eq <B2B-application-name> (shown in Asgardeo docs) [1]
  • Another example of chaining with and (same syntax style, shown for Roles): filter=displayName eq <admin-role-name> and audience.value eq <role-audience-value> [1]

Practical tips:

  • URL-encode spaces: ?filter=name%20eq%20MyApp
  • If your value contains spaces/special chars, quote it and URL-encode quotes, e.g. ?filter=name%20eq%20%22My%20App%22 (works with the same grammar shown above) [1]

Sources: Asgardeo endpoint context (/api/server/v1/applications) [2], and the docs example showing the filter grammar in use [1].

[1] (wso2.com)
[2] (wso2.com)

Citations:


Fix the ASG API filter syntax.

The filter expression filter=clientId+co+${jwtSubject}=&attributes=advancedConfigurations has incorrect syntax. Per SCIM RFC 7644 and Asgardeo API documentation, filter values must be properly quoted. The trailing = before &attributes is a syntax error. The filter should be:

filter=clientId co "${jwtSubject}"&attributes=advancedConfigurations

Or with URL encoding:

filter=clientId%20co%20%22${jwtSubject}%22&attributes=advancedConfigurations
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utilities/jwtValidator.bal` around lines 65 - 66, The ASG API filter string
passed to asgAdminClient->get is malformed; update the request in
utilities/jwtValidator.bal where asgAdminClient->get(...) is called (the call
that uses jwtSubject) to build a properly quoted/encoded filter: use
filter=clientId co "${jwtSubject}" (or URL-encode as
filter=clientId%20co%20%22${jwtSubject}%22) and remove the stray `=` before
&attributes so the query reads ...filter=clientId co
"${jwtSubject}"&attributes=advancedConfigurations.

if appData is http:ClientError {
string errorMessage = "Error while retrieving application details for clientId: " + jwtSubject;
return logAndThrowException(errorMessage, ());
}
string appId = appData.applications[0].id;
string jwksUri = "";
json[] additionalSpProperties = appData.applications[0].advancedConfigurations.additionalSpProperties;
Comment on lines +71 to +73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing bounds check before array access.

Accessing appData.applications[0] without verifying the array is non-empty will cause an index-out-of-bounds panic if no application matches the filter.

🐛 Proposed fix to add bounds check
+        if appData.applications.length() == 0 {
+            string errorMessage = "No application found for clientId: " + jwtSubject;
+            return logAndThrowException(errorMessage, "invalid_client");
+        }
         string appId = appData.applications[0].id;
         string jwksUri = "";
         json[] additionalSpProperties = appData.applications[0].advancedConfigurations.additionalSpProperties;
📝 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
string appId = appData.applications[0].id;
string jwksUri = "";
json[] additionalSpProperties = appData.applications[0].advancedConfigurations.additionalSpProperties;
if appData.applications.length() == 0 {
string errorMessage = "No application found for clientId: " + jwtSubject;
return logAndThrowException(errorMessage, "invalid_client");
}
string appId = appData.applications[0].id;
string jwksUri = "";
json[] additionalSpProperties = appData.applications[0].advancedConfigurations.additionalSpProperties;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utilities/jwtValidator.bal` around lines 71 - 73, The code accesses
appData.applications[0] without checking that the array has elements, which can
cause an index-out-of-bounds panic; update the logic in
utilities/jwtValidator.bal (where appId, jwksUri and additionalSpProperties are
read from appData.applications[0]) to first verify appData.applications is
non-nil and has length > 0, and handle the empty case by returning/throwing an
appropriate error or logging and using safe defaults instead of indexing into
[0].

foreach json item in additionalSpProperties {
if (check item.name).toString() == "jwksURI" {
jwksUri = (check item.value).toString();
break;
}
}
Comment on lines +72 to +79
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle missing JWKS URI gracefully.

If no jwksURI property exists in additionalSpProperties, jwksUri remains empty and subsequent JWT validation will fail with an unclear error. Consider explicit error handling.

🛡️ Proposed fix to validate JWKS URI
         foreach json item in additionalSpProperties {
             if (check item.name).toString() == "jwksURI" {
                 jwksUri = (check item.value).toString();
                 break;
             }
         }
+        if jwksUri == "" {
+            string errorMessage = "JWKS URI not configured for clientId: " + jwtSubject;
+            return logAndThrowException(errorMessage, "invalid_client");
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utilities/jwtValidator.bal` around lines 72 - 79, The code extracts jwksUri
from additionalSpProperties into the jwksUri variable but does not handle the
case where no "jwksURI" key is found; after the foreach over
additionalSpProperties, add an explicit check that jwksUri is not empty and if
it is, return or throw a clear, recoverable error or log and abort JWT
validation (e.g., return an error from the surrounding validation function),
ensuring callers receive a descriptive message like "missing jwksURI in
additionalSpProperties" so downstream JWT validation logic fails with a clear
reason; reference the jwksUri variable and the additionalSpProperties loop to
locate where to add this check.


jwt:ValidatorConfig validatorConfig = {
issuer: jwtSubject,
audience: audience,
clockSkew: 60,
signatureConfig: {
jwksConfig: {
url: jwksUri
}
}
};

// Validates the created JWT and extracts clientId clientSecret.
jwt:Payload validatedPayload = check jwt:validate(jwtString, validatorConfig);
log:printDebug("JWT is valid. Payload: " + validatedPayload.toJsonString());
string clientId = validatedPayload.hasKey("sub") ? validatedPayload.get("sub").toString() : "";

OIDCInboundProtocolConfig|http:ClientError oidcProtocols =
asgAdminClient->get(string `api/server/v1/applications/${appId}/inbound-protocols/oidc`);
if oidcProtocols is http:ClientError {
string errorMessage = "Error while retrieving OIDC protocol details for clientId: " + jwtSubject;
return logAndThrowException(errorMessage, ());
}

string clientSecret = oidcProtocols.clientSecret;
log:printDebug("JWT assertion validated successfully for clientId: " + clientId);
return {isValid: true, clientId: clientId, clientSecret: clientSecret};
} on fail error e {
string errorMessage = "JWT validation failed: " + e.message();
return logAndThrowException(errorMessage, "invalid_client");
}
}

isolated function resolveSubject(jwt:Payload payload) returns string? {
return payload.hasKey("sub") ? payload.get("sub").toString() : ();
}

isolated function logAndThrowException(string message, string? errorCode) returns OAuthClientAuthnException {
log:printError(message);
return {
message: message,
errorCode: errorCode ?: "server_error"
};
}
Loading