Skip to content

Commit c49638e

Browse files
Merge pull request #3611 from mevan-karu/api_key_impl
Add new API Key authenticator impl
2 parents 55a8b00 + fa62f95 commit c49638e

File tree

6 files changed

+240
-33
lines changed

6 files changed

+240
-33
lines changed

enforcer-parent/enforcer/src/main/java/org/wso2/choreo/connect/enforcer/api/Utils.java

+7
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ static void populateRemoveAndProtectedHeaders(RequestContext requestContext) {
9292
return;
9393
}
9494

95+
// Choreo-API-Key is considered as a protected header, hence header value should be treated
96+
// same as other security headers.
97+
if (ConfigHolder.getInstance().getConfig().getApiKeyConfig().getApiKeyInternalHeader() != null) {
98+
requestContext.getProtectedHeaders().add(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
99+
.getApiKeyInternalHeader().toLowerCase());
100+
}
101+
95102
// Internal-Key credential is considered to be protected headers, such that the
96103
// header would not be sent
97104
// to backend and traffic manager.

enforcer-parent/enforcer/src/main/java/org/wso2/choreo/connect/enforcer/security/AuthFilter.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ public class AuthFilter implements Filter {
5353
private List<Authenticator> authenticators = new ArrayList<>();
5454
private static final Logger log = LogManager.getLogger(AuthFilter.class);
5555

56+
private static boolean isAPIKeyEnabled = false;
57+
58+
static {
59+
if (System.getenv("API_KEY_ENABLED") != null) {
60+
isAPIKeyEnabled = Boolean.parseBoolean(System.getenv("API_KEY_ENABLED"));
61+
}
62+
}
63+
5664
@Override
5765
public void init(APIConfig apiConfig, Map<String, String> configProperties) {
5866
initializeAuthenticators(apiConfig);
@@ -85,7 +93,8 @@ private void initializeAuthenticators(APIConfig apiConfig) {
8593
} else if (apiSecurityLevel.trim().
8694
equalsIgnoreCase(APIConstants.API_SECURITY_OAUTH_BASIC_AUTH_API_KEY_MANDATORY)) {
8795
isOAuthBasicAuthMandatory = true;
88-
} else if (apiSecurityLevel.trim().equalsIgnoreCase(APIConstants.SWAGGER_API_KEY_AUTH_TYPE_NAME)) {
96+
} else if (isAPIKeyEnabled &&
97+
apiSecurityLevel.trim().equalsIgnoreCase(APIConstants.SWAGGER_API_KEY_AUTH_TYPE_NAME)) {
8998
isApiKeyProtected = true;
9099
}
91100
}

enforcer-parent/enforcer/src/main/java/org/wso2/choreo/connect/enforcer/security/jwt/APIKeyAuthenticator.java

+54-20
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
import net.minidev.json.JSONValue;
2323
import org.apache.logging.log4j.LogManager;
2424
import org.apache.logging.log4j.Logger;
25+
import org.wso2.choreo.connect.enforcer.common.CacheProvider;
2526
import org.wso2.choreo.connect.enforcer.commons.model.AuthenticationContext;
2627
import org.wso2.choreo.connect.enforcer.commons.model.RequestContext;
2728
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
2829
import org.wso2.choreo.connect.enforcer.constants.APIConstants;
2930
import org.wso2.choreo.connect.enforcer.constants.APISecurityConstants;
3031
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;
32+
import org.wso2.choreo.connect.enforcer.util.FilterUtils;
3133

3234
import java.util.Base64;
3335
import java.util.Map;
36+
import java.util.Optional;
3437

3538
/**
3639
* API Key authenticator.
@@ -39,14 +42,6 @@ public class APIKeyAuthenticator extends JWTAuthenticator {
3942

4043
private static final Logger log = LogManager.getLogger(APIKeyAuthenticator.class);
4144

42-
private static boolean isAPIKeyEnabled = false;
43-
44-
static {
45-
if (System.getenv("API_KEY_ENABLED") != null) {
46-
isAPIKeyEnabled = Boolean.parseBoolean(System.getenv("API_KEY_ENABLED"));
47-
}
48-
}
49-
5045
public APIKeyAuthenticator() {
5146
super();
5247
log.debug("API key authenticator initialized.");
@@ -55,17 +50,32 @@ public APIKeyAuthenticator() {
5550
@Override
5651
public boolean canAuthenticate(RequestContext requestContext) {
5752

58-
if (!isAPIKeyEnabled) {
59-
return false;
60-
}
6153
String apiKeyValue = getAPIKeyFromRequest(requestContext);
62-
return apiKeyValue != null && apiKeyValue.startsWith(APIKeyConstants.API_KEY_PREFIX);
54+
return apiKeyValue != null && apiKeyValue.startsWith(APIKeyConstants.API_KEY_PREFIX) &&
55+
apiKeyValue.length() > 10;
6356
}
6457

6558
@Override
6659
public AuthenticationContext authenticate(RequestContext requestContext) throws APISecurityException {
6760

68-
return super.authenticate(requestContext);
61+
AuthenticationContext authCtx = super.authenticate(requestContext);
62+
// Drop the API key data from the API key header.
63+
dropAPIKeyDataFromAPIKeyHeader(requestContext);
64+
return authCtx;
65+
}
66+
67+
private void dropAPIKeyDataFromAPIKeyHeader(RequestContext requestContext) throws APISecurityException {
68+
69+
String apiKeyHeaderValue = getAPIKeyFromRequest(requestContext).trim();
70+
String checksum = apiKeyHeaderValue.substring(apiKeyHeaderValue.length() - 6);
71+
JSONObject jsonObject = getDecodedAPIKeyData(apiKeyHeaderValue);
72+
jsonObject.remove(APIKeyConstants.API_KEY_JSON_KEY);
73+
// Update the header with the new API key data.
74+
String encodedKeyData = Base64.getEncoder().encodeToString(jsonObject.toJSONString().getBytes());
75+
String newAPIKeyHeaderValue = APIKeyConstants.API_KEY_PREFIX + encodedKeyData + checksum;
76+
// Add the new header.
77+
requestContext.addOrModifyHeaders(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
78+
.getApiKeyInternalHeader().toLowerCase(), newAPIKeyHeaderValue);
6979
}
7080

7181
private String getAPIKeyFromRequest(RequestContext requestContext) {
@@ -74,26 +84,50 @@ private String getAPIKeyFromRequest(RequestContext requestContext) {
7484
.getApiKeyInternalHeader().toLowerCase());
7585
}
7686

77-
@Override
78-
protected String retrieveTokenFromRequestCtx(RequestContext requestContext) throws APISecurityException {
79-
87+
private JSONObject getDecodedAPIKeyData(String apiKeyHeaderValue) throws APISecurityException {
8088
try {
81-
String apiKeyHeaderValue = getAPIKeyFromRequest(requestContext).trim();
8289
// Skipping the prefix(`chk_`) and checksum.
8390
String apiKeyData = apiKeyHeaderValue.substring(4, apiKeyHeaderValue.length() - 6);
8491
// Base 64 decode key data.
8592
String decodedKeyData = new String(Base64.getDecoder().decode(apiKeyData));
8693
// Convert data into JSON.
87-
JSONObject jsonObject = (JSONObject) JSONValue.parse(decodedKeyData);
88-
// Extracting the jwt token.
89-
return jsonObject.getAsString(APIKeyConstants.API_KEY_JSON_KEY);
94+
return (JSONObject) JSONValue.parse(decodedKeyData);
9095
} catch (Exception e) {
9196
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
9297
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
9398
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
9499
}
95100
}
96101

102+
@Override
103+
protected String retrieveTokenFromRequestCtx(RequestContext requestContext) throws APISecurityException {
104+
105+
String apiKey = getAPIKeyFromRequest(requestContext).trim();
106+
if (!APIKeyUtils.isValidAPIKey(apiKey)) {
107+
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
108+
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
109+
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
110+
}
111+
String keyHash = APIKeyUtils.generateAPIKeyHash(apiKey);
112+
Object cachedJWT = CacheProvider.getGatewayAPIKeyJWTCache().getIfPresent(keyHash);
113+
if (cachedJWT != null && !APIKeyUtils.isJWTExpired((String) cachedJWT)) {
114+
if (log.isDebugEnabled()) {
115+
log.debug("Token retrieved from the cache. Token: " + FilterUtils.getMaskedToken(keyHash));
116+
}
117+
return (String) cachedJWT;
118+
}
119+
// Exchange the API Key to a JWT token.
120+
Optional<String> jwt = APIKeyUtils.exchangeAPIKeyToJWT(keyHash);
121+
if (jwt.isEmpty()) {
122+
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
123+
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
124+
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
125+
}
126+
// Cache the JWT token.
127+
CacheProvider.getGatewayAPIKeyJWTCache().put(keyHash, jwt.get());
128+
return jwt.get();
129+
}
130+
97131
@Override
98132
public String getChallengeString() {
99133
return "";

enforcer-parent/enforcer/src/main/java/org/wso2/choreo/connect/enforcer/security/jwt/APIKeyUtils.java

+20-11
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
import org.apache.logging.log4j.LogManager;
3636
import org.apache.logging.log4j.Logger;
3737
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
38+
import org.wso2.choreo.connect.enforcer.constants.APIConstants;
39+
import org.wso2.choreo.connect.enforcer.constants.APISecurityConstants;
40+
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;
3841
import org.wso2.choreo.connect.enforcer.util.FilterUtils;
3942

4043
import java.io.InputStream;
@@ -80,18 +83,24 @@ public static boolean isValidAPIKey(String apiKey) {
8083
* @param apiKey API Key
8184
* @return key hash
8285
*/
83-
public static String generateAPIKeyHash(String apiKey) {
86+
public static String generateAPIKeyHash(String apiKey) throws APISecurityException {
8487

85-
// Skipping the prefix(`chp_`) and checksum.
86-
String keyData = apiKey.substring(4, apiKey.length() - 6);
87-
// Base 64 decode key data.
88-
String decodedKeyData = new String(Base64.getDecoder().decode(keyData));
89-
// Convert data into JSON.
90-
JSONObject jsonObject = (JSONObject) JSONValue.parse(decodedKeyData);
91-
// Extracting the key.
92-
String key = jsonObject.getAsString(APIKeyConstants.API_KEY_JSON_KEY);
93-
// Return SHA256 hash of the key.
94-
return DigestUtils.sha256Hex(key);
88+
try {
89+
// Skipping the prefix(`chp_`) and checksum.
90+
String keyData = apiKey.substring(4, apiKey.length() - 6);
91+
// Base 64 decode key data.
92+
String decodedKeyData = new String(Base64.getDecoder().decode(keyData));
93+
// Convert data into JSON.
94+
JSONObject jsonObject = (JSONObject) JSONValue.parse(decodedKeyData);
95+
// Extracting the key.
96+
String key = jsonObject.getAsString(APIKeyConstants.API_KEY_JSON_KEY);
97+
// Return SHA256 hash of the key.
98+
return DigestUtils.sha256Hex(key);
99+
} catch (Exception e) {
100+
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
101+
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
102+
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
103+
}
95104
}
96105

97106
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com)
3+
*
4+
* WSO2 LLC. 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.choreo.connect.enforcer.security.jwt;
20+
21+
import com.google.common.cache.LoadingCache;
22+
import org.junit.Assert;
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.mockito.Mockito;
27+
import org.powermock.api.mockito.PowerMockito;
28+
import org.powermock.core.classloader.annotations.PrepareForTest;
29+
import org.powermock.modules.junit4.PowerMockRunner;
30+
import org.wso2.carbon.apimgt.common.gateway.dto.JWTConfigurationDto;
31+
import org.wso2.choreo.connect.enforcer.common.CacheProvider;
32+
import org.wso2.choreo.connect.enforcer.commons.model.APIConfig;
33+
import org.wso2.choreo.connect.enforcer.commons.model.RequestContext;
34+
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
35+
import org.wso2.choreo.connect.enforcer.config.EnforcerConfig;
36+
import org.wso2.choreo.connect.enforcer.config.dto.APIKeyDTO;
37+
import org.wso2.choreo.connect.enforcer.config.dto.CacheDto;
38+
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;
39+
40+
import java.util.HashMap;
41+
import java.util.Map;
42+
import java.util.Optional;
43+
44+
@RunWith(PowerMockRunner.class)
45+
@PrepareForTest({APIKeyUtils.class, CacheProvider.class, ConfigHolder.class})
46+
public class APIKeyAuthenticatorTest {
47+
48+
@Before
49+
public void setup() {
50+
PowerMockito.mockStatic(ConfigHolder.class);
51+
ConfigHolder configHolder = PowerMockito.mock(ConfigHolder.class);
52+
PowerMockito.when(ConfigHolder.getInstance()).thenReturn(configHolder);
53+
EnforcerConfig enforcerConfig = PowerMockito.mock(EnforcerConfig.class);
54+
PowerMockito.when(configHolder.getConfig()).thenReturn(enforcerConfig);
55+
APIKeyDTO apiKeyDTO = PowerMockito.mock(APIKeyDTO.class);
56+
PowerMockito.when(enforcerConfig.getApiKeyConfig()).thenReturn(apiKeyDTO);
57+
PowerMockito.when(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
58+
.getApiKeyInternalHeader()).thenReturn("choreo-api-key");
59+
CacheDto cacheDto = Mockito.mock(CacheDto.class);
60+
Mockito.when(cacheDto.isEnabled()).thenReturn(true);
61+
Mockito.when(enforcerConfig.getCacheDto()).thenReturn(cacheDto);
62+
JWTConfigurationDto jwtConfigurationDto = Mockito.mock(JWTConfigurationDto.class);
63+
Mockito.when(jwtConfigurationDto.isEnabled()).thenReturn(false);
64+
Mockito.when(enforcerConfig.getJwtConfigurationDto()).thenReturn(jwtConfigurationDto);
65+
}
66+
67+
@Test
68+
public void retrieveTokenFromRequestCtxTest_invalidKey() {
69+
70+
RequestContext.Builder requestContextBuilder = new RequestContext.Builder("/api-key");
71+
requestContextBuilder.matchedAPI(new APIConfig.Builder("Petstore")
72+
.basePath("/test")
73+
.apiType("REST")
74+
.build());
75+
Map<String, String> headersMap = new HashMap<>();
76+
headersMap.put("choreo-api-key",
77+
"chk_eyJrZXkiOiJieTlpYXQ5d3MycDY0dWF6anFkbzQ4cnAyYnY3aWoxdWRuYmRzNzN6ZWx5OWNoZHJ2YiJ97JYpag");
78+
requestContextBuilder.headers(headersMap);
79+
RequestContext requestContext = requestContextBuilder.build();
80+
81+
APIKeyAuthenticator apiKeyAuthenticator = new APIKeyAuthenticator();
82+
Assert.assertThrows(APISecurityException.class, () ->
83+
apiKeyAuthenticator.retrieveTokenFromRequestCtx(requestContext));
84+
}
85+
86+
@Test
87+
public void retrieveTokenFromRequestCtxTest_cached_validKey() throws APISecurityException {
88+
89+
String mockJWT = "eyJrZXkiOiJieTlpYXQ5d3MycDY0dWF6anFkbzQ4cnAyYnY3aWoxdWRuYmRzNzN6ZWx5OWNoZHJ2YiJ97JYPAg";
90+
PowerMockito.mockStatic(APIKeyUtils.class);
91+
PowerMockito.when(APIKeyUtils.isValidAPIKey(Mockito.anyString())).thenReturn(true);
92+
PowerMockito.when(APIKeyUtils.generateAPIKeyHash(Mockito.anyString())).thenReturn("key_hash");
93+
PowerMockito.when(APIKeyUtils.isJWTExpired(Mockito.anyString())).thenReturn(false);
94+
95+
PowerMockito.mockStatic(CacheProvider.class);
96+
LoadingCache gatewayAPIKeyJWTCache = PowerMockito.mock(LoadingCache.class);
97+
PowerMockito.when(CacheProvider.getGatewayAPIKeyJWTCache()).thenReturn(gatewayAPIKeyJWTCache);
98+
PowerMockito.when(gatewayAPIKeyJWTCache.getIfPresent(Mockito.anyString())).thenReturn(mockJWT);
99+
100+
RequestContext.Builder requestContextBuilder = new RequestContext.Builder("/api-key");
101+
requestContextBuilder.matchedAPI(new APIConfig.Builder("Petstore")
102+
.basePath("/test")
103+
.apiType("REST")
104+
.build());
105+
Map<String, String> headersMap = new HashMap<>();
106+
headersMap.put("choreo-api-key",
107+
"chk_eyJhdHRyMSI6InYxIiwiY29ubmVjdGlvbklkIjoiNjAwM2EzYjctYWYwZi00ZmIzLTg1M2UtYTY1NjJiMjM0N" +
108+
"WYyIiwia2V5IjoieG5lcGVxZmZ4eWx2Y2Q4a3FnNHprZDFpMHoxMnA2dTBqcW50aDUyM3JlN292a2pudncifQBdZRRQ");
109+
requestContextBuilder.headers(headersMap);
110+
RequestContext requestContext = requestContextBuilder.build();
111+
112+
APIKeyAuthenticator apiKeyAuthenticator = new APIKeyAuthenticator();
113+
String token = apiKeyAuthenticator.retrieveTokenFromRequestCtx(requestContext);
114+
Assert.assertEquals(mockJWT, token);
115+
}
116+
117+
@Test
118+
public void retrieveTokenFromRequestCtxTest_validKey() throws APISecurityException {
119+
120+
PowerMockito.mockStatic(APIKeyUtils.class);
121+
String mockJWT = "eyJrZXkiOiJieTlpYXQ5d3MycDY0dWF6anFkbzQ4cnAyYnY3aWoxdWRuYmRzNzN6ZWx5OWNoZHJ2YiJ97JYPAg";
122+
PowerMockito.when(APIKeyUtils.exchangeAPIKeyToJWT(Mockito.anyString())).thenReturn(Optional.of(mockJWT));
123+
PowerMockito.when(APIKeyUtils.isValidAPIKey(Mockito.anyString())).thenReturn(true);
124+
PowerMockito.when(APIKeyUtils.generateAPIKeyHash(Mockito.anyString())).thenReturn("key_hash");
125+
126+
PowerMockito.mockStatic(CacheProvider.class);
127+
LoadingCache gatewayAPIKeyJWTCache = PowerMockito.mock(LoadingCache.class);
128+
PowerMockito.when(CacheProvider.getGatewayAPIKeyJWTCache()).thenReturn(gatewayAPIKeyJWTCache);
129+
PowerMockito.when(gatewayAPIKeyJWTCache.getIfPresent(Mockito.anyString())).thenReturn(null);
130+
131+
RequestContext.Builder requestContextBuilder = new RequestContext.Builder("/api-key");
132+
requestContextBuilder.matchedAPI(new APIConfig.Builder("Petstore")
133+
.basePath("/test")
134+
.apiType("REST")
135+
.build());
136+
Map<String, String> headersMap = new HashMap<>();
137+
headersMap.put("choreo-api-key",
138+
"chk_eyJhdHRyMSI6InYxIiwiY29ubmVjdGlvbklkIjoiNjAwM2EzYjctYWYwZi00ZmIzLTg1M2UtYTY1NjJiMjM0N" +
139+
"WYyIiwia2V5IjoieG5lcGVxZmZ4eWx2Y2Q4a3FnNHprZDFpMHoxMnA2dTBqcW50aDUyM3JlN292a2pudncifQBdZRRQ");
140+
requestContextBuilder.headers(headersMap);
141+
RequestContext requestContext = requestContextBuilder.build();
142+
143+
APIKeyAuthenticator apiKeyAuthenticator = new APIKeyAuthenticator();
144+
String token = apiKeyAuthenticator.retrieveTokenFromRequestCtx(requestContext);
145+
Assert.assertEquals(mockJWT, token);
146+
}
147+
}

enforcer-parent/enforcer/src/test/java/org/wso2/choreo/connect/enforcer/security/jwt/APIKeyUtilsTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.powermock.core.classloader.annotations.PrepareForTest;
2525
import org.powermock.modules.junit4.PowerMockRunner;
2626
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
27+
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;
2728

2829
@RunWith(PowerMockRunner.class)
2930
@PrepareForTest({ConfigHolder.class})
@@ -45,7 +46,7 @@ public void testIsValidAPIKey_invalid() {
4546
}
4647

4748
@Test
48-
public void testGenerateAPIKeyHash() {
49+
public void testGenerateAPIKeyHash() throws APISecurityException {
4950
String apiKey = "chp_eyJrZXkiOiJlanp6am8yaGc5MnA2MTF6NTI2OXMzNzU1ZnJzbnFlNm9vb2hldWd0djBjbmQ3bXdobCJ9dknDJA";
5051
String expectedKeyHash = "62f73948188c9f773414d4ec77eae6e8caab21556e4ad18f94b7c6c5b018524c";
5152
String generatedAPIKeyHash = APIKeyUtils.generateAPIKeyHash(apiKey);

0 commit comments

Comments
 (0)