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);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue