Skip to content

Conversation

@Kwok-he-Chu
Copy link
Member

@Kwok-he-Chu Kwok-he-Chu commented Nov 18, 2025

PR-2 of #1204

Description

This PR introduces the mustache.templates needed for the upgrade of the OpenApiGenerator to v7.16.0 used in the Adyen sdk-automation to generate .NET models & services from the Adyen OpenAPI Specifications.

  • Removed the old templates

@Kwok-he-Chu Kwok-he-Chu requested review from a team as code owners November 18, 2025 15:53
@Kwok-he-Chu Kwok-he-Chu changed the base branch from main to v7-generator/1-add-core-classes November 18, 2025 15:53
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant upgrade, replacing old templates with new ones for the generichost library to support OpenAPI Generator v7.16.0. The changes are extensive and introduce a new structure for C# code generation. My review of the new mustache templates has identified a few critical and high-severity issues related to null handling, incorrect logic in authentication flows, and attempts to instantiate abstract classes. These could lead to runtime exceptions or unexpected behavior in the generated code. I've provided detailed feedback and code suggestions to improve the robustness and correctness of the new templates.

Comment on lines 417 to 458
catch (global::System.FormatException)
{
StringReader str = new StringReader(pvkstr);

//-------- read PEM encryption info. lines and extract salt -----
if (!str.ReadLine(){{nrt!}}.StartsWith("Proc-Type: 4,ENCRYPTED")) // TODO: what do we do here if ReadLine is null?
return null;

String saltline = str.ReadLine(){{nrt!}}; // TODO: what do we do here if ReadLine is null?
if (!saltline.StartsWith("DEK-Info: DES-EDE3-CBC,"))
return null;

String saltstr = saltline.Substring(saltline.IndexOf(",") + 1).Trim();
byte[] salt = new byte[saltstr.Length / 2];
for (int i = 0; i < salt.Length; i++)
salt[i] = Convert.ToByte(saltstr.Substring(i * 2, 2), 16);

if (!(str.ReadLine() == ""))
return null;

//------ remaining b64 data is encrypted RSA key ----
String encryptedstr = str.ReadToEnd();

try
{ //should have b64 encrypted RSA key now
binkey = Convert.FromBase64String(encryptedstr);
}
catch (global::System.FormatException)
{ //data is not in base64 format
return null;
}

// TODO: what do we do here if keyPassPhrase is null?
byte[] deskey = GetEncryptedKey(salt, keyPassPhrase{{nrt!}}, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes
if (deskey == null)
return null;

//------ Decrypt the encrypted 3des-encrypted RSA private key ------
byte[]{{nrt?}} rsakey = DecryptKey(binkey, deskey, salt); //OpenSSL uses salt value in PEM header also as 3DES IV

return rsakey;
}

Choose a reason for hiding this comment

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

critical

This catch block has several potential NullReferenceException issues and lacks proper error handling for encrypted keys without a passphrase.

  1. str.ReadLine() can return null, but the null-forgiving operator (!) is used, which can lead to a crash. You should use null-conditional access (?.).
  2. If an encrypted key is provided without a keyPassPhrase, the code will crash at line 450. There should be a check for a null keyPassPhrase before attempting to decrypt the key.

I suggest refactoring this block for safer null handling and clearer error reporting.

            catch (global::System.FormatException)
            {
                StringReader str = new StringReader(pvkstr);

                //-------- read PEM encryption info. lines and extract salt -----
                if (str.ReadLine()?.StartsWith("Proc-Type: 4,ENCRYPTED") != true)
                    return null;
                
                String saltline = str.ReadLine();
                if (saltline?.StartsWith("DEK-Info: DES-EDE3-CBC,") != true)
                    return null;
                
                String saltstr = saltline.Substring(saltline.IndexOf(",") + 1).Trim();
                byte[] salt = new byte[saltstr.Length / 2];
                for (int i = 0; i < salt.Length; i++)
                    salt[i] = Convert.ToByte(saltstr.Substring(i * 2, 2), 16);

                if (!(str.ReadLine() == ""))
                    return null;

                //------ remaining b64 data is encrypted RSA key ----
                String encryptedstr = str.ReadToEnd();

                try
                {   //should have b64 encrypted RSA key now
                    binkey = Convert.FromBase64String(encryptedstr);
                }
                catch (global::System.FormatException)
                {   //data is not in base64 format
                    return null;
                }

                if (keyPassPhrase == null)
                    throw new ArgumentException("A passphrase is required for an encrypted private key.", nameof(keyPassPhrase));

                byte[] deskey = GetEncryptedKey(salt, keyPassPhrase, 1, 2);    // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes
                if (deskey == null)
                    return null;

                //------ Decrypt the encrypted 3des-encrypted RSA private key ------
                byte[]{{nrt?}} rsakey = DecryptKey(binkey, deskey, salt); //OpenSSL uses salt value in PEM header also as 3DES IV

                return rsakey;
            }

Copy link
Member Author

@Kwok-he-Chu Kwok-he-Chu Nov 19, 2025

Choose a reason for hiding this comment

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

This template is used in two test files, I've removed them here

Comment on lines +81 to +85
/// <inheritdoc/>
public bool IsValidHmacSignature(string json, string hmacSignature)
{
return {{packageName}}.{{corePackageName}}.Utilities.HmacValidatorUtility.IsHmacSignatureValid(hmacSignature, _adyenHmacKey, json);
}

Choose a reason for hiding this comment

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

critical

The current implementation of IsValidHmacSignature has a critical flaw: it uses the _adyenHmacKey field which is never initialized, causing HMAC validation to always use a null key.

To fix this and follow best practices, I recommend the following changes:

  1. Store the provider: In the {{apiName}}Handler constructor, store the injected ITokenProvider<HmacKeyToken> in a private readonly field (e.g., _hmacKeyProvider).
  2. Make validation async: Change IsValidHmacSignature to be an async method (e.g., IsValidHmacSignatureAsync) in both the {{interfacePrefix}}{{apiName}}Handler interface and the {{apiName}}Handler class. This is crucial to avoid blocking on I/O when retrieving the key.
  3. Implement async logic: In the new async method, await the key from _hmacKeyProvider.GetAsync() and then perform the validation.

This approach ensures correct, non-blocking HMAC validation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Gemini reviews this template as if it was C# code, but this would generate the constructor with the HmacKey using the correct ITokenProvider<>,

   public {{apiName}}Handler({{packageName}}.{{apiName}}.{{clientPackage}}.JsonSerializerOptionsProvider jsonSerializerOptionsProvider, ITokenProvider<HmacKeyToken> hmacKeyProvider = null)
        {
            JsonSerializerOptionsProvider = jsonSerializerOptionsProvider;
        }

Resolved: The WebhookHandler is acting as a service-class (we only suffix it with -Handler because we call it the same in the other Adyen Libraries), developers can decide to call this function -> if no ADYEN_HMAC_KEY is provided, the application would still start.

Comment on lines +74 to +75
default:
break;

Choose a reason for hiding this comment

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

high

The default case in this switch statement does nothing, which means if an unsupported OAuthFlow is provided, the _grantType field will remain uninitialized. This will likely cause a null value to be sent in the token request later. It's better to throw an exception for unsupported flows to fail early and clearly.

                default:
                    throw new NotSupportedException($"The OAuth flow '{flow}' is not supported.");

Copy link
Member Author

Choose a reason for hiding this comment

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

This mustache template is unused

Copy link
Member Author

Choose a reason for hiding this comment

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

@gcatanese Q: Do we leave this in? I've only included it for completeness as part of the v7 generator upgrade

Choose a reason for hiding this comment

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

IMO I'd keep only if it already existed in the project and you are just moving things. If this is coming in from the upgrade, then I'd remove.

@Adyen Adyen deleted a comment from gemini-code-assist bot Nov 18, 2025
@Kwok-he-Chu Kwok-he-Chu changed the title OpenAPIGenerator upgrade to v7.16.0 - Added new mustache templates OpenAPIGenerator upgrade to v7.16.0 - [2] Added new mustache templates Nov 18, 2025
@Kwok-he-Chu Kwok-he-Chu self-assigned this Nov 18, 2025
Kwok-he-Chu added a commit that referenced this pull request Nov 19, 2025
Remove unused templates ApiTests, DependencyInjectionTests
#1208 (comment)
@Kwok-he-Chu
Copy link
Member Author

@gcatanese The mustache templates include the generated templates for the classes in the /Core-folder, these templates can be removed (e.g. Options.mustache template etc.)

@Kwok-he-Chu Kwok-he-Chu force-pushed the v7-generator/2-add-mustache-templates branch from 6d99b32 to 2e33dc4 Compare November 26, 2025 15:26
@Kwok-he-Chu Kwok-he-Chu changed the base branch from v7-generator/1-add-core-classes to v7-generator/development-branch November 26, 2025 15:51
@Kwok-he-Chu Kwok-he-Chu force-pushed the v7-generator/2-add-mustache-templates branch from 2e33dc4 to d49b160 Compare November 28, 2025 12:38
@@ -0,0 +1,7 @@
// {{{name}}} ({{{dataType}}}) pattern

Choose a reason for hiding this comment

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

There are two files with this name now and they are slightly different. Do we need both?

This version is the most similar to the original

@@ -0,0 +1,41 @@
\// <auto-generated>

Choose a reason for hiding this comment

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

Is this formatting ok?

if (DateOnly.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out DateOnly result))
return result;

throw new NotSupportedException();

Choose a reason for hiding this comment

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

I think this is out of sync with the generated file


{{/children}}
{{/discriminator}}
{{! start | support oneOf without discriminator.mapping property - WriteProperties }}

Choose a reason for hiding this comment

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

I see that this is new, do you have a test for this on the other PR?

@@ -0,0 +1 @@
{{#pathParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}}, {{/pathParams}}{{#queryParams}}{{#required}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}}, {{/required}}{{/queryParams}}{{#bodyParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}}, {{/bodyParams}}{{#queryParams}}{{^required}}Option<{{{dataType}}}{{>NullConditionalParameter}}> {{paramName}}{{#notRequiredOrIsNullable}} = default{{/notRequiredOrIsNullable}}, {{/required}}{{/queryParams}} RequestOptions? requestOptions = default, System.Threading.CancellationToken cancellationToken = default{{^netstandard20OrLater}}(global::System.Threading.CancellationToken){{/netstandard20OrLater}} No newline at end of file

Choose a reason for hiding this comment

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

I see that this version is a bit more complicated than the original. No need to change for now but we can create a follow up about maintainability later


{{/required}}
{{^required}}
if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}._{{name}}Option.IsSet)

Choose a reason for hiding this comment

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

Today we also add this underscore ?

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.

3 participants