Skip to content

Commit 9720efb

Browse files
author
Naduni Pamudika
committed
Add opaque api key validation logic
1 parent d81b735 commit 9720efb

File tree

17 files changed

+629
-126
lines changed

17 files changed

+629
-126
lines changed

components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/model/APIKeyInfo.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public class APIKeyInfo {
2222
private String keyType;
2323
private String lastUsedTime;
2424
private String createdTime;
25+
private String salt;
26+
private String lookupKey;
2527
private long validityPeriod;
2628
private byte[] properties;
2729
private String apiKeyHash;
@@ -68,6 +70,22 @@ public void setCreatedTime(String createdTime) {
6870
this.createdTime = createdTime;
6971
}
7072

73+
public String getSalt() {
74+
return salt;
75+
}
76+
77+
public void setSalt(String salt) {
78+
this.salt = salt;
79+
}
80+
81+
public String getLookupKey() {
82+
return lookupKey;
83+
}
84+
85+
public void setLookupKey(String lookupKey) {
86+
this.lookupKey = lookupKey;
87+
}
88+
7189
public long getValidityPeriod() {
7290
return validityPeriod;
7391
}

components/apimgt/org.wso2.carbon.apimgt.eventing/src/main/java/org/wso2/carbon/apimgt/eventing/EventPublisherType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public enum EventPublisherType {
3030
GLOBAL_CACHE_INVALIDATION,
3131
TOKEN_REVOCATION,
3232
API_KEY_USAGE,
33+
API_KEY_INFO,
3334
ASYNC_WEBHOOKS,
3435
ORGANIZATION_PURGE,
3536
LLMPROVIDER_EVENT

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

Lines changed: 152 additions & 60 deletions
Large diffs are not rendered by default.

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/inbound/websocket/Authentication/ApiKeyAuthenticator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public InboundProcessorResponseDTO authenticate(InboundMessageContext inboundMes
110110
apiKey, tenantDomain, payload);
111111
ApiKeyAuthenticatorUtils.validateAPIKeyRestrictions(payload, inboundMessageContext.getUserIP(),
112112
apiContext, apiVersion, inboundMessageContext.getRequestHeaders().
113-
get(APIMgtGatewayConstants.REFERER));
113+
get(APIMgtGatewayConstants.REFERER), null);
114114
APIKeyValidationInfoDTO apiKeyValidationInfoDTO = GatewayUtils.validateAPISubscription(apiContext, apiVersion,
115115
payload, splitToken[0]);
116116
String endUserToken = ApiKeyAuthenticatorUtils.getEndUserToken(apiKeyValidationInfoDTO, jwtConfigurationDto,

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/internal/DataHolder.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.wso2.carbon.apimgt.api.APIManagementException;
2727
import org.wso2.carbon.apimgt.api.gateway.GatewayAPIDTO;
2828
import org.wso2.carbon.apimgt.api.gateway.GraphQLSchemaDTO;
29+
import org.wso2.carbon.apimgt.api.model.APIKeyInfo;
2930
import org.wso2.carbon.apimgt.api.model.LLMProviderInfo;
3031
import org.wso2.carbon.apimgt.api.model.VHost;
3132
import org.wso2.carbon.apimgt.common.gateway.jwtgenerator.AbstractAPIMgtGatewayJWTGenerator;
@@ -58,6 +59,7 @@ public class DataHolder {
5859
private Map<String,Map<String, API>> tenantAPIMap = new HashMap<>();
5960
private Map<String, Boolean> tenantDeployStatus = new HashMap<>();
6061
private Map<String, LLMProviderInfo> llmProviderMap = new HashMap<>();
62+
private Map<String, APIKeyInfo> apiKeyInfoHashMap = new HashMap<>();
6163
private final Map<String, Cache<String, Long>> apiSuspendedEndpoints = new ConcurrentHashMap<>();
6264
private final ConcurrentMap<String, AbstractAPIMgtGatewayJWTGenerator> jwtGeneratorTenantMap =
6365
new ConcurrentHashMap<>();
@@ -147,6 +149,35 @@ public static DataHolder getInstance() {
147149
return Instance;
148150
}
149151

152+
/**
153+
* Adds a new opaque api key info.
154+
*
155+
* @param apiKeyInfo the api key info to add
156+
*/
157+
public void addOpaqueAPIKeyInfo(APIKeyInfo apiKeyInfo) {
158+
159+
apiKeyInfoHashMap.put(apiKeyInfo.getLookupKey(), apiKeyInfo);
160+
}
161+
162+
/**
163+
* Returns opaque api key info for the given lookup key
164+
*
165+
*/
166+
public APIKeyInfo getOpaqueAPIKeyInfo(String lookupKey) {
167+
168+
return apiKeyInfoHashMap.get(lookupKey);
169+
}
170+
171+
/**
172+
* Removes an opaque api key info.
173+
*
174+
* @param apiKeyHash the api key hash to remove
175+
*/
176+
public void removeOpaqueAPIKeyInfo(String apiKeyHash) {
177+
178+
apiKeyInfoHashMap.remove(apiKeyHash);
179+
}
180+
150181
public void addApiToAliasList(String apiId, List<String> aliasList) {
151182

152183
apiToCertificatesMap.put(apiId, aliasList);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
3+
*
4+
* WSO2 Inc. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
package org.wso2.carbon.apimgt.gateway.listeners;
20+
21+
import com.fasterxml.jackson.core.JsonProcessingException;
22+
import com.fasterxml.jackson.databind.JsonNode;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import org.apache.commons.codec.binary.Base64;
25+
import org.apache.commons.lang3.StringUtils;
26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
28+
import org.wso2.carbon.apimgt.api.model.APIKeyInfo;
29+
import org.wso2.carbon.apimgt.gateway.internal.DataHolder;
30+
import org.wso2.carbon.apimgt.impl.APIConstants;
31+
32+
import javax.jms.JMSException;
33+
import javax.jms.Message;
34+
import javax.jms.MessageListener;
35+
import javax.jms.TextMessage;
36+
import javax.jms.Topic;
37+
import java.util.HashMap;
38+
39+
public class OpaqueAPIKeyMessageListener implements MessageListener {
40+
41+
private static final Log log = LogFactory.getLog(OpaqueAPIKeyMessageListener.class);
42+
43+
public void onMessage(Message message) {
44+
45+
try {
46+
if (message != null) {
47+
if (log.isDebugEnabled()) {
48+
log.debug("Event received in JMS Event Receiver - " + message);
49+
}
50+
Topic jmsDestination = (Topic) message.getJMSDestination();
51+
if (message instanceof TextMessage) {
52+
String textMessage = ((TextMessage) message).getText();
53+
JsonNode payloadData = new ObjectMapper().readTree(textMessage).path(APIConstants.EVENT_PAYLOAD).
54+
path(APIConstants.EVENT_PAYLOAD_DATA);
55+
56+
if (APIConstants.TopicNames.OPAQUE_API_KEY_INFO.equalsIgnoreCase(jmsDestination.getTopicName())) {
57+
if (payloadData.get(APIConstants.NotificationEvent.API_KEY_HASH).asText() != null) {
58+
/*
59+
* This message contains opaque api key data
60+
*/
61+
handleOpaqueAPIKeyInfoMessage(payloadData.get(APIConstants.ENCODED_API_KEY_INFO).asText());
62+
}
63+
}
64+
} else {
65+
log.warn("Event dropped due to unsupported message type " + message.getClass());
66+
}
67+
} else {
68+
log.warn("Dropping the empty/null event received through jms receiver");
69+
}
70+
} catch (JMSException | JsonProcessingException e) {
71+
log.error("JMSException occurred when processing the received message ", e);
72+
}
73+
}
74+
75+
private void handleOpaqueAPIKeyInfoMessage(String apiKeyInfoEvent) {
76+
77+
if (StringUtils.isEmpty(apiKeyInfoEvent)) {
78+
return;
79+
}
80+
HashMap<String, Object> opaqueApiKeyInfoMap = base64Decode(apiKeyInfoEvent);
81+
if (opaqueApiKeyInfoMap.containsKey(APIConstants.NotificationEvent.LOOKUP_KEY) &&
82+
opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.LOOKUP_KEY) != null) {
83+
APIKeyInfo apiKeyInfo = new APIKeyInfo();
84+
apiKeyInfo.setLookupKey((String) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.LOOKUP_KEY));
85+
apiKeyInfo.setApiKeyHash((String) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.API_KEY_HASH));
86+
apiKeyInfo.setApplicationId((String) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.APPLICATION_ID));
87+
apiKeyInfo.setSalt((String) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.SALT));
88+
apiKeyInfo.setKeyType((String) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.KEY_TYPE));
89+
apiKeyInfo.setValidityPeriod((Long) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.VALIDITY_PERIOD));
90+
apiKeyInfo.setStatus((String) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.STATUS));
91+
apiKeyInfo.setProperties((byte[]) opaqueApiKeyInfoMap.get(APIConstants.NotificationEvent.ADDITIONAL_PROPERTIES));
92+
DataHolder.getInstance().addOpaqueAPIKeyInfo(apiKeyInfo);
93+
}
94+
}
95+
96+
private HashMap<String, Object> base64Decode(String encodedOpaqueAPIKeyInfo) {
97+
98+
byte[] eventDecoded = Base64.decodeBase64(encodedOpaqueAPIKeyInfo);
99+
String eventJson = new String(eventDecoded);
100+
ObjectMapper objectMapper = new ObjectMapper();
101+
try {
102+
return objectMapper.readValue(eventJson, HashMap.class);
103+
} catch (JsonProcessingException e) {
104+
log.error("Error while decoding opaque api key event.");
105+
}
106+
return new HashMap<>();
107+
}
108+
}

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,12 @@
5151
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
5252

5353
import javax.cache.Cache;
54+
import java.io.ByteArrayInputStream;
55+
import java.io.IOException;
56+
import java.io.ObjectInputStream;
5457
import java.util.Base64;
5558
import java.util.HashMap;
59+
import java.util.Properties;
5660

5761
/**
5862
* This class contains the common utility methods required for API Key authentication.
@@ -279,11 +283,28 @@ public static void checkTokenExpired(boolean isGatewayTokenCacheEnabled, String
279283
* @throws APISecurityException If the API Key is not allowed to access the API.
280284
*/
281285
public static void validateAPIKeyRestrictions(JWTClaimsSet payload, String clientIP, String apiContext,
282-
String apiVersion, String referer) throws APISecurityException {
286+
String apiVersion, String referer, byte[] additionalProperties)
287+
throws APISecurityException, APIManagementException {
283288

284289
String permittedIPList = null;
285-
if (payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_IP) != null) {
286-
permittedIPList = (String) payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_IP);
290+
String permittedRefererList = null;
291+
if (payload != null) {
292+
if (payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_IP) != null) {
293+
permittedIPList = (String) payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_IP);
294+
}
295+
if (payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_REFERER) != null) {
296+
permittedRefererList = (String) payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_REFERER);
297+
}
298+
} else {
299+
try {
300+
// Taking values from the DB for an opaque API key
301+
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(additionalProperties));
302+
Properties props = (Properties) ois.readObject();
303+
permittedIPList = props.getProperty("permittedIP");
304+
permittedRefererList = props.getProperty("permittedReferer");
305+
} catch (IOException | ClassNotFoundException e) {
306+
throw new APIManagementException("Error while parsing API key additional properties", e);
307+
}
287308
}
288309
if (StringUtils.isNotEmpty(permittedIPList)) {
289310
// Validate client IP against permitted IPs
@@ -304,11 +325,6 @@ public static void validateAPIKeyRestrictions(JWTClaimsSet payload, String clien
304325
"Access forbidden for the invocations");
305326
}
306327
}
307-
308-
String permittedRefererList = null;
309-
if (payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_REFERER) != null) {
310-
permittedRefererList = (String) payload.getClaim(APIConstants.JwtTokenConstants.PERMITTED_REFERER);
311-
}
312328
if (StringUtils.isNotEmpty(permittedRefererList)) {
313329
// Validate http referer against the permitted referrers
314330
if (StringUtils.isNotEmpty(referer)) {

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/GatewayUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,11 @@ public static AuthenticationContext generateAuthenticationContext(String tokenSi
696696
AuthenticationContext authContext = new AuthenticationContext();
697697
authContext.setAuthenticated(true);
698698
authContext.setApiKey(tokenSignature);
699+
if (payload != null) {
699700
authContext.setUsername(payload.getSubject());
701+
} else {
702+
authContext.setUsername(apiKeyValidationInfoDTO.getEndUserName());
703+
}
700704

701705
if (apiKeyValidationInfoDTO != null) {
702706
authContext.setApiTier(apiKeyValidationInfoDTO.getApiTier());
@@ -720,7 +724,7 @@ public static AuthenticationContext generateAuthenticationContext(String tokenSi
720724
authContext.setGraphQLMaxComplexity(apiKeyValidationInfoDTO.getGraphQLMaxComplexity());
721725
}
722726
// Set JWT token sent to the backend
723-
if (StringUtils.isNotEmpty(endUserToken)) {
727+
if (StringUtils.isNotEmpty(endUserToken) && endUserToken != null) {
724728
authContext.setCallerToken(endUserToken);
725729
}
726730
return authContext;

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIConstants.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,7 @@ private AI() {
827827
public static final String BLOCKING_CONDITIONS_STREAM_ID = "org.wso2.blocking.request.stream:1.0.0";
828828
public static final String TOKEN_REVOCATION_STREAM_ID = "org.wso2.apimgt.token.revocation.stream:1.0.0";
829829
public static final String API_KEY_USAGE_STREAM_ID = "org.wso2.apimgt.api.key.usage.stream:1.0.0";
830+
public static final String API_KEY_INFO_STREAM_ID = "org.wso2.apimgt.api.key.info.stream:1.0.0";
830831
public static final String CACHE_INVALIDATION_STREAM_ID = "org.wso2.apimgt.cache.invalidation.stream:1.0.0";
831832
public static final String NOTIFICATION_STREAM_ID = "org.wso2.apimgt.notification.stream:1.0.0";
832833
public static final String WEBHOOKS_SUBSCRIPTION_STREAM_ID = "org.wso2.apimgt.webhooks.request.stream:1.0.0";
@@ -2333,6 +2334,7 @@ public enum RegistryResourceTypesForUI {
23332334
public static final String BLOCK_CONDITION_TYPE = "conditionType";
23342335
public static final String BLOCK_CONDITION_VALUE = "conditionValue";
23352336
public static final String REVOKED_TOKEN_KEY = "revokedToken";
2337+
public static final String ENCODED_API_KEY_INFO = "encodedApiKeyInfo";
23362338
public static final String REVOKED_TOKEN_EXPIRY_TIME = "expiryTime";
23372339
public static final String EVENT_TYPE = "eventType";
23382340
public static final String EVENT_WAITING_TIME_CONFIG = "EventWaitingTime";
@@ -3286,6 +3288,7 @@ public static class TopicNames {
32863288
public static final String TOPIC_CACHE_INVALIDATION = "cacheInvalidation";
32873289
public static final String TOPIC_KEY_MANAGER = "keyManager";
32883290
public static final String TOPIC_NOTIFICATION = "notification";
3291+
public static final String OPAQUE_API_KEY_INFO = "opaqueApiKeyInfo";
32893292
public static final String TOPIC_ASYNC_WEBHOOKS_DATA = "asyncWebhooksData";
32903293
}
32913294

@@ -3360,15 +3363,24 @@ public static class NotificationEvent {
33603363

33613364
public static final String TOKEN_TYPE = "token_type";
33623365
public static final String USAGE_TYPE = "usage_type";
3366+
public static final String INFO_TYPE = "info_type";
33633367
public static final String TOKEN_REVOCATION_EVENT = "token_revocation";
3364-
public static final String API_KEY_USAGE_EVENT = "key_usage";
3368+
public static final String API_KEY_USAGE_EVENT = "api_key_usage";
3369+
public static final String API_KEY_INFO_EVENT = "api_key_info";
33653370
public static final String CONSUMER_APP_REVOCATION_EVENT
33663371
= "consumer_app_revocation_event";
33673372
public static final String SUBJECT_ENTITY_REVOCATION_EVENT
33683373
= "subject_entity_revocation_event";
33693374
public static final String CONSUMER_KEY = "consumer_key";
33703375
public static final String API_KEY_HASH = "apiKeyHash";
33713376
public static final String API_KEY = "apiKey";
3377+
public static final String SALT = "salt";
3378+
public static final String LOOKUP_KEY = "lookupKey";
3379+
public static final String KEY_TYPE = "keyType";
3380+
public static final String ADDITIONAL_PROPERTIES = "additionalProperties";
3381+
public static final String APPLICATION_ID = "applicationId";
3382+
public static final String VALIDITY_PERIOD = "validityPeriod";
3383+
public static final String STATUS = "status";
33723384
public static final String EVENT_ID = "eventId";
33733385
public static final String TENANT_ID = "tenantId";
33743386
public static final String TENANT_DOMAIN = "tenant_domain";

0 commit comments

Comments
 (0)