Skip to content

Commit

Permalink
Merge pull request #3611 from mevan-karu/api_key_impl
Browse files Browse the repository at this point in the history
Add new API Key authenticator impl
  • Loading branch information
renuka-fernando authored Oct 29, 2024
2 parents 55a8b00 + fa62f95 commit c49638e
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ static void populateRemoveAndProtectedHeaders(RequestContext requestContext) {
return;
}

// Choreo-API-Key is considered as a protected header, hence header value should be treated
// same as other security headers.
if (ConfigHolder.getInstance().getConfig().getApiKeyConfig().getApiKeyInternalHeader() != null) {
requestContext.getProtectedHeaders().add(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
.getApiKeyInternalHeader().toLowerCase());
}

// Internal-Key credential is considered to be protected headers, such that the
// header would not be sent
// to backend and traffic manager.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ public class AuthFilter implements Filter {
private List<Authenticator> authenticators = new ArrayList<>();
private static final Logger log = LogManager.getLogger(AuthFilter.class);

private static boolean isAPIKeyEnabled = false;

static {
if (System.getenv("API_KEY_ENABLED") != null) {
isAPIKeyEnabled = Boolean.parseBoolean(System.getenv("API_KEY_ENABLED"));
}
}

@Override
public void init(APIConfig apiConfig, Map<String, String> configProperties) {
initializeAuthenticators(apiConfig);
Expand Down Expand Up @@ -85,7 +93,8 @@ private void initializeAuthenticators(APIConfig apiConfig) {
} else if (apiSecurityLevel.trim().
equalsIgnoreCase(APIConstants.API_SECURITY_OAUTH_BASIC_AUTH_API_KEY_MANDATORY)) {
isOAuthBasicAuthMandatory = true;
} else if (apiSecurityLevel.trim().equalsIgnoreCase(APIConstants.SWAGGER_API_KEY_AUTH_TYPE_NAME)) {
} else if (isAPIKeyEnabled &&
apiSecurityLevel.trim().equalsIgnoreCase(APIConstants.SWAGGER_API_KEY_AUTH_TYPE_NAME)) {
isApiKeyProtected = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@
import net.minidev.json.JSONValue;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.wso2.choreo.connect.enforcer.common.CacheProvider;
import org.wso2.choreo.connect.enforcer.commons.model.AuthenticationContext;
import org.wso2.choreo.connect.enforcer.commons.model.RequestContext;
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
import org.wso2.choreo.connect.enforcer.constants.APIConstants;
import org.wso2.choreo.connect.enforcer.constants.APISecurityConstants;
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;
import org.wso2.choreo.connect.enforcer.util.FilterUtils;

import java.util.Base64;
import java.util.Map;
import java.util.Optional;

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

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

private static boolean isAPIKeyEnabled = false;

static {
if (System.getenv("API_KEY_ENABLED") != null) {
isAPIKeyEnabled = Boolean.parseBoolean(System.getenv("API_KEY_ENABLED"));
}
}

public APIKeyAuthenticator() {
super();
log.debug("API key authenticator initialized.");
Expand All @@ -55,17 +50,32 @@ public APIKeyAuthenticator() {
@Override
public boolean canAuthenticate(RequestContext requestContext) {

if (!isAPIKeyEnabled) {
return false;
}
String apiKeyValue = getAPIKeyFromRequest(requestContext);
return apiKeyValue != null && apiKeyValue.startsWith(APIKeyConstants.API_KEY_PREFIX);
return apiKeyValue != null && apiKeyValue.startsWith(APIKeyConstants.API_KEY_PREFIX) &&
apiKeyValue.length() > 10;
}

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

return super.authenticate(requestContext);
AuthenticationContext authCtx = super.authenticate(requestContext);
// Drop the API key data from the API key header.
dropAPIKeyDataFromAPIKeyHeader(requestContext);
return authCtx;
}

private void dropAPIKeyDataFromAPIKeyHeader(RequestContext requestContext) throws APISecurityException {

String apiKeyHeaderValue = getAPIKeyFromRequest(requestContext).trim();
String checksum = apiKeyHeaderValue.substring(apiKeyHeaderValue.length() - 6);
JSONObject jsonObject = getDecodedAPIKeyData(apiKeyHeaderValue);
jsonObject.remove(APIKeyConstants.API_KEY_JSON_KEY);
// Update the header with the new API key data.
String encodedKeyData = Base64.getEncoder().encodeToString(jsonObject.toJSONString().getBytes());
String newAPIKeyHeaderValue = APIKeyConstants.API_KEY_PREFIX + encodedKeyData + checksum;
// Add the new header.
requestContext.addOrModifyHeaders(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
.getApiKeyInternalHeader().toLowerCase(), newAPIKeyHeaderValue);
}

private String getAPIKeyFromRequest(RequestContext requestContext) {
Expand All @@ -74,26 +84,50 @@ private String getAPIKeyFromRequest(RequestContext requestContext) {
.getApiKeyInternalHeader().toLowerCase());
}

@Override
protected String retrieveTokenFromRequestCtx(RequestContext requestContext) throws APISecurityException {

private JSONObject getDecodedAPIKeyData(String apiKeyHeaderValue) throws APISecurityException {
try {
String apiKeyHeaderValue = getAPIKeyFromRequest(requestContext).trim();
// Skipping the prefix(`chk_`) and checksum.
String apiKeyData = apiKeyHeaderValue.substring(4, apiKeyHeaderValue.length() - 6);
// Base 64 decode key data.
String decodedKeyData = new String(Base64.getDecoder().decode(apiKeyData));
// Convert data into JSON.
JSONObject jsonObject = (JSONObject) JSONValue.parse(decodedKeyData);
// Extracting the jwt token.
return jsonObject.getAsString(APIKeyConstants.API_KEY_JSON_KEY);
return (JSONObject) JSONValue.parse(decodedKeyData);
} catch (Exception e) {
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
}
}

@Override
protected String retrieveTokenFromRequestCtx(RequestContext requestContext) throws APISecurityException {

String apiKey = getAPIKeyFromRequest(requestContext).trim();
if (!APIKeyUtils.isValidAPIKey(apiKey)) {
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
}
String keyHash = APIKeyUtils.generateAPIKeyHash(apiKey);
Object cachedJWT = CacheProvider.getGatewayAPIKeyJWTCache().getIfPresent(keyHash);
if (cachedJWT != null && !APIKeyUtils.isJWTExpired((String) cachedJWT)) {
if (log.isDebugEnabled()) {
log.debug("Token retrieved from the cache. Token: " + FilterUtils.getMaskedToken(keyHash));
}
return (String) cachedJWT;
}
// Exchange the API Key to a JWT token.
Optional<String> jwt = APIKeyUtils.exchangeAPIKeyToJWT(keyHash);
if (jwt.isEmpty()) {
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
}
// Cache the JWT token.
CacheProvider.getGatewayAPIKeyJWTCache().put(keyHash, jwt.get());
return jwt.get();
}

@Override
public String getChallengeString() {
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
import org.wso2.choreo.connect.enforcer.constants.APIConstants;
import org.wso2.choreo.connect.enforcer.constants.APISecurityConstants;
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;
import org.wso2.choreo.connect.enforcer.util.FilterUtils;

import java.io.InputStream;
Expand Down Expand Up @@ -80,18 +83,24 @@ public static boolean isValidAPIKey(String apiKey) {
* @param apiKey API Key
* @return key hash
*/
public static String generateAPIKeyHash(String apiKey) {
public static String generateAPIKeyHash(String apiKey) throws APISecurityException {

// Skipping the prefix(`chp_`) and checksum.
String keyData = apiKey.substring(4, apiKey.length() - 6);
// Base 64 decode key data.
String decodedKeyData = new String(Base64.getDecoder().decode(keyData));
// Convert data into JSON.
JSONObject jsonObject = (JSONObject) JSONValue.parse(decodedKeyData);
// Extracting the key.
String key = jsonObject.getAsString(APIKeyConstants.API_KEY_JSON_KEY);
// Return SHA256 hash of the key.
return DigestUtils.sha256Hex(key);
try {
// Skipping the prefix(`chp_`) and checksum.
String keyData = apiKey.substring(4, apiKey.length() - 6);
// Base 64 decode key data.
String decodedKeyData = new String(Base64.getDecoder().decode(keyData));
// Convert data into JSON.
JSONObject jsonObject = (JSONObject) JSONValue.parse(decodedKeyData);
// Extracting the key.
String key = jsonObject.getAsString(APIKeyConstants.API_KEY_JSON_KEY);
// Return SHA256 hash of the key.
return DigestUtils.sha256Hex(key);
} catch (Exception e) {
throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com)
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.choreo.connect.enforcer.security.jwt;

import com.google.common.cache.LoadingCache;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.wso2.carbon.apimgt.common.gateway.dto.JWTConfigurationDto;
import org.wso2.choreo.connect.enforcer.common.CacheProvider;
import org.wso2.choreo.connect.enforcer.commons.model.APIConfig;
import org.wso2.choreo.connect.enforcer.commons.model.RequestContext;
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
import org.wso2.choreo.connect.enforcer.config.EnforcerConfig;
import org.wso2.choreo.connect.enforcer.config.dto.APIKeyDTO;
import org.wso2.choreo.connect.enforcer.config.dto.CacheDto;
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@RunWith(PowerMockRunner.class)
@PrepareForTest({APIKeyUtils.class, CacheProvider.class, ConfigHolder.class})
public class APIKeyAuthenticatorTest {

@Before
public void setup() {
PowerMockito.mockStatic(ConfigHolder.class);
ConfigHolder configHolder = PowerMockito.mock(ConfigHolder.class);
PowerMockito.when(ConfigHolder.getInstance()).thenReturn(configHolder);
EnforcerConfig enforcerConfig = PowerMockito.mock(EnforcerConfig.class);
PowerMockito.when(configHolder.getConfig()).thenReturn(enforcerConfig);
APIKeyDTO apiKeyDTO = PowerMockito.mock(APIKeyDTO.class);
PowerMockito.when(enforcerConfig.getApiKeyConfig()).thenReturn(apiKeyDTO);
PowerMockito.when(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
.getApiKeyInternalHeader()).thenReturn("choreo-api-key");
CacheDto cacheDto = Mockito.mock(CacheDto.class);
Mockito.when(cacheDto.isEnabled()).thenReturn(true);
Mockito.when(enforcerConfig.getCacheDto()).thenReturn(cacheDto);
JWTConfigurationDto jwtConfigurationDto = Mockito.mock(JWTConfigurationDto.class);
Mockito.when(jwtConfigurationDto.isEnabled()).thenReturn(false);
Mockito.when(enforcerConfig.getJwtConfigurationDto()).thenReturn(jwtConfigurationDto);
}

@Test
public void retrieveTokenFromRequestCtxTest_invalidKey() {

RequestContext.Builder requestContextBuilder = new RequestContext.Builder("/api-key");
requestContextBuilder.matchedAPI(new APIConfig.Builder("Petstore")
.basePath("/test")
.apiType("REST")
.build());
Map<String, String> headersMap = new HashMap<>();
headersMap.put("choreo-api-key",
"chk_eyJrZXkiOiJieTlpYXQ5d3MycDY0dWF6anFkbzQ4cnAyYnY3aWoxdWRuYmRzNzN6ZWx5OWNoZHJ2YiJ97JYpag");
requestContextBuilder.headers(headersMap);
RequestContext requestContext = requestContextBuilder.build();

APIKeyAuthenticator apiKeyAuthenticator = new APIKeyAuthenticator();
Assert.assertThrows(APISecurityException.class, () ->
apiKeyAuthenticator.retrieveTokenFromRequestCtx(requestContext));
}

@Test
public void retrieveTokenFromRequestCtxTest_cached_validKey() throws APISecurityException {

String mockJWT = "eyJrZXkiOiJieTlpYXQ5d3MycDY0dWF6anFkbzQ4cnAyYnY3aWoxdWRuYmRzNzN6ZWx5OWNoZHJ2YiJ97JYPAg";
PowerMockito.mockStatic(APIKeyUtils.class);
PowerMockito.when(APIKeyUtils.isValidAPIKey(Mockito.anyString())).thenReturn(true);
PowerMockito.when(APIKeyUtils.generateAPIKeyHash(Mockito.anyString())).thenReturn("key_hash");
PowerMockito.when(APIKeyUtils.isJWTExpired(Mockito.anyString())).thenReturn(false);

PowerMockito.mockStatic(CacheProvider.class);
LoadingCache gatewayAPIKeyJWTCache = PowerMockito.mock(LoadingCache.class);
PowerMockito.when(CacheProvider.getGatewayAPIKeyJWTCache()).thenReturn(gatewayAPIKeyJWTCache);
PowerMockito.when(gatewayAPIKeyJWTCache.getIfPresent(Mockito.anyString())).thenReturn(mockJWT);

RequestContext.Builder requestContextBuilder = new RequestContext.Builder("/api-key");
requestContextBuilder.matchedAPI(new APIConfig.Builder("Petstore")
.basePath("/test")
.apiType("REST")
.build());
Map<String, String> headersMap = new HashMap<>();
headersMap.put("choreo-api-key",
"chk_eyJhdHRyMSI6InYxIiwiY29ubmVjdGlvbklkIjoiNjAwM2EzYjctYWYwZi00ZmIzLTg1M2UtYTY1NjJiMjM0N" +
"WYyIiwia2V5IjoieG5lcGVxZmZ4eWx2Y2Q4a3FnNHprZDFpMHoxMnA2dTBqcW50aDUyM3JlN292a2pudncifQBdZRRQ");
requestContextBuilder.headers(headersMap);
RequestContext requestContext = requestContextBuilder.build();

APIKeyAuthenticator apiKeyAuthenticator = new APIKeyAuthenticator();
String token = apiKeyAuthenticator.retrieveTokenFromRequestCtx(requestContext);
Assert.assertEquals(mockJWT, token);
}

@Test
public void retrieveTokenFromRequestCtxTest_validKey() throws APISecurityException {

PowerMockito.mockStatic(APIKeyUtils.class);
String mockJWT = "eyJrZXkiOiJieTlpYXQ5d3MycDY0dWF6anFkbzQ4cnAyYnY3aWoxdWRuYmRzNzN6ZWx5OWNoZHJ2YiJ97JYPAg";
PowerMockito.when(APIKeyUtils.exchangeAPIKeyToJWT(Mockito.anyString())).thenReturn(Optional.of(mockJWT));
PowerMockito.when(APIKeyUtils.isValidAPIKey(Mockito.anyString())).thenReturn(true);
PowerMockito.when(APIKeyUtils.generateAPIKeyHash(Mockito.anyString())).thenReturn("key_hash");

PowerMockito.mockStatic(CacheProvider.class);
LoadingCache gatewayAPIKeyJWTCache = PowerMockito.mock(LoadingCache.class);
PowerMockito.when(CacheProvider.getGatewayAPIKeyJWTCache()).thenReturn(gatewayAPIKeyJWTCache);
PowerMockito.when(gatewayAPIKeyJWTCache.getIfPresent(Mockito.anyString())).thenReturn(null);

RequestContext.Builder requestContextBuilder = new RequestContext.Builder("/api-key");
requestContextBuilder.matchedAPI(new APIConfig.Builder("Petstore")
.basePath("/test")
.apiType("REST")
.build());
Map<String, String> headersMap = new HashMap<>();
headersMap.put("choreo-api-key",
"chk_eyJhdHRyMSI6InYxIiwiY29ubmVjdGlvbklkIjoiNjAwM2EzYjctYWYwZi00ZmIzLTg1M2UtYTY1NjJiMjM0N" +
"WYyIiwia2V5IjoieG5lcGVxZmZ4eWx2Y2Q4a3FnNHprZDFpMHoxMnA2dTBqcW50aDUyM3JlN292a2pudncifQBdZRRQ");
requestContextBuilder.headers(headersMap);
RequestContext requestContext = requestContextBuilder.build();

APIKeyAuthenticator apiKeyAuthenticator = new APIKeyAuthenticator();
String token = apiKeyAuthenticator.retrieveTokenFromRequestCtx(requestContext);
Assert.assertEquals(mockJWT, token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.wso2.choreo.connect.enforcer.config.ConfigHolder;
import org.wso2.choreo.connect.enforcer.exception.APISecurityException;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ConfigHolder.class})
Expand All @@ -45,7 +46,7 @@ public void testIsValidAPIKey_invalid() {
}

@Test
public void testGenerateAPIKeyHash() {
public void testGenerateAPIKeyHash() throws APISecurityException {
String apiKey = "chp_eyJrZXkiOiJlanp6am8yaGc5MnA2MTF6NTI2OXMzNzU1ZnJzbnFlNm9vb2hldWd0djBjbmQ3bXdobCJ9dknDJA";
String expectedKeyHash = "62f73948188c9f773414d4ec77eae6e8caab21556e4ad18f94b7c6c5b018524c";
String generatedAPIKeyHash = APIKeyUtils.generateAPIKeyHash(apiKey);
Expand Down

0 comments on commit c49638e

Please sign in to comment.