VYPR
Moderate severityOSV Advisory· Published Dec 10, 2025· Updated Dec 11, 2025

Improper Memory Cleanup in the Okta Java SDK

CVE-2025-66033

Description

Okta Java Management SDK facilitates interactions with the Okta management API. In versions 21.0.0 through 24.0.0, specific multithreaded implementations may encounter memory issues as threads are not properly cleaned up after requests are completed. Over time, this can degrade performance and availability in long-running applications and may result in a denial-of-service condition under sustained load. In addition to using the affected versions, users may be at risk if they are implementing a long-running application using the ApiClient in a multi-threaded manner. This issue is fixed in version 24.0.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Memory cleanup failure in Okta Java SDK threads can cause denial of service in long-running multithreaded applications.

Vulnerability

Overview

CVE-2025-66033 is a memory cleanup vulnerability in the Okta Java Management SDK, affecting versions 21.0.0 through 24.0.0. The issue arises from improper thread cleanup after completing API requests within multithreaded implementations, leading to memory leaks that accumulate over time [1][3].

Attack

Vector

The vulnerability is triggered when using the ApiClient in a multithreaded, long-running application. While no external authentication is required for exploitation, the attacker must be able to generate sustained API request load against the application. Over time, uncollected thread resources degrade performance and ultimately cause a denial-of-service condition under sustained load [1][3][4].

Impact

An attacker exploiting this vulnerability can degrade application performance and availability, potentially causing a full denial-of-service condition in environments running affected SDK versions with multithreaded access [3][4].

Mitigation

The issue is fixed in version 24.0.1 of the Okta Java SDK. Users of affected versions should upgrade to 24.0.1 or later. Official advisories provide the fix commit and migration guidance [suggest updates for all downstream integrations [1][2][4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
com.okta.sdk:okta-sdk-rootMaven
>= 21.0.0, < 24.0.124.0.1

Affected products

2

Patches

1
1daa9229a70f

Merge commit from fork

https://github.com/okta/okta-sdk-javasameerbamanha-oktaNov 26, 2025via ghsa
9 files changed · +1038 61
  • api/src/main/java/com/okta/sdk/helper/PaginationUtil.java+9 5 modified
    @@ -15,11 +15,6 @@
      */
     package com.okta.sdk.helper;
     
    -import com.okta.commons.lang.Assert;
    -import com.okta.sdk.resource.client.ApiClient;
    -import org.slf4j.Logger;
    -import org.slf4j.LoggerFactory;
    -
     import java.io.UnsupportedEncodingException;
     import java.net.MalformedURLException;
     import java.net.URL;
    @@ -29,6 +24,12 @@
     import java.util.List;
     import java.util.Map;
     
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import com.okta.commons.lang.Assert;
    +import com.okta.sdk.resource.client.ApiClient;
    +
     /**
      * Helper class for Pagination related functions.
      *
    @@ -45,7 +46,9 @@ public class PaginationUtil {
          *
          * @param apiClient {@link ApiClient} instance
          * @return the 'after' resource id
    +     * @deprecated Use {@link com.okta.sdk.resource.client.PagedIterable} instead for automatic pagination
          */
    +    @Deprecated(forRemoval = true, since = "24.1.0")
         public static String getAfter(ApiClient apiClient) {
             return getAfter(getNextPage(apiClient));
         }
    @@ -79,6 +82,7 @@ private static String getAfter(String nextPage) {
          * @param apiClient the {@link ApiClient} instance
          * @return the next page URL string
          */
    +    @SuppressWarnings("removal")
         private static String getNextPage(ApiClient apiClient) {
     
             Assert.notNull(apiClient, "apiClient cannot be null");
    
  • api/src/main/java/com/okta/sdk/resource/client/ApiResponse.java+110 0 added
    @@ -0,0 +1,110 @@
    +/*
    + * Copyright (c) 2025-Present, Okta, Inc.
    + *
    + * 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.resource.client;
    +
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * A stateless data object that represents a complete API response.
    + * 
    + * This class packages together everything from an HTTP response:
    + * - The response body (deserialized data)
    + * - The response headers (including Link headers for pagination)
    + * - The HTTP status code (200, 404, etc.)
    + * 
    + * Unlike the old approach which stored headers in a shared HashMap,
    + * ApiResponse is an immutable, thread-safe object that can be passed
    + * around without side effects.
    + * 
    + * Example usage:
    + * <pre>
    + * ApiResponse&lt;List&lt;User&gt;&gt; response = apiClient.invokeAPIWithHttpInfo(...);
    + * 
    + * int status = response.getStatusCode();              // e.g., 200
    + * List&lt;User&gt; users = response.getBody();             // The actual data
    + * Map&lt;String, List&lt;String&gt;&gt; headers = response.getHeaders();  // Response headers
    + * 
    + * // Check for pagination
    + * List&lt;String&gt; linkHeaders = headers.get("Link");
    + * </pre>
    + *
    + * @param <T> The type of the response body (e.g., List&lt;User&gt;, Group, etc.)
    + * @since 17.0.0
    + */
    +public class ApiResponse<T> {
    +
    +    private final int statusCode;
    +    private final Map<String, List<String>> headers;
    +    private final T body;
    +
    +    /**
    +     * Constructs a new ApiResponse with the given status code, headers, and body.
    +     *
    +     * @param statusCode the HTTP status code (e.g., 200, 404, 500)
    +     * @param headers the response headers as a map (header name → list of values)
    +     * @param body the deserialized response body (can be null for 204 No Content)
    +     */
    +    public ApiResponse(int statusCode, Map<String, List<String>> headers, T body) {
    +        this.statusCode = statusCode;
    +        this.headers = headers;
    +        this.body = body;
    +    }
    +
    +    /**
    +     * Gets the HTTP status code from the response.
    +     *
    +     * @return the status code (e.g., 200 for success, 404 for not found)
    +     */
    +    public int getStatusCode() {
    +        return statusCode;
    +    }
    +
    +    /**
    +     * Gets the response headers.
    +     * 
    +     * Common headers include:
    +     * - "Link": Pagination links (e.g., rel="next")
    +     * - "Content-Type": Response content type
    +     * - "X-Rate-Limit-Remaining": API rate limit info
    +     *
    +     * @return a map of header names to lists of header values
    +     */
    +    public Map<String, List<String>> getHeaders() {
    +        return headers;
    +    }
    +
    +    /**
    +     * Gets the deserialized response body.
    +     * 
    +     * This is the actual data returned by the API, already converted
    +     * from JSON into Java objects.
    +     *
    +     * @return the response body, or null if the response was empty (204 No Content)
    +     */
    +    public T getBody() {
    +        return body;
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "ApiResponse{" +
    +                "statusCode=" + statusCode +
    +                ", headersCount=" + (headers != null ? headers.size() : 0) +
    +                ", body=" + (body != null ? body.getClass().getSimpleName() : "null") +
    +                '}';
    +    }
    +}
    
  • api/src/main/java/com/okta/sdk/resource/client/PagedIterable.java+63 0 added
    @@ -0,0 +1,63 @@
    +/*
    + * Copyright (c) 2025-Present, Okta, Inc.
    + *
    + * 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.resource.client;
    +
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +/**
    + * A new lazy, paginated iterable.
    + *
    + * This class is a "recipe" for fetching paginated data. It does no
    + * work until the iterator() method is called (e.g., by a for-each loop).
    + *
    + * Each call to iterator() creates a new, independent PagedIterator instance,
    + * ensuring thread-safety and isolation of pagination state.
    + *
    + * @param <T> The type of item in the collection (e.g., User)
    + */
    +public class PagedIterable<T> implements Iterable<T> {
    +
    +    /**
    +     * The "strategy" or "recipe" for fetching one page.
    +     * The String input is the 'nextUrl' (or null for the first page).
    +     * The ApiResponse output contains the page of results.
    +     */
    +    private final Function<String, ApiResponse<List<T>>> pageFetcher;
    +
    +    /**
    +     * Constructs a new PagedIterable with the given page fetching strategy.
    +     *
    +     * @param pageFetcher A function that takes a next URL (or null for the first page)
    +     *                    and returns an ApiResponse containing a list of items and headers.
    +     */
    +    public PagedIterable(Function<String, ApiResponse<List<T>>> pageFetcher) {
    +        this.pageFetcher = pageFetcher;
    +    }
    +
    +    /**
    +     * Creates a new, stateful iterator.
    +     * This is the key to thread-safety: every loop gets its
    +     * own private iterator to manage its own private state.
    +     *
    +     * @return A new PagedIterator instance for this iteration.
    +     */
    +    @Override
    +    public Iterator<T> iterator() {
    +        return new PagedIterator<>(pageFetcher);
    +    }
    +}
    
  • api/src/main/java/com/okta/sdk/resource/client/PagedIterator.java+136 0 added
    @@ -0,0 +1,136 @@
    +/*
    + * Copyright (c) 2025-Present, Okta, Inc.
    + *
    + * 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.resource.client;
    +
    +import com.okta.commons.lang.Collections;
    +
    +import java.util.Iterator;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NoSuchElementException;
    +import java.util.Queue;
    +import java.util.function.Function;
    +import java.util.regex.Matcher;
    +import java.util.regex.Pattern;
    +
    +/**
    + * The new stateful, but *isolated*, iterator for pagination.
    + *
    + * This object is created for each loop and holds the state
    + * (the next page URL and the current page's items) for that
    + * loop *only*. It is not shared, making it thread-safe.
    + *
    + * @param <T> The type of item in the collection (e.g., User)
    + */
    +public class PagedIterator<T> implements Iterator<T> {
    +
    +    // Regex to find the "next" link in a Link header
    +    private static final Pattern NEXT_LINK_PATTERN = Pattern.compile("<([^>]+)>;\\s*rel=\"next\"");
    +    
    +    private final Function<String, ApiResponse<List<T>>> pageFetcher;
    +    
    +    // --- THIS IS THE ISOLATED STATE ---
    +    private String nextUrl = null;
    +    private boolean isFirstPage = true;
    +    private Queue<T> itemBuffer = new LinkedList<>();
    +    // ---
    +
    +    /**
    +     * Constructs a new PagedIterator with the given page fetching strategy.
    +     *
    +     * @param pageFetcher A function that takes a next URL (or null for the first page)
    +     *                    and returns an ApiResponse containing a list of items and headers.
    +     */
    +    public PagedIterator(Function<String, ApiResponse<List<T>>> pageFetcher) {
    +        this.pageFetcher = pageFetcher;
    +    }
    +
    +    @Override
    +    public boolean hasNext() {
    +        // 1. If we have items in our buffer, we're good.
    +        if (!itemBuffer.isEmpty()) {
    +            return true;
    +        }
    +
    +        // 2. If it's the first page OR we have a next link,
    +        //    we must try to fetch the next page.
    +        if (isFirstPage || nextUrl != null) {
    +            fetchNextPage();
    +        }
    +
    +        // 3. After fetching, check the buffer again.
    +        //    If it's still empty, we've reached the end.
    +        return !itemBuffer.isEmpty();
    +    }
    +
    +    @Override
    +    public T next() {
    +        // hasNext() ensures the buffer is populated if needed.
    +        if (!hasNext()) {
    +            throw new NoSuchElementException("No more items in pagination.");
    +        }
    +        // Return the next item from the current page's buffer.
    +        return itemBuffer.poll();
    +    }
    +
    +    /**
    +     * The main engine. Calls the fetcher "strategy",
    +     * fills the buffer, and saves the next link.
    +     */
    +    private void fetchNextPage() {
    +        // Call the "strategy" function from API class
    +        ApiResponse<List<T>> response = pageFetcher.apply(isFirstPage ? null : nextUrl);
    +        
    +        // Add all items from the response body to our buffer
    +        if (response.getBody() != null) {
    +            itemBuffer.addAll(response.getBody());
    +        }
    +
    +        // Update our *private* state with the next link from the headers
    +        this.nextUrl = parseNextLinkFromHeaders(response.getHeaders());
    +        this.isFirstPage = false;
    +    }
    +
    +    /**
    +     * Helper to parse the 'Link' header for rel="next"
    +     *
    +     * @param headers The response headers map
    +     * @return The next URL if found, null otherwise
    +     */
    +    private String parseNextLinkFromHeaders(Map<String, List<String>> headers) {
    +        if (headers == null) return null;
    +        
    +        // Try both "Link" and "link" as HTTP headers can vary in case
    +        List<String> linkHeaders = headers.get("Link");
    +        if (Collections.isEmpty(linkHeaders)) {
    +            linkHeaders = headers.get("link");
    +        }
    +        if (Collections.isEmpty(linkHeaders)) {
    +            return null;
    +        }
    +        
    +        for (String linkHeader : linkHeaders) {
    +            Matcher matcher = NEXT_LINK_PATTERN.matcher(linkHeader);
    +            if (matcher.find()) {
    +                String url = matcher.group(1);
    +                return url; // The URL
    +            }
    +        }
    +        
    +        return null; // No "next" link found
    +    }
    +}
    
  • api/src/main/resources/custom_templates/ApiClient.mustache+203 49 modified
    @@ -161,21 +161,10 @@ protected List<ServerConfiguration> servers = new ArrayList<ServerConfiguration>
     
         private Cache<String, Object> cache;
     
    -    /**
    -     * Stores the last HTTP status code for each thread.
    -     */
    -    private final Map<Long, Integer> lastStatusCodeByThread = new ConcurrentHashMap<>();
    -
    -    /**
    -     * Stores the last HTTP response headers for each thread.
    -     */
    -    private final Map<Long, Map<String, List<String>>> lastResponseHeadersByThread = new ConcurrentHashMap<>();
    -
    -    /**
    -     * Utility to track and warn about multi-threaded usage patterns.
    -     */
    -    private final com.okta.sdk.client.MultiThreadingWarningUtil threadingWarningUtil =
    -        new com.okta.sdk.client.MultiThreadingWarningUtil();
    +    // ThreadLocal for backward compatibility with old pagination methods (PaginationUtil)
    +    // These are automatically cleaned up when threads die, preventing memory leaks
    +    private static final ThreadLocal<Integer> lastStatusCode = new ThreadLocal<>();
    +    private static final ThreadLocal<Map<String, List<String>>> lastResponseHeaders = new ThreadLocal<>();
     
             private DateFormat dateFormat;
     
    @@ -332,40 +321,32 @@ protected List<ServerConfiguration> servers = new ArrayList<ServerConfiguration>
                         /**
                         * Gets the status code of the previous request
                         *
    +                    * @deprecated Use invokeAPIWithHttpInfo() methods to get ApiResponse with status code.
    +                    * This method will be removed in a future version.
    +                    *
                         * @return Status code
                         */
    -                    @Deprecated
    +                    @Deprecated(forRemoval = true, since = "24.1.0")
                         public int getStatusCode() {
    -                    // Record thread access for monitoring multi-threaded usage
    -                    threadingWarningUtil.recordThreadAccess();
    -                    Long threadId = Thread.currentThread().getId();
    -                    Integer statusCode = lastStatusCodeByThread.get(threadId);
    +                    Integer statusCode = lastStatusCode.get();
                         return statusCode != null ? statusCode : 0;
                         }
     
                         /**
                         * Gets the response headers of the previous request
    -                    * @return Response headers
    +                    * 
    +                    * @deprecated This method is NOT thread-safe and causes memory leaks.
    +                    * It relies on storing state by thread ID, which fails in thread pools.
    +                    * Use the new paginated methods (e.g., listUsersPaged()) that return
    +                    * Iterable instead, or use invokeAPIWithHttpInfo() to get ApiResponse objects.
    +                    * This method will be removed in a future version.
    +                    * 
    +                    * @return Response headers (may be null or stale in multi-threaded environments)
                         */
    -                    @Deprecated
    +                    @Deprecated(forRemoval = true, since = "24.1.0")
                         public Map<String, List<String>> getResponseHeaders() {
    -                        // Record thread access for monitoring multi-threaded usage
    -                        threadingWarningUtil.recordThreadAccess();
    -                        Long threadId = Thread.currentThread().getId();
    -                        return lastResponseHeadersByThread.get(threadId);
    -                        }
    -
    -                        /**
    -                        * Gets the count of unique threads that have accessed this ApiClient instance.
    -                        * This is useful for monitoring and debugging multi-threaded usage patterns.
    -                        * 
    -                        * <p><strong>Note:</strong> A count greater than 1 indicates multi-threaded usage,
    -                        * which may lead to unexpected behavior with pagination and other stateful operations.</p>
    -                        *
    -                        * @return the number of unique threads that have accessed this ApiClient
    -                        */
    -                        public int getUniqueThreadCount() {
    -                        return threadingWarningUtil.getUniqueThreadCount();
    +                        Map<String, List<String>> headers = lastResponseHeaders.get();
    +                        return headers != null ? headers : Collections.emptyMap();
                             }
     
                             /**
    @@ -1133,24 +1114,198 @@ protected List<ServerConfiguration> servers = new ArrayList<ServerConfiguration>
     
                                                                         protected <T> T processResponse(CloseableHttpResponse response, TypeReference<T> returnType) throws ApiException, IOException, ParseException {
                                                                             int statusCode = response.getCode();
    -                                                                        // Record thread access for monitoring multi-threaded usage
    -                                                                        threadingWarningUtil.recordThreadAccess();
    -                                                                        Long threadId = Thread.currentThread().getId();
    -                                                                        lastStatusCodeByThread.put(threadId, statusCode);
    +                                                                        
    +                                                                        // Store in ThreadLocal for backward compatibility with PaginationUtil
    +                                                                        // ThreadLocal automatically cleans up when thread dies (no memory leak)
    +                                                                        lastStatusCode.set(statusCode);
    +                                                                        
                                                                             if (statusCode == HttpStatus.SC_NO_CONTENT) {
                                                                             return null;
                                                                             }
     
                                                                             Map<String, List<String>> responseHeaders = transformResponseHeaders(response.getHeaders());
    -                                                                            lastResponseHeadersByThread.put(threadId, responseHeaders);
    +                                                                        lastResponseHeaders.set(responseHeaders);
    +
    +                                                                                // Note: We keep headers in ThreadLocal for backward compatibility with PaginationUtil
    +                                                                                // ThreadLocal is automatically cleaned up when thread dies
    +                                                                                if (isSuccessfulStatus(statusCode)) {
    +                                                                                return this.deserialize(response, returnType);
    +                                                                                } else {
    +                                                                                String message = EntityUtils.toString(response.getEntity());
    +                                                                                throw new ApiException(message, statusCode, responseHeaders, message);
    +                                                                                }
    +                                                                              }
     
    +                                                                        /**
    +                                                                        * NEW STATELESS METHOD: Process response and return full ApiResponse object.
    +                                                                        * This method does NOT store state in thread-local maps.
    +                                                                        * It returns the complete response including body, headers, and status code.
    +                                                                        *
    +                                                                        * @param <T> Type
    +                                                                        * @param response The HTTP response
    +                                                                        * @param returnType The expected return type
    +                                                                        * @return ApiResponse object containing body, headers, and status
    +                                                                        * @throws ApiException API exception
    +                                                                        * @throws IOException IO exception
    +                                                                        * @throws ParseException Parse exception
    +                                                                        */
    +                                                                        protected <T> ApiResponse<T> processResponseWithHttpInfo(CloseableHttpResponse response, TypeReference<T> returnType) throws ApiException, IOException, ParseException {
    +                                                                            int statusCode = response.getCode();
    +                                                                            Map<String, List<String>> responseHeaders = transformResponseHeaders(response.getHeaders());
    +                                                                            
    +                                                                            if (statusCode == HttpStatus.SC_NO_CONTENT) {
    +                                                                                return new ApiResponse<>(statusCode, responseHeaders, null);
    +                                                                            }
    +                                                                            
                                                                                 if (isSuccessfulStatus(statusCode)) {
    -                                                                            return this.deserialize(response, returnType);
    +                                                                                T body = this.deserialize(response, returnType);
    +                                                                                return new ApiResponse<>(statusCode, responseHeaders, body);
                                                                                 } else {
    -                                                                            String message = EntityUtils.toString(response.getEntity());
    -                                                                            throw new ApiException(message, statusCode, responseHeaders, message);
    -                                                                            }
    +                                                                                String message = EntityUtils.toString(response.getEntity());
    +                                                                                throw new ApiException(message, statusCode, responseHeaders, message);
                                                                                 }
    +                                                                        }
    +
    +                                                                        /**
    +                                                                        * NEW STATELESS METHOD: Invoke API with full URL and return ApiResponse object.
    +                                                                        * This is used for subsequent pages in pagination where the full URL comes from the Link header.
    +                                                                        *
    +                                                                        * @param <T> Type
    +                                                                        * @param fullUrl The complete URL to invoke
    +                                                                        * @param method The request method, one of "GET", "POST", "PUT", and "DELETE"
    +                                                                        * @param headerParams The header parameters
    +                                                                        * @param accept The request's Accept header
    +                                                                        * @param contentType The request's Content-Type header
    +                                                                        * @param authNames The authentications to apply
    +                                                                        * @param returnType Return type
    +                                                                        * @return ApiResponse object containing body, headers, and status
    +                                                                        * @throws ApiException API exception
    +                                                                        */
    +                                                                        public <T> ApiResponse<T> invokeAPIWithHttpInfoFullURL(
    +                                                                            String fullUrl,
    +                                                                            String method,
    +                                                                            Map<String, String> headerParams,
    +                                                                            String accept,
    +                                                                            String contentType,
    +                                                                            String[] authNames,
    +                                                                            TypeReference<T> returnType) throws ApiException {
    +                                                                                
    +                                                                                updateParamsForAuth(authNames, new ArrayList<>(), headerParams, new HashMap<>());
    +
    +                                                                                ClassicRequestBuilder builder = ClassicRequestBuilder.create(method);
    +                                                                                builder.setUri(fullUrl);
    +
    +                                                                                if (accept != null) {
    +                                                                                    builder.addHeader("Accept", accept);
    +                                                                                }
    +                                                                                for (Entry<String, String> keyValue : headerParams.entrySet()) {
    +                                                                                    builder.addHeader(keyValue.getKey(), keyValue.getValue());
    +                                                                                }
    +                                                                                for (Map.Entry<String,String> keyValue : defaultHeaderMap.entrySet()) {
    +                                                                                    if (!headerParams.containsKey(keyValue.getKey())) {
    +                                                                                        builder.addHeader(keyValue.getKey(), keyValue.getValue());
    +                                                                                    }
    +                                                                                }
    +
    +                                                                                ContentType contentTypeObj = getContentType(contentType);
    +                                                                                builder.setEntity(new StringEntity("", contentTypeObj));
    +
    +                                                                                try (CloseableHttpResponse response = httpClient.execute(builder.build())) {
    +                                                                                    return processResponseWithHttpInfo(response, returnType);
    +                                                                                } catch (IOException | ParseException e) {
    +                                                                                    throw new ApiException(e);
    +                                                                                }
    +                                                                        }
    +
    +                                                                        /**
    +                                                                        * NEW STATELESS METHOD: Invoke API and return full ApiResponse object.
    +                                                                        * This is the thread-safe, stateless version that returns ApiResponse instead of just the body.
    +                                                                        * Use this method for the first page of paginated APIs to get access to response headers.
    +                                                                        *
    +                                                                        * @param <T> Type
    +                                                                        * @param path The sub-path of the HTTP URL
    +                                                                        * @param method The request method, one of "GET", "POST", "PUT", and "DELETE"
    +                                                                        * @param queryParams The query parameters
    +                                                                        * @param collectionQueryParams The collection query parameters
    +                                                                        * @param urlQueryDeepObject A URL query string for deep object parameters
    +                                                                        * @param body The request body object - if it is not binary, otherwise null
    +                                                                        * @param headerParams The header parameters
    +                                                                        * @param cookieParams The cookie parameters
    +                                                                        * @param formParams The form parameters
    +                                                                        * @param accept The request's Accept header
    +                                                                        * @param contentType The request's Content-Type header
    +                                                                        * @param authNames The authentications to apply
    +                                                                        * @param returnType Return type
    +                                                                        * @return ApiResponse object containing body, headers, and status
    +                                                                        * @throws ApiException API exception
    +                                                                        */
    +                                                                        public <T> ApiResponse<T> invokeAPIWithHttpInfo(
    +                                                                            String path,
    +                                                                            String method,
    +                                                                            List<Pair> queryParams,
    +                                                                            List<Pair> collectionQueryParams,
    +                                                                            String urlQueryDeepObject,
    +                                                                            Object body,
    +                                                                            Map<String, String> headerParams,
    +                                                                            Map<String, String> cookieParams,
    +                                                                            Map<String, Object> formParams,
    +                                                                            String accept,
    +                                                                            String contentType,
    +                                                                            String[] authNames,
    +                                                                            TypeReference<T> returnType) throws ApiException {
    +                                                                                
    +                                                                                if (body != null && !formParams.isEmpty()) {
    +                                                                                    throw new ApiException("Cannot have body and form params");
    +                                                                                }
    +
    +                                                                                updateParamsForAuth(authNames, queryParams, headerParams, cookieParams);
    +                                                                                final String url = buildUrl(path, queryParams, collectionQueryParams, urlQueryDeepObject);
    +
    +                                                                                ClassicRequestBuilder builder = ClassicRequestBuilder.create(method);
    +                                                                                builder.setUri(url);
    +
    +                                                                                if (accept != null) {
    +                                                                                    builder.addHeader("Accept", accept);
    +                                                                                }
    +                                                                                for (Entry<String, String> keyValue : headerParams.entrySet()) {
    +                                                                                    builder.addHeader(keyValue.getKey(), keyValue.getValue());
    +                                                                                }
    +                                                                                for (Map.Entry<String,String> keyValue : defaultHeaderMap.entrySet()) {
    +                                                                                    if (!headerParams.containsKey(keyValue.getKey())) {
    +                                                                                        builder.addHeader(keyValue.getKey(), keyValue.getValue());
    +                                                                                    }
    +                                                                                }
    +
    +                                                                                BasicCookieStore store = new BasicCookieStore();
    +                                                                                for (Entry<String, String> keyValue : cookieParams.entrySet()) {
    +                                                                                    store.addCookie(buildCookie(keyValue.getKey(), keyValue.getValue(), builder.getUri()));
    +                                                                                }
    +                                                                                for (Entry<String,String> keyValue : defaultCookieMap.entrySet()) {
    +                                                                                    if (!cookieParams.containsKey(keyValue.getKey())) {
    +                                                                                        store.addCookie(buildCookie(keyValue.getKey(), keyValue.getValue(), builder.getUri()));
    +                                                                                    }
    +                                                                                }
    +
    +                                                                                HttpClientContext context = HttpClientContext.create();
    +                                                                                context.setCookieStore(store);
    +
    +                                                                                ContentType contentTypeObj = getContentType(contentType);
    +                                                                                if (body != null || !formParams.isEmpty()) {
    +                                                                                    if (isBodyAllowed(method)) {
    +                                                                                        builder.setEntity(serialize(body, formParams, contentTypeObj));
    +                                                                                    } else {
    +                                                                                        throw new ApiException("method " + method + " does not support a request body");
    +                                                                                    }
    +                                                                                } else {
    +                                                                                    builder.setEntity(new StringEntity("", contentTypeObj));
    +                                                                                }
    +
    +                                                                                try (CloseableHttpResponse response = httpClient.execute(builder.build(), context)) {
    +                                                                                    return processResponseWithHttpInfo(response, returnType);
    +                                                                                } catch (IOException | ParseException e) {
    +                                                                                    throw new ApiException(e);
    +                                                                                }
    +                                                                        }
     
                                                                             /**
                                                                             * Invoke API by sending HTTP request with the given options.
    @@ -1318,7 +1473,6 @@ protected List<ServerConfiguration> servers = new ArrayList<ServerConfiguration>
                                                                                                 }
                                                                                                 }
     
    -
                                                                                                 /**
                                                                                                 * Update query and header parameters based on authentication settings.
                                                                                                 *
    
  • api/src/main/resources/custom_templates/api.mustache+117 0 modified
    @@ -14,12 +14,15 @@
         limitations under the License.
     }}
     {{>licenseInfo}}
    +// CUSTOM TEMPLATE VERSION - PAGINATION ENABLED
     package {{package}};
     
     import com.fasterxml.jackson.core.type.TypeReference;
     
     import {{invokerPackage}}.ApiException;
     import {{invokerPackage}}.ApiClient;
    +import {{invokerPackage}}.ApiResponse;
    +import {{invokerPackage}}.PagedIterable;
     import {{invokerPackage}}.Configuration;
     import {{modelPackage}}.*;
     import {{invokerPackage}}.Pair;
    @@ -220,6 +223,120 @@ import org.openapitools.jackson.nullable.JsonNullableModule;
                 );
                 }
     
    +        {{#returnType}}
    +        {{#isArray}}
    +
    +        /**
    +        * {{summary}} (Paginated)
    +        * {{notes}}
    +        * 
    +        * This method returns a lazy, paginated iterable that automatically handles pagination.
    +        * It is thread-safe and does not cause memory leaks. Use this method in a for-each loop
    +        * to automatically fetch all pages without manual cursor management.
    +        {{#allParams}}
    +            * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}}
    +        {{/allParams}}
    +            * @return Iterable&lt;{{returnBaseType}}&gt; lazy iterable over all pages
    +        {{#externalDocs}}
    +            * @see <a href="{{url}}">{{summary}} Documentation</a>
    +        {{/externalDocs}}
    +        */
    +        public Iterable<{{{returnBaseType}}}> {{operationId}}Paged({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) {
    +            return this.{{operationId}}Paged({{#allParams}}{{paramName}}, {{/allParams}}Collections.emptyMap());
    +        }
    +
    +        /**
    +        * {{summary}} (Paginated with additional headers)
    +        * {{notes}}
    +        * 
    +        * This method returns a lazy, paginated iterable that automatically handles pagination.
    +        * It is thread-safe and does not cause memory leaks.
    +        {{#allParams}}
    +            * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}}
    +        {{/allParams}}
    +            * @param additionalHeaders additional headers for this call
    +            * @return Iterable&lt;{{returnBaseType}}&gt; lazy iterable over all pages
    +        */
    +        public Iterable<{{{returnBaseType}}}> {{operationId}}Paged({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}Map<String, String> additionalHeaders) {
    +            return new PagedIterable<>(nextUrl -> {
    +                try {
    +                    if (nextUrl == null) {
    +                        // First page
    +                        Object localVarPostBody = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}};
    +                        String localVarPath = "{{{path}}}"{{#pathParams}}.replaceAll("\\{" + "{{baseName}}" + "\\}", apiClient.escapeString({{{paramName}}}.toString())){{/pathParams}};
    +                        
    +                        StringJoiner localVarQueryStringJoiner = new StringJoiner("&");
    +                        List<Pair> localVarQueryParams = new ArrayList<Pair>();
    +                        List<Pair> localVarCollectionQueryParams = new ArrayList<Pair>();
    +                        {{#queryParams}}
    +                        {{#collectionFormat}}localVarCollectionQueryParams.addAll(apiClient.parameterToPairs("{{{collectionFormat}}}", {{/collectionFormat}}{{^collectionFormat}}localVarQueryParams.addAll(apiClient.parameterToPair({{/collectionFormat}}"{{baseName}}", {{paramName}}));
    +                        {{/queryParams}}
    +                        
    +                        Map<String, String> localVarHeaderParams = new HashMap<String, String>();
    +                        {{#headerParams}}if ({{paramName}} != null)
    +                            localVarHeaderParams.put("{{baseName}}", apiClient.parameterToString({{paramName}}));
    +                        {{/headerParams}}
    +                        localVarHeaderParams.putAll(additionalHeaders);
    +                        
    +                        final String[] localVarAccepts = {
    +                            {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}
    +                        };
    +                        final String localVarAccept = apiClient.selectHeaderAccept(localVarAccepts, localVarPath);
    +                        final String[] localVarContentTypes = {"text/xml", "application/json"{{#consumes}}, "{{{mediaType}}}"{{/consumes}}};
    +                        final String localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes);
    +                        String[] localVarAuthNames = new String[] { {{#authMethods}}"{{name}}"{{^-last}}, {{/-last}}{{/authMethods}} };
    +                        
    +                        TypeReference<List<{{{returnBaseType}}}>> localVarReturnType = new TypeReference<List<{{{returnBaseType}}}>>() {};
    +                        return apiClient.invokeAPIWithHttpInfo(
    +                            localVarPath,
    +                            "{{httpMethod}}",
    +                            localVarQueryParams,
    +                            localVarCollectionQueryParams,
    +                            localVarQueryStringJoiner.toString(),
    +                            localVarPostBody,
    +                            localVarHeaderParams,
    +                            new HashMap<>(),
    +                            new HashMap<>(),
    +                            localVarAccept,
    +                            localVarContentType,
    +                            localVarAuthNames,
    +                            localVarReturnType
    +                        );
    +                    } else {
    +                        // Subsequent pages - use full URL from Link header
    +                        Map<String, String> localVarHeaderParams = new HashMap<String, String>();
    +                        {{#headerParams}}if ({{paramName}} != null)
    +                            localVarHeaderParams.put("{{baseName}}", apiClient.parameterToString({{paramName}}));
    +                        {{/headerParams}}
    +                        localVarHeaderParams.putAll(additionalHeaders);
    +                        
    +                        final String[] localVarAccepts = {
    +                            {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}
    +                        };
    +                        final String localVarAccept = apiClient.selectHeaderAccept(localVarAccepts, nextUrl);
    +                        final String[] localVarContentTypes = {"text/xml", "application/json"{{#consumes}}, "{{{mediaType}}}"{{/consumes}}};
    +                        final String localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes);
    +                        String[] localVarAuthNames = new String[] { {{#authMethods}}"{{name}}"{{^-last}}, {{/-last}}{{/authMethods}} };
    +                        
    +                        TypeReference<List<{{{returnBaseType}}}>> localVarReturnType = new TypeReference<List<{{{returnBaseType}}}>>() {};
    +                        return apiClient.invokeAPIWithHttpInfoFullURL(
    +                            nextUrl,
    +                            "{{httpMethod}}",
    +                            localVarHeaderParams,
    +                            localVarAccept,
    +                            localVarContentType,
    +                            localVarAuthNames,
    +                            localVarReturnType
    +                        );
    +                    }
    +                } catch (ApiException e) {
    +                    throw new RuntimeException("Failed to fetch page", e);
    +                }
    +            });
    +        }
    +        {{/isArray}}
    +        {{/returnType}}
    +
         {{/operation}}
     
             protected static ObjectMapper getObjectMapper() {
    
  • examples/quickstart/pom.xml+0 5 modified
    @@ -48,11 +48,6 @@
                 <artifactId>testng</artifactId>
                 <scope>test</scope>
             </dependency>
    -        <dependency>
    -            <groupId>com.okta.sdk</groupId>
    -            <artifactId>okta-sdk-api</artifactId>
    -            <version>23.0.1</version>
    -        </dependency>
         </dependencies>
     
         <build>
    
  • integration-tests/src/test/groovy/com/okta/sdk/tests/it/PaginationIT.groovy+398 0 added
    @@ -0,0 +1,398 @@
    +/*
    + * Copyright 2025-Present Okta, Inc.
    + *
    + * 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.it
    +
    +import com.okta.sdk.resource.api.UserApi
    +import com.okta.sdk.resource.api.GroupApi
    +import com.okta.sdk.resource.api.ApplicationApi
    +import com.okta.sdk.resource.model.*
    +import com.okta.sdk.tests.it.util.ITSupport
    +import org.testng.annotations.Test
    +
    +import static org.hamcrest.MatcherAssert.assertThat
    +import static org.hamcrest.Matchers.*
    +
    +/**
    + * Integration tests for the new PagedIterable pagination API
    + * Tests pagination across multiple API endpoints: Users, Groups, Applications, etc.
    + */
    +class PaginationIT extends ITSupport {
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableWithUsers() {
    +        println "\n=== Testing Users Pagination ==="
    +        UserApi userApi = new UserApi(getClient())
    +        
    +        // Create multiple test users to ensure pagination
    +        def usersToCreate = 5
    +        def createdUsers = []
    +        
    +        try {
    +            println "Creating ${usersToCreate} test users..."
    +            for (int i = 0; i < usersToCreate; i++) {
    +                def email = "pag${i}-${uniqueTestName.take(30)}@ex.com"
    +                User user = createUser(userApi, email, "PagTest${i}", "User${i}")
    +                createdUsers.add(user)
    +                registerForCleanup(user)
    +            }
    +            
    +            // Allow time for users to be indexed
    +            Thread.sleep(getTestOperationDelay())
    +            
    +            // Test: Iterate using the new PagedIterable API with small page size
    +            println "Fetching users with PagedIterable (limit=2 per page)..."
    +            def collectedUsers = []
    +            def pageCount = 0
    +            
    +            // Use listUsersPaged with limit=2 to force pagination
    +            // Signature: listUsersPaged(String contentType, String q, String after, Integer limit, String filter, String search, String sortBy, String sortOrder)
    +            for (User user : userApi.listUsersPaged(null, null, null, 2, null, null, null, null)) {
    +                collectedUsers.add(user)
    +                if (collectedUsers.size() % 2 == 0) {
    +                    pageCount++
    +                    println "  Fetched page ${pageCount} (${collectedUsers.size()} total users so far)"
    +                }
    +                // Stop after collecting enough users to find all ours (collect more to be safe)
    +                if (collectedUsers.size() >= usersToCreate * 2) {
    +                    break
    +                }
    +            }
    +            
    +            println "✓ Collected ${collectedUsers.size()} users across ${pageCount} pages"
    +            
    +            assertThat("Should have collected enough users", 
    +                       collectedUsers.size(), greaterThanOrEqualTo(usersToCreate))
    +            assertThat("Should have fetched multiple pages", pageCount, greaterThan(1))
    +            
    +            // Verify we can find at least some of our created users (not all may be in the first pages)
    +            def foundEmails = collectedUsers.collect { it.profile.email }
    +            def ourEmails = createdUsers.collect { it.profile.email }
    +            def foundOurUsers = ourEmails.findAll { email -> foundEmails.contains(email) }
    +            
    +            println "  Found ${foundOurUsers.size()} of our ${usersToCreate} created users"
    +            assertThat("Should find most of our created users", 
    +                      foundOurUsers.size(), greaterThanOrEqualTo(3)) // At least 3 out of 5
    +            
    +        } finally {
    +            // Cleanup is handled by registerForCleanup
    +        }
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableWithGroups() {
    +        println "\n=== Testing Groups Pagination ==="
    +        GroupApi groupApi = new GroupApi(getClient())
    +        
    +        def groupsToCreate = 5
    +        def createdGroups = []
    +        
    +        try {
    +            println "Creating ${groupsToCreate} test groups..."
    +            for (int i = 0; i < groupsToCreate; i++) {
    +                AddGroupRequest request = new AddGroupRequest()
    +                OktaUserGroupProfile profile = new OktaUserGroupProfile()
    +                profile.name = "PagTest${i}-${uniqueTestName}".take(100) // Ensure name isn't too long
    +                profile.description = "Test group ${i}"
    +                request.profile = profile
    +                
    +                Group created = groupApi.addGroup(request)
    +                createdGroups.add(created)
    +                registerForCleanup(created)
    +            }
    +            
    +            Thread.sleep(getTestOperationDelay())
    +            
    +            println "Fetching groups with PagedIterable (limit=2 per page)..."
    +            def collectedGroups = []
    +            def pageCount = 0
    +            
    +            // Use listGroupsPaged with limit=2 to force pagination
    +            // Signature: listGroupsPaged(String q, String filter, String after, Integer limit, String expand, String search, String sortBy, String sortOrder)
    +            for (Group group : groupApi.listGroupsPaged(null, null, null, 2, null, null, null, null)) {
    +                collectedGroups.add(group)
    +                if (collectedGroups.size() % 2 == 0) {
    +                    pageCount++
    +                    println "  Fetched page ${pageCount} (${collectedGroups.size()} total groups so far)"
    +                }
    +                // Collect more groups to ensure we find all ours
    +                if (collectedGroups.size() >= groupsToCreate * 2) {
    +                    break
    +                }
    +            }
    +            
    +            println "✓ Collected ${collectedGroups.size()} groups across ${pageCount} pages"
    +            
    +            assertThat("Should have collected enough groups", 
    +                       collectedGroups.size(), greaterThanOrEqualTo(groupsToCreate))
    +            assertThat("Should have fetched multiple pages", pageCount, greaterThan(1))
    +            
    +            // Verify we can find at least some of our created groups
    +            def foundNames = collectedGroups.collect { it.profile.name }
    +            def ourNames = createdGroups.collect { it.profile.name }
    +            def foundOurGroups = ourNames.findAll { name -> foundNames.contains(name) }
    +            
    +            println "  Found ${foundOurGroups.size()} of our ${groupsToCreate} created groups"
    +            assertThat("Should find most of our created groups", 
    +                      foundOurGroups.size(), greaterThanOrEqualTo(3)) // At least 3 out of 5
    +            
    +        } finally {
    +            // Cleanup is handled by registerForCleanup
    +        }
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableWithApplications() {
    +        println "\n=== Testing Applications Pagination ==="
    +        ApplicationApi appApi = new ApplicationApi(getClient())
    +        
    +        println "Fetching applications with PagedIterable (limit=5 per page)..."
    +        def collectedApps = []
    +        def pageCount = 0
    +        
    +        // Use listApplicationsPaged
    +        // Signature: listApplicationsPaged(String q, String after, Boolean useOptimization, Integer limit, String filter, String expand, Boolean includeNonDeleted)
    +        for (Application app : appApi.listApplicationsPaged(null, null, null, 5, null, null, null)) {
    +            collectedApps.add(app)
    +            if (collectedApps.size() % 5 == 0) {
    +                pageCount++
    +                println "  Fetched page ${pageCount} (${collectedApps.size()} total apps so far)"
    +            }
    +            // Limit to avoid processing too many apps
    +            if (collectedApps.size() >= 10) {
    +                break
    +            }
    +        }
    +        
    +        println "✓ Collected ${collectedApps.size()} applications across multiple pages"
    +        
    +        assertThat("Should have collected some applications", 
    +                   collectedApps.size(), greaterThan(0))
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableWithGroupMembers() {
    +        println "\n=== Testing Group Members Pagination ==="
    +        UserApi userApi = new UserApi(getClient())
    +        GroupApi groupApi = new GroupApi(getClient())
    +        
    +        // Create a group
    +        AddGroupRequest request = new AddGroupRequest()
    +        OktaUserGroupProfile profile = new OktaUserGroupProfile()
    +        profile.name = "PagMemb-${uniqueTestName}".take(100)
    +        profile.description = "Test group for member pagination"
    +        request.profile = profile
    +        
    +        Group createdGroup = groupApi.addGroup(request)
    +        registerForCleanup(createdGroup)
    +        
    +        // Create and add users to the group
    +        def usersToAdd = 5
    +        def createdUsers = []
    +        
    +        try {
    +            println "Creating ${usersToAdd} users and adding to group..."
    +            for (int i = 0; i < usersToAdd; i++) {
    +                def email = "pagmem${i}-${uniqueTestName.take(30)}@ex.com"
    +                User user = createUser(userApi, email, "MemberTest${i}", "User${i}")
    +                createdUsers.add(user)
    +                registerForCleanup(user)
    +                
    +                // Add user to group
    +                groupApi.assignUserToGroup(createdGroup.id, user.id)
    +            }
    +            
    +            Thread.sleep(getTestOperationDelay())
    +            
    +            println "Fetching group members with PagedIterable (limit=2 per page)..."
    +            def collectedMembers = []
    +            def pageCount = 0
    +            
    +            // Use listGroupUsersPaged with limit=2
    +            for (User member : groupApi.listGroupUsersPaged(createdGroup.id, null, 2)) {
    +                collectedMembers.add(member)
    +                if (collectedMembers.size() % 2 == 0) {
    +                    pageCount++
    +                    println "  Fetched page ${pageCount} (${collectedMembers.size()} total members so far)"
    +                }
    +            }
    +            
    +            println "✓ Collected ${collectedMembers.size()} members across ${pageCount} pages"
    +            
    +            assertThat("Should have collected at least 3 members", 
    +                       collectedMembers.size(), greaterThanOrEqualTo(3))
    +            assertThat("Should have fetched multiple pages", pageCount, greaterThan(1))
    +            
    +        } finally {
    +            // Cleanup is handled by registerForCleanup
    +        }
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableThreadSafety() {
    +        println "\n=== Testing Thread-Safety ==="
    +        UserApi userApi = new UserApi(getClient())
    +        
    +        println "Starting 3 concurrent threads to iterate users..."
    +        // Test that multiple threads can safely iterate
    +        def threads = []
    +        def errors = Collections.synchronizedList([])
    +        def results = Collections.synchronizedList([])
    +        
    +        for (int i = 0; i < 3; i++) {
    +            def threadNum = i + 1
    +            def thread = Thread.start {
    +                try {
    +                    def count = 0
    +                    for (User user : userApi.listUsersPaged(null, null, null, 10, null, null, null, null)) {
    +                        count++
    +                        if (count >= 10) break // Limit to avoid long test
    +                    }
    +                    results.add(count)
    +                    println "  Thread ${threadNum} collected ${count} users"
    +                } catch (Exception e) {
    +                    errors.add(e)
    +                    println "  Thread ${threadNum} encountered error: ${e.message}"
    +                }
    +            }
    +            threads.add(thread)
    +        }
    +        
    +        // Wait for all threads
    +        threads.each { it.join(30000) } // 30 second timeout
    +        
    +        println "✓ All threads completed successfully"
    +        
    +        assertThat("No errors should occur in multi-threaded access", 
    +                   errors, empty())
    +        assertThat("All threads should collect users", 
    +                   results.size(), equalTo(3))
    +        results.each {
    +            assertThat("Each thread should collect users", it, greaterThan(0))
    +        }
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableEarlyBreak() {
    +        println "\n=== Testing Early Break ==="
    +        UserApi userApi = new UserApi(getClient())
    +        
    +        println "Fetching users but breaking early after 5 items..."
    +        // Test that we can break early without fetching all pages
    +        def collectedCount = 0
    +        def limit = 5
    +        
    +        for (User user : userApi.listUsersPaged(null, null, null, 10, null, null, null, null)) {
    +            collectedCount++
    +            if (collectedCount >= limit) {
    +                println "  Breaking at ${collectedCount} users"
    +                break
    +            }
    +        }
    +        
    +        println "✓ Successfully stopped at ${collectedCount} users (early break works)"
    +        
    +        assertThat("Should collect some users and be able to break", 
    +                   collectedCount, greaterThan(0))
    +        assertThat("Should not exceed the limit", 
    +                   collectedCount, lessThanOrEqualTo(limit))
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableWithFilter() {
    +        println "\n=== Testing Filtered Pagination ==="
    +        UserApi userApi = new UserApi(getClient())
    +        
    +        def email = "pagfilt-${uniqueTestName.take(30)}@ex.com"
    +        println "Creating user: ${email}"
    +        User createdUser = createUser(userApi, email, "FilterTest", "User")
    +        registerForCleanup(createdUser)
    +        
    +        // Allow time for indexing
    +        Thread.sleep(getTestOperationDelay())
    +        
    +        println "Fetching users with filter: profile.email eq \"${email}\""
    +        // Use filter with PagedIterable
    +        def found = false
    +        def count = 0
    +        for (User user : userApi.listUsersPaged(null, null, null, 10, 
    +                "profile.email eq \"${email}\"", null, null, null)) {
    +            count++
    +            println "  Found user: ${user.profile.email}"
    +            if (user.profile.email == email) {
    +                found = true
    +                break
    +            }
    +        }
    +        
    +        println "✓ Filter worked - found ${count} user(s) matching criteria"
    +        
    +        assertThat("Should find filtered user", found, equalTo(true))
    +    }
    +
    +    @Test(groups = "group3")
    +    void testPagedIterableMultipleIterations() {
    +        println "\n=== Testing Multiple Iterations ==="
    +        UserApi userApi = new UserApi(getClient())
    +        
    +        println "Creating PagedIterable for users..."
    +        // Test that we can iterate multiple times on the same iterable
    +        def iterable = userApi.listUsersPaged(null, null, null, 5, null, null, null, null)
    +        
    +        println "First iteration (collecting 5 users)..."
    +        def firstCount = 0
    +        for (User user : iterable) {
    +            firstCount++
    +            if (firstCount >= 5) break
    +        }
    +        
    +        println "Second iteration (collecting 5 users)..."
    +        def secondCount = 0
    +        for (User user : iterable) {
    +            secondCount++
    +            if (secondCount >= 5) break
    +        }
    +        
    +        println "✓ First iteration: ${firstCount} users, Second iteration: ${secondCount} users"
    +        
    +        assertThat("First iteration should collect users", firstCount, greaterThan(0))
    +        assertThat("Second iteration should also collect users", secondCount, greaterThan(0))
    +        assertThat("Both iterations should collect same number", firstCount, equalTo(secondCount))
    +    }
    +
    +    private User createUser(UserApi userApi, String email, String firstName, String lastName) {
    +        CreateUserRequest createUserRequest = new CreateUserRequest()
    +        
    +        UserProfile userProfile = new UserProfile()
    +        userProfile.firstName = firstName
    +        userProfile.lastName = lastName
    +        userProfile.email = email
    +        userProfile.login = email
    +        
    +        User user = new User()
    +        user.profile = userProfile
    +        
    +        UserCredentials credentials = new UserCredentials()
    +        PasswordCredential password = new PasswordCredential()
    +        password.value = 'Abcd1234!@#$'
    +        credentials.password = password
    +        user.credentials = credentials
    +        
    +        createUserRequest.profile = userProfile
    +        createUserRequest.credentials = credentials
    +        
    +        return userApi.createUser(createUserRequest, true, false, null)
    +    }
    +}
    
  • pom.xml+2 2 modified
    @@ -68,12 +68,12 @@
                 <dependency>
                     <groupId>com.okta.sdk</groupId>
                     <artifactId>okta-sdk-api</artifactId>
    -                <version>23.0.1</version>
    +                <version>${project.version}</version>
                 </dependency>
                 <dependency>
                     <groupId>com.okta.sdk</groupId>
                     <artifactId>okta-sdk-impl</artifactId>
    -                <version>23.0.1</version>
    +                <version>${project.version}</version>
                 </dependency>
     
                 <!-- Other Okta Projects -->
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.