Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new API Key authenticator impl #3611

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
VirajSalaka marked this conversation as resolved.
Show resolved Hide resolved
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);
VirajSalaka marked this conversation as resolved.
Show resolved Hide resolved
// 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);
VirajSalaka marked this conversation as resolved.
Show resolved Hide resolved
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);
VirajSalaka marked this conversation as resolved.
Show resolved Hide resolved
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
Loading