Skip to content

Comments

Add API Key Feature Enhancements#13569

Draft
npamudika wants to merge 24 commits intowso2:masterfrom
npamudika:master
Draft

Add API Key Feature Enhancements#13569
npamudika wants to merge 24 commits intowso2:masterfrom
npamudika:master

Conversation

@npamudika
Copy link
Contributor

Fixes related to wso2/api-manager#4642

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Naduni Pamudika seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces opaque API key management functionality across the API Gateway Manager system. It adds new interfaces, data models, authentication flows, event publishing, database operations, and REST API endpoints to support generating, revoking, regenerating, and tracking opaque API keys with display names and metadata.

Changes

Cohort / File(s) Summary
API Consumer Interface & Implementation
components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/APIConsumer.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConsumerImpl.java
Added keyDisplayName parameter to generateApiKey() method; introduced three new public methods: getApiKeys(), revokeAPIKey(), and regenerateAPIKey() for opaque key management; extended implementation to support JWT vs. opaque key generation paths with event publishing and metadata serialization.
Data Models
components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/model/APIKeyInfo.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dto/APIKeyDTO.java
Added APIKeyInfo POJO with 15 fields including keyDisplayName, apiKeyHash, salt, and metadata; added APIKeyDTO serializable DTO with 10 fields for key properties transfer.
Gateway Authentication
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/security/apikey/ApiKeyAuthenticator.java, components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/ApiKeyAuthenticatorUtils.java, components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/GatewayUtils.java
Introduced validateOpaqueApiKey() method for opaque key authentication with hash validation and metadata lookup; extended validateAPIKeyRestrictions() to handle additional properties from opaque keys; added null-safety and override parameters to validateAPISubscription() methods.
Gateway Data & Configuration
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/internal/DataHolder.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIManagerConfiguration.java
Added in-memory storage for opaque API key info via three new DataHolder methods; introduced realtimeOpaqueApiKeyNotifierProperties field and getter in APIManagerConfiguration.
Event Publishing Infrastructure
components/apimgt/org.wso2.carbon.apimgt.eventing/src/main/java/org/wso2/carbon/apimgt/eventing/EventPublisherType.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConstants.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/utils/APIUtil.java
Added API_KEY_USAGE and API_KEY_INFO enum constants; introduced stream identifiers, topic names, and event payload field constants; added crypto utilities (sha256HashWithSalt(), generateLookupKey(), generateSalt()) and event publisher initialization.
Opaque API Key Notifier
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/token/OpaqueAPIKeyNotifier.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/publishers/OpaqueApiKeyPublisher.java, components/apimgt/org.wso2.carbon.apimgt.notification/src/main/java/org/wso2/carbon/apimgt/notification/OpaqueAPIKeyNotifierImpl.java, components/apimgt/org.wso2.carbon.apimgt.notification/src/main/java/org/wso2/carbon/apimgt/notification/internal/OpaqueApiKeyNotifierComponent.java
Added OpaqueAPIKeyNotifier interface for realtime notifications; implemented OpaqueAPIKeyPublisher singleton with event publishing methods; implemented OpaqueAPIKeyNotifierImpl with sendLastUsedTimeOnRealtime() and sendApiKeyInfoOnRealtime() methods; added OSGi component for lifecycle management.
JMS Event Listeners
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/listeners/OpaqueAPIKeyInfoListener.java, components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/listeners/GatewayStartupListener.java, components/apimgt/org.wso2.carbon.apimgt.jms.listener/src/main/java/org/wso2/carbon/apimgt/jms/listener/utils/APIKeyUsageListener.java, components/apimgt/org.wso2.carbon.apimgt.jms.listener/src/main/java/org/wso2/carbon/apimgt/jms/listener/utils/JMSListenerStartupShutdownListener.java
Added OpaqueAPIKeyInfoListener to parse JSON messages and cache API key metadata; added APIKeyUsageListener to update key usage timestamps; registered both listeners on startup with respective JMS topics.
Database Layer
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/ApiMgtDAO.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/constants/SQLConstants.java
Extensive additions to ApiMgtDAO for API key CRUD operations and metadata management; added six new SQL constants for key insertion, retrieval, update, and deletion operations.
Service References
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/internal/ServiceReferenceHolder.java, components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/internal/APIManagerComponent.java
Added opaqueApiKeyNotifier field with getter/setter in ServiceReferenceHolder; removed unused GatewayNotificationConfiguration import from APIManagerComponent.
Utility Functions
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/utils/APIKeyUtils.java
Added isJWTAPIKeyGenerationEnabled() method to check JWT key generation configuration with fallback to true.
REST Admin API
components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/admin/v1/impl/ApiKeysApiServiceImpl.java, components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/resources/admin-api.yaml
Added ApiKeysApiServiceImpl with stub endpoints for GET all keys and DELETE by application/type/name; extended admin-api.yaml with new /api-keys path and associated schemas (APIKey, APIKeyList).
REST Store API
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/impl/ApplicationsApiServiceImpl.java, components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/mappings/ApplicationKeyMappingUtil.java, components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/resources/devportal-api.yaml
Added three new endpoints for GET by type, DELETE by name, and POST regenerate; extended key generation to capture and propagate keyDisplayName; added formApiKeyListToDTOList() mapping utility; updated devportal-api.yaml with regeneration, retrieval, and revocation endpoints with new schemas.
REST Common API Definitions
components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/admin-api.yaml, components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/devportal-api.yaml
Extended common admin and devportal API YAML specifications with API key management endpoints, schemas, and path parameters (APIKey, APIKeyList, APIKeyRenewalRequest, APIKeyInfo, keyDisplayName).
Gateway WebSocket Authentication
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/inbound/websocket/Authentication/ApiKeyAuthenticator.java, components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/security/oauth/OAuthAuthenticator.java
Updated API key validation calls to pass additional parameters (null arguments for compatibility); applied whitespace normalization in exception handling.
Event Stream & Publisher Configuration
features/apimgt/org.wso2.carbon.apimgt.throttling.siddhi.extension.feature/src/main/resources/conf/eventstreams/*, features/apimgt/org.wso2.carbon.apimgt.throttling.siddhi.extension.feature/src/main/resources/conf/eventpublishers/*, features/apimgt/org.wso2.carbon.apimgt.throttling.siddhi.extension.feature/src/main/resources/conf/eventreceivers/*
Added four new event stream definitions, two JMS publisher configurations, and two event receiver configurations for API key usage and info events with JSON mapping and JNDI routing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main feature being added (API Key Feature Enhancements) and accurately reflects the core changeset focus on API key management improvements.
Description check ✅ Passed The description references a GitHub issue (4642) which provides context for the changes, and while brief, it is directly related to the API key feature enhancements in the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@AnuGayan
Copy link
Contributor

AnuGayan commented Feb 10, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/GatewayUtils.java (1)

921-921: ⚠️ Potential issue | 🔴 Critical

Unsafe cast to Long — may throw ClassCastException for small numeric values.

application.get(APPLICATION_ID) returns an Object from the JSON parser, which could be Integer or Long depending on the magnitude of the value. Casting directly to Long will fail with a ClassCastException when the value is parsed as Integer. The other overload at line 820 avoids this by using Integer.parseInt(application.getAsString(...)), which is safer.

🐛 Proposed fix — use consistent and safe parsing
-                appId = ((Long) application.get(APIConstants.JwtTokenConstants.APPLICATION_ID)).intValue();
+                appId = Integer.parseInt(application.getAsString(APIConstants.JwtTokenConstants.APPLICATION_ID));
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/utils/APIKeyUtils.java (1)

29-29: ⚠️ Potential issue | 🟡 Minor

Bug: Logger initialized with wrong class.

LogFactory.getLog(JWTUtil.class) should be LogFactory.getLog(APIKeyUtils.class). This causes all log messages from this class (including the new method's error logging at line 64) to be attributed to JWTUtil instead of APIKeyUtils.

Proposed fix
-    private static final Log log = LogFactory.getLog(JWTUtil.class);
+    private static final Log log = LogFactory.getLog(APIKeyUtils.class);
🤖 Fix all issues with AI agents
In
`@components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/security/apikey/ApiKeyAuthenticator.java`:
- Around line 237-238: The hardcoded lookupSecret in ApiKeyAuthenticator (the
String lookupSecret and its use in APIUtil.generateLookupKey(apiKey,
lookupSecret)) must be removed and loaded from external configuration or a
vault; change the logic to fetch the secret from APIManagerConfiguration (or a
secure vault provider) at startup or when needed, validate it is present (log
and fail the authentication flow if missing) and pass that configured secret
into APIUtil.generateLookupKey instead of the literal; ensure any
caching/rotation is supported (do not store the secret in source code or
constants) and add clear error handling/logging when the configured secret is
unavailable.

In
`@components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/internal/DataHolder.java`:
- Around line 157-179: The removal method removeOpaqueAPIKeyInfo currently uses
a different identifier (apiKeyHash) than the map key
(apiKeyInfo.getLookupKey()), causing entries to never be removed; fix it either
by changing removeOpaqueAPIKeyInfo to accept and use the lookupKey (rename the
parameter to lookupKey and call apiKeyInfoHashMap.remove(lookupKey)) if
lookupKey is the intended key, or implement removal-by-hash by iterating
apiKeyInfoHashMap.entrySet(), comparing each APIKeyInfo.getApiKeyHash() (or the
correct hash accessor) and calling iterator.remove() when matched; update the
method signature and javadoc accordingly (references: addOpaqueAPIKeyInfo,
getOpaqueAPIKeyInfo, removeOpaqueAPIKeyInfo, apiKeyInfoHashMap,
APIKeyInfo.getLookupKey()).

In
`@components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/publishers/OpaqueApiKeyPublisher.java`:
- Around line 40-47: The constructor for OpaqueApiKeyPublisher should guard
against nulls before calling init: check that realtimeNotifierProperties != null
AND opaqueApiKeyNotifier != null before invoking opaqueApiKeyNotifier.init(...),
and only set realtimeNotifierEnabled = true when both are present; if either is
null, skip calling opaqueApiKeyNotifier.init and ensure realtimeNotifierEnabled
remains false and add a debug/warn log explaining why; this prevents the NPE
thrown from OpaqueAPIKeyNotifierImpl.init() when realTimeNotifierProperties is
null and avoids calling methods on a null opaqueApiKeyNotifier instance.

In
`@components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/utils/APIUtil.java`:
- Around line 9211-9213: generateSalt() currently returns a zeroed 16-byte array
which makes all salts identical; change it to produce cryptographically-random
bytes (e.g., allocate a 16-byte array and fill it via SecureRandom.nextBytes) so
sha256HashWithSalt() receives a proper random salt when APIConsumerImpl creates
API keys; consider using a SecureRandom instance (static final or
SecureRandom.getInstanceStrong()) to generate the bytes before returning them.

In
`@components/apimgt/org.wso2.carbon.apimgt.notification/src/main/java/org/wso2/carbon/apimgt/notification/OpaqueAPIKeyNotifierImpl.java`:
- Around line 104-107: The init method in OpaqueAPIKeyNotifierImpl currently
calls realTimeNotifierProperties.clone() and will NPE if passed null; change
init(Properties realTimeNotifierProperties) to check for null before cloning –
if realTimeNotifierProperties is non-null assign this.realTimeNotifierProperties
= (Properties) realTimeNotifierProperties.clone(), otherwise assign an empty new
Properties() (or Collections.emptyProperties equivalent) so the field is never
null; refer to the init method and the this.realTimeNotifierProperties field
when making this change.
🟠 Major comments (16)
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/GatewayUtils.java-699-703 (1)

699-703: ⚠️ Potential issue | 🟠 Major

Potential NPE when payload is null and apiKeyValidationInfoDTO is also null.

Line 702 accesses apiKeyValidationInfoDTO.getEndUserName() without a null guard. While the apiKeyValidationInfoDTO != null check exists at line 705, the else-branch at line 702 is reached before that check. If a caller passes both payload = null and apiKeyValidationInfoDTO = null, this will throw a NullPointerException.

🛡️ Proposed fix
         if (payload != null) {
-        authContext.setUsername(payload.getSubject());
+            authContext.setUsername(payload.getSubject());
+        } else if (apiKeyValidationInfoDTO != null) {
+            authContext.setUsername(apiKeyValidationInfoDTO.getEndUserName());
         } else {
-            authContext.setUsername(apiKeyValidationInfoDTO.getEndUserName());
+            authContext.setUsername(null);
         }
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/constants/SQLConstants.java-3925-3931 (1)

3925-3931: ⚠️ Potential issue | 🟠 Major

Filter revoked API keys from lookups/updates.

GET_API_KEY_FROM_DISPLAY_NAME_SQL and UPDATE_API_KEY_LAST_USED_SQL don’t restrict by STATUS, so revoked keys can still be returned/updated. Align with the active-only expectation used by GET_API_KEY_SQL to avoid inadvertently treating revoked keys as valid.

✅ Suggested fix
 public static final String GET_API_KEY_FROM_DISPLAY_NAME_SQL =
-        "SELECT API_KEY_PROPERTIES, AUTHZ_USER, VALIDITY_PERIOD, LAST_USED FROM AM_API_KEY WHERE APPLICATION_ID = ? AND KEY_TYPE = ? AND API_KEY_NAME = ?";
+        "SELECT API_KEY_PROPERTIES, AUTHZ_USER, VALIDITY_PERIOD, LAST_USED FROM AM_API_KEY " +
+        "WHERE APPLICATION_ID = ? AND KEY_TYPE = ? AND API_KEY_NAME = ? AND STATUS = 'ACTIVE'";
 public static final String DELETE_API_KEY_SQL =
         "UPDATE AM_API_KEY SET STATUS = 'REVOKED' WHERE APPLICATION_ID = ? AND KEY_TYPE = ? AND API_KEY_NAME = ?";
 public static final String UPDATE_API_KEY_LAST_USED_SQL =
-        "UPDATE AM_API_KEY SET LAST_USED = ? WHERE API_KEY_HASH = ?";
+        "UPDATE AM_API_KEY SET LAST_USED = ? WHERE API_KEY_HASH = ? AND STATUS = 'ACTIVE'";
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/internal/DataHolder.java-62-62 (1)

62-62: ⚠️ Potential issue | 🟠 Major

Plain HashMap may cause data races under concurrent access from JMS listener threads.

apiKeyInfoHashMap is a regular HashMap, but it will be mutated by JMS listener threads (via OpaqueAPIKeyInfoListener) and read by gateway request-handling threads. This can lead to corrupted state or infinite loops in HashMap.get() under concurrent modification. Consider using ConcurrentHashMap instead.

I note that other maps in this class (e.g., llmProviderMap) also use HashMap, so this may be an accepted pattern. However, since opaque API key lookups are on the hot request path, the risk is higher here.

components/apimgt/org.wso2.carbon.apimgt.jms.listener/src/main/java/org/wso2/carbon/apimgt/jms/listener/utils/APIKeyUsageListener.java-54-63 (1)

54-63: 🛠️ Refactor suggestion | 🟠 Major

Use the established JMS payload extraction pattern.

The existing JMS listeners in this module (e.g., CertificateManagerJMSMessageListener, KeyManagerJMSMessageListener) use payloadData.get(APIConstants.CONSTANT_NAME).asText() rather than path(...).asText(). The path() approach silently returns a "missing node" instead of null on absent keys, masking missing data. Additionally, consider using constants from APIConstants for the field names "apiKeyHash" and "lastUsedTime" if they exist.

Also, there's no validation that the extracted apiKeyHash or lastUsedTime are non-empty before calling the DAO update, which could write empty strings to the database.

Proposed fix
-                    ObjectMapper objectMapper = new ObjectMapper();
-                    // Navigate to payloadData
-                    JsonNode payload = null;
-                    payload = objectMapper.readTree(textMessage)
-                            .path("event")
-                            .path("payloadData");
-
-                    APIKeyInfo apiKeyInfo = new APIKeyInfo();
-                    apiKeyInfo.setApiKeyHash(payload.path("apiKeyHash").asText());
-                    apiKeyInfo.setLastUsedTime(payload.path("lastUsedTime").asText());
-
-                    // Add to AM_API_KEY
-                    ApiMgtDAO.getInstance().updateAPIKeyUsage(apiKeyInfo.getApiKeyHash(), apiKeyInfo.getLastUsedTime());
+                    ObjectMapper objectMapper = new ObjectMapper();
+                    JsonNode payloadData = objectMapper.readTree(textMessage)
+                            .path("event")
+                            .path("payloadData");
+
+                    String apiKeyHash = payloadData.get("apiKeyHash").asText();
+                    String lastUsedTime = payloadData.get("lastUsedTime").asText();
+
+                    if (apiKeyHash != null && !apiKeyHash.isEmpty()) {
+                        ApiMgtDAO.getInstance().updateAPIKeyUsage(apiKeyHash, lastUsedTime);
+                    } else {
+                        log.warn("Received API key usage event with empty apiKeyHash");
+                    }

Based on learnings: In WSO2 API Manager JMS listeners, the standard pattern for extracting payload data is payloadData.get(APIConstants.CONSTANT_NAME).asText(), not path(...).asText(null).

components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/admin-api.yaml-7103-7127 (1)

7103-7127: ⚠️ Potential issue | 🟠 Major

API key list response lacks revocation identifiers and exposes raw secrets.
Clients can’t revoke keys from the list response because applicationId, keyType, and keyDisplayName aren’t returned. Also, listing raw API keys is risky; prefer returning masked values plus metadata.

🔧 Proposed schema adjustment
     APIKey:
       title: API Key details to invoke APIs
       type: object
       properties:
-        apikey:
-          type: string
-          description: API Key
-          example: eyJoZWxsbyI6IndvcmxkIn0=.eyJ3c28yIjoiYXBpbSJ9.eyJ3c28yIjoic2lnbmF0dXJlIn0=
+        apiKeyMasked:
+          type: string
+          description: Masked API key value (never return the full key)
+          example: "****"
+        applicationId:
+          type: string
+          description: Application UUID
+          example: d7cf8523-9180-4255-84fa-6cb171c1f779
+        keyType:
+          type: string
+          enum:
+            - PRODUCTION
+            - SANDBOX
+        keyDisplayName:
+          type: string
+          description: Name of the API key
         validityTime:
           type: integer
           format: int32
           example: 3600
components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/admin-api.yaml-4583-4641 (1)

4583-4641: ⚠️ Potential issue | 🟠 Major

Edit the source Admin API spec, not this generated copy.
This file is auto-generated during the build; changes here will be overwritten. Please move these endpoint additions to the source spec file components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/resources/admin-api.yaml.

Based on learnings: "In wso2/carbon-apimgt, the file components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/admin-api.yaml is an automated copy file that gets updated during compilation/build from the source file components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/resources/admin-api.yaml; changes should be made to the source file only."

components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/resources/admin-api.yaml-7103-7127 (1)

7103-7127: ⚠️ Potential issue | 🟠 Major

Separate list response schema to avoid exposing full API keys.

GET /api-keys returns APIKeyList containing full APIKey objects with raw secrets in the response. While this endpoint is restricted to admin users (apim:admin scope), the schema should use a separate response type (e.g., APIKeyListItem or APIKeySummary) that omits the raw key and provides only metadata (display name, validity time, etc.). Reserve the full APIKey schema for create/regenerate endpoints where users need the secret.

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/ApiMgtDAO.java-16648-16672 (1)

16648-16672: ⚠️ Potential issue | 🟠 Major

ResultSet resource leak — rs is never closed.

rs is assigned at Line 16660 but APIMgtDBUtil.closeAllConnections(ps, conn, null) at Line 16672 passes null instead of rs. This leaks the ResultSet. The other read methods (getAPIKeys, getAllAPIKeys) correctly use try-with-resources for the ResultSet; this method should follow the same pattern.

🐛 Proposed fix — use try-with-resources for the ResultSet
-        ResultSet rs = null;
         try {
             conn = APIMgtDBUtil.getConnection();
-            conn.setAutoCommit(false);
 
             // This query to access the AM_API_KEY table
             String sqlQuery = SQLConstants.GET_API_KEY_FROM_DISPLAY_NAME_SQL;
             // Retrieving data from the AM_API_KEY table
             ps = conn.prepareStatement(sqlQuery);
             ps.setString(1, applicationId);
             ps.setString(2, keyType);
             ps.setString(3, keyDisplayName);
-            rs = ps.executeQuery();
-            if (rs.next()) {
-                keyInfo.setKeyDisplayName(keyDisplayName);
-                keyInfo.setValidityPeriod(rs.getLong("VALIDITY_PERIOD"));
-                keyInfo.setLastUsedTime(rs.getString("LAST_USED"));
-                keyInfo.setApplicationId(applicationId);
-                keyInfo.setKeyType(keyType);
-                keyInfo.setProperties(rs.getBytes("API_KEY_PROPERTIES"));
+            try (ResultSet rs = ps.executeQuery()) {
+                if (rs.next()) {
+                    keyInfo.setKeyDisplayName(keyDisplayName);
+                    keyInfo.setValidityPeriod(rs.getLong("VALIDITY_PERIOD"));
+                    keyInfo.setLastUsedTime(rs.getString("LAST_USED"));
+                    keyInfo.setApplicationId(applicationId);
+                    keyInfo.setKeyType(keyType);
+                    keyInfo.setProperties(rs.getBytes("API_KEY_PROPERTIES"));
+                }
             }
         } catch (SQLException e) {
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/security/apikey/ApiKeyAuthenticator.java-265-269 (1)

265-269: ⚠️ Potential issue | 🟠 Major

Raw API key logged in debug output — security/compliance risk.

Line 268 logs the full raw API key: "Token: " + apiKey. Even at debug level, this is a security concern as keys can end up in log aggregators and audit trails. Use a masked representation instead.

Proposed fix
                 if (log.isDebugEnabled()) {
                     log.debug("User is subscribed to the API: " + apiContext + ", " +
-                            "version: " + apiVersion + ". Token: " + apiKey);
+                            "version: " + apiVersion + ". Token: " + GatewayUtils.getMaskedToken(apiKey));
                 }
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/security/apikey/ApiKeyAuthenticator.java-291-293 (1)

291-293: ⚠️ Potential issue | 🟠 Major

Raw API key passed as tokenIdentifier to generateAuthenticationContext.

On line 293, the raw apiKey value is passed as the first argument to generateAuthenticationContext(apiKey, null, ...). In the JWT path (line 178), this parameter is the JWT ID (tokenIdentifier). Passing the raw opaque key here means it may be stored in the AuthenticationContext and propagated to analytics, logs, or downstream headers. Consider using the apiKeyHash or lookupKey instead.

Proposed fix
-        return GatewayUtils.generateAuthenticationContext(apiKey, null, apiKeyValidationInfoDTO, null);
+        return GatewayUtils.generateAuthenticationContext(lookupKey, null, apiKeyValidationInfoDTO, null);

Note: lookupKey would need to be passed as a parameter or recomputed. Alternatively, use apiKeyHash.

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/publishers/OpaqueApiKeyPublisher.java-57-74 (1)

57-74: ⚠️ Potential issue | 🟠 Major

publishApiKeyUsageEvents / publishApiKeyInfoEvents will NPE if notifier is null.

Both methods invoke opaqueApiKeyNotifier.sendLastUsedTimeOnRealtime(...) / sendApiKeyInfoOnRealtime(...) when realtimeNotifierEnabled is true, but don't guard against a null opaqueApiKeyNotifier. If the constructor fix above is applied (disabling realtimeNotifierEnabled when notifier is null), this becomes safe. Otherwise, add a null guard here as well.

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/ApiKeyAuthenticatorUtils.java-299-303 (1)

299-303: ⚠️ Potential issue | 🟠 Major

NPE when additionalProperties is null for opaque keys without additional properties.

When payload is null (opaque key path), the code directly calls additionalProperties.get(...) without a null check. If APIKeyInfo.getAdditionalProperties() returns null (e.g., no IP/referrer restrictions were set), this will throw a NullPointerException.

Proposed fix
         } else {
             // Taking values from the DB for an opaque API key
-            permittedIPList = additionalProperties.get("permittedIP");
-            permittedRefererList = additionalProperties.get("permittedReferer");
+            if (additionalProperties != null) {
+                permittedIPList = additionalProperties.get(APIConstants.JwtTokenConstants.PERMITTED_IP);
+                permittedRefererList = additionalProperties.get(APIConstants.JwtTokenConstants.PERMITTED_REFERER);
+            }
         }
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConsumerImpl.java-3654-3657 (1)

3654-3657: ⚠️ Potential issue | 🟠 Major

Publish API_KEY_INFO event for regenerated opaque keys.

generateApiKey emits API key info events, but regeneration only writes to the DB. If gateways depend on the info stream for cache hydration, the new key won’t be recognized immediately. Consider reusing sendAPIKeyInfoEvent after insertion.

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConsumerImpl.java-420-422 (1)

420-422: ⚠️ Potential issue | 🟠 Major

Remove hardcoded lookup secret.

Embedding a fixed lookup secret in source makes lookup keys predictable and non-rotatable. Load this from secure configuration/secret store and fail fast if missing.

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConsumerImpl.java-3621-3648 (1)

3621-3648: ⚠️ Potential issue | 🟠 Major

Guard against missing key info and null property values in regeneration.

getAPIKey(...) can return null (especially after revocation), and Properties#setProperty throws if values are null. Fetch before revoking and default missing properties.

Proposed fix
-        // Revoke the existing key
-        revokeAPIKey(applicationId, keyType, keyDisplayName, tenantDomain);
-        // Generate a new key with the same display name and other additional properties
-        APIKeyInfo apiKeyInfo = apiMgtDAO.getAPIKey(applicationId, keyType, keyDisplayName);
+        // Load existing metadata before revocation (revocation may remove/alter it)
+        APIKeyInfo apiKeyInfo = apiMgtDAO.getAPIKey(applicationId, keyType, keyDisplayName);
+        if (apiKeyInfo == null) {
+            throw new APIMgtResourceNotFoundException(
+                    "API key not found for display name: " + keyDisplayName);
+        }
+        // Revoke the existing key
+        revokeAPIKey(applicationId, keyType, keyDisplayName, tenantDomain);
@@
-            props.setProperty("permittedIP", oldProps.getProperty("permittedIP"));
-            props.setProperty("permittedReferer", oldProps.getProperty("permittedReferer"));
+            props.setProperty("permittedIP",
+                    StringUtils.defaultString(oldProps.getProperty("permittedIP")));
+            props.setProperty("permittedReferer",
+                    StringUtils.defaultString(oldProps.getProperty("permittedReferer")));
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConsumerImpl.java-3603-3618 (1)

3603-3618: ⚠️ Potential issue | 🟠 Major

Publish the actual API key value, not the display name, to the revocation event.

The gateway cache lookup uses the actual API key to invalidate entries, not the display name. Publishing keyDisplayName (e.g., "Test_Key") will fail to remove the cached API key since the gateway's removeApiKeyFromGatewayCache() searches by the actual token value.

Fetch the key details using apiMgtDAO.getAPIKey(applicationId, keyType, keyDisplayName) and publish either apiKeyInfo.getApiKey() or apiKeyInfo.getLookupKey() instead of keyDisplayName.

🟡 Minor comments (18)
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/GatewayUtils.java-727-727 (1)

727-727: ⚠️ Potential issue | 🟡 Minor

Redundant null check — StringUtils.isNotEmpty is already null-safe.

StringUtils.isNotEmpty(endUserToken) returns false when endUserToken is null, so the trailing && endUserToken != null is redundant. Additionally, the logical order is inverted: the null check should precede the content check if both were needed. The previous version of this line used only StringUtils.isNotEmpty(endUserToken), which was already correct.

♻️ Proposed fix
-        if (StringUtils.isNotEmpty(endUserToken) && endUserToken != null) {
+        if (StringUtils.isNotEmpty(endUserToken)) {
components/apimgt/org.wso2.carbon.apimgt.jms.listener/src/main/java/org/wso2/carbon/apimgt/jms/listener/utils/APIKeyUsageListener.java-71-72 (1)

71-72: ⚠️ Potential issue | 🟡 Minor

Error log message is misleading for non-JMS exceptions.

The catch block catches JMSException | JsonProcessingException | APIManagementException but the log message always says "JMSException occurred". Use a generic message instead.

Proposed fix
-        } catch (JMSException | JsonProcessingException | APIManagementException e) {
-            log.error("JMSException occurred when processing the received message ", e);
+        } catch (JMSException | JsonProcessingException | APIManagementException e) {
+            log.error("Error occurred when processing the API key usage message ", e);
         }
components/apimgt/org.wso2.carbon.apimgt.jms.listener/src/main/java/org/wso2/carbon/apimgt/jms/listener/utils/APIKeyUsageListener.java-45-45 (1)

45-45: ⚠️ Potential issue | 🟡 Minor

Remove debug artifact from production code.

The log.info("🔥 API Key Usage JMS message RECEIVED") line with an emoji is clearly a development/debug leftover. It should either be removed or converted to log.debug with a professional message, consistent with other JMS listeners in this module.

Proposed fix
-        log.info("🔥 API Key Usage JMS message RECEIVED");
+        if (log.isDebugEnabled()) {
+            log.debug("API Key Usage JMS message received");
+        }
components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/resources/admin-api.yaml-4609-4611 (1)

4609-4611: ⚠️ Potential issue | 🟡 Minor

Replace hardcoded Bearer token in the GET sample.

This looks like a real token and will trip secret scanners. Use a placeholder instead.

🧩 Proposed change
-          source: 'curl -k -H "Authorization: Bearer ae4eae22-3f65-387b-a171-d37eaa366fa8"
+          source: 'curl -k -H "Authorization: Bearer <ACCESS_TOKEN>"
components/apimgt/org.wso2.carbon.apimgt.rest.api.admin.v1/src/main/resources/admin-api.yaml-7481-7487 (1)

7481-7487: ⚠️ Potential issue | 🟡 Minor

Clarify encoding constraints for keyDisplayName path param.

Display names often include spaces or reserved characters; as a path segment, this needs URL‑encoding guidance to prevent routing issues.

🧩 Proposed change
     keyDisplayName:
       name: keyDisplayName
       in: path
       description: |
-        Name of the API key.
+        Name of the API key. URL-encode this value if it contains reserved path characters.
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/resources/devportal-api.yaml-3327-3330 (1)

3327-3330: ⚠️ Potential issue | 🟡 Minor

Change If-Match to If-None-Match for this GET endpoint.

Line 3329 uses If-Match on a GET endpoint for retrieving API keys. This is inconsistent with REST API semantics and the pattern used across all other GET endpoints in this spec, which use If-None-Match for conditional request validation and caching. If-Match is intended for mutating operations (PUT/DELETE/PATCH) to validate preconditions before modification.

🔧 Suggested fix
-        - $ref: '#/components/parameters/If-Match'
+        - $ref: '#/components/parameters/If-None-Match'
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/resources/devportal-api.yaml-5993-6012 (1)

5993-6012: ⚠️ Potential issue | 🟡 Minor

Update timestamp field examples and descriptions for clarity.

The issuedOn example 2026-02-06 23:45:07 does not match backend behavior, which returns epoch milliseconds as a string (e.g., 1738893907000). Additionally, lastUsed can be either a timestamp string or the sentinel NOT_USED, which needs clarification.

Keep both fields as type: string for backward compatibility, but fix the example values and descriptions:

🧭 Suggested fix
         issuedOn:
           type: string
-          description: Created Time
-          example: 2026-02-06 23:45:07
+          description: Created time as epoch milliseconds (numeric string).
+          example: 1738893907000
@@
         lastUsed:
           type: string
-          description: Last Used Time
-          example: NOT_USED
+          description: Last used time as epoch milliseconds, or NOT_USED if never used.
+          example: NOT_USED
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/resources/devportal-api.yaml-3368-3377 (1)

3368-3377: ⚠️ Potential issue | 🟡 Minor

Add 404 response for consistency with similar endpoints.

The /regenerate endpoint with the same keyDisplayName parameter pattern documents a 404 response, so the revoke endpoint should too. This provides clients consistent error handling across similar operations on API keys.

Suggested fix
           $ref: '#/components/responses/BadRequest'
+        404:
+          $ref: '#/components/responses/NotFound'
         412:
           $ref: '#/components/responses/PreconditionFailed'
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/mappings/ApplicationKeyMappingUtil.java-152-164 (1)

152-164: ⚠️ Potential issue | 🟡 Minor

Narrowing cast from long to int on validityPeriod can silently truncate.

APIKeyInfo.getValidityPeriod() returns long, but dto.setValidityPeriod((int) src.getValidityPeriod()) on Line 158 truncates it. If validity periods are ever stored in milliseconds or as very large values (e.g., -1L for "never expires" is fine, but values > Integer.MAX_VALUE will wrap), this silently produces incorrect results.

The same pattern appears in ApplicationsApiServiceImpl Line 803–804 with the regenerate flow. If the DTO's validityPeriod field type cannot be changed to long, consider adding a bounds check or using Math.toIntExact() (which throws ArithmeticException on overflow) to fail explicitly rather than silently.

🔧 Safer cast option
-                    dto.setValidityPeriod((int) src.getValidityPeriod());
+                    dto.setValidityPeriod(Math.toIntExact(src.getValidityPeriod()));
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/impl/ApplicationsApiServiceImpl.java-748-789 (1)

748-789: ⚠️ Potential issue | 🟡 Minor

Multiple issues in the DELETE endpoint.

  1. Double negative typo (Line 771): "doesn't not exist" → should be "doesn't exist" or "does not exist".

  2. Misleading error message (Line 773): When the application is not found, the error returned is "Validation failed for the given token". It should indicate the application was not found, similar to other endpoints that use RestApiUtil.handleResourceNotFoundError.

  3. Unguarded debug log (Line 785): log.debug(...) is not guarded by if (log.isDebugEnabled()), unlike Lines 762 and 770 in the same method. This forces string concatenation even when debug is disabled.

🐛 Suggested fixes
                 } else {
-                    if(log.isDebugEnabled()) {
-                        log.debug("Application with given id " + applicationId + " doesn't not exist ");
-                    }
-                    RestApiUtil.handleBadRequest("Validation failed for the given token ", log);
+                    RestApiUtil.handleResourceNotFoundError(RestApiConstants.RESOURCE_APPLICATION,
+                            applicationId, log);
                 }
         } else {
-            log.debug("Provided API Key " + keyDisplayName + " is not valid");
+            if (log.isDebugEnabled()) {
+                log.debug("Provided API Key display name " + keyDisplayName + " is not valid");
+            }
             RestApiUtil.handleBadRequest("Provided API Key isn't valid ", log);
         }
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/impl/ApplicationsApiServiceImpl.java-791-834 (1)

791-834: ⚠️ Potential issue | 🟡 Minor

Same issues repeated in the REGENERATE endpoint.

  1. Double negative typo (Line 816): "doesn't not exist" — same fix needed as in DELETE.

  2. Misleading error message (Line 818): Should use handleResourceNotFoundError instead of handleBadRequest when the application is not found.

  3. Unguarded debug log (Line 830): Same issue — log.debug(...) not guarded.

  4. long to int truncation (Line 803–804): (int) apiKeyInfo.getValidityPeriod() — same narrowing cast concern flagged in ApplicationKeyMappingUtil.

🐛 Suggested fixes
                 } else {
-                    if(log.isDebugEnabled()) {
-                        log.debug("Application with given id " + applicationId + " doesn't not exist ");
-                    }
-                    RestApiUtil.handleBadRequest("Validation failed for the given API Key ", log);
+                    RestApiUtil.handleResourceNotFoundError(RestApiConstants.RESOURCE_APPLICATION,
+                            applicationId, log);
                 }
         } else {
-            log.debug("Provided API Key " + keyDisplayName + " is not valid");
+            if (log.isDebugEnabled()) {
+                log.debug("Provided API Key display name " + keyDisplayName + " is not valid");
+            }
             RestApiUtil.handleBadRequest("Provided API Key isn't valid ", log);
         }
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/impl/ApplicationsApiServiceImpl.java-719-746 (1)

719-746: ⚠️ Potential issue | 🟡 Minor

Add keyType validation to DELETE and REGENERATE endpoints for consistency.

The GET endpoint (lines 731–734) validates that keyType is either PRODUCTION or SANDBOX. The DELETE (line 759) and REGENERATE (line 802) endpoints pass keyType directly to the backend without validation. The backend methods do not validate either. For consistency and to prevent unexpected behavior, add the same keyType validation to DELETE and REGENERATE endpoints before calling the consumer methods.

components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/devportal-api.yaml-7781-7788 (1)

7781-7788: ⚠️ Potential issue | 🟡 Minor

Document/encode keyDisplayName path parameter to avoid routing issues.

Display names can contain spaces/special chars; clarifying URL encoding (or marking encoded) prevents 404s from unencoded paths.

🔧 Suggested update
     keyDisplayName:
       name: keyDisplayName
       in: path
       description: |
-        Name of the API key.
+        Name of the API key. URL-encode reserved characters.
       required: true
       schema:
         type: string
+      x-encoded: true
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/ApiMgtDAO.java-16714-16715 (1)

16714-16715: ⚠️ Potential issue | 🟡 Minor

Typo in Javadoc: "LAst" → "Last".

-     * `@param` lastUsedTime LAst used time
+     * `@param` lastUsedTime Last used time
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/ApiMgtDAO.java-16576-16576 (1)

16576-16576: ⚠️ Potential issue | 🟡 Minor

Potential NPE if TIME_CREATED is NULL in the database.

rs.getTimestamp("TIME_CREATED").toString() will throw a NullPointerException if the column value is NULL. The same pattern appears at Line 16615 in getAllAPIKeys.

🛡️ Proposed defensive fix
-                    keyInfo.setCreatedTime(rs.getTimestamp("TIME_CREATED").toString());
+                    Timestamp createdTime = rs.getTimestamp("TIME_CREATED");
+                    keyInfo.setCreatedTime(createdTime != null ? createdTime.toString() : null);
components/apimgt/org.wso2.carbon.apimgt.notification/src/main/java/org/wso2/carbon/apimgt/notification/OpaqueAPIKeyNotifierImpl.java-86-86 (1)

86-86: ⚠️ Potential issue | 🟡 Minor

Long.parseLong on a potentially null property — NumberFormatException.

properties.getProperty(APIConstants.NotificationEvent.VALIDITY_PERIOD) may return null if the property is not set, causing Long.parseLong(null) to throw NumberFormatException. A default value or null check is needed.

Proposed fix
-        long validityPeriod = Long.parseLong(properties.getProperty(APIConstants.NotificationEvent.VALIDITY_PERIOD));
+        String validityPeriodStr = properties.getProperty(APIConstants.NotificationEvent.VALIDITY_PERIOD);
+        long validityPeriod = validityPeriodStr != null ? Long.parseLong(validityPeriodStr) : 0L;
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/listeners/OpaqueAPIKeyInfoListener.java-43-43 (1)

43-43: ⚠️ Potential issue | 🟡 Minor

Remove debug artifact: emoji log at INFO level.

This log.info("🔥 Opaque API Key JMS message RECEIVED") is clearly a development/debug artifact. It should be removed or converted to a log.debug call without the emoji before merging.

Proposed fix
-        log.info("🔥 Opaque API Key JMS message RECEIVED");
+        if (log.isDebugEnabled()) {
+            log.debug("Opaque API Key JMS message received");
+        }
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/listeners/OpaqueAPIKeyInfoListener.java-68-75 (1)

68-75: ⚠️ Potential issue | 🟡 Minor

asText() on a missing JSON node returns "", not null — null check is ineffective.

payload.path("additionalProperties").asText() returns an empty string "" when the node is absent, so the null check on line 70 will always pass. If additionalProperties is genuinely missing, StringEscapeUtils.unescapeJson("") produces "" and objectMapper.readValue("", ...) throws JsonProcessingException—caught but misleading.

Check for StringUtils.isNotEmpty instead, or use asText(null) which returns null for missing nodes.

Proposed fix
-                    String additionalPropsEscaped = payload.path("additionalProperties").asText();
+                    String additionalPropsEscaped = payload.path("additionalProperties").asText(null);
                     Map<String, String> additionalPropsMap = null;
                     if (additionalPropsEscaped != null) {
🧹 Nitpick comments (19)
components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/model/APIKeyInfo.java (2)

23-38: Consider using long for timestamp fields instead of String.

lastUsedTime and createdTime are declared as String, but timestamps are typically represented as long (epoch millis) in Java model classes. String timestamps are fragile—they depend on consistent formatting across producers and consumers. The JMS listener (in APIKeyUsageListener) passes these strings directly to the DAO, which could fail silently or store inconsistent formats.

That said, if the REST API contract requires string representation for backward compatibility, this may be intentional.


112-118: Mutable byte[] exposed via getter/setter.

getProperties() returns the internal byte[] reference, allowing callers to mutate the object's state. For a model class used across modules, consider defensive copying. This is a minor concern but worth noting.

components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/APIConsumer.java (1)

720-738: Javadoc for regenerateAPIKey is missing @return and @param username tags.

The revokeAPIKey Javadoc also omits @throws description, though that's consistent with other methods in this interface. The regenerateAPIKey Javadoc is notably missing documentation for the username parameter and the return type.

📝 Suggested Javadoc fix
     /**
      * Regenerate opaque api key for the given key display name with same properties
      * `@param` applicationId Id of the application
      * `@param` keyType Key type of the token
      * `@param` keyDisplayName Api key name
      * `@param` tenantDomain Tenant domain
+     * `@param` username Username of the user requesting regeneration
+     * `@return` APIKeyInfo containing the regenerated key details
      * `@throws` APIManagementException
      */
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dto/APIKeyDTO.java (2)

1-101: New DTO looks reasonable; note the naming overlap with the Admin REST API's APIKeyDTO.

There is already an APIKeyDTO in org.wso2.carbon.apimgt.rest.api.admin.v1.dto (with different fields: apikey + validityTime). While they reside in separate packages, the identical class name may confuse developers working across modules. Consider whether a more distinctive name (e.g., OpaqueAPIKeyDTO) would reduce ambiguity. This is a minor concern since packages disambiguate at compile time.


54-60: getApiKeyProperties() exposes the internal mutable byte[] array directly.

Callers can mutate the DTO's internal state through the returned reference. For a Serializable DTO, consider returning a defensive copy if immutability matters in your usage context.

🛡️ Defensive copy option
     public byte[] getApiKeyProperties() {
-        return apiKeyProperties;
+        return apiKeyProperties != null ? apiKeyProperties.clone() : null;
     }

     public void setApiKeyProperties(byte[] apiKeyProperties) {
-        this.apiKeyProperties = apiKeyProperties;
+        this.apiKeyProperties = apiKeyProperties != null ? apiKeyProperties.clone() : null;
     }
components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/mappings/ApplicationKeyMappingUtil.java (1)

36-37: Wildcard import java.util.* replaces specific imports.

Minor style nit — specific imports are generally preferred for readability and avoiding accidental namespace collisions, consistent with other files in this PR that use explicit imports.

components/apimgt/org.wso2.carbon.apimgt.rest.api.store.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/store/v1/impl/ApplicationsApiServiceImpl.java (1)

62-62: Wildcard import for store.v1.dto.*.

Same observation as in ApplicationKeyMappingUtil — explicit imports are generally preferred. This replaces what were specific imports, reducing import-level clarity.

components/apimgt/org.wso2.carbon.apimgt.rest.api.common/src/main/resources/devportal-api.yaml (1)

5994-6025: Clarify timestamp vs sentinel values in APIKeyInfo.

issuedOn looks like a timestamp while lastUsed can be "NOT_USED"; document the expected format and sentinel value to avoid client parsing ambiguity.

📝 Suggested doc clarification
         issuedOn:
           type: string
-          description: Created Time
+          description: Created time (e.g., "yyyy-MM-dd HH:mm:ss")
           example: 2026-02-06 23:45:07
         validityPeriod:
           type: integer
           format: int32
           example: 3600
         lastUsed:
           type: string
-          description: Last Used Time
+          description: Last used time in the same format as issuedOn; use "NOT_USED" when the key has never been used.
           example: NOT_USED
components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/dao/ApiMgtDAO.java (4)

45-45: Wildcard imports reduce readability and can cause ambiguous references.

Wildcard imports (org.wso2.carbon.apimgt.api.model.* and org.wso2.carbon.apimgt.impl.dto.* at Line 76) make it harder to determine which specific classes are used and can lead to conflicts if new classes are added to those packages in the future. Consider using explicit imports instead.


16513-16547: Missing explicit rollback on failure in write operations.

addAPIKey, revokeAPIKey, and updateAPIKeyUsage all call conn.setAutoCommit(false) and conn.commit() on success, but none issue conn.rollback() in the catch block before handleException rethrows. While most connection pools auto-rollback on close, the JDBC spec does not guarantee this. An explicit rollback is safer.

This applies to all three write methods in this PR (lines 16513, 16685, 16717).

♻️ Example fix for addAPIKey (apply same pattern to revokeAPIKey and updateAPIKeyUsage)
         } catch (SQLException e) {
+            if (conn != null) {
+                try {
+                    conn.rollback();
+                } catch (SQLException rollbackEx) {
+                    log.error("Failed to rollback adding API key", rollbackEx);
+                }
+            }
             handleException("Failed to add generated API keys", e);
         } finally {

16561-16563: Unnecessary setAutoCommit(false) on read-only queries.

getAPIKeys, getAllAPIKeys, and getAPIKey all disable auto-commit for pure SELECT operations. This opens an explicit transaction that is never committed, holding database resources until the connection is returned to the pool. For read-only methods, simply remove conn.setAutoCommit(false) or use the default auto-commit behavior.

♻️ Proposed fix (apply similarly to getAllAPIKeys and getAPIKey)
             conn = APIMgtDBUtil.getConnection();
-            conn.setAutoCommit(false);

16693-16701: Misleading SQL constant name DELETE_API_KEY_SQL for a soft-delete (status update).

The comment at Line 16695 says "Updating data… by setting STATUS to REVOKED", but the constant is named DELETE_API_KEY_SQL. If this is truly a status update rather than a DELETE FROM statement, the constant name is misleading. Consider renaming it to something like REVOKE_API_KEY_SQL for clarity.

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/utils/APIKeyUtils.java (1)

55-67: New method follows existing pattern — consider extracting a shared helper.

isJWTAPIKeyGenerationEnabled() duplicates the structure of isLightweightAPIKeyGenerationEnabled() line-for-line with only the config property key differing. A small private helper like getBooleanConfig(String propertyKey, boolean defaultValue) would reduce duplication.

Example refactor
+    private static boolean getBooleanConfig(String propertyKey, boolean defaultValue) {
+        try {
+            APIManagerConfiguration config = ServiceReferenceHolder.getInstance()
+                    .getAPIManagerConfigurationService().getAPIManagerConfiguration();
+            if (config != null) {
+                return Boolean.parseBoolean(config.getFirstProperty(propertyKey));
+            }
+        } catch (Exception e) {
+            log.error("Error while reading configuration: " + propertyKey, e);
+        }
+        return defaultValue;
+    }
+
     public static boolean isLightweightAPIKeyGenerationEnabled() {
-        try {
-            APIManagerConfiguration config = ServiceReferenceHolder.getInstance().getAPIManagerConfigurationService()
-                    .getAPIManagerConfiguration();
-            if (config != null) {
-                String lightweightAPIKeyGenerationEnabled = config.getFirstProperty(APIConstants.LIGHTWEIGHT_API_KEY_GENERATION_ENABLED);
-                return Boolean.parseBoolean(lightweightAPIKeyGenerationEnabled);
-            }
-        } catch (Exception e) {
-            log.error("Error while reading Lightweight API Key Generation configuration", e);
-        }
-        return true;
+        return getBooleanConfig(APIConstants.LIGHTWEIGHT_API_KEY_GENERATION_ENABLED, true);
     }

     public static boolean isJWTAPIKeyGenerationEnabled() {
-        try {
-            APIManagerConfiguration config = ServiceReferenceHolder.getInstance().getAPIManagerConfigurationService()
-                    .getAPIManagerConfiguration();
-            if (config != null) {
-                String jwtAPIKeyGenerationEnabled = config.getFirstProperty(APIConstants.JWT_API_KEY_GENERATION_ENABLED);
-                return Boolean.parseBoolean(jwtAPIKeyGenerationEnabled);
-            }
-        } catch (Exception e) {
-            log.error("Error while reading JWT API Key Generation configuration", e);
-        }
-        return true;
+        return getBooleanConfig(APIConstants.JWT_API_KEY_GENERATION_ENABLED, true);
     }
components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/listeners/OpaqueAPIKeyInfoListener.java (1)

52-52: Avoid creating ObjectMapper instances per message — use a shared static instance.

ObjectMapper is thread-safe for read operations and expensive to construct. Creating two instances per message (lines 52 and 73) adds unnecessary allocation overhead on the JMS message-processing hot path.

Proposed fix
 public class OpaqueAPIKeyInfoListener implements MessageListener {
 
     private static final Log log = LogFactory.getLog(OpaqueAPIKeyInfoListener.class);
+    private static final ObjectMapper objectMapper = new ObjectMapper();
 
     public void onMessage(Message message) {
         ...
-                    ObjectMapper objectMapper = new ObjectMapper();
                     // Navigate to payloadData
-                    JsonNode payload = null;
-                    payload = objectMapper.readTree(textMessage)
+                    JsonNode payload = objectMapper.readTree(textMessage)
                                 .path("event")
                                 .path("payloadData");
                     ...
-                    additionalPropsMap = new ObjectMapper().readValue(unescaped,
+                    additionalPropsMap = objectMapper.readValue(unescaped,
                             new TypeReference<Map<String, String>>() {});

Also applies to: 73-74

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/ApiKeyAuthenticatorUtils.java (1)

54-56: Unused imports: ByteArrayInputStream, IOException, ObjectInputStream.

These three imports don't appear to be used in the current file. They may be leftovers from an earlier iteration.

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/security/apikey/ApiKeyAuthenticator.java (2)

133-196: Redundant validateAPIKeyFormat call after length check.

Line 136 checks splitToken.length == 3 and line 137 calls validateAPIKeyFormat(splitToken) which only throws if length ≠ 3. The call is always a no-op here.

Proposed fix — remove the redundant guard or the redundant call
             if (StringUtils.isNotEmpty(apiKey) && apiKey.contains(APIConstants.DOT)) {
                 String[] splitToken = apiKey.split("\\.");
                 if (splitToken.length == 3) {
-                    ApiKeyAuthenticatorUtils.validateAPIKeyFormat(splitToken);
                     SignedJWT signedJWT = (SignedJWT) JWTParser.parse(apiKey);

288-289: TODO: Missing expiry check and caching for opaque API keys.

These TODO comments indicate that expiry validation and caching are not yet implemented. Without expiry checks, revoked or expired opaque keys may continue to authenticate successfully until the gateway restarts or the cache entry is evicted.

Would you like me to help draft the expiry check and caching logic, or open an issue to track this?

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/utils/APIUtil.java (2)

9215-9220: Avoid duplicate byte-to-hex helpers.
bytesToHex already exists; reuse it to prevent drift.

♻️ Proposed refactor
 public static String convertBytesToHex(byte[] bytes) {
-    StringBuilder hex = new StringBuilder(bytes.length * 2);
-    for (byte b : bytes) {
-        hex.append(String.format("%02x", b));
-    }
-    return hex.toString();
+    return bytesToHex(bytes);
 }

9198-9208: Add defensive validation for sharedSecret parameter.
While the current call sites use non-blank hardcoded values, validating the parameter follows the defensive programming pattern used throughout APIUtil and protects against future misconfiguration. Consider adding a null/blank check at method entry to fail fast with a clear error.

🛠️ Suggested improvement
 public static String generateLookupKey(String apiKey, String sharedSecret) throws APIManagementException {
+    if (StringUtils.isBlank(sharedSecret)) {
+        throw new APIManagementException("Shared secret is not configured for API key lookup");
+    }
     try {
         Mac mac = Mac.getInstance(APIConstants.HMAC_SHA_256);
         SecretKeySpec keySpec = new SecretKeySpec(sharedSecret.getBytes(StandardCharsets.UTF_8),

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 10, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 12, 2026
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.

4 participants