Skip to content

Commit ce2a3da

Browse files
authored
Merge pull request #307 from kinde-oss/feat/connection-id
feat: add optional connection_id to authorize URLs
2 parents 7507080 + c08b241 commit ce2a3da

13 files changed

Lines changed: 570 additions & 54 deletions

File tree

kinde-core/src/main/java/com/kinde/KindeClientSession.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,63 @@ default AuthorizationUrl login(String invitationCode) {
2121
return login();
2222
}
2323

24+
/**
25+
* Starts a login flow, optionally including an invitation code and/or connection ID.
26+
* A connection ID skips Kinde's identity picker and sends the user to a specific
27+
* authentication method (for example a social or enterprise connection).
28+
*
29+
* @param invitationCode optional invitation code; ignored when null or blank
30+
* @param connectionId optional connection ID; ignored when null or blank
31+
* @return the authorization URL to redirect the user to
32+
* @throws UnsupportedOperationException if {@code connectionId} is non-blank and this
33+
* implementation does not support connection IDs
34+
*/
35+
default AuthorizationUrl login(String invitationCode, String connectionId) {
36+
requireConnectionIdSupport(connectionId);
37+
return login(invitationCode);
38+
}
39+
2440
AuthorizationUrl createOrg(String orgName);
2541

2642
default AuthorizationUrl createOrg(String orgName, String invitationCode) {
2743
return createOrg(orgName);
2844
}
2945

46+
/**
47+
* Starts a create-organization flow, optionally including an invitation code and/or connection ID.
48+
*
49+
* @param orgName the organization name; must be non-blank
50+
* @param invitationCode optional invitation code; ignored when null or blank
51+
* @param connectionId optional connection ID; ignored when null or blank
52+
* @return the authorization URL to redirect the user to
53+
* @throws UnsupportedOperationException if {@code connectionId} is non-blank and this
54+
* implementation does not support connection IDs
55+
*/
56+
default AuthorizationUrl createOrg(String orgName, String invitationCode, String connectionId) {
57+
requireConnectionIdSupport(connectionId);
58+
return createOrg(orgName, invitationCode);
59+
}
60+
3061
AuthorizationUrl register();
3162

3263
default AuthorizationUrl register(String invitationCode) {
3364
return register();
3465
}
3566

67+
/**
68+
* Starts a registration flow, optionally including an invitation code and/or connection ID.
69+
*
70+
* @param invitationCode optional invitation code; ignored when null or blank
71+
* @param connectionId optional connection ID; ignored when null or blank
72+
* @return the authorization URL to redirect the user to
73+
* @throws UnsupportedOperationException if {@code connectionId} is non-blank and this
74+
* implementation does not support connection IDs
75+
*/
76+
default AuthorizationUrl register(String invitationCode, String connectionId) {
77+
requireConnectionIdSupport(connectionId);
78+
return register(invitationCode);
79+
}
80+
3681
default AuthorizationUrl handleInvitation(String invitationCode) {
3782
throw new UnsupportedOperationException("handleInvitation is not supported by this implementation");
3883
}
@@ -70,4 +115,10 @@ default String getDomain() {
70115
default String getAccessToken() {
71116
return null;
72117
}
118+
119+
private static void requireConnectionIdSupport(String connectionId) {
120+
if (connectionId != null && !connectionId.isBlank()) {
121+
throw new UnsupportedOperationException("connectionId is not supported by this implementation");
122+
}
123+
}
73124
}

kinde-core/src/main/java/com/kinde/constants/KindeConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public class KindeConstants {
88
public final static String LANG = "lang";
99
public final static String ORG_NAME = "org_name";
1010
public final static String INVITATION_CODE = "invitation_code";
11+
public final static String CONNECTION_ID = "connection_id";
1112
public final static String SCOPE = "openid,email,profile";
1213
}

kinde-core/src/main/java/com/kinde/session/KindeClientSessionImpl.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,15 @@ public AuthorizationUrl login() {
255255

256256
@Override
257257
public AuthorizationUrl login(String invitationCode) {
258+
return login(invitationCode, null);
259+
}
260+
261+
@Override
262+
public AuthorizationUrl login(String invitationCode, String connectionId) {
258263
Map<String, String> parameters = new HashMap<>();
259264
parameters.put("supports_reauth", "true");
260265
addInvitationParams(parameters, invitationCode);
266+
addConnectionIdParam(parameters, connectionId);
261267
return authorizationUrlWithParameters(parameters);
262268
}
263269

@@ -268,6 +274,11 @@ public AuthorizationUrl createOrg(String orgName) {
268274

269275
@Override
270276
public AuthorizationUrl createOrg(String orgName, String invitationCode) {
277+
return createOrg(orgName, invitationCode, null);
278+
}
279+
280+
@Override
281+
public AuthorizationUrl createOrg(String orgName, String invitationCode, String connectionId) {
271282
if (orgName == null || orgName.isBlank()) {
272283
throw new IllegalArgumentException("createOrg requires a non-blank orgName");
273284
}
@@ -276,6 +287,7 @@ public AuthorizationUrl createOrg(String orgName, String invitationCode) {
276287
parameters.put("is_create_org", Boolean.TRUE.toString());
277288
parameters.put("org_name", orgName.trim());
278289
addInvitationParams(parameters, invitationCode);
290+
addConnectionIdParam(parameters, connectionId);
279291
return authorizationUrlWithParameters(parameters);
280292
}
281293

@@ -286,10 +298,16 @@ public AuthorizationUrl register() {
286298

287299
@Override
288300
public AuthorizationUrl register(String invitationCode) {
301+
return register(invitationCode, null);
302+
}
303+
304+
@Override
305+
public AuthorizationUrl register(String invitationCode, String connectionId) {
289306
Map<String, String> parameters = new HashMap<>();
290307
parameters.put("prompt", Prompt.Type.CREATE.toString());
291308
parameters.put("supports_reauth", "true");
292309
addInvitationParams(parameters, invitationCode);
310+
addConnectionIdParam(parameters, connectionId);
293311
return authorizationUrlWithParameters(parameters);
294312
}
295313

@@ -310,6 +328,12 @@ private void addInvitationParams(Map<String, String> parameters, String invitati
310328
}
311329
}
312330

331+
private void addConnectionIdParam(Map<String, String> parameters, String connectionId) {
332+
if (connectionId != null && !connectionId.isBlank()) {
333+
parameters.put(KindeRequestParameters.CONNECTION_ID, connectionId.trim());
334+
}
335+
}
336+
313337
public AuthorizationUrl logout() throws Exception {
314338
if (this.kindeConfig.logoutRedirectUri() == null || this.kindeConfig.logoutRedirectUri().isEmpty()) {
315339
throw new Exception("Logout url is not provided");

kinde-core/src/main/java/com/kinde/session/KindeRequestParameters.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public class KindeRequestParameters {
77
public final static String ORG_CODE = "org_code";
88
public final static String INVITATION_CODE = "invitation_code";
99
public final static String IS_INVITATION = "is_invitation";
10+
public final static String CONNECTION_ID = "connection_id";
1011
}

kinde-core/src/test/java/com/kinde/session/KindeClientSessionImplTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,5 +290,94 @@ void createOrgTrimsOrgName() throws Exception {
290290
assertTrue(url.contains("org_name=TestOrg"), "URL should contain trimmed org_name");
291291
assertFalse(url.contains("org_name=+"), "URL should not contain leading whitespace in org_name");
292292
}
293+
294+
@Test
295+
@DisplayName("login with connectionId includes connection_id on the authorize URL")
296+
void loginWithConnectionIdIncludesParam() throws Exception {
297+
KindeClientSessionImpl session = createSessionWithOidc();
298+
AuthorizationUrl result = session.login(null, "conn_abc123");
299+
300+
assertNotNull(result);
301+
String url = result.getUrl().toString();
302+
assertTrue(url.contains("connection_id=conn_abc123"), "URL should contain connection_id");
303+
assertTrue(url.contains("supports_reauth=true"), "URL should still contain supports_reauth");
304+
assertFalse(url.contains("invitation_code"), "URL should not contain invitation_code");
305+
}
306+
307+
@Test
308+
@DisplayName("login with invitationCode and connectionId includes both params")
309+
void loginWithInvitationAndConnectionIdIncludesBothParams() throws Exception {
310+
KindeClientSessionImpl session = createSessionWithOidc();
311+
AuthorizationUrl result = session.login("inv_login123", "conn_abc123");
312+
313+
assertNotNull(result);
314+
String url = result.getUrl().toString();
315+
assertTrue(url.contains("invitation_code=inv_login123"), "URL should contain invitation_code");
316+
assertTrue(url.contains("is_invitation=true"), "URL should contain is_invitation=true");
317+
assertTrue(url.contains("connection_id=conn_abc123"), "URL should contain connection_id");
318+
assertTrue(url.contains("supports_reauth=true"), "URL should contain supports_reauth");
319+
}
320+
321+
@Test
322+
@DisplayName("login with whitespace-only connectionId omits connection_id")
323+
void loginWithWhitespaceOnlyConnectionIdOmitsParam() throws Exception {
324+
KindeClientSessionImpl session = createSessionWithOidc();
325+
AuthorizationUrl result = session.login(null, " ");
326+
327+
assertNotNull(result);
328+
String url = result.getUrl().toString();
329+
assertFalse(url.contains("connection_id"), "URL should not contain connection_id");
330+
assertTrue(url.contains("supports_reauth=true"), "URL should still contain supports_reauth");
331+
}
332+
333+
@Test
334+
@DisplayName("login trims connectionId in the generated URL")
335+
void loginTrimsConnectionId() throws Exception {
336+
KindeClientSessionImpl session = createSessionWithOidc();
337+
AuthorizationUrl result = session.login(null, " conn_abc123 ");
338+
339+
assertNotNull(result);
340+
String url = result.getUrl().toString();
341+
assertTrue(url.contains("connection_id=conn_abc123"), "URL should contain trimmed connection_id");
342+
}
343+
344+
@Test
345+
@DisplayName("register with connectionId includes connection_id and register params")
346+
void registerWithConnectionIdIncludesParam() throws Exception {
347+
KindeClientSessionImpl session = createSessionWithOidc();
348+
AuthorizationUrl result = session.register(null, "conn_reg456");
349+
350+
assertNotNull(result);
351+
String url = result.getUrl().toString();
352+
assertTrue(url.contains("connection_id=conn_reg456"), "URL should contain connection_id");
353+
assertTrue(url.contains("prompt=create"), "URL should contain register-specific prompt=create");
354+
assertTrue(url.contains("supports_reauth=true"), "URL should contain supports_reauth");
355+
}
356+
357+
@Test
358+
@DisplayName("createOrg with connectionId includes connection_id and createOrg params")
359+
void createOrgWithConnectionIdIncludesParam() throws Exception {
360+
KindeClientSessionImpl session = createSessionWithOidc();
361+
AuthorizationUrl result = session.createOrg("TestOrg", null, "conn_org789");
362+
363+
assertNotNull(result);
364+
String url = result.getUrl().toString();
365+
assertTrue(url.contains("connection_id=conn_org789"), "URL should contain connection_id");
366+
assertTrue(url.contains("org_name=TestOrg"), "URL should contain org_name");
367+
assertTrue(url.contains("is_create_org=true"), "URL should contain is_create_org");
368+
assertFalse(url.contains("invitation_code"), "URL should not contain invitation_code");
369+
}
370+
371+
@Test
372+
@DisplayName("authorizationUrlWithParameters includes connection_id from the map")
373+
void authorizationUrlWithParametersIncludesConnectionId() throws Exception {
374+
KindeClientSessionImpl session = createSessionWithOidc();
375+
AuthorizationUrl result = session.authorizationUrlWithParameters(
376+
java.util.Map.of("connection_id", "conn_map123"));
377+
378+
assertNotNull(result);
379+
String url = result.getUrl().toString();
380+
assertTrue(url.contains("connection_id=conn_map123"), "URL should contain connection_id from parameters map");
381+
}
293382
}
294383

kinde-j2ee/src/main/java/com/kinde/filter/KindeAuthenticationFilter.java

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,40 +51,20 @@ protected void doFilter(ServletRequest servletRequest, ServletResponse servletRe
5151
}
5252

5353
String code = req.getParameter("code");
54-
String rawInvitationCode = req.getParameter(INVITATION_CODE);
55-
String invitationCode = (rawInvitationCode != null && !rawInvitationCode.isBlank()) ? rawInvitationCode.trim() : null;
54+
String invitationCode = optionalQueryParam(req, INVITATION_CODE);
55+
String connectionId = optionalQueryParam(req, CONNECTION_ID);
5656
Principal userPrincipal = (Principal) req.getSession().getAttribute(AUTHENTICATED_USER);
5757
AuthorizationUrl authorizationUrl = (AuthorizationUrl)req.getSession().getAttribute(AUTHORIZATION_URL);
5858
if (invitationCode != null) {
5959
// Invitation code always starts a new auth flow, even if already authenticated
6060
KindeClientSession kindeClientSession = createKindeClientSession(req);
61-
if (kindeAuthenticationAction == KindeAuthenticationAction.LOGIN) {
62-
authorizationUrl = kindeClientSession.login(invitationCode);
63-
} else if (kindeAuthenticationAction == KindeAuthenticationAction.REGISTER) {
64-
authorizationUrl = kindeClientSession.register(invitationCode);
65-
} else if (kindeAuthenticationAction == KindeAuthenticationAction.CREATE_ORG) {
66-
String orgName = req.getParameter(ORG_NAME);
67-
if (orgName == null || orgName.isBlank()) {
68-
throw new ServletException("Must provide org_name query parameter to create an organisation.");
69-
}
70-
authorizationUrl = kindeClientSession.createOrg(orgName.trim(), invitationCode);
71-
}
61+
authorizationUrl = startAuthorization(kindeClientSession, kindeAuthenticationAction, req, invitationCode, connectionId);
7262
req.getSession().setAttribute(AUTHORIZATION_URL, authorizationUrl);
7363
resp.sendRedirect(authorizationUrl.getUrl().toString());
7464
} else if (userPrincipal == null || authorizationUrl == null) {
7565
// Redirect to the OAuth provider's authorization page
7666
KindeClientSession kindeClientSession = createKindeClientSession(req);
77-
if (kindeAuthenticationAction == KindeAuthenticationAction.LOGIN) {
78-
authorizationUrl = kindeClientSession.login();
79-
} else if (kindeAuthenticationAction == KindeAuthenticationAction.REGISTER) {
80-
authorizationUrl = kindeClientSession.register();
81-
} else if (kindeAuthenticationAction == KindeAuthenticationAction.CREATE_ORG) {
82-
String orgName = req.getParameter(ORG_NAME);
83-
if (orgName == null || orgName.isBlank()) {
84-
throw new ServletException("Must provide org_name query parameter to create an organisation.");
85-
}
86-
authorizationUrl = kindeClientSession.createOrg(orgName.trim());
87-
}
67+
authorizationUrl = startAuthorization(kindeClientSession, kindeAuthenticationAction, req, null, connectionId);
8868
req.getSession().setAttribute(AUTHORIZATION_URL,authorizationUrl);
8969
resp.sendRedirect(authorizationUrl.getUrl().toString());
9070
} else if (code != null) {
@@ -112,6 +92,45 @@ protected void doFilter(ServletRequest servletRequest, ServletResponse servletRe
11292
}
11393
}
11494

95+
private static AuthorizationUrl startAuthorization(
96+
KindeClientSession kindeClientSession,
97+
KindeAuthenticationAction kindeAuthenticationAction,
98+
HttpServletRequest req,
99+
String invitationCode,
100+
String connectionId) throws ServletException {
101+
if (kindeAuthenticationAction == KindeAuthenticationAction.LOGIN) {
102+
if (connectionId != null) {
103+
return kindeClientSession.login(invitationCode, connectionId);
104+
}
105+
return invitationCode != null ? kindeClientSession.login(invitationCode) : kindeClientSession.login();
106+
}
107+
if (kindeAuthenticationAction == KindeAuthenticationAction.REGISTER) {
108+
if (connectionId != null) {
109+
return kindeClientSession.register(invitationCode, connectionId);
110+
}
111+
return invitationCode != null ? kindeClientSession.register(invitationCode) : kindeClientSession.register();
112+
}
113+
if (kindeAuthenticationAction == KindeAuthenticationAction.CREATE_ORG) {
114+
String orgName = req.getParameter(ORG_NAME);
115+
if (orgName == null || orgName.isBlank()) {
116+
throw new ServletException("Must provide org_name query parameter to create an organisation.");
117+
}
118+
String trimmedOrgName = orgName.trim();
119+
if (connectionId != null) {
120+
return kindeClientSession.createOrg(trimmedOrgName, invitationCode, connectionId);
121+
}
122+
return invitationCode != null
123+
? kindeClientSession.createOrg(trimmedOrgName, invitationCode)
124+
: kindeClientSession.createOrg(trimmedOrgName);
125+
}
126+
throw new ServletException("Unknown authentication action: " + kindeAuthenticationAction);
127+
}
128+
129+
private static String optionalQueryParam(HttpServletRequest req, String name) {
130+
String raw = req.getParameter(name);
131+
return (raw != null && !raw.isBlank()) ? raw.trim() : null;
132+
}
133+
115134
private static KindeClientSession createKindeClientSession(HttpServletRequest req) {
116135
return KindeSingleton
117136
.getInstance()

0 commit comments

Comments
 (0)