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
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public class KindeConstants {
public final static String ORG_CODE = "org_code";
public final static String LANG = "lang";
public final static String ORG_NAME = "org_name";
public final static String CONNECTION_ID = "connection_id";
public final static String SCOPE = "openid,email,profile";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public class KindeRequestParameters {
public final static String HAS_SUCCESS_PAGE = "has_success_page";
public final static String LANG = "lang";
public final static String ORG_CODE = "org_code";
public final static String CONNECTION_ID = "connection_id";
}
28 changes: 27 additions & 1 deletion kinde-core/src/main/java/com/kinde/token/BaseToken.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.kinde.token;

import com.google.inject.Inject;
import com.kinde.accounts.KindeAccountsClient;
import com.kinde.accounts.dto.PermissionDto;
import com.kinde.accounts.dto.RoleDto;
Expand Down Expand Up @@ -91,6 +90,33 @@ public Object getClaim(String key) {
return this.signedJWT.getJWTClaimsSet().getClaim(key);
}

@Override
@SneakyThrows
public String getConnectionId() {
if (this.signedJWT == null) {
return null;
}

// First, try direct connection_id claim
Object connectionId = getClaim("connection_id");
if (connectionId instanceof String) {
return (String) connectionId;
}

// Then, try nested ext_provider.connection_id structure
Object extProvider = getClaim("ext_provider");
if (extProvider instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> extProviderMap = (Map<String, Object>) extProvider;
Object nestedConnectionId = extProviderMap.get("connection_id");
if (nestedConnectionId instanceof String) {
return (String) nestedConnectionId;
}
}

return null;
}

@SuppressWarnings("unchecked")
@Override
public List<String> getPermissions() {
Expand Down
11 changes: 11 additions & 0 deletions kinde-core/src/main/java/com/kinde/token/KindeToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ public interface KindeToken {

Object getClaim(String key);

/**
* Gets the connection ID from the token.
* This method checks for connection_id in the token claims, including nested structures
* like ext_provider.connection_id for external identity providers.
*
* @return The connection ID string, or null if not found
*/
default String getConnectionId() {
return null;
}

List<String> getPermissions();

/**
Expand Down
167 changes: 167 additions & 0 deletions kinde-core/src/test/java/com/kinde/session/ConnectionIdTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.kinde.session;

import com.kinde.KindeClient;
import com.kinde.KindeClientBuilder;
import com.kinde.KindeClientSession;
import com.kinde.authorization.AuthorizationType;
import com.kinde.authorization.AuthorizationUrl;
import com.kinde.client.KindeCoreGuiceTestModule;
import com.kinde.guice.KindeEnvironmentSingleton;
import com.kinde.guice.KindeGuiceSingleton;
import com.kinde.token.KindeTokenGuiceTestModule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

public class ConnectionIdTest {

@BeforeEach
public void setUp() {
KindeGuiceSingleton.fin();
KindeEnvironmentSingleton.fin();
KindeEnvironmentSingleton.init(KindeEnvironmentSingleton.State.TEST);

KindeGuiceSingleton.init(
new KindeCoreGuiceTestModule(),
new KindeTokenGuiceTestModule());
}

@Test
@DisplayName("authorizationUrlWithParameters should include connection_id when provided")
public void testAuthorizationUrlWithConnectionId() {
KindeClient kindeClient = KindeClientBuilder.builder()
.domain("http://localhost:8089")
.clientId("test")
.clientSecret("test")
.redirectUri("http://localhost:8080/")
.build();

KindeClientSession kindeClientSession = kindeClient.initClientSession("test", null);

Map<String, String> parameters = new HashMap<>();
parameters.put(KindeRequestParameters.CONNECTION_ID, "conn_123456789");

AuthorizationUrl authorizationUrl = kindeClientSession.authorizationUrlWithParameters(parameters);

assertNotNull(authorizationUrl);
assertNotNull(authorizationUrl.getUrl());
String urlString = authorizationUrl.getUrl().toString();
assertTrue(urlString.contains("connection_id=conn_123456789"),
"URL should contain connection_id parameter. URL: " + urlString);
}

@Test
@DisplayName("login should support connection_id via authorizationUrlWithParameters")
public void testLoginWithConnectionId() {
KindeClient kindeClient = KindeClientBuilder.builder()
.domain("http://localhost:8089")
.clientId("test")
.clientSecret("test")
.redirectUri("http://localhost:8080/")
.build();

KindeClientSession kindeClientSession = kindeClient.initClientSession("test", null);

Map<String, String> parameters = new HashMap<>();
parameters.put("supports_reauth", "true");
parameters.put(KindeRequestParameters.CONNECTION_ID, "conn_social_google");

AuthorizationUrl authorizationUrl = kindeClientSession.authorizationUrlWithParameters(parameters);

assertNotNull(authorizationUrl);
assertNotNull(authorizationUrl.getUrl());
String urlString = authorizationUrl.getUrl().toString();
assertTrue(urlString.contains("connection_id=conn_social_google"),
"URL should contain connection_id parameter. URL: " + urlString);
assertTrue(urlString.contains("supports_reauth=true"),
"URL should contain supports_reauth parameter. URL: " + urlString);
}

@Test
@DisplayName("register should support connection_id via authorizationUrlWithParameters")
public void testRegisterWithConnectionId() {
KindeClient kindeClient = KindeClientBuilder.builder()
.domain("http://localhost:8089")
.clientId("test")
.clientSecret("test")
.redirectUri("http://localhost:8080/")
.build();

KindeClientSession kindeClientSession = kindeClient.initClientSession("test", null);

Map<String, String> parameters = new HashMap<>();
parameters.put("prompt", "create");
parameters.put(KindeRequestParameters.CONNECTION_ID, "conn_enterprise_saml");

AuthorizationUrl authorizationUrl = kindeClientSession.authorizationUrlWithParameters(parameters);

assertNotNull(authorizationUrl);
assertNotNull(authorizationUrl.getUrl());
String urlString = authorizationUrl.getUrl().toString();
assertTrue(urlString.contains("connection_id=conn_enterprise_saml"),
"URL should contain connection_id parameter. URL: " + urlString);
assertTrue(urlString.contains("prompt=create"),
"URL should contain prompt parameter. URL: " + urlString);
}

@Test
@DisplayName("connection_id should work with CODE grant type")
public void testConnectionIdWithCodeGrant() {
KindeClient kindeClient = KindeClientBuilder.builder()
.domain("http://localhost:8089")
.clientId("test")
.clientSecret("test")
.redirectUri("http://localhost:8080/")
.grantType(AuthorizationType.CODE)
.build();

KindeClientSession kindeClientSession = kindeClient.initClientSession("test", null);

Map<String, String> parameters = new HashMap<>();
parameters.put(KindeRequestParameters.CONNECTION_ID, "conn_123456789");

AuthorizationUrl authorizationUrl = kindeClientSession.authorizationUrlWithParameters(parameters);

assertNotNull(authorizationUrl);
assertNotNull(authorizationUrl.getUrl());
assertNotNull(authorizationUrl.getCodeVerifier(), "Code verifier should be present for CODE grant type");
String urlString = authorizationUrl.getUrl().toString();
assertTrue(urlString.contains("connection_id=conn_123456789"),
"URL should contain connection_id parameter. URL: " + urlString);
}

@Test
@DisplayName("connection_id should work with other parameters like org_code and lang")
public void testConnectionIdWithOtherParameters() {
KindeClient kindeClient = KindeClientBuilder.builder()
.domain("http://localhost:8089")
.clientId("test")
.clientSecret("test")
.redirectUri("http://localhost:8080/")
.orgCode("ORG123")
.lang("en")
.build();

KindeClientSession kindeClientSession = kindeClient.initClientSession("test", null);

Map<String, String> parameters = new HashMap<>();
parameters.put(KindeRequestParameters.CONNECTION_ID, "conn_123456789");

AuthorizationUrl authorizationUrl = kindeClientSession.authorizationUrlWithParameters(parameters);

assertNotNull(authorizationUrl);
assertNotNull(authorizationUrl.getUrl());
String urlString = authorizationUrl.getUrl().toString();
assertTrue(urlString.contains("connection_id=conn_123456789"),
"URL should contain connection_id parameter. URL: " + urlString);
assertTrue(urlString.contains("org_code=ORG123"),
"URL should contain org_code parameter. URL: " + urlString);
assertTrue(urlString.contains("lang=en"),
"URL should contain lang parameter. URL: " + urlString);
}
}
124 changes: 124 additions & 0 deletions kinde-core/src/test/java/com/kinde/token/ConnectionIdTokenTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.kinde.token;

import com.kinde.token.jwt.JwtGenerator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class ConnectionIdTokenTest {

@Test
@DisplayName("getConnectionId should return connection_id from token when present as direct claim")
public void testGetConnectionIdDirectClaim() throws Exception {
String connectionId = "conn_123456789";
String tokenString = JwtGenerator.generateIDTokenWithConnectionId(connectionId);

KindeToken kindeToken = IDToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertEquals(connectionId, kindeToken.getConnectionId(),
"getConnectionId() should return the connection_id from the token");
}

@Test
@DisplayName("getConnectionId should return connection_id from ext_provider nested structure")
public void testGetConnectionIdFromExtProvider() throws Exception {
String connectionId = "conn_enterprise_saml_789";
String tokenString = JwtGenerator.generateIDTokenWithExtProviderConnectionId(connectionId);

KindeToken kindeToken = IDToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertEquals(connectionId, kindeToken.getConnectionId(),
"getConnectionId() should return the connection_id from ext_provider.connection_id");
}

@Test
@DisplayName("getConnectionId should return null when connection_id is not present")
public void testGetConnectionIdWhenNotPresent() throws Exception {
String tokenString = JwtGenerator.generateIDToken();

KindeToken kindeToken = IDToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertNull(kindeToken.getConnectionId(),
"getConnectionId() should return null when connection_id is not in the token");
}

@Test
@DisplayName("getConnectionId should prefer direct connection_id over nested ext_provider.connection_id")
public void testGetConnectionIdPreferDirectOverNested() throws Exception {
// Create a token with both direct and nested connection_id to test preference
String directConnectionId = "conn_direct_123";
String nestedConnectionId = "conn_nested_456";

String tokenString = JwtGenerator.generateIDTokenWithBothConnectionIds(directConnectionId, nestedConnectionId);

KindeToken kindeToken = IDToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertEquals(directConnectionId, kindeToken.getConnectionId(),
"getConnectionId() should prefer direct connection_id over ext_provider.connection_id");
}

@Test
@DisplayName("getConnectionId should work with AccessToken")
public void testGetConnectionIdWithAccessToken() throws Exception {
String connectionId = "conn_access_token_123";
String tokenString = JwtGenerator.generateIDTokenWithConnectionId(connectionId);

// AccessToken uses the same BaseToken implementation
KindeToken kindeToken = AccessToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertEquals(connectionId, kindeToken.getConnectionId(),
"getConnectionId() should work with AccessToken");
}

@Test
@DisplayName("getConnectionId should return null for invalid token")
public void testGetConnectionIdWithInvalidToken() throws Exception {
String tokenString = "invalid.token.string";

KindeToken kindeToken = IDToken.init(tokenString, false);

assertNotNull(kindeToken);
assertFalse(kindeToken.valid());
assertNull(kindeToken.getConnectionId(),
"getConnectionId() should return null for invalid tokens");
}

@Test
@DisplayName("getConnectionId should handle null ext_provider gracefully")
public void testGetConnectionIdWithNullExtProvider() throws Exception {
// Token without ext_provider should work fine
String tokenString = JwtGenerator.generateIDToken();

KindeToken kindeToken = IDToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertNull(kindeToken.getConnectionId(),
"getConnectionId() should handle missing ext_provider gracefully");
}

@Test
@DisplayName("getConnectionId should handle ext_provider without connection_id gracefully")
public void testGetConnectionIdWithExtProviderButNoConnectionId() throws Exception {
// This test verifies that if ext_provider exists but doesn't have connection_id, it returns null
String tokenString = JwtGenerator.generateIDTokenWithExtProviderButNoConnectionId();

KindeToken kindeToken = IDToken.init(tokenString, true);

assertNotNull(kindeToken);
assertTrue(kindeToken.valid());
assertNull(kindeToken.getConnectionId(),
"getConnectionId() should return null when ext_provider exists but has no connection_id");
}
}
Loading