Skip to content

Commit

Permalink
Adding API-KEY authentication support for web socket APIs (#12228)
Browse files Browse the repository at this point in the history
  • Loading branch information
thisaltennakoon authored Feb 1, 2024
1 parent ed44382 commit 5683b16
Show file tree
Hide file tree
Showing 35 changed files with 1,577 additions and 765 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,16 @@ List<OperationPolicyData> getAllAPISpecificOperationPolicies(String apiUUID, Str
Map<String,Object> searchPaginatedAPIProducts(String searchQuery, String tenantDomain,int start,int end) throws
APIManagementException;

/**
* Returns security scheme of an API
*
* @param uuid UUID of the API's registry artifact
* @param organization Identifier of an organization
* @return A String containing security scheme of the API
* @throws APIManagementException if failed get API from APIIdentifier
*/
String getSecuritySchemeOfAPI(String uuid, String organization) throws APIManagementException;

/**
* Returns details of an API
* @param uuid UUID of the API's registry artifact
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class APIMgtGatewayConstants {
public static final String REQUEST_MEDIATION_LATENCY = "request_mediation_latency";
public static final String RESPONSE_MEDIATION_LATENCY = "response_mediation_latency";
public static final String AM_CORRELATION_ID = "am.correlationID";
public static final String REFERER = "Referer";

/**
* Constants for regex protector.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,32 @@ public static TreeMap<String, org.wso2.carbon.apimgt.keymgt.model.entity.API> ge

return selectedAPIMap;
}

/**
* Get the security scheme of the given API
*
* @param context API context
* @param version API version
* @param tenantDomain Tenant domain
* @return List of security schemes
*/
public static List<String> getSecuritySchemeOfWebSocketAPI(String context, String version, String tenantDomain) {

List<String> securitySchemeList = new ArrayList<>();
SubscriptionDataStore tenantSubscriptionStore =
SubscriptionDataHolder.getInstance().getTenantSubscriptionStore(tenantDomain);
if (tenantSubscriptionStore != null) {
org.wso2.carbon.apimgt.keymgt.model.entity.API api = tenantSubscriptionStore.getApiByContextAndVersion(context, version);
if (api != null) {
String securityScheme = api.getSecurityScheme();
if (securityScheme != null) {
securitySchemeList = Arrays.asList(securityScheme.split(","));
}
}
}
return securitySchemeList;
}

private static class ContextLengthSorter implements Comparator<String> {

@Override
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.wso2.carbon.apimgt.api.gateway.GraphQLSchemaDTO;
import org.wso2.carbon.apimgt.gateway.handlers.security.AuthenticationContext;
import org.wso2.carbon.apimgt.gateway.dto.GraphQLOperationDTO;
import org.wso2.carbon.apimgt.gateway.inbound.websocket.Authentication.Authenticator;
import org.wso2.carbon.apimgt.impl.dto.APIKeyValidationInfoDTO;
import org.wso2.carbon.apimgt.impl.dto.ResourceInfoDTO;
import org.wso2.carbon.apimgt.impl.jwt.SignedJWTInfo;
Expand Down Expand Up @@ -60,6 +61,7 @@ public class InboundMessageContext {
private String matchingResource; //invoking API resource
private ChannelHandlerContext ctx;
private boolean isJWTToken;
private Authenticator authenticator;

//Graphql Subscription specific connection context information
private GraphQLSchemaDTO graphQLSchemaDTO;
Expand Down Expand Up @@ -252,4 +254,12 @@ public boolean isJWTToken() {
public void setJWTToken(boolean JWTToken) {
isJWTToken = JWTToken;
}

public void setAuthenticator (Authenticator authenticator) {
this.authenticator = authenticator;
}

public Authenticator getAuthenticator () {
return this.authenticator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright (c) 2023, 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.carbon.apimgt.gateway.inbound.websocket.Authentication;

import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.api.APIManagementException;
import org.wso2.carbon.apimgt.gateway.APIMgtGatewayConstants;
import org.wso2.carbon.apimgt.gateway.handlers.security.APISecurityException;
import org.wso2.carbon.apimgt.gateway.handlers.security.APISecurityConstants;
import org.wso2.carbon.apimgt.gateway.handlers.security.AuthenticationContext;
import org.wso2.carbon.apimgt.gateway.handlers.streaming.websocket.WebSocketApiConstants;
import org.wso2.carbon.apimgt.gateway.inbound.InboundMessageContext;
import org.wso2.carbon.apimgt.gateway.inbound.websocket.InboundProcessorResponseDTO;
import org.wso2.carbon.apimgt.gateway.inbound.websocket.utils.InboundWebsocketProcessorUtil;
import org.wso2.carbon.apimgt.gateway.internal.ServiceReferenceHolder;
import org.wso2.carbon.apimgt.gateway.utils.ApiKeyAuthenticatorUtils;
import org.wso2.carbon.apimgt.gateway.utils.GatewayUtils;
import org.wso2.carbon.apimgt.impl.APIConstants;
import org.wso2.carbon.apimgt.impl.dto.ExtendedJWTConfigurationDto;

import java.text.ParseException;

/**
* This class is used to authenticate web socket API requests when using API Key as the authentication mechanism.
*/
public class ApiKeyAuthenticator implements Authenticator {

private static final Log log = LogFactory.getLog(ApiKeyAuthenticator.class);
private String apiKey;
private String[] splitToken;
private JWSHeader decodedHeader;
private JWTClaimsSet payload;
private SignedJWT signedJWT;

@Override
public boolean validateToken(InboundMessageContext inboundMessageContext) throws APISecurityException {

if (InboundWebsocketProcessorUtil.isAuthenticatorEnabled(APIConstants.API_SECURITY_API_KEY,
inboundMessageContext)) {
log.debug("ApiKey Authentication initialized");
try {
apiKey = inboundMessageContext.getRequestHeaders().get(APIConstants.API_KEY_HEADER_QUERY_PARAM);
splitToken = apiKey.split("\\.");
ApiKeyAuthenticatorUtils.validateAPIKeyFormat(splitToken);
signedJWT = (SignedJWT) JWTParser.parse(apiKey);
decodedHeader = signedJWT.getHeader();
payload = signedJWT.getJWTClaimsSet();
return ApiKeyAuthenticatorUtils.isAPIKey(splitToken, decodedHeader, payload);
} catch (ParseException e) {
log.error("Error while parsing API Key", e);
return false;
}
}
return false;
}

@Override
public InboundProcessorResponseDTO authenticate(InboundMessageContext inboundMessageContext)
throws APISecurityException {

if (InboundWebsocketProcessorUtil.isAuthenticatorEnabled(APIConstants.API_SECURITY_API_KEY,
inboundMessageContext)) {
try {
String certAlias = decodedHeader.getKeyID();
String tokenIdentifier = payload.getJWTID();
String tenantDomain = GatewayUtils.getTenantDomain();
String apiContext = inboundMessageContext.getApiContext();
String apiVersion = inboundMessageContext.getVersion();
String matchingResource = inboundMessageContext.getMatchingResource();
String cacheKey = GatewayUtils.getAccessTokenCacheKey(tokenIdentifier, apiContext, apiVersion,
matchingResource, null);
boolean isGatewayTokenCacheEnabled = GatewayUtils.isGatewayTokenCacheEnabled();
boolean isVerified = ApiKeyAuthenticatorUtils.verifyAPIKeySignatureFromTokenCache(isGatewayTokenCacheEnabled,
tokenIdentifier, cacheKey, apiKey, splitToken[0]);
if (!isVerified) {
// Not found in cache or caching disabled
isVerified = ApiKeyAuthenticatorUtils.verifyAPIKeySignature(signedJWT, certAlias);
}
ApiKeyAuthenticatorUtils.addTokenToTokenCache(isGatewayTokenCacheEnabled, tokenIdentifier, isVerified,
tenantDomain);

// If Api Key signature is verified
if (isVerified) {
ExtendedJWTConfigurationDto jwtConfigurationDto = ServiceReferenceHolder.getInstance().
getAPIManagerConfiguration().getJwtConfigurationDto();
ApiKeyAuthenticatorUtils.overridePayloadFromDataCache(isGatewayTokenCacheEnabled, cacheKey, payload);
ApiKeyAuthenticatorUtils.checkTokenExpired(isGatewayTokenCacheEnabled, cacheKey, tokenIdentifier,
apiKey, tenantDomain, payload);
ApiKeyAuthenticatorUtils.validateAPIKeyRestrictions(payload, inboundMessageContext.getUserIP(),
apiContext, apiVersion, inboundMessageContext.getRequestHeaders().
get(APIMgtGatewayConstants.REFERER));
net.minidev.json.JSONObject api = GatewayUtils.validateAPISubscription(apiContext, apiVersion,
payload, splitToken[0]);
String endUserToken = ApiKeyAuthenticatorUtils.getEndUserToken(api, jwtConfigurationDto, apiKey,
signedJWT, payload, tokenIdentifier, isGatewayTokenCacheEnabled);
AuthenticationContext authenticationContext = GatewayUtils.generateAuthenticationContext(
tokenIdentifier, payload, api, null, endUserToken, null);
if (!InboundWebsocketProcessorUtil.validateAuthenticationContext(authenticationContext,
inboundMessageContext)) {
return InboundWebsocketProcessorUtil.getFrameErrorDTO(
WebSocketApiConstants.FrameErrorConstants.API_AUTH_INVALID_CREDENTIALS,
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE, true);
}
log.debug("User is authorized to access the resource using Api Key.");
InboundProcessorResponseDTO inboundProcessorResponseDTO = new InboundProcessorResponseDTO();
inboundProcessorResponseDTO.setErrorCode(0);
inboundProcessorResponseDTO.setErrorMessage(null);
return inboundProcessorResponseDTO;
}
if (log.isDebugEnabled()) {
log.debug("Api Key signature verification failure. Api Key: " +
GatewayUtils.getMaskedToken(splitToken[0]));
}
log.error("Invalid Api Key. Signature verification failed.");
throw new APISecurityException(APISecurityConstants.API_AUTH_INVALID_CREDENTIALS,
APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE);
} catch (ParseException e) {
log.error("Error while parsing API Key", e);
return InboundWebsocketProcessorUtil.getFrameErrorDTO(APISecurityConstants.API_AUTH_GENERAL_ERROR,
APISecurityConstants.API_AUTH_GENERAL_ERROR_MESSAGE, true);
} catch (APIManagementException e) {
log.error("Error while setting public cert/private key for backend jwt generation", e);
return InboundWebsocketProcessorUtil.getFrameErrorDTO(APISecurityConstants.API_AUTH_GENERAL_ERROR,
APISecurityConstants.API_AUTH_GENERAL_ERROR_MESSAGE, true);
}
}
return InboundWebsocketProcessorUtil.getFrameErrorDTO(
WebSocketApiConstants.FrameErrorConstants.API_AUTH_GENERAL_ERROR,
"Authentication has not enabled for the Authentication type: " + APIConstants.
API_SECURITY_API_KEY, true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2023, 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.carbon.apimgt.gateway.inbound.websocket.Authentication;

import org.wso2.carbon.apimgt.gateway.handlers.security.APISecurityException;
import org.wso2.carbon.apimgt.gateway.inbound.InboundMessageContext;
import org.wso2.carbon.apimgt.gateway.inbound.websocket.InboundProcessorResponseDTO;

import java.text.ParseException;

/**
* This interface is used to authenticate web socket API requests, and it contains methods to validate the received
* token and authenticate the request if the validation is successful.
*/
public interface Authenticator {

/**
* This method is used to validate the received token.
*
* @param inboundMessageContext InboundMessageContext object containing the request details
* @return A boolean value indicating whether the token is valid or not
* @throws APISecurityException if an error occurs while validating the token
*/
boolean validateToken(InboundMessageContext inboundMessageContext) throws APISecurityException;

/**
* This method is used to authenticate the token.
*
* @param inboundMessageContext InboundMessageContext object containing the request details
* @return An InboundProcessorResponseDTO object containing the authentication status
* @throws APISecurityException if an error occurs while authenticating the token
*/
InboundProcessorResponseDTO authenticate(InboundMessageContext inboundMessageContext) throws APISecurityException;
}
Loading

0 comments on commit 5683b16

Please sign in to comment.