Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* [Thread safety considerations](#thread-safety-considerations)
* [Spring Support](#spring-support)
* [Configuration reference](#configuration-reference)
* [WireMock Integration Testing](#wiremock-integration-testing)
* [Building the SDK](#building-the-sdk)
* [Contributing](#contributing)

Expand Down Expand Up @@ -803,8 +804,21 @@ ApiClient client = Clients.builder()
```
[//]: # (end: disableCaching)

## WireMock Integration Testing

WireMock can be configured to serve HTTPS with a self-signed certificate and custom KeyStore. The SDK's HTTP client can be configured with a custom SSLContext and TrustManager to accept the certificate. This implementation demonstrates both: automatic self-signed certificate generation, WireMock HTTPS configuration, and SDK HTTP client setup with a custom TrustManager. It also uses dynamic port allocation for thread-safe parallel test execution.

### Running the Tests

```bash
mvn test -Dtest=WireMockOktaClientTest -pl integration-tests
```

See the complete implementation in `integration-tests/src/test/java/com/okta/sdk/tests/WireMockOktaClientTest.java`.

## Building the SDK


In most cases, you won't need to build the SDK from source. If you want to build it yourself, take a look at the [build instructions wiki](https://github.com/okta/okta-sdk-java/wiki/Build-It) (though just cloning the repo and running `mvn install` should get you going).

> **Note**: The SDK uses a large OpenAPI specification file (~84,000 lines). If you encounter memory issues during build:
Expand Down
4 changes: 2 additions & 2 deletions integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>2.27.2</version>
<artifactId>wiremock-jre8</artifactId>
<version>2.35.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
* Copyright 2017 Okta
*
* 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 com.okta.sdk.tests;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.okta.sdk.client.Clients;
import com.okta.sdk.resource.api.UserApi;
import com.okta.sdk.resource.client.ApiClient;
import com.okta.sdk.resource.client.ApiException;
import com.okta.sdk.resource.model.User;
import com.okta.sdk.resource.model.UserProfile;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.List;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertEquals;

import java.net.ServerSocket;

/**
* Integration test demonstrating WireMock + Okta SDK with HTTPS using self-signed certificates.
* This test proves the solution works end-to-end without hitting actual Okta servers.
*
* Thread-safe design: Uses dynamic port allocation for each test instance,
* allowing parallel test execution without port conflicts.
*/
public class WireMockOktaClientTest {

private WireMockServer wireMockServer;
private ApiClient client;
private UserApi userApi;
private int wireMockHttpsPort; // Dynamic port for thread-safety
private String wireMockHost; // Computed from dynamic port
private static final String KEYSTORE_PATH = "../../wiremock-keystore.jks"; // Path from integration-tests module
private static final String KEYSTORE_PASSWORD = "password";
private static final Object KEYSTORE_LOCK = new Object(); // Lock for thread-safe keystore generation

@BeforeMethod
public void setup() throws Exception {
// Generate WireMock keystore if it doesn't exist (synchronized for thread-safety)
String keystorePath = Paths.get(KEYSTORE_PATH).toAbsolutePath().toString();
synchronized(KEYSTORE_LOCK) {
java.io.File keystoreFile = new java.io.File(keystorePath);
if (!keystoreFile.exists()) {
System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
"Generating WireMock keystore at: " + keystorePath);
ProcessBuilder pb = new ProcessBuilder(
"keytool", "-genkey", "-alias", "wiremock", "-keyalg", "RSA",
"-keystore", keystorePath,
"-storepass", KEYSTORE_PASSWORD, "-keypass", KEYSTORE_PASSWORD,
"-dname", "CN=localhost", "-validity", "365", "-noprompt"
);
int exitCode = pb.start().waitFor();
if (exitCode != 0) {
throw new RuntimeException("Failed to generate WireMock keystore. " +
"Ensure 'keytool' is in your PATH (comes with Java)");
}
System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
"WireMock keystore generated successfully");
}
}

// Allocate a dynamic HTTPS port for this test instance (thread-safe)
wireMockHttpsPort = allocateAvailablePort();
wireMockHost = "https://localhost:" + wireMockHttpsPort;
System.out.println("[Thread: " + Thread.currentThread().getName() + "] " +
"Using dynamic HTTPS port: " + wireMockHttpsPort);

// Start WireMock on dynamic HTTPS port with self-signed certificate
wireMockServer = new WireMockServer(
WireMockConfiguration.wireMockConfig()
.httpsPort(wireMockHttpsPort)
.keystorePath(KEYSTORE_PATH)
.keystorePassword(KEYSTORE_PASSWORD)
);
wireMockServer.start();

// Configure custom SSL context with the self-signed keystore
KeyStore trustStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
trustStore.load(fis, KEYSTORE_PASSWORD.toCharArray());
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());

// Build HttpClient with custom SSL context using HTTP Client 5 APIs
// We need to set up the connection manager with custom SSL context
org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient =
HttpClients.custom()
.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(
new org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory(sslContext)
)
.build()
)
.build();

// Create ApiClient with the custom HttpClient and a disabled cache manager
client = new ApiClient(httpClient, new com.okta.sdk.impl.cache.DisabledCacheManager());
client.setBasePath(wireMockHost); // Use dynamic host with dynamic port

userApi = new UserApi(client);
}

/**
* Allocates an available port by binding to port 0 (OS assigns available port).
* This ensures thread-safe, collision-free port allocation.
*
* @return an available port number
* @throws Exception if port allocation fails
*/
private int allocateAvailablePort() throws Exception {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}

@AfterMethod
public void teardown() {
if (wireMockServer != null) {
wireMockServer.stop();
}
}

@Test
public void testGetUser() throws ApiException {
// Mock the Okta API endpoint for getting a user
String userId = "00ub0oNGTSWTBKOLGLHN";
stubFor(get(urlEqualTo("/api/v1/users/" + userId))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{" +
"\"id\":\"" + userId + "\"," +
"\"status\":\"ACTIVE\"," +
"\"created\":\"2013-06-24T16:39:18.000Z\"," +
"\"activated\":\"2013-06-24T16:39:19.000Z\"," +
"\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
"\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
"\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
"\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
"\"profile\":{" +
"\"firstName\":\"Isaac\"," +
"\"lastName\":\"Brock\"," +
"\"email\":\"isaac.brock@example.com\"," +
"\"login\":\"isaac.brock@example.com\"," +
"\"mobilePhone\":null" +
"}" +
"}")
));

// Call the SDK to get the user
User user = userApi.getUser(userId, null, null);

// Verify the response
assertNotNull(user);
assertEquals(userId, user.getId());
assertEquals("ACTIVE", user.getStatus().toString());
assertNotNull(user.getProfile());
assertEquals("isaac.brock@example.com", user.getProfile().getEmail());
assertEquals("Isaac", user.getProfile().getFirstName());
assertEquals("Brock", user.getProfile().getLastName());
}

@Test
public void testListUsers() throws ApiException {
// Mock the Okta API endpoint for listing users
stubFor(get(urlEqualTo("/api/v1/users"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("[" +
"{" +
"\"id\":\"00ub0oNGTSWTBKOLGLHN\"," +
"\"status\":\"ACTIVE\"," +
"\"created\":\"2013-06-24T16:39:18.000Z\"," +
"\"activated\":\"2013-06-24T16:39:19.000Z\"," +
"\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
"\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
"\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
"\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
"\"profile\":{" +
"\"firstName\":\"Isaac\"," +
"\"lastName\":\"Brock\"," +
"\"email\":\"isaac.brock@example.com\"," +
"\"login\":\"isaac.brock@example.com\"," +
"\"mobilePhone\":null" +
"}" +
"}," +
"{" +
"\"id\":\"00ub0oNGTSWTBKOLGLHO\"," +
"\"status\":\"ACTIVE\"," +
"\"created\":\"2013-06-24T16:39:18.000Z\"," +
"\"activated\":\"2013-06-24T16:39:19.000Z\"," +
"\"statusChanged\":\"2013-06-24T16:39:19.000Z\"," +
"\"lastLogin\":\"2013-10-02T14:06:25.000Z\"," +
"\"lastUpdated\":\"2013-10-02T14:06:25.000Z\"," +
"\"passwordChanged\":\"2013-09-11T23:30:26.000Z\"," +
"\"profile\":{" +
"\"firstName\":\"Jane\"," +
"\"lastName\":\"Developer\"," +
"\"email\":\"jane.developer@example.com\"," +
"\"login\":\"jane.developer@example.com\"," +
"\"mobilePhone\":null" +
"}" +
"}" +
"]")
));

// Call the SDK to list users
List<User> users = userApi.listUsers(null, null, null, null, null, null, null, null, null, null);

// Verify the response
assertNotNull(users);
assertEquals(2, users.size());

User firstUser = users.get(0);
assertEquals("00ub0oNGTSWTBKOLGLHN", firstUser.getId());
assertEquals("isaac.brock@example.com", firstUser.getProfile().getEmail());

User secondUser = users.get(1);
assertEquals("00ub0oNGTSWTBKOLGLHO", secondUser.getId());
assertEquals("jane.developer@example.com", secondUser.getProfile().getEmail());
}

@Test
public void testWireMockHttps() {
// This test simply verifies that the WireMock server is running on HTTPS
// and the SSL context is properly configured
assertNotNull(wireMockServer);
assertNotNull(client);
assertNotNull(userApi);
}
}