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 2 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,34 @@ 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);
}

@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;
// Remove the existing header.
requestContext.getRemoveHeaders().add(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we do not need it here and it won't work in the expected way as first we add the headers and then process removeHeader map and remove them.

.getApiKeyInternalHeader().toLowerCase());
// Add the new header.
requestContext.addOrModifyHeaders(ConfigHolder.getInstance().getConfig().getApiKeyConfig()
.getApiKeyInternalHeader().toLowerCase(), newAPIKeyHeaderValue);
}

private String getAPIKeyFromRequest(RequestContext requestContext) {
Expand All @@ -74,26 +86,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);
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 @@ -557,7 +557,7 @@ public int getPriority() {
* @param jwtToken JWT Token
* @throws APISecurityException in case of scope validation failure
*/
private void validateScopes(String apiContext, String apiVersion, ResourceConfig matchingResource,
protected void validateScopes(String apiContext, String apiVersion, ResourceConfig matchingResource,
VirajSalaka marked this conversation as resolved.
Show resolved Hide resolved
JWTValidationInfo jwtValidationInfo, SignedJWTInfo jwtToken) throws APISecurityException {
try {
APIKeyValidationInfoDTO apiKeyValidationInfoDTO = new APIKeyValidationInfoDTO();
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);
}
}
Loading