diff --git a/README.md b/README.md index b5160e6c..e848a0d7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,13 @@ String apiKey = "5a826da2-1e3a-49df-85ba-cd88575e4e9d"; FusionAuthClient client = new FusionAuthClient(apiKey, "http://localhost:9011"); ``` +### Build the SSL Client + +```java +String apiKey = "5a826da2-1e3a-49df-85ba-cd88575e4e9d"; +FusionAuthClient client = new FusionAuthClient(apiKey, "http://localhost:9011", Boolean.TRUE, "BASE64 Encode ssl certificate value"); +``` + ### Login a user ```java diff --git a/src/main/java/io/fusionauth/client/FusionAuthClient.java b/src/main/java/io/fusionauth/client/FusionAuthClient.java index f75d664e..2130e214 100644 --- a/src/main/java/io/fusionauth/client/FusionAuthClient.java +++ b/src/main/java/io/fusionauth/client/FusionAuthClient.java @@ -188,6 +188,7 @@ import io.fusionauth.domain.oauth2.OAuthError; import io.fusionauth.domain.oauth2.JWKSResponse; import io.fusionauth.domain.provider.IdentityProviderType; +import io.fusionauth.ssl.SSLRestClient; /** * Client that connects to a FusionAuth server and provides access to the full set of FusionAuth APIs. @@ -224,24 +225,73 @@ public class FusionAuthClient { public int readTimeout; + /** + * fusion auth client support https + */ + public final Boolean sslEnable; + + /** + * ssl certificate context + */ + public final String sslCertificate; + public FusionAuthClient(String apiKey, String baseURL) { this(apiKey, baseURL, null); } public FusionAuthClient(String apiKey, String baseURL, String tenantId) { - this(apiKey, baseURL, 2000, 2000, tenantId); + this(apiKey, baseURL, 2000, 2000, tenantId, Boolean.FALSE, null); } public FusionAuthClient(String apiKey, String baseURL, int connectTimeout, int readTimeout) { - this(apiKey, baseURL, connectTimeout, readTimeout, null); + this(apiKey, baseURL, connectTimeout, readTimeout, null, Boolean.FALSE, null); + } + + /** + * ssl construct + * @param apiKey + * @param baseURL + * @param sslEnable + * @param sslCertificate + */ + public FusionAuthClient(String apiKey, String baseURL, Boolean sslEnable, String sslCertificate) { + this(apiKey, baseURL, null, sslEnable, sslCertificate); } - public FusionAuthClient(String apiKey, String baseURL, int connectTimeout, int readTimeout, String tenantId) { + /** + * sslCertificate + * @param apiKey + * @param baseURL + * @param tenantId + * @param sslEnable + * @param sslCertificate + */ + public FusionAuthClient(String apiKey, String baseURL, String tenantId, Boolean sslEnable, String sslCertificate) { + this(apiKey, baseURL, 2000, 2000, tenantId, sslEnable, sslCertificate); + } + + /** + * sslCertificate + * @param apiKey + * @param baseURL + * @param connectTimeout + * @param readTimeout + * @param sslEnable + * @param sslCertificate + */ + public FusionAuthClient(String apiKey, String baseURL, int connectTimeout, int readTimeout, Boolean sslEnable, String sslCertificate) { + this(apiKey, baseURL, connectTimeout, readTimeout, null, sslEnable, sslCertificate); + } + + public FusionAuthClient(String apiKey, String baseURL, int connectTimeout, int readTimeout, String tenantId, + Boolean sslEnable, String sslCertificate) { this.apiKey = apiKey; this.baseURL = baseURL; this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; this.tenantId = tenantId; + this.sslEnable = sslEnable; + this.sslCertificate = sslCertificate; } /** @@ -258,7 +308,7 @@ public FusionAuthClient setTenantId(UUID tenantId) { return this; } - return new FusionAuthClient(apiKey, baseURL, connectTimeout, readTimeout, tenantId.toString()); + return new FusionAuthClient(apiKey, baseURL, connectTimeout, readTimeout, tenantId.toString(), sslEnable, sslCertificate); } /** @@ -4992,12 +5042,25 @@ protected RESTClient start(Class type, Class errorType) { } protected RESTClient startAnonymous(Class type, Class errorType) { - RESTClient client = new RESTClient<>(type, errorType) - .successResponseHandler(type != Void.TYPE ? new JSONResponseHandler<>(type, objectMapper) : null) - .errorResponseHandler(errorType != Void.TYPE ? new JSONResponseHandler<>(errorType, objectMapper) : null) - .url(baseURL) - .connectTimeout(connectTimeout) - .readTimeout(readTimeout); + RESTClient client; + + if(Boolean.TRUE.equals(sslEnable)){ + client = new SSLRestClient<>(type, errorType) + .successResponseHandler(type != Void.TYPE ? new JSONResponseHandler<>(type, objectMapper) : null) + .errorResponseHandler(errorType != Void.TYPE ? new JSONResponseHandler<>(errorType, objectMapper) : null) + .url(baseURL) + .connectTimeout(connectTimeout) + .readTimeout(readTimeout) + .certificate(sslCertificate) + .disableSNIVerification(); + }else { + client = new RESTClient<>(type, errorType) + .successResponseHandler(type != Void.TYPE ? new JSONResponseHandler<>(type, objectMapper) : null) + .errorResponseHandler(errorType != Void.TYPE ? new JSONResponseHandler<>(errorType, objectMapper) : null) + .url(baseURL) + .connectTimeout(connectTimeout) + .readTimeout(readTimeout); + } if (tenantId != null) { client.header(TENANT_ID_HEADER, tenantId); diff --git a/src/main/java/io/fusionauth/ssl/SSLCertificateContext.java b/src/main/java/io/fusionauth/ssl/SSLCertificateContext.java new file mode 100644 index 00000000..d734dddc --- /dev/null +++ b/src/main/java/io/fusionauth/ssl/SSLCertificateContext.java @@ -0,0 +1,68 @@ +package io.fusionauth.ssl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Base64; + +/** + * @author zcyang + * @description get Singleton SSLSocketFactory + */ +public class SSLCertificateContext { + + private volatile static SSLCertificateContext instance = null; + + private static final Logger logger = LoggerFactory.getLogger(SSLCertificateContext.class); + + private final SSLSocketFactory sslSocketFactory; + + public static SSLCertificateContext getInstance(String cert) { + if (instance == null) { + synchronized (SSLCertificateContext.class) { + if (instance == null) { + instance = new SSLCertificateContext(getSSLContext(cert)); + } + } + } + return instance; + } + + private SSLCertificateContext(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + public static SSLSocketFactory getSSLContext(String certificate) { + SSLContext context = null; + SSLSocketFactory factory = null; + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + InputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(certificate)); + Certificate ca = certificateFactory.generateCertificate(inputStream); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(null, null); + keystore.setCertificateEntry("ca", ca); + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm); + trustManagerFactory.init(keystore); + context = SSLContext.getInstance("TLS"); + context.init(null, trustManagerFactory.getTrustManagers(), null); + factory = context.getSocketFactory(); + } catch (Exception ex) { + logger.error(ex.getMessage()); + } + return factory; + } +} \ No newline at end of file diff --git a/src/main/java/io/fusionauth/ssl/SSLRestClient.java b/src/main/java/io/fusionauth/ssl/SSLRestClient.java new file mode 100644 index 00000000..6132e55f --- /dev/null +++ b/src/main/java/io/fusionauth/ssl/SSLRestClient.java @@ -0,0 +1,231 @@ +package io.fusionauth.ssl; + +import com.inversoft.net.ssl.SSLTools; +import com.inversoft.rest.ClientResponse; +import com.inversoft.rest.RESTClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HttpsURLConnection; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @author zcyang + * @description custom RESTClient go() method + */ +public class SSLRestClient extends RESTClient { + + private static final Logger logger = LoggerFactory.getLogger(SSLRestClient.class); + + public SSLRestClient(Class successType, Class errorType) { + super(successType, errorType); + } + + @Override + public ClientResponse go() { + if (this.url.length() == 0) { + throw new IllegalStateException("You must specify a URL"); + } else { + Objects.requireNonNull(this.method, "You must specify a HTTP method"); + if (this.successType != Void.TYPE && this.successResponseHandler == null) { + throw new IllegalStateException("You specified a success response type, you must then provide a success response handler."); + } else if (this.errorType != Void.TYPE && this.errorResponseHandler == null) { + throw new IllegalStateException("You specified an error response type, you must then provide an error response handler."); + } else { + ClientResponse response = new ClientResponse(); + response.request = this.bodyHandler != null ? this.bodyHandler.getBodyObject() : null; + response.method = this.method; + + HttpURLConnection huc; + try { + if (this.parameters.size() > 0) { + if (this.url.indexOf("?") == -1) { + this.url.append("?"); + } + + Iterator i = this.parameters.entrySet().iterator(); + + while (i.hasNext()) { + Map.Entry> entry = (Map.Entry) i.next(); + Iterator j = ((List) entry.getValue()).iterator(); + + while (j.hasNext()) { + Object value = j.next(); + this.url.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")).append("=").append(URLEncoder.encode(value.toString(), "UTF-8")); + if (j.hasNext()) { + this.url.append("&"); + } + } + + if (i.hasNext()) { + this.url.append("&"); + } + } + } + + response.url = new URL(this.url.toString()); + huc = (HttpURLConnection) response.url.openConnection(); + if (response.url.getProtocol().toLowerCase().equals("https")) { + HttpsURLConnection hsuc = (HttpsURLConnection) huc; + if (this.certificate != null) { + if (this.key != null) { + hsuc.setSSLSocketFactory(SSLTools.getSSLServerContext(this.certificate, this.key).getSocketFactory()); + } else { + // I don't know why this method get ssl context not available, so custom it + //hsuc.setSSLSocketFactory(SSLTools.getSSLSocketFactory(this.certificate)); + hsuc.setSSLSocketFactory(SSLCertificateContext.getInstance(this.certificate).getSslSocketFactory()); + } + } + + if (this.sniVerificationDisabled) { + hsuc.setHostnameVerifier((hostname, session) -> { + return true; + }); + } + } + + huc.setDoOutput(this.bodyHandler != null); + huc.setConnectTimeout(this.connectTimeout); + huc.setReadTimeout(this.readTimeout); + huc.setRequestMethod(this.method.toString()); + if (this.headers.keySet().stream().noneMatch((name) -> { + return name.equalsIgnoreCase("User-Agent"); + })) { + this.headers.put("User-Agent", this.userAgent); + } + + if (this.headers.size() > 0) { + this.headers.forEach(huc::addRequestProperty); + } + + if (this.bodyHandler != null) { + this.bodyHandler.setHeaders(huc); + } + + huc.connect(); + if (this.bodyHandler != null) { + OutputStream os = huc.getOutputStream(); + Throwable var74 = null; + + try { + this.bodyHandler.accept(os); + os.flush(); + } catch (Throwable var64) { + var74 = var64; + throw var64; + } finally { + if (os != null) { + if (var74 != null) { + try { + os.close(); + } catch (Throwable var58) { + var74.addSuppressed(var58); + } + } else { + os.close(); + } + } + + } + } + } catch (Exception var70) { + logger.debug("Error calling REST WebService at [" + this.url + "]", var70); + response.status = -1; + response.exception = var70; + return response; + } + + int status; + try { + status = huc.getResponseCode(); + } catch (Exception var63) { + logger.debug("Error calling REST WebService at [" + this.url + "]", var63); + response.status = -1; + response.exception = var63; + return response; + } + + response.status = status; + InputStream is; + Throwable var76; + if (status >= 200 && status <= 299) { + if (this.successResponseHandler == null || this.method == RESTClient.HTTPMethod.HEAD) { + return response; + } + + try { + is = huc.getInputStream(); + var76 = null; + + try { + response.successResponse = this.successResponseHandler.apply(is); + } catch (Throwable var61) { + var76 = var61; + throw var61; + } finally { + if (is != null) { + if (var76 != null) { + try { + is.close(); + } catch (Throwable var59) { + var76.addSuppressed(var59); + } + } else { + is.close(); + } + } + + } + } catch (Exception var67) { + logger.debug("Error calling REST WebService at [" + this.url + "]", var67); + response.exception = var67; + return response; + } + } else { + if (this.errorResponseHandler == null) { + return response; + } + + try { + is = huc.getErrorStream(); + var76 = null; + + try { + response.errorResponse = this.errorResponseHandler.apply(is); + } catch (Throwable var62) { + var76 = var62; + throw var62; + } finally { + if (is != null) { + if (var76 != null) { + try { + is.close(); + } catch (Throwable var60) { + var76.addSuppressed(var60); + } + } else { + is.close(); + } + } + + } + } catch (Exception var69) { + logger.debug("Error calling REST WebService at [" + this.url + "]", var69); + response.exception = var69; + return response; + } + } + + return response; + } + } + } +} diff --git a/src/test/java/io/fusionauth/client/FusionAuthSSLClientTest.java b/src/test/java/io/fusionauth/client/FusionAuthSSLClientTest.java new file mode 100644 index 00000000..c87aecfd --- /dev/null +++ b/src/test/java/io/fusionauth/client/FusionAuthSSLClientTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, FusionAuth, All Rights Reserved + * + * Licensed 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.fusionauth.client; + +import com.inversoft.error.Errors; +import com.inversoft.rest.ClientResponse; +import io.fusionauth.domain.User; +import io.fusionauth.domain.api.UserRequest; +import io.fusionauth.domain.api.UserResponse; +import io.fusionauth.domain.api.user.ForgotPasswordRequest; +import io.fusionauth.domain.api.user.ForgotPasswordResponse; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +/** + * @author zcyang + * @description fusionauth ssl client test + */ +public class FusionAuthSSLClientTest { + @Test + public void forgotPassword_with_and_without_api_key() { + String fusionauthURL = System.getenv().getOrDefault("FUSIONAUTH_URL", "https://localhost:9011"); + String fusionauthApiKey = System.getenv().getOrDefault("FUSIONAUTH_API_KEY", "api-key"); + FusionAuthClient apiKeyClient = new FusionAuthClient(fusionauthApiKey, fusionauthURL, Boolean.TRUE, "BASE64 Encode ssl certificate value"); + + ClientResponse userResponse = apiKeyClient.retrieveUserByEmail("client_java@fusionauth.io"); + if (userResponse.status != 404) { + apiKeyClient.deleteUser(userResponse.successResponse.user.id); + } + + ClientResponse response = apiKeyClient.createUser(null, new UserRequest(null, new User() + .with(u -> u.email = "client_java@fusionauth.io") + .with(u -> u.password = "password"))); + assertTrue(response.wasSuccessful()); + + // w/ API key, success with response body + ClientResponse forgotPasswordResponse = apiKeyClient.forgotPassword(new ForgotPasswordRequest(response.successResponse.user.email, false)); + assertTrue(forgotPasswordResponse.wasSuccessful()); + assertNotNull(forgotPasswordResponse.successResponse); + assertNotNull(forgotPasswordResponse.successResponse.changePasswordId); + + + // w/out API Key, success but no response body + FusionAuthClient noApiKeyClient = new FusionAuthClient(null, fusionauthURL, Boolean.TRUE, "BASE64 Encode ssl certificate value"); + forgotPasswordResponse = noApiKeyClient.forgotPassword(new ForgotPasswordRequest(response.successResponse.user.email, false)); + assertTrue(forgotPasswordResponse.wasSuccessful()); + assertNull(forgotPasswordResponse.successResponse); + } +}