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
147 changes: 122 additions & 25 deletions ballerina-interpreter/agent.bal
Original file line number Diff line number Diff line change
Expand Up @@ -350,37 +350,134 @@ function mapToHttpClientAuth(ClientAuthentication? auth) returns http:ClientAuth
"bearer" => {
return rest.cloneWithType(http:BearerTokenConfig);
}
"api-key" => {
return error("API key authentication is not yet supported for MCP/webhook transport in the Ballerina interpreter");
}
"oauth2" => {
// record {string grantType;}|error oauth2Config = check rest.cloneWithType();
// if oauth2Config is error {
// return error("OAuth2 authentication requires 'grantType' field", oauth2Config);
// }

// var {grantType, ...oauth2ConfigRest} = oauth2Config;

// match grantType.toLowerAscii() {
// "client_credentials" => {
// return oauth2ConfigRest.cloneWithType(http:OAuth2ClientCredentialsGrantConfig);
// }
// "password" => {
// return oauth2ConfigRest.cloneWithType(http:OAuth2PasswordGrantConfig);
// }
// "refresh_token" => {
// return oauth2ConfigRest.cloneWithType(http:OAuth2RefreshTokenGrantConfig);
// }
// "jwt" => {
// return oauth2Config.cloneWithType(http:OAuth2JwtBearerGrantConfig);
// }
// }
// panic error(string `Unsupported OAuth2 grant type: ${grantType}`);
return error("OAuth2 authentication not yet supported");
OAuth2Config|error oauth2Config = rest.cloneWithType();
if oauth2Config is error {
return error("Invalid OAuth2 authentication configuration", oauth2Config);
}
return buildOAuth2GrantConfig(oauth2Config);
}
"jwt" => {
// return rest.cloneWithType(http:JwtIssuerConfig);
return error("JWT authentication not yet supported");
JwtAuthConfig|error jwtConfig = rest.cloneWithType();
if jwtConfig is error {
return error("Invalid JWT authentication configuration", jwtConfig);
}
return buildJwtIssuerConfig(jwtConfig);
}
_ => {
return error(string `Unsupported authentication type: ${'type}`);
}
}
}

type JwtAuthConfig record {|
string issuer;
string|string[] audience;
string signing_key;
string algorithm = "RS256";
string key_id?;
string subject?;
map<json> custom_claims?;
decimal expiry_seconds = 300;
|};

function buildJwtIssuerConfig(JwtAuthConfig jwtConfig) returns http:JwtIssuerConfig|error {
string algorithm = jwtConfig.algorithm;
boolean isHmac = algorithm == "HS256" || algorithm == "HS384" || algorithm == "HS512";
json signatureKeyConfig = isHmac ? jwtConfig.signing_key : {keyFile: jwtConfig.signing_key};

map<json> issuerConfig = {
issuer: jwtConfig.issuer,
audience: jwtConfig.audience,
expTime: jwtConfig.expiry_seconds,
signatureConfig: {
algorithm,
config: signatureKeyConfig
}
};

string? keyId = jwtConfig?.key_id;
if keyId is string {
issuerConfig["keyId"] = keyId;
}
string? subject = jwtConfig?.subject;
if subject is string {
issuerConfig["username"] = subject;
}
map<json>? customClaims = jwtConfig?.custom_claims;
Comment on lines +402 to +410
if customClaims is map<json> {
issuerConfig["customClaims"] = customClaims;
}

http:JwtIssuerConfig|error result = issuerConfig.cloneWithType();
if result is error {
return error("Invalid JWT authentication configuration", result);
}
return result;
}

type OAuth2Config record {|
string grant_type;
string token_url?;
string refresh_url?;
string client_id?;
string client_secret?;
string username?;
string password?;
string refresh_token?;
string assertion?;
string[] scopes?;
|};

function buildOAuth2GrantConfig(OAuth2Config cfg) returns http:OAuth2GrantConfig|error {
string grant = cfg.grant_type.toLowerAscii();
match grant {
"client_credentials" => {
map<json> grantConfig = {tokenUrl: cfg?.token_url, clientId: cfg?.client_id, clientSecret: cfg?.client_secret};
addScopes(grantConfig, cfg?.scopes);
return wrapOAuth2(grantConfig.cloneWithType(http:OAuth2ClientCredentialsGrantConfig));
}
"password" => {
map<json> grantConfig = {tokenUrl: cfg?.token_url, username: cfg?.username, password: cfg?.password};
addOptional(grantConfig, "clientId", cfg?.client_id);
addOptional(grantConfig, "clientSecret", cfg?.client_secret);
addScopes(grantConfig, cfg?.scopes);
return wrapOAuth2(grantConfig.cloneWithType(http:OAuth2PasswordGrantConfig));
}
"refresh_token" => {
map<json> grantConfig = {refreshUrl: cfg?.refresh_url, refreshToken: cfg?.refresh_token, clientId: cfg?.client_id, clientSecret: cfg?.client_secret};
addScopes(grantConfig, cfg?.scopes);
return wrapOAuth2(grantConfig.cloneWithType(http:OAuth2RefreshTokenGrantConfig));
}
"jwt_bearer" => {
map<json> grantConfig = {tokenUrl: cfg?.token_url, assertion: cfg?.assertion};
addOptional(grantConfig, "clientId", cfg?.client_id);
addOptional(grantConfig, "clientSecret", cfg?.client_secret);
addScopes(grantConfig, cfg?.scopes);
return wrapOAuth2(grantConfig.cloneWithType(http:OAuth2JwtBearerGrantConfig));
Comment on lines +439 to +460
}
}
return error(string `Unsupported OAuth2 grant type: ${cfg.grant_type}`);
}

function wrapOAuth2(http:OAuth2GrantConfig|error result) returns http:OAuth2GrantConfig|error {
if result is error {
return error("Invalid OAuth2 authentication configuration", result);
}
return result;
}

function addScopes(map<json> target, string[]? scopes) {
if scopes is string[] {
target["scopes"] = scopes;
}
}

function addOptional(map<json> target, string key, string? value) {
if value is string {
target[key] = value;
}
}
154 changes: 154 additions & 0 deletions ballerina-interpreter/parser.bal
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ function parseAfm(string content) returns AFMRecord|error {
body = resolvedContent;
}

check validateMetadataAuthentication(metadata);

// Extract Role and Instructions sections
string[] bodyLines = splitLines(body);
string role = "";
Expand Down Expand Up @@ -305,6 +307,158 @@ function authenticationContainsHttpVariable(ClientAuthentication? authentication
return false;
}

final readonly & string[] RECOGNIZED_AUTH_TYPES = ["bearer", "basic", "api-key", "jwt", "oauth2"];

function validateMetadataAuthentication(AgentMetadata? metadata) returns error? {
if metadata is () {
return;
}

Model? model = metadata.model;
if model is Model {
check validateAuthentication(model.authentication);
}

Tools? tools = metadata.tools;
if tools is Tools {
MCPServer[]? mcp = tools.mcp;
if mcp is MCPServer[] {
foreach MCPServer server in mcp {
Transport transport = server.transport;
if transport is HttpTransport {
check validateAuthentication(transport.authentication);
}
}
}
}

Interface[]? interfaces = metadata.interfaces;
if interfaces is Interface[] {
foreach Interface interface in interfaces {
if interface is WebhookInterface {
check validateAuthentication(interface.subscription.authentication);
}
}
}
}

function validateAuthentication(ClientAuthentication? auth) returns error? {
if auth is () {
return;
}

string authType = auth.'type.toLowerAscii();
if RECOGNIZED_AUTH_TYPES.indexOf(authType) is () {
return error(string `unknown authentication type '${auth.'type}'. Supported types: bearer, basic, api-key, jwt, oauth2`);
}

if authType == "oauth2" {
return validateOAuth2(auth);
}

string[]? allowed = allowedAuthFields(authType);
if allowed is () {
return;
}

string[] provided = from string key in auth.keys() where key != "type" select key;

foreach string required in requiredAuthFields(authType) {
if provided.indexOf(required) is () {
return error(string `type '${authType}' requires '${required}' field`);
}
Comment on lines +364 to +369

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:

# First, let's locate and examine the parser.bal file
fd -t f "parser.bal" | head -20

Repository: wso2/reference-implementations-afm

Length of output: 111


🏁 Script executed:

# Check the file size to ensure we can read it
wc -l ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 115


🏁 Script executed:

# Read the specific lines mentioned in the review (364-369 and 429-434)
sed -n '360,375p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 613


🏁 Script executed:

# Also read the second location
sed -n '425,440p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 691


🏁 Script executed:

# Search for the function definitions to understand context
rg -n "validateMetadataAuthentication|validateAuthentication|validateOAuth2" ballerina-interpreter/parser.bal | head -20

Repository: wso2/reference-implementations-afm

Length of output: 646


🏁 Script executed:

# Look for test files related to auth validation
fd -t f ".*test.*\.bal" | grep -i "auth\|parse" | head -20

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Find ClientAuthentication type definition
rg -n "type ClientAuthentication" ballerina-interpreter/

Repository: wso2/reference-implementations-afm

Length of output: 148


🏁 Script executed:

# Search for test files
find . -type f -name "*.bal" | xargs grep -l "test\|Test" | grep -i "auth\|parse" | head -10

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Look at the broader context of validateAuthentication function
sed -n '345,380p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 1195


🏁 Script executed:

# Check if there's any null checking already
rg -n "is \(\)" ballerina-interpreter/parser.bal | head -20

Repository: wso2/reference-implementations-afm

Length of output: 678


🏁 Script executed:

# Look for auth field initialization patterns
rg -n "authentication.*=" ballerina-interpreter/parser.bal | head -20

Repository: wso2/reference-implementations-afm

Length of output: 136


🏁 Script executed:

# Check the ClientAuthentication record definition
sed -n '49,80p' ballerina-interpreter/types.bal

Repository: wso2/reference-implementations-afm

Length of output: 603


🏁 Script executed:

# Look for any existing parse tests
find . -type f -name "*.bal" -o -name "*.yaml" -o -name "*.json" | xargs grep -l "bearer\|basic\|api-key" 2>/dev/null | head -10

Repository: wso2/reference-implementations-afm

Length of output: 236


🏁 Script executed:

# Check for test resources or test data
find . -type d -name "*test*" | head -10

Repository: wso2/reference-implementations-afm

Length of output: 203


🏁 Script executed:

# Check the test files for auth validation testing
cat -n ballerina-interpreter/tests/agent_test.bal | head -50

Repository: wso2/reference-implementations-afm

Length of output: 2020


🏁 Script executed:

# Check main_test.bal
cat -n ballerina-interpreter/tests/main_test.bal | head -50

Repository: wso2/reference-implementations-afm

Length of output: 1932


🏁 Script executed:

# Look for YAML/JSON test files with authentication configs
find . -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.json" \) | xargs grep -l "bearer\|basic\|oauth2" 2>/dev/null | head -10

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Check if there are AFM YAML files with auth sections
find . -type f -name "*.yaml" -o -name "*.yml" | xargs grep -A5 "authentication:" 2>/dev/null | head -40

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Check for sample AFM files that might contain auth configs
find . -type f -name "*.afm*" -o -name "*.md" | xargs grep -l "authentication" 2>/dev/null | head -5

Repository: wso2/reference-implementations-afm

Length of output: 367


🏁 Script executed:

# Look at sample_agent.afm.md if it exists
find . -name "sample_agent.afm.md" -exec cat {} \;

Repository: wso2/reference-implementations-afm

Length of output: 3252


🏁 Script executed:

# Check requiredAuthFields and allowedAuthFields implementations
sed -n '380,410p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 872


🏁 Script executed:

# Check what fields are actually required for each auth type
rg -n "requiredAuthFields|allowedAuthFields" -A 20 ballerina-interpreter/parser.bal | head -80

Repository: wso2/reference-implementations-afm

Length of output: 1945


🏁 Script executed:

# Check how auth records are created from YAML to understand null-field behavior
sed -n '200,250p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 1898


🏁 Script executed:

# Look for the YAML parsing logic that creates ClientAuthentication records
rg -n "ClientAuthentication\|fromYaml\|fromJson" ballerina-interpreter/parser.bal -B2 -A5 | head -60

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Verify Ballerina record key behavior with null values - check related code
rg -n "\.keys()" ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 260


🏁 Script executed:

# Check how the YAML/JSON is parsed into ClientAuthentication records
rg -n "parsing\|Parser\|yaml\|json" ballerina-interpreter/parser.bal | head -30

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Look for the from/map conversion that creates ClientAuthentication
sed -n '1,100p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 3406


🏁 Script executed:

# Verify if there are any null value tests in the test files
grep -n "null\|:\s*\(\)" ballerina-interpreter/tests/main_test.bal | head -30

Repository: wso2/reference-implementations-afm

Length of output: 1427


🏁 Script executed:

# Check how the deserialized auth records are actually used in the runtime
grep -n "auth\[" ballerina-interpreter/parser.bal | head -20

Repository: wso2/reference-implementations-afm

Length of output: 131


🏁 Script executed:

# Look for any usage of auth fields after validation
rg -n "authentication\." ballerina-interpreter/ | head -30

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Check agent.bal to see how auth is used at runtime
head -100 ballerina-interpreter/agent.bal

Repository: wso2/reference-implementations-afm

Length of output: 3340


🏁 Script executed:

# Check the full ClientAuthentication type definition and its usage
sed -n '49,60p' ballerina-interpreter/types.bal

Repository: wso2/reference-implementations-afm

Length of output: 274


🏁 Script executed:

# Look for how auth values are actually used at runtime
rg -n "transport\.authentication\|model\.authentication" ballerina-interpreter/ -A3 -B1 | head -40

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Check the mapToHttpClientAuth function to see if it handles null values
rg -n "mapToHttpClientAuth" ballerina-interpreter/ -A20 | head -50

Repository: wso2/reference-implementations-afm

Length of output: 4060


🏁 Script executed:

# Find the mapToHttpClientAuth function implementation
rg -n "function mapToHttpClientAuth" ballerina-interpreter/ -A30 | head -50

Repository: wso2/reference-implementations-afm

Length of output: 2319


🏁 Script executed:

# Check specifically for how token/password fields are accessed
rg -n "auth\[\"token\"\]|auth\[\"password\"\]|auth\[\"username\"\]|auth\[\"client_" ballerina-interpreter/ -B2 -A2 | head -60

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Check if there's any test with null auth fields
rg -n "token.*\(\)|password.*\(\)|null" ballerina-interpreter/tests/main_test.bal | head -20

Repository: wso2/reference-implementations-afm

Length of output: 149


🏁 Script executed:

# Check if ClientAuthentication is actually a record with optional fields or how it's defined
sed -n '49,65p' ballerina-interpreter/types.bal

Repository: wso2/reference-implementations-afm

Length of output: 351


🏁 Script executed:

# Look at what fromJsonWithType does - check the imports and usage
head -20 ballerina-interpreter/parser.bal | grep import

Repository: wso2/reference-implementations-afm

Length of output: 127


🏁 Script executed:

# Check if there's a test with explicitly null auth fields that should fail
grep -n "null\|:\s*\(\)" ballerina-interpreter/tests/main_test.bal | grep -i "auth\|bearer\|basic" | head -20

Repository: wso2/reference-implementations-afm

Length of output: 1564


🏁 Script executed:

# Verify how the YAML is parsed and if null fields are even possible
sed -n '48,70p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 711


🏁 Script executed:

# Check if there are any Ballerina tests that actually set auth fields to null
rg -n "null|():" ballerina-interpreter/tests/main_test.bal | grep -i "authentication\|bearer\|basic\|oauth" -B2 -A2 | head -40

Repository: wso2/reference-implementations-afm

Length of output: 2107


🏁 Script executed:

# Check the complete test for missing fields to understand validation behavior
sed -n '418,426p' ballerina-interpreter/tests/main_test.bal

Repository: wso2/reference-implementations-afm

Length of output: 402


🏁 Script executed:

# Search for any null-field handling in mapToHttpClientAuth or related functions
rg -n "is \(\)|!is \(\)" ballerina-interpreter/agent.bal | head -30

Repository: wso2/reference-implementations-afm

Length of output: 534


🏁 Script executed:

# Verify if YAML deserialization includes null fields or skips them
python3 << 'PY'
# Check if Ballerina's YAML parsing would include null fields
# by examining how records handle null in the context

# The key question: does fromJsonWithType include null fields in the record?
# In Ballerina, when a YAML field is null, does it:
# 1. Omit the field from the record?
# 2. Include the field with null value?

# Let's check the test to see if we can infer behavior
test_code = """
ClientAuthentication auth = {'type: "api-key"};
"""
print("Test shows creation without optional fields - suggests fields are optional")
print("This implies missing fields don't cause issues, only present ones with wrong values")
PY

Repository: wso2/reference-implementations-afm

Length of output: 237


🏁 Script executed:

# Check if there's documentation or examples showing how Ballerina handles null in YAML->record conversion
rg -n "null|null:" ballerina-interpreter/tests/ | head -20

Repository: wso2/reference-implementations-afm

Length of output: 191


🏁 Script executed:

# Look at how the error message "api_key not found" is generated
rg -n "not found in" ballerina-interpreter/

Repository: wso2/reference-implementations-afm

Length of output: 947


🏁 Script executed:

# Check the getApiKey function that generates that error
rg -n "function getApiKey" ballerina-interpreter/ -A10

Repository: wso2/reference-implementations-afm

Length of output: 923


🏁 Script executed:

# Look for any actual null field tests in validation tests
sed -n '418,453p' ballerina-interpreter/tests/main_test.bal

Repository: wso2/reference-implementations-afm

Length of output: 1299


🏁 Script executed:

# Search for tests that might create auth with explicit null fields
rg -n "token.*null|\(\)" ballerina-interpreter/tests/ | grep -i "auth\|bearer\|basic" | head -20

Repository: wso2/reference-implementations-afm

Length of output: 2428


🏁 Script executed:

# Check if there's documentation about how null values behave in Ballerina record deserialization
rg -n "cloneWithType" ballerina-interpreter/ -B3 -A3 | head -40

Repository: wso2/reference-implementations-afm

Length of output: 3101


🏁 Script executed:

# Look for how auth is actually constructed from YAML in edge cases
sed -n '320,340p' ballerina-interpreter/agent.bal

Repository: wso2/reference-implementations-afm

Length of output: 751


🏁 Script executed:

# Check the complete getAuthTokenOrApiKey to understand null handling at usage time
sed -n '321,335p' ballerina-interpreter/agent.bal

Repository: wso2/reference-implementations-afm

Length of output: 608


🏁 Script executed:

# Check if there are actual tests showing null field behavior in AFM YAML
find . -type f -name "*.md" -o -name "*.afm" | xargs grep -l "null" 2>/dev/null | head -5

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Look at how the OAuth2Config and other config types handle null/missing fields
rg -n "type OAuth2Config|type BearerTokenConfig|type CredentialsConfig" ballerina-interpreter/

Repository: wso2/reference-implementations-afm

Length of output: 142


🏁 Script executed:

# Check if there's any Ballerina documentation or test showing null field behavior in record deserialization
sed -n '1,50p' ballerina-interpreter/tests/agent_test.bal

Repository: wso2/reference-implementations-afm

Length of output: 1670


🏁 Script executed:

# Look for any test that actually creates an auth record with explicit null value
rg -n ":\s*\(\)\|null" ballerina-interpreter/tests/agent_test.bal | head -20

Repository: wso2/reference-implementations-afm

Length of output: 60


🏁 Script executed:

# Check if Ballerina's YAML module preserves null values or omits them
rg -n "data.yaml|fromJson" ballerina-interpreter/parser.bal -B2 -A2 | head -30

Repository: wso2/reference-implementations-afm

Length of output: 399


🏁 Script executed:

# Check the OAuth2Config and other config types to understand field definitions
sed -n '422,440p' ballerina-interpreter/agent.bal

Repository: wso2/reference-implementations-afm

Length of output: 712


🏁 Script executed:

# Create a test to understand how Ballerina handles null in record deserialization
python3 << 'PY'
import json

# Simulate what happens in Ballerina:
# 1. YAML with null field: {type: "bearer", token: null}
# 2. Deserialize to ClientAuthentication record

# In Ballerina, when deserializing to a typed record:
# - If a field is declared as string, null cannot be assigned
# - The deserialization would fail OR omit the null field
# - Extra fields go into ...rest as map<anydata>

# The key question: does auth.keys() include null-valued fields?
# In Ballerina maps, a key with null value IS included in keys()
# But ClientAuthentication is a record, not a raw map

# The validation uses:
# 1. auth.keys() - gets keys from the record's dynamic fields
# 2. indexOf() - checks key presence only

# The real issue: if auth has {token: ()} (null), keys() includes "token"
# But then auth.get("token") returns null

print("In Ballerina:")
print("- auth.keys() includes keys even if value is null")
print("- The parser validation only checks key presence")
print("- Runtime validation auth.get(key).ensureType() would fail on null")
print("")
print("The concern is valid: parse-time validation allows null values to pass")
print("Runtime would catch it, but parse-time should be stricter")
PY

Repository: wso2/reference-implementations-afm

Length of output: 388


🏁 Script executed:

# Look for how fields are actually used at parse time vs runtime
rg -n "ensureType" ballerina-interpreter/ -B2 -A2

Repository: wso2/reference-implementations-afm

Length of output: 2939


🏁 Script executed:

# Check the complete validation flow - from YAML to validation
sed -n '27,35p' ballerina-interpreter/parser.bal

Repository: wso2/reference-implementations-afm

Length of output: 353


Required-field validation currently checks key presence, not non-nil values.

At lines 364–369 and 429–434, provided is derived from auth.keys(), allowing required fields that are explicitly set to null to pass parse-time validation. The runtime validation via ensureType() in getAuthTokenOrApiKey() (line 334) would later reject null values, but parse-time validation should catch this earlier for fail-fast feedback.

Add filtering to exclude null-valued fields from the required-field check:

Proposed approach
    string[] provided = from string key in auth.keys() where key != "type" select key;
+   string[] providedWithValue = [];
+   foreach string key in provided {
+       if auth[key] !is () {
+           providedWithValue.push(key);
+       }
+   }

    foreach string required in requiredAuthFields(authType) {
-       if provided.indexOf(required) is () {
+       if providedWithValue.indexOf(required) is () {
            return error(string `type '${authType}' requires '${required}' field`);
        }
    }

Apply the same pattern at line 429–434 for OAuth2 grant validation.

📝 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[] provided = from string key in auth.keys() where key != "type" select key;
foreach string required in requiredAuthFields(authType) {
if provided.indexOf(required) is () {
return error(string `type '${authType}' requires '${required}' field`);
}
string[] provided = from string key in auth.keys() where key != "type" select key;
string[] providedWithValue = [];
foreach string key in provided {
if auth[key] !is () {
providedWithValue.push(key);
}
}
foreach string required in requiredAuthFields(authType) {
if providedWithValue.indexOf(required) is () {
return error(string `type '${authType}' requires '${required}' field`);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballerina-interpreter/parser.bal` around lines 364 - 369, The required-field
validation in the auth parsing logic currently only checks if keys are present
in auth.keys(), but does not validate that those fields have non-null values.
This allows null-valued fields to pass parse-time validation even though they
would be rejected later at runtime by ensureType() in getAuthTokenOrApiKey().
Modify the provided array derivation to filter out keys whose corresponding
values are null, ensuring required fields both exist and have non-null values.
Apply this same filtering pattern at both validation locations: in the
requiredAuthFields loop check around lines 364-369 and in the OAuth2 grant
validation around lines 429-434.

Source: Coding guidelines

}

foreach string fieldName in provided {
if allowed.indexOf(fieldName) is () {
return error(string `type '${authType}' does not support '${fieldName}' field`);
}
}
}

function allowedAuthFields(string authType) returns string[]? {
match authType {
"bearer" => {
return ["token"];
}
"basic" => {
return ["username", "password"];
}
"api-key" => {
return ["api_key", "header_name"];
}
"jwt" => {
return ["issuer", "audience", "signing_key", "algorithm", "key_id", "subject", "custom_claims", "expiry_seconds"];
}
}
return ();
}

function requiredAuthFields(string authType) returns string[] {
match authType {
"bearer" => {
return ["token"];
}
"basic" => {
return ["username", "password"];
}
"api-key" => {
return ["api_key"];
}
"jwt" => {
return ["issuer", "audience", "signing_key"];
}
}
return [];
}

function validateOAuth2(ClientAuthentication auth) returns error? {
anydata grantTypeValue = auth["grant_type"];
if grantTypeValue !is string {
return error("type 'oauth2' requires 'grant_type' field");
}

string grant = grantTypeValue.toLowerAscii();
[string[], string[]]? grantFields = oauth2GrantFields(grant);
if grantFields is () {
return error("oauth2 grant_type '" + grantTypeValue + "' is not supported. Supported grant types: client_credentials, password, refresh_token, jwt_bearer");
}

[string[], string[]] [required, optional] = grantFields;
string[] allowed = ["grant_type", ...required, ...optional];
string[] provided = from string key in auth.keys() where key != "type" select key;

foreach string req in required {
if provided.indexOf(req) is () {
return error("oauth2 grant_type '" + grant + "' requires '" + req + "' field");
}
}

foreach string fieldName in provided {
if allowed.indexOf(fieldName) is () {
return error("oauth2 grant_type '" + grant + "' does not support '" + fieldName + "' field");
}
}
}

function oauth2GrantFields(string grant) returns [string[], string[]]? {
match grant {
"client_credentials" => {
return [["token_url", "client_id", "client_secret"], ["scopes"]];
}
"password" => {
return [["token_url", "username", "password", "client_id", "client_secret"], ["scopes"]];
}
"refresh_token" => {
return [["refresh_url", "refresh_token", "client_id", "client_secret"], ["scopes"]];
}
"jwt_bearer" => {
return [["token_url", "assertion"], ["client_id", "client_secret", "scopes"]];
}
}
return ();
}

function signatureContainsHttpVariable(Signature signature) returns boolean =>
jsonSchemaContainsHttpVariable(signature.input) || jsonSchemaContainsHttpVariable(signature.output);

Expand Down
Loading