Skip to content

Commit f99053f

Browse files
authored
Merge pull request #218 from kinde-oss/bugfix/kindetokenfactory-not-bound
Fix Guice MissingImplementation errors and infinite recursion in KindeAccountsClient session binding
2 parents fb53ea6 + b2d21d4 commit f99053f

4 files changed

Lines changed: 110 additions & 46 deletions

File tree

kinde-core/src/main/java/com/kinde/accounts/KindeAccountsClient.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.google.inject.AbstractModule;
44
import com.google.inject.Guice;
55
import com.google.inject.Injector;
6+
import com.google.inject.Provides;
67
import com.google.inject.Inject;
78
import com.kinde.KindeClientSession;
89
import com.kinde.accounts.dto.*;
@@ -64,14 +65,19 @@ public KindeAccountsClient(KindeClientSession session, boolean useDirectInstanti
6465
throw new IllegalArgumentException("session cannot be null");
6566
}
6667

67-
// Create a child injector with both the module and session binding
68+
// Create a child injector with both the module and session binding.
69+
// Use @Provides instead of toInstance() to avoid Guice performing member
70+
// injection on the session, which would fail due to unbound @Inject setters
71+
// on KindeClientSessionImpl (KindeTokenFactory, KindeEntitlements).
6872
Injector injector = Guice.createInjector(new AbstractModule() {
6973
@Override
7074
protected void configure() {
71-
// Install the KindeAccountsModule to provide all manager bindings
7275
install(new KindeAccountsModule());
73-
// Bind the session instance
74-
bind(KindeClientSession.class).toInstance(session);
76+
}
77+
78+
@Provides
79+
KindeClientSession provideSession() {
80+
return session;
7581
}
7682
});
7783

kinde-core/src/main/java/com/kinde/accounts/manager/impl/EntitlementsManagerImpl.java

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
package com.kinde.accounts.manager.impl;
22

3-
import com.google.inject.Inject;
4-
import com.kinde.accounts.dto.EntitlementDto;
5-
import com.kinde.accounts.manager.EntitlementsManager;
6-
import com.kinde.accounts.util.ApiResponseHandler;
7-
import com.kinde.accounts.util.PaginationHelper;
8-
import com.kinde.accounts.dto.DtoConverter;
9-
import com.kinde.KindeClientSession;
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.ExecutionException;
7+
108
import org.openapitools.client.ApiException;
119
import org.openapitools.client.api.DefaultApi;
12-
import org.openapitools.client.model.Entitlement;
1310
import org.openapitools.client.model.EntitlementResponse;
1411
import org.openapitools.client.model.EntitlementsResponse;
1512
import org.openapitools.client.model.EntitlementsResponseData;
1613
import org.slf4j.Logger;
1714
import org.slf4j.LoggerFactory;
1815

19-
import java.util.ArrayList;
20-
import java.util.List;
21-
import java.util.concurrent.CompletableFuture;
22-
import java.util.concurrent.ExecutionException;
16+
import com.google.inject.Inject;
17+
import com.kinde.KindeClientSession;
18+
import com.kinde.accounts.dto.DtoConverter;
19+
import com.kinde.accounts.dto.EntitlementDto;
20+
import com.kinde.accounts.manager.EntitlementsManager;
21+
import com.kinde.accounts.util.ApiResponseHandler;
22+
2323

2424
/**
2525
* Implementation of EntitlementsManager that handles entitlements operations.
@@ -30,40 +30,39 @@ public class EntitlementsManagerImpl implements EntitlementsManager {
3030
private static final Logger log = LoggerFactory.getLogger(EntitlementsManagerImpl.class);
3131

3232
private final DefaultApi apiClient;
33-
private final PaginationHelper paginationHelper;
3433
private final ApiResponseHandler responseHandler;
3534
private final KindeClientSession session;
3635

3736
@Inject
3837
public EntitlementsManagerImpl(
3938
DefaultApi apiClient,
40-
PaginationHelper paginationHelper,
4139
ApiResponseHandler responseHandler,
4240
KindeClientSession session) {
4341
this.apiClient = apiClient;
44-
this.paginationHelper = paginationHelper;
4542
this.responseHandler = responseHandler;
4643
this.session = session;
47-
configureApiClient();
4844
}
4945

5046
/**
5147
* Configures the API client with authentication headers from the session.
48+
* Called lazily before API use to avoid infinite recursion when the session's
49+
* getAccessToken() triggers retrieveTokens() which creates a KindeAccountsClient.
5250
*/
5351
private void configureApiClient() {
5452
String accessToken = session.getAccessToken();
55-
if (accessToken != null && !accessToken.isEmpty()) {
56-
apiClient.getApiClient().setBearerToken(accessToken);
57-
}
53+
apiClient.getApiClient().setBearerToken(
54+
(accessToken != null && !accessToken.isEmpty()) ? accessToken : null);
5855
}
5956

6057
@Override
6158
public List<EntitlementDto> getAllEntitlements() {
6259
try {
6360
return getAllEntitlementsAsync().get();
64-
} catch (InterruptedException | ExecutionException e) {
61+
} catch (InterruptedException e) {
6562
Thread.currentThread().interrupt();
6663
throw new RuntimeException("Failed to get entitlements", e);
64+
} catch (ExecutionException e) {
65+
throw new RuntimeException("Failed to get entitlements", e);
6766
}
6867
}
6968

@@ -72,9 +71,11 @@ public EntitlementDto getEntitlement(String key) {
7271
responseHandler.validateKey(key, "entitlement");
7372
try {
7473
return getEntitlementAsync(key).get();
75-
} catch (InterruptedException | ExecutionException e) {
74+
} catch (InterruptedException e) {
7675
Thread.currentThread().interrupt();
7776
throw new RuntimeException("Failed to get entitlement: " + key, e);
77+
} catch (ExecutionException e) {
78+
throw new RuntimeException("Failed to get entitlement: " + key, e);
7879
}
7980
}
8081

@@ -83,6 +84,7 @@ public CompletableFuture<List<EntitlementDto>> getAllEntitlementsAsync() {
8384
log.debug("Getting all entitlements asynchronously");
8485

8586
return CompletableFuture.supplyAsync(() -> {
87+
configureApiClient();
8688
try {
8789
EntitlementsResponse allEntitlements = new EntitlementsResponse();
8890
EntitlementsResponseData allData = new EntitlementsResponseData();
@@ -142,6 +144,7 @@ public CompletableFuture<EntitlementDto> getEntitlementAsync(String key) {
142144
log.debug("Getting entitlement with key: {} asynchronously", key);
143145

144146
return CompletableFuture.supplyAsync(() -> {
147+
configureApiClient();
145148
try {
146149
EntitlementResponse response = apiClient.getEntitlement(key);
147150

kinde-core/src/main/java/com/kinde/accounts/manager/impl/PermissionsManagerImpl.java

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
package com.kinde.accounts.manager.impl;
22

3-
import com.google.inject.Inject;
4-
import com.kinde.accounts.dto.PermissionDto;
5-
import com.kinde.accounts.manager.PermissionsManager;
6-
import com.kinde.accounts.util.ApiResponseHandler;
7-
import com.kinde.accounts.util.PaginationHelper;
8-
import com.kinde.accounts.dto.DtoConverter;
9-
import com.kinde.KindeClientSession;
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.ExecutionException;
7+
108
import org.openapitools.client.ApiException;
119
import org.openapitools.client.api.DefaultApi;
12-
import org.openapitools.client.model.Permission;
1310
import org.openapitools.client.model.PermissionResponse;
1411
import org.openapitools.client.model.PermissionsResponse;
1512
import org.slf4j.Logger;
1613
import org.slf4j.LoggerFactory;
1714

18-
import java.util.ArrayList;
19-
import java.util.List;
20-
import java.util.concurrent.CompletableFuture;
21-
import java.util.concurrent.ExecutionException;
15+
import com.google.inject.Inject;
16+
import com.kinde.KindeClientSession;
17+
import com.kinde.accounts.dto.DtoConverter;
18+
import com.kinde.accounts.dto.PermissionDto;
19+
import com.kinde.accounts.manager.PermissionsManager;
20+
import com.kinde.accounts.util.ApiResponseHandler;
21+
2222

2323
/**
2424
* Implementation of PermissionsManager that handles permissions operations.
@@ -29,40 +29,39 @@ public class PermissionsManagerImpl implements PermissionsManager {
2929
private static final Logger log = LoggerFactory.getLogger(PermissionsManagerImpl.class);
3030

3131
private final DefaultApi apiClient;
32-
private final PaginationHelper paginationHelper;
3332
private final ApiResponseHandler responseHandler;
3433
private final KindeClientSession session;
3534

3635
@Inject
3736
public PermissionsManagerImpl(
3837
DefaultApi apiClient,
39-
PaginationHelper paginationHelper,
4038
ApiResponseHandler responseHandler,
4139
KindeClientSession session) {
4240
this.apiClient = apiClient;
43-
this.paginationHelper = paginationHelper;
4441
this.responseHandler = responseHandler;
4542
this.session = session;
46-
configureApiClient();
4743
}
4844

4945
/**
5046
* Configures the API client with authentication headers from the session.
47+
* Called lazily before API use to avoid infinite recursion when the session's
48+
* getAccessToken() triggers retrieveTokens() which creates a KindeAccountsClient.
5149
*/
5250
private void configureApiClient() {
5351
String accessToken = session.getAccessToken();
54-
if (accessToken != null && !accessToken.isEmpty()) {
55-
apiClient.getApiClient().setBearerToken(accessToken);
56-
}
52+
apiClient.getApiClient().setBearerToken(
53+
(accessToken != null && !accessToken.isEmpty()) ? accessToken : null);
5754
}
5855

5956
@Override
6057
public List<PermissionDto> getAllPermissions() {
6158
try {
6259
return getAllPermissionsAsync().get();
63-
} catch (InterruptedException | ExecutionException e) {
60+
} catch (InterruptedException e) {
6461
Thread.currentThread().interrupt();
6562
throw new RuntimeException("Failed to get permissions", e);
63+
} catch (ExecutionException e) {
64+
throw new RuntimeException("Failed to get permissions", e);
6665
}
6766
}
6867

@@ -71,9 +70,11 @@ public PermissionDto getPermission(String key) {
7170
responseHandler.validateKey(key, "permission");
7271
try {
7372
return getPermissionAsync(key).get();
74-
} catch (InterruptedException | ExecutionException e) {
73+
} catch (InterruptedException e) {
7574
Thread.currentThread().interrupt();
7675
throw new RuntimeException("Failed to get permission: " + key, e);
76+
} catch (ExecutionException e) {
77+
throw new RuntimeException("Failed to get permission: " + key, e);
7778
}
7879
}
7980

@@ -82,6 +83,7 @@ public CompletableFuture<List<PermissionDto>> getAllPermissionsAsync() {
8283
log.debug("Getting all permissions asynchronously");
8384

8485
return CompletableFuture.supplyAsync(() -> {
86+
configureApiClient();
8587
try {
8688
PermissionsResponse allPermissions = new PermissionsResponse();
8789
allPermissions.setData(new ArrayList<>());
@@ -126,6 +128,7 @@ public CompletableFuture<PermissionDto> getPermissionAsync(String key) {
126128
log.debug("Getting permission with key: {} asynchronously", key);
127129

128130
return CompletableFuture.supplyAsync(() -> {
131+
configureApiClient();
129132
try {
130133
PermissionResponse response = apiClient.getPermission(key);
131134

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.kinde.accounts;
2+
3+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4+
import static org.junit.jupiter.api.Assertions.fail;
5+
import static org.mockito.Mockito.never;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.when;
8+
9+
import java.util.concurrent.ExecutionException;
10+
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.mockito.Mock;
14+
import org.mockito.junit.jupiter.MockitoExtension;
15+
16+
import com.kinde.KindeClientSession;
17+
18+
/**
19+
* Regression test for the @Provides-based KindeClientSession binding in KindeAccountsClient.
20+
* Verifies that construction via the manual path does not trigger member injection
21+
* (which would call getAccessToken() and cause Guice MissingImplementation / infinite recursion).
22+
*/
23+
@ExtendWith(MockitoExtension.class)
24+
class KindeAccountsClientGuiceBindingTest {
25+
26+
@Mock
27+
private KindeClientSession mockSession;
28+
29+
@Test
30+
void construction_doesNotCallGetAccessToken() {
31+
KindeAccountsClient client = new KindeAccountsClient(mockSession, true);
32+
33+
assertNotNull(client);
34+
verify(mockSession, never()).getAccessToken();
35+
}
36+
37+
@Test
38+
void constructedClient_canCallManagerMethod_withoutGuiceErrors() {
39+
when(mockSession.getAccessToken()).thenReturn("test-token");
40+
41+
KindeAccountsClient client = new KindeAccountsClient(mockSession, true);
42+
43+
try {
44+
client.getEntitlements();
45+
} catch (RuntimeException e) {
46+
if (e.getCause() instanceof ExecutionException) {
47+
return;
48+
}
49+
fail("Unexpected RuntimeException (not an API failure): " + e.getMessage());
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)