Skip to content
Merged
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
51 changes: 51 additions & 0 deletions kinde-core/src/main/java/com/kinde/KindeClientSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,63 @@ default AuthorizationUrl login(String invitationCode) {
return login();
}

/**
* Starts a login flow, optionally including an invitation code and/or connection ID.
* A connection ID skips Kinde's identity picker and sends the user to a specific
* authentication method (for example a social or enterprise connection).
*
* @param invitationCode optional invitation code; ignored when null or blank
* @param connectionId optional connection ID; ignored when null or blank
* @return the authorization URL to redirect the user to
* @throws UnsupportedOperationException if {@code connectionId} is non-blank and this
* implementation does not support connection IDs
*/
default AuthorizationUrl login(String invitationCode, String connectionId) {
requireConnectionIdSupport(connectionId);
return login(invitationCode);
}

AuthorizationUrl createOrg(String orgName);

default AuthorizationUrl createOrg(String orgName, String invitationCode) {
return createOrg(orgName);
}

/**
* Starts a create-organization flow, optionally including an invitation code and/or connection ID.
*
* @param orgName the organization name; must be non-blank
* @param invitationCode optional invitation code; ignored when null or blank
* @param connectionId optional connection ID; ignored when null or blank
* @return the authorization URL to redirect the user to
* @throws UnsupportedOperationException if {@code connectionId} is non-blank and this
* implementation does not support connection IDs
*/
default AuthorizationUrl createOrg(String orgName, String invitationCode, String connectionId) {
requireConnectionIdSupport(connectionId);
return createOrg(orgName, invitationCode);
}

AuthorizationUrl register();

default AuthorizationUrl register(String invitationCode) {
return register();
}

/**
* Starts a registration flow, optionally including an invitation code and/or connection ID.
*
* @param invitationCode optional invitation code; ignored when null or blank
* @param connectionId optional connection ID; ignored when null or blank
* @return the authorization URL to redirect the user to
* @throws UnsupportedOperationException if {@code connectionId} is non-blank and this
* implementation does not support connection IDs
*/
default AuthorizationUrl register(String invitationCode, String connectionId) {
requireConnectionIdSupport(connectionId);
return register(invitationCode);
}

default AuthorizationUrl handleInvitation(String invitationCode) {
throw new UnsupportedOperationException("handleInvitation is not supported by this implementation");
}
Expand Down Expand Up @@ -70,4 +115,10 @@ default String getDomain() {
default String getAccessToken() {
return null;
}

private static void requireConnectionIdSupport(String connectionId) {
if (connectionId != null && !connectionId.isBlank()) {
throw new UnsupportedOperationException("connectionId is not supported by this implementation");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public class KindeConstants {
public final static String LANG = "lang";
public final static String ORG_NAME = "org_name";
public final static String INVITATION_CODE = "invitation_code";
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 @@ -255,9 +255,15 @@ public AuthorizationUrl login() {

@Override
public AuthorizationUrl login(String invitationCode) {
return login(invitationCode, null);
}

@Override
public AuthorizationUrl login(String invitationCode, String connectionId) {
Map<String, String> parameters = new HashMap<>();
parameters.put("supports_reauth", "true");
addInvitationParams(parameters, invitationCode);
addConnectionIdParam(parameters, connectionId);
return authorizationUrlWithParameters(parameters);
}

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

@Override
public AuthorizationUrl createOrg(String orgName, String invitationCode) {
return createOrg(orgName, invitationCode, null);
}

@Override
public AuthorizationUrl createOrg(String orgName, String invitationCode, String connectionId) {
if (orgName == null || orgName.isBlank()) {
throw new IllegalArgumentException("createOrg requires a non-blank orgName");
}
Expand All @@ -276,6 +287,7 @@ public AuthorizationUrl createOrg(String orgName, String invitationCode) {
parameters.put("is_create_org", Boolean.TRUE.toString());
parameters.put("org_name", orgName.trim());
addInvitationParams(parameters, invitationCode);
addConnectionIdParam(parameters, connectionId);
return authorizationUrlWithParameters(parameters);
}

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

@Override
public AuthorizationUrl register(String invitationCode) {
return register(invitationCode, null);
}

@Override
public AuthorizationUrl register(String invitationCode, String connectionId) {
Map<String, String> parameters = new HashMap<>();
parameters.put("prompt", Prompt.Type.CREATE.toString());
parameters.put("supports_reauth", "true");
addInvitationParams(parameters, invitationCode);
addConnectionIdParam(parameters, connectionId);
return authorizationUrlWithParameters(parameters);
}

Expand All @@ -310,6 +328,12 @@ private void addInvitationParams(Map<String, String> parameters, String invitati
}
}

private void addConnectionIdParam(Map<String, String> parameters, String connectionId) {
if (connectionId != null && !connectionId.isBlank()) {
parameters.put(KindeRequestParameters.CONNECTION_ID, connectionId.trim());
}
}

public AuthorizationUrl logout() throws Exception {
if (this.kindeConfig.logoutRedirectUri() == null || this.kindeConfig.logoutRedirectUri().isEmpty()) {
throw new Exception("Logout url is not provided");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public class KindeRequestParameters {
public final static String ORG_CODE = "org_code";
public final static String INVITATION_CODE = "invitation_code";
public final static String IS_INVITATION = "is_invitation";
public final static String CONNECTION_ID = "connection_id";
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,94 @@ void createOrgTrimsOrgName() throws Exception {
assertTrue(url.contains("org_name=TestOrg"), "URL should contain trimmed org_name");
assertFalse(url.contains("org_name=+"), "URL should not contain leading whitespace in org_name");
}

@Test
@DisplayName("login with connectionId includes connection_id on the authorize URL")
void loginWithConnectionIdIncludesParam() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.login(null, "conn_abc123");

assertNotNull(result);
String url = result.getUrl().toString();
assertTrue(url.contains("connection_id=conn_abc123"), "URL should contain connection_id");
assertTrue(url.contains("supports_reauth=true"), "URL should still contain supports_reauth");
assertFalse(url.contains("invitation_code"), "URL should not contain invitation_code");
}

@Test
@DisplayName("login with invitationCode and connectionId includes both params")
void loginWithInvitationAndConnectionIdIncludesBothParams() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.login("inv_login123", "conn_abc123");

assertNotNull(result);
String url = result.getUrl().toString();
assertTrue(url.contains("invitation_code=inv_login123"), "URL should contain invitation_code");
assertTrue(url.contains("is_invitation=true"), "URL should contain is_invitation=true");
assertTrue(url.contains("connection_id=conn_abc123"), "URL should contain connection_id");
assertTrue(url.contains("supports_reauth=true"), "URL should contain supports_reauth");
}

@Test
@DisplayName("login with whitespace-only connectionId omits connection_id")
void loginWithWhitespaceOnlyConnectionIdOmitsParam() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.login(null, " ");

assertNotNull(result);
String url = result.getUrl().toString();
assertFalse(url.contains("connection_id"), "URL should not contain connection_id");
assertTrue(url.contains("supports_reauth=true"), "URL should still contain supports_reauth");
}

@Test
@DisplayName("login trims connectionId in the generated URL")
void loginTrimsConnectionId() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.login(null, " conn_abc123 ");

assertNotNull(result);
String url = result.getUrl().toString();
assertTrue(url.contains("connection_id=conn_abc123"), "URL should contain trimmed connection_id");
}

@Test
@DisplayName("register with connectionId includes connection_id and register params")
void registerWithConnectionIdIncludesParam() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.register(null, "conn_reg456");

assertNotNull(result);
String url = result.getUrl().toString();
assertTrue(url.contains("connection_id=conn_reg456"), "URL should contain connection_id");
assertTrue(url.contains("prompt=create"), "URL should contain register-specific prompt=create");
assertTrue(url.contains("supports_reauth=true"), "URL should contain supports_reauth");
}

@Test
@DisplayName("createOrg with connectionId includes connection_id and createOrg params")
void createOrgWithConnectionIdIncludesParam() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.createOrg("TestOrg", null, "conn_org789");

assertNotNull(result);
String url = result.getUrl().toString();
assertTrue(url.contains("connection_id=conn_org789"), "URL should contain connection_id");
assertTrue(url.contains("org_name=TestOrg"), "URL should contain org_name");
assertTrue(url.contains("is_create_org=true"), "URL should contain is_create_org");
assertFalse(url.contains("invitation_code"), "URL should not contain invitation_code");
}

@Test
@DisplayName("authorizationUrlWithParameters includes connection_id from the map")
void authorizationUrlWithParametersIncludesConnectionId() throws Exception {
KindeClientSessionImpl session = createSessionWithOidc();
AuthorizationUrl result = session.authorizationUrlWithParameters(
java.util.Map.of("connection_id", "conn_map123"));

assertNotNull(result);
String url = result.getUrl().toString();
assertTrue(url.contains("connection_id=conn_map123"), "URL should contain connection_id from parameters map");
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -51,40 +51,20 @@ protected void doFilter(ServletRequest servletRequest, ServletResponse servletRe
}

String code = req.getParameter("code");
String rawInvitationCode = req.getParameter(INVITATION_CODE);
String invitationCode = (rawInvitationCode != null && !rawInvitationCode.isBlank()) ? rawInvitationCode.trim() : null;
String invitationCode = optionalQueryParam(req, INVITATION_CODE);
String connectionId = optionalQueryParam(req, CONNECTION_ID);
Principal userPrincipal = (Principal) req.getSession().getAttribute(AUTHENTICATED_USER);
AuthorizationUrl authorizationUrl = (AuthorizationUrl)req.getSession().getAttribute(AUTHORIZATION_URL);
if (invitationCode != null) {
// Invitation code always starts a new auth flow, even if already authenticated
KindeClientSession kindeClientSession = createKindeClientSession(req);
if (kindeAuthenticationAction == KindeAuthenticationAction.LOGIN) {
authorizationUrl = kindeClientSession.login(invitationCode);
} else if (kindeAuthenticationAction == KindeAuthenticationAction.REGISTER) {
authorizationUrl = kindeClientSession.register(invitationCode);
} else if (kindeAuthenticationAction == KindeAuthenticationAction.CREATE_ORG) {
String orgName = req.getParameter(ORG_NAME);
if (orgName == null || orgName.isBlank()) {
throw new ServletException("Must provide org_name query parameter to create an organisation.");
}
authorizationUrl = kindeClientSession.createOrg(orgName.trim(), invitationCode);
}
authorizationUrl = startAuthorization(kindeClientSession, kindeAuthenticationAction, req, invitationCode, connectionId);
req.getSession().setAttribute(AUTHORIZATION_URL, authorizationUrl);
resp.sendRedirect(authorizationUrl.getUrl().toString());
} else if (userPrincipal == null || authorizationUrl == null) {
// Redirect to the OAuth provider's authorization page
KindeClientSession kindeClientSession = createKindeClientSession(req);
if (kindeAuthenticationAction == KindeAuthenticationAction.LOGIN) {
authorizationUrl = kindeClientSession.login();
} else if (kindeAuthenticationAction == KindeAuthenticationAction.REGISTER) {
authorizationUrl = kindeClientSession.register();
} else if (kindeAuthenticationAction == KindeAuthenticationAction.CREATE_ORG) {
String orgName = req.getParameter(ORG_NAME);
if (orgName == null || orgName.isBlank()) {
throw new ServletException("Must provide org_name query parameter to create an organisation.");
}
authorizationUrl = kindeClientSession.createOrg(orgName.trim());
}
authorizationUrl = startAuthorization(kindeClientSession, kindeAuthenticationAction, req, null, connectionId);
req.getSession().setAttribute(AUTHORIZATION_URL,authorizationUrl);
resp.sendRedirect(authorizationUrl.getUrl().toString());
} else if (code != null) {
Expand Down Expand Up @@ -112,6 +92,45 @@ protected void doFilter(ServletRequest servletRequest, ServletResponse servletRe
}
}

private static AuthorizationUrl startAuthorization(
KindeClientSession kindeClientSession,
KindeAuthenticationAction kindeAuthenticationAction,
HttpServletRequest req,
String invitationCode,
String connectionId) throws ServletException {
if (kindeAuthenticationAction == KindeAuthenticationAction.LOGIN) {
if (connectionId != null) {
return kindeClientSession.login(invitationCode, connectionId);
}
return invitationCode != null ? kindeClientSession.login(invitationCode) : kindeClientSession.login();
}
if (kindeAuthenticationAction == KindeAuthenticationAction.REGISTER) {
if (connectionId != null) {
return kindeClientSession.register(invitationCode, connectionId);
}
return invitationCode != null ? kindeClientSession.register(invitationCode) : kindeClientSession.register();
}
if (kindeAuthenticationAction == KindeAuthenticationAction.CREATE_ORG) {
String orgName = req.getParameter(ORG_NAME);
if (orgName == null || orgName.isBlank()) {
throw new ServletException("Must provide org_name query parameter to create an organisation.");
}
String trimmedOrgName = orgName.trim();
if (connectionId != null) {
return kindeClientSession.createOrg(trimmedOrgName, invitationCode, connectionId);
}
return invitationCode != null
? kindeClientSession.createOrg(trimmedOrgName, invitationCode)
: kindeClientSession.createOrg(trimmedOrgName);
}
throw new ServletException("Unknown authentication action: " + kindeAuthenticationAction);
}

private static String optionalQueryParam(HttpServletRequest req, String name) {
String raw = req.getParameter(name);
return (raw != null && !raw.isBlank()) ? raw.trim() : null;
}

private static KindeClientSession createKindeClientSession(HttpServletRequest req) {
return KindeSingleton
.getInstance()
Expand Down
Loading