Add Oauth client service

Rajitha Kumara 1 month ago
parent a45820e845
commit 934de80796

@ -0,0 +1,40 @@
/*
* Copyright (c) 2018 - 2024, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
*
* Entgra (Pvt) Ltd. 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 io.entgra.device.mgt.core.apimgt.extension.rest.api;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.bean.OAuthClientResponse;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions.BadRequestException;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions.OAuthClientException;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions.UnexpectedResponseException;
import okhttp3.Request;
public interface IOAuthClientService {
/**
* Handle execution of a APIM REST services invocation request. Token and cache handling will be handled by the
* service itself.
* @param request Instance of {@link Request} to execute
* @return Instance of {@link OAuthClientResponse} when successful invocation happens
* @throws OAuthClientException {@link OAuthClientException}
* @throws BadRequestException {@link BadRequestException}
* @throws UnexpectedResponseException {@link UnexpectedResponseException}
*/
OAuthClientResponse execute(Request request) throws OAuthClientException, BadRequestException,
UnexpectedResponseException;
}

@ -0,0 +1,385 @@
/*
* Copyright (c) 2018 - 2024, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
*
* Entgra (Pvt) Ltd. 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 io.entgra.device.mgt.core.apimgt.extension.rest.api;
import com.google.gson.Gson;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.bean.OAuthClientResponse;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.constants.Constants;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions.BadRequestException;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions.OAuthClientException;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions.UnexpectedResponseException;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.util.HttpsTrustManagerUtils;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONObject;
import org.wso2.carbon.apimgt.impl.APIManagerConfiguration;
import org.wso2.carbon.apimgt.impl.internal.ServiceReferenceHolder;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreException;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class OAuthClient implements IOAuthClientService {
private static final Log log = LogFactory.getLog(OAuthClient.class);
private static final OkHttpClient client = new OkHttpClient(HttpsTrustManagerUtils.getSSLClient().newBuilder());
private static final Gson gson = new Gson();
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String IDN_DCR_CLIENT_PREFIX = "_REST_API_INVOKER_SERVICE";
private static final APIManagerConfiguration config =
ServiceReferenceHolder.getInstance().getAPIManagerConfigurationService().getAPIManagerConfiguration();
private static final String tokenEndpoint = config.getFirstProperty(Constants.TOKE_END_POINT);
private static final String dcrEndpoint = config.getFirstProperty(Constants.DCR_END_POINT);
private static final Map<String, CacheWrapper> cache = new ConcurrentHashMap<>();
private static final int MAX_RETRY_ATTEMPT = 2;
private OAuthClient() {
}
public static OAuthClient getInstance() {
return OAuthClientHolder.INSTANCE;
}
/**
* Handle execution of a APIM REST services invocation request. Token and cache handling will be handled by the
* service itself.
* @param request Instance of {@link Request} to execute
* @return Instance of {@link OAuthClientResponse} when successful invocation happens
* @throws OAuthClientException {@link OAuthClientException}
* @throws BadRequestException {@link BadRequestException}
* @throws UnexpectedResponseException {@link UnexpectedResponseException}
*/
@Override
public OAuthClientResponse execute(Request request) throws OAuthClientException, BadRequestException,
UnexpectedResponseException {
int currentRetryAttempt = 0;
OAuthClientResponse oAuthClientResponse;
while (true) {
try {
request = intercept(request);
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
oAuthClientResponse = mapToOAuthClientResponse(response);
break;
}
if (response.code() == HttpStatus.SC_NOT_FOUND) {
if (log.isDebugEnabled()) {
log.info("Resource not found for the request [ " + request.url() + " ]");
}
oAuthClientResponse = mapToOAuthClientResponse(response);
break;
}
// entering to the retrying phase, so increment the counter
currentRetryAttempt++;
if (response.code() == HttpStatus.SC_UNAUTHORIZED) {
if (currentRetryAttempt <= MAX_RETRY_ATTEMPT) {
refresh();
} else {
String msg =
"Request [ " + request.url() + " ] failed with code : [ " + response.code() + " ]" +
" & body : [ " + (response.body() != null ?
response.body().string() : " empty body received!") + " ]";
log.error(msg);
throw new UnexpectedResponseException(msg);
}
} else if (HttpStatus.SC_BAD_REQUEST == response.code()) {
String msg =
"Encountered a bad request! Request [ " + request.url() + " ] failed with code : " +
"[ " + response.code() + " ] & body : [ " + (response.body() != null ?
response.body().string() : " empty body received!") + " ]";
log.error(msg);
throw new BadRequestException(msg);
} else {
String msg =
"Request [ " + request.url() + " ]failed with code : [ " + response.code() + " ] & " +
"body : [ " + (response.body() != null ? response.body().string() : " empty " +
"body received!") + " ]";
log.error(msg);
throw new UnexpectedResponseException(msg);
}
}
} catch (IOException ex) {
String msg =
"Error occurred while executing the request : [ " + request.method() + ":" + request.url() +
" ]";
throw new OAuthClientException(msg, ex);
}
}
return oAuthClientResponse;
}
/**
* Dynamic client registration will be handled through here. These clients can be located under carbon console's
* service provider section in respective tenants.
*
* @return Instance of {@link Keys} containing the dcr client's credentials
* @throws UserStoreException Throws when error encountered while obtaining user realm service
* @throws IOException Throws when error encountered while executing dcr request
* @throws OAuthClientException Throws when failed to register dcr client
*/
private Keys idnDynamicClientRegistration() throws UserStoreException, IOException, OAuthClientException {
UserRealm userRealm = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm();
String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
String tenantAdminUsername = userRealm.getRealmConfiguration().getAdminUserName();
String tenantAdminPassword = userRealm.getRealmConfiguration().getAdminPassword();
String tenantAwareClientName = tenantDomain.toUpperCase() + IDN_DCR_CLIENT_PREFIX;
List<String> grantTypes = Arrays.asList(Constants.PASSWORD_GRANT_TYPE, Constants.REFRESH_TOKEN_GRANT_TYPE);
String dcrRequestJsonStr = (new JSONObject())
.put("clientName", tenantAwareClientName)
.put("owner", tenantAdminUsername)
.put("saasApp", true)
.put("grantType", String.join(Constants.SPACE, grantTypes))
.toString();
RequestBody requestBody = RequestBody.Companion.create(dcrRequestJsonStr, JSON);
Request dcrRequest = new Request.Builder()
.url(dcrEndpoint)
.addHeader(Constants.AUTHORIZATION_HEADER_NAME, Credentials.basic(tenantAdminUsername,
tenantAdminPassword))
.post(requestBody)
.build();
try (Response response = client.newCall(dcrRequest).execute()) {
if (response.isSuccessful()) {
return mapKeys(response.body());
}
}
String msg = "Error encountered while processing DCR request. Tried client : [ " + tenantAwareClientName + " ]";
log.error(msg);
throw new OAuthClientException(msg);
}
/**
* Token obtaining procedure will be handled here. Since the required permissions for invoking the APIM REST
* services are available for the tenant admins, this procedure will use admin credentials for obtaining tokens.
* Also, please note that these tokens are only use for invoking APIM REST services only. The password grant uses
* since it facilitates the use of refresh token.
*
* @param keys Dcr client credentials to obtain a token
* @return Instance of {@link Tokens} containing the tokens
* @throws UserStoreException Throws when error encountered while obtaining user realm service
* @throws IOException Throws when error encountered while executing token request
* @throws OAuthClientException Throws when failed to obtain tokens
*/
private Tokens idnTokenGeneration(Keys keys) throws UserStoreException, IOException, OAuthClientException {
UserRealm userRealm = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm();
String tenantAdminUsername = userRealm.getRealmConfiguration().getAdminUserName();
String tenantAdminPassword = userRealm.getRealmConfiguration().getAdminPassword();
String tokenRequestJsonStr = (new JSONObject())
.put("grant_type", Constants.PASSWORD_GRANT_TYPE)
.put("username", tenantAdminUsername)
.put("password", tenantAdminPassword)
.put("scope", Constants.SCOPES)
.toString();
RequestBody requestBody = RequestBody.Companion.create(tokenRequestJsonStr, JSON);
Request tokenRequest = new Request.Builder()
.url(tokenEndpoint)
.addHeader(Constants.AUTHORIZATION_HEADER_NAME, Credentials.basic(keys.consumerKey,
keys.consumerSecret))
.post(requestBody)
.build();
try (Response response = client.newCall(tokenRequest).execute()) {
if (response.isSuccessful()) {
return mapTokens(response.body());
}
}
String msg = "Error encountered while processing token registration request";
log.error(msg);
throw new OAuthClientException(msg);
}
/**
* Obtain and refresh idn auth tokens. Note that in the first try it try to obtain new tokens via refresh_token
* grant type, if it fails it tries to obtain new tokens via password grant type.
* @param keys Instance of {@link Keys} containing the dcr client's credentials
* @param refreshToken Refresh token
* @return Instance of {@link Tokens} containing the tokens
* @throws UserStoreException Throws when error encountered while obtaining user realm service
* @throws IOException Throws when error encountered while executing token request
* @throws OAuthClientException Throws when failed to obtain tokens
*/
private Tokens idnTokenRefresh(Keys keys, String refreshToken) throws UserStoreException, IOException,
OAuthClientException {
String tokenRequestJsonStr = (new JSONObject())
.put("grant_type", Constants.REFRESH_TOKEN_GRANT_TYPE)
.put("refresh_token", refreshToken)
.put("scope", Constants.SCOPES)
.toString();
RequestBody requestBody = RequestBody.Companion.create(tokenRequestJsonStr, JSON);
Request tokenRequest = new Request.Builder()
.url(tokenEndpoint)
.addHeader(Constants.AUTHORIZATION_HEADER_NAME, Credentials.basic(keys.consumerKey,
keys.consumerSecret))
.post(requestBody)
.build();
try (Response response = client.newCall(tokenRequest).execute()) {
if (response.isSuccessful()) {
return mapTokens(response.body());
} else {
return idnTokenGeneration(keys);
}
}
}
/**
* Intercept the request and add authorization header base on available tokens.
* @param request Instance of the {@link Request} to intercept
* @return Intercepted request
* @throws OAuthClientException Throws when error encountered while adding authorization header
*/
private Request intercept(Request request) throws OAuthClientException {
String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
CacheWrapper cacheWrapper = cache.computeIfAbsent(tenantDomain, key -> {
CacheWrapper constructedWrapper = null;
try {
Keys keys = idnDynamicClientRegistration();
Tokens tokens = idnTokenGeneration(keys);
constructedWrapper = new CacheWrapper(keys, tokens);
} catch (Exception e) {
log.error("Error encountered while updating the cache", e);
}
return constructedWrapper;
});
if (cacheWrapper == null) {
throw new OAuthClientException("Failed to obtain tokens. Hence aborting request intercepting process");
}
return new Request.Builder(request).header(Constants.AUTHORIZATION_HEADER_NAME,
Constants.AUTHORIZATION_HEADER_PREFIX_BEARER + cacheWrapper.tokens.accessToken).build();
}
/**
* Refresh cached tokens.
* @throws OAuthClientException Throws when error encountered while refreshing the tokens
*/
private void refresh() throws OAuthClientException {
String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
CacheWrapper cacheWrapper = cache.computeIfPresent(tenantDomain, (key, value) -> {
CacheWrapper updatedCacheWrapper = null;
try {
Tokens tokens = idnTokenRefresh(value.keys, value.tokens.refreshToken);
updatedCacheWrapper = new CacheWrapper(value.keys, tokens);
} catch (Exception e) {
log.error("Error encountered while updating the cache", e);
}
return updatedCacheWrapper;
});
if (cacheWrapper == null) {
throw new OAuthClientException("Failed to refresh tokens. Hence aborting request executing process");
}
}
private Keys mapKeys(ResponseBody responseBody) {
Keys keys = new Keys();
if (responseBody == null) {
if (log.isDebugEnabled()) {
log.debug("Received empty request body for mapping into keys");
}
return keys;
}
JSONObject jsonObject = gson.fromJson(responseBody.toString(), JSONObject.class);
keys.consumerKey = jsonObject.getString("clientId");
keys.consumerSecret = jsonObject.getString("clientSecret");
return keys;
}
private Tokens mapTokens(ResponseBody responseBody) {
Tokens tokens = new Tokens();
if (responseBody == null) {
if (log.isDebugEnabled()) {
log.debug("Received empty request body for mapping into tokens");
}
return tokens;
}
JSONObject jsonObject = gson.fromJson(responseBody.toString(), JSONObject.class);
tokens.accessToken = jsonObject.getString("access_token");
tokens.refreshToken = jsonObject.getString("refresh_token");
return tokens;
}
private OAuthClientResponse mapToOAuthClientResponse(Response response) throws IOException {
return new OAuthClientResponse(response.code(),
response.body() != null ? response.body().string() : null, response.isSuccessful());
}
/**
* Holder for {@link OAuthClient} instance
*/
private static class OAuthClientHolder {
private static final OAuthClient INSTANCE = new OAuthClient();
}
/**
* Act as an internal data class for containing dcr credentials, hence no need of expose as a bean
*/
private class Keys {
String consumerKey;
String consumerSecret;
}
/**
* Act as an internal data class for containing dcr tokens, hence no need of expose as a bean
*/
private class Tokens {
String accessToken;
String refreshToken;
}
/**
* Act as an internal data class for containing cached tokens and keys, hence no need of expose as a bean
*/
private class CacheWrapper {
Keys keys;
Tokens tokens;
CacheWrapper(Keys keys, Tokens tokens) {
this.keys = keys;
this.tokens = tokens;
}
}
}

@ -0,0 +1,44 @@
/*
* Copyright (c) 2018 - 2024, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
*
* Entgra (Pvt) Ltd. 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 io.entgra.device.mgt.core.apimgt.extension.rest.api.bean;
public class OAuthClientResponse {
private final int code;
private final String body;
private final boolean isSuccessful;
public OAuthClientResponse(int code, String body, boolean isSuccessful) {
this.code = code;
this.body = body;
this.isSuccessful = isSuccessful;
}
public int getCode() {
return code;
}
public String getBody() {
return body;
}
public boolean isSuccessful() {
return isSuccessful;
}
}

@ -0,0 +1,30 @@
/*
* Copyright (c) 2018 - 2024, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
*
* Entgra (Pvt) Ltd. 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 io.entgra.device.mgt.core.apimgt.extension.rest.api.exceptions;
public class OAuthClientException extends Exception {
public OAuthClientException(String msg) {
super(msg);
}
public OAuthClientException(String msg, Throwable throwable) {
super(msg, throwable);
}
}

@ -22,6 +22,8 @@ import io.entgra.device.mgt.core.apimgt.extension.rest.api.APIApplicationService
import io.entgra.device.mgt.core.apimgt.extension.rest.api.APIApplicationServicesImpl;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.ConsumerRESTAPIServices;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.ConsumerRESTAPIServicesImpl;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.IOAuthClientService;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.OAuthClient;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.PublisherRESTAPIServices;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.PublisherRESTAPIServicesImpl;
import org.apache.commons.logging.Log;
@ -58,6 +60,9 @@ public class APIManagerServiceComponent {
bundleContext.registerService(ConsumerRESTAPIServices.class.getName(), consumerRESTAPIServices, null);
APIManagerServiceDataHolder.getInstance().setConsumerRESTAPIServices(consumerRESTAPIServices);
IOAuthClientService ioAuthClientService = OAuthClient.getInstance();
bundleContext.registerService(IOAuthClientService.class, ioAuthClientService, null);
APIManagerServiceDataHolder.getInstance().setIoAuthClientService(ioAuthClientService);
if (log.isDebugEnabled()) {
log.debug("API Application bundle has been successfully initialized");
}

@ -19,6 +19,7 @@
package io.entgra.device.mgt.core.apimgt.extension.rest.api.internal;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.APIApplicationServices;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.IOAuthClientService;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.PublisherRESTAPIServices;
import io.entgra.device.mgt.core.apimgt.extension.rest.api.ConsumerRESTAPIServices;
import org.wso2.carbon.apimgt.impl.APIManagerConfigurationService;
@ -32,6 +33,7 @@ public class APIManagerServiceDataHolder {
private PublisherRESTAPIServices publisherRESTAPIServices;
private RealmService realmService;
private TenantManager tenantManager;
private IOAuthClientService ioAuthClientService;
private static APIManagerServiceDataHolder thisInstance = new APIManagerServiceDataHolder();
@ -102,4 +104,15 @@ public class APIManagerServiceDataHolder {
public void setConsumerRESTAPIServices(ConsumerRESTAPIServices consumerRESTAPIServices) {
this.consumerRESTAPIServices = consumerRESTAPIServices;
}
public IOAuthClientService getIoAuthClientService() {
if (ioAuthClientService == null) {
throw new IllegalStateException("OAuth client service is not initialized properly");
}
return ioAuthClientService;
}
public void setIoAuthClientService(IOAuthClientService ioAuthClientService) {
this.ioAuthClientService = ioAuthClientService;
}
}

Loading…
Cancel
Save