diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
index 81ac48e7ed..a5c6e21aa4 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
@@ -27,15 +27,23 @@ import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext;
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
-import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionTokenHandler;
import org.keycloak.authentication.requiredactions.TermsAndConditions;
+import org.keycloak.common.Profile;
+import org.keycloak.common.Profile.Feature;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.*;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.OrganizationModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RequiredActionProviderModel;
+import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -50,6 +58,7 @@ import org.keycloak.userprofile.UserProfile;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.List;
+import java.util.function.Consumer;
/**
* @author Bill Burke
@@ -72,13 +81,16 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
@Override
public void validate(ValidationContext context) {
MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
- MultivaluedMap queryParameters = context.getHttpRequest().getUri().getQueryParameters();
-
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfile profile = getOrCreateUserProfile(context, formData);
Attributes attributes = profile.getAttributes();
String email = attributes.getFirst(UserModel.EMAIL);
+
+ if (!validateOrganizationInvitation(context, formData, email)) {
+ return;
+ }
+
String username = attributes.getFirst(UserModel.USERNAME);
String firstName = attributes.getFirst(UserModel.FIRST_NAME);
String lastName = attributes.getFirst(UserModel.LAST_NAME);
@@ -112,27 +124,6 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
context.validationError(formData, errors);
return;
}
-
- // handle parsing of an organization invite token from the url
- String tokenFromQuery = queryParameters.getFirst(Constants.ORG_TOKEN);
- if (tokenFromQuery != null) {
- TokenVerifier tokenVerifier = TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class);
- try {
- InviteOrgActionToken aToken = tokenVerifier.getToken();
- if (aToken.isExpired() || !aToken.getActionId().equals(InviteOrgActionToken.TOKEN_TYPE) || !aToken.getEmail().equals(email)) {
- throw new VerificationException("The provided token is not valid. It may be expired or issued for a different email");
- }
- // TODO probably need to check if string is empty or null
- if (context.getSession().getProvider(OrganizationProvider.class).getById(aToken.getOrgId()) == null) {
- throw new VerificationException("The provided token contains an invalid organization id");
- }
- } catch (VerificationException e) {
- // TODO we can be more specific here just trying to get something working...
- context.getEvent().detail(Messages.INVALID_ORG_INVITE, tokenFromQuery);
- context.error(Errors.INVALID_TOKEN);
- return;
- }
- }
context.success();
}
@@ -146,19 +137,6 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
checkNotOtherUserAuthenticating(context);
MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
- String tokenFromQuery = context.getHttpRequest().getUri().getQueryParameters().getFirst(Constants.ORG_TOKEN);
-
- DefaultActionTokenKey aToken = null;
- if(tokenFromQuery != null) {
- try {
- TokenVerifier tokenVerifier = TokenVerifier.create(tokenFromQuery, DefaultActionTokenKey.class);
- aToken = tokenVerifier.getToken();
- } catch (VerificationException e) {
- // TODO in theory this should never happen since we already validated. We should either encapsulate decoding the token somehow (add to context or make new class?)
- // for now we can panic run this exception if we somehow end up here
- throw new RuntimeException(e);
- }
- }
String email = formData.getFirst(UserModel.EMAIL);
String username = formData.getFirst(UserModel.USERNAME);
@@ -174,16 +152,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
UserProfile profile = getOrCreateUserProfile(context, formData);
UserModel user = profile.create();
- // since we already validated the token we can just add the user to the organization
- if (aToken != null) {
- String org = aToken.getOtherClaims().get("org_id").toString();
- KeycloakSession session = context.getSession();
- OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
- OrganizationModel orgModel = provider.getById(org);
- provider.addMember(orgModel, user);
- context.getEvent().detail(Details.ORG_ID, org);
- context.getAuthenticationSession().setRedirectUri(aToken.getOtherClaims().get("reduri").toString());
- }
+ addOrganizationMember(context, user);
user.setEnabled(true);
@@ -318,4 +287,71 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
}
return profile;
}
+
+ private boolean validateOrganizationInvitation(ValidationContext context, MultivaluedMap formData, String email) {
+ if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
+ MultivaluedMap queryParameters = context.getHttpRequest().getUri().getQueryParameters();
+ String tokenFromQuery = queryParameters.getFirst(Constants.ORG_TOKEN);
+
+ if (tokenFromQuery == null) {
+ return true;
+ }
+
+ Consumer> error = messages -> {
+ context.getEvent().detail(Messages.INVALID_ORG_INVITE, tokenFromQuery);
+ context.error(Errors.INVALID_TOKEN);
+ context.validationError(formData, messages);
+ };
+
+ TokenVerifier tokenVerifier = TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class);
+ InviteOrgActionToken token;
+
+ try {
+ token = tokenVerifier.getToken();
+ } catch (VerificationException e) {
+ error.accept(List.of(new FormMessage("Unexpected error parsing the invitation token")));
+ return false;
+ }
+
+ KeycloakSession session = context.getSession();
+ OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
+ OrganizationModel organization = provider.getById(token.getOrgId());
+
+ if (organization == null) {
+ error.accept(List.of(new FormMessage("The provided token contains an invalid organization id")));
+ return false;
+ }
+
+ // make sure the organization is set to the session so that UP org-related validators can run
+ session.setAttribute(OrganizationModel.class.getName(), organization);
+ session.setAttribute(InviteOrgActionToken.class.getName(), token);
+
+ if (token.isExpired() || !token.getActionId().equals(InviteOrgActionToken.TOKEN_TYPE)) {
+ error.accept(List.of(new FormMessage("The provided token is not valid or has expired.")));
+ return false;
+ }
+
+ if (!token.getEmail().equals(email)) {
+ error.accept(List.of(new FormMessage(UserModel.EMAIL, "Email does not match the invitation")));
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void addOrganizationMember(FormContext context, UserModel user) {
+ if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
+ InviteOrgActionToken token = (InviteOrgActionToken) context.getSession().getAttribute(InviteOrgActionToken.class.getName());
+
+ if (token != null) {
+ KeycloakSession session = context.getSession();
+ OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
+ OrganizationModel orgModel = provider.getById(token.getOrgId());
+ provider.addMember(orgModel, user);
+ context.getEvent().detail(Details.ORG_ID, orgModel.getId());
+ context.getAuthenticationSession().setRedirectUri(token.getRedirectUri());
+ }
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java
index 2936c1a4f5..de1ff3856f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java
@@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.containsString;
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import jakarta.mail.internet.MimeMessage;
import jakarta.ws.rs.core.Response;
@@ -98,7 +99,6 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
}
-
@Test
public void testInviteNewUserRegistration() throws IOException {
UserRepresentation user = UserBuilder.create()
@@ -124,9 +124,69 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
Assert.assertFalse(users.isEmpty());
// user is a member
Assert.assertNotNull(organization.members().member(users.get(0).getId()).toRepresentation());
+ getCleanup().addCleanup(() -> testRealm().users().get(users.get(0).getId()).remove());
// authenticated to the account console
Assert.assertTrue(driver.getPageSource().contains("Account Management"));
Assert.assertNotNull(driver.manage().getCookieNamed(CookieType.IDENTITY.getName()));
}
+
+ @Test
+ public void testEmailDoesNotChangeOnRegistration() throws IOException {
+ UserRepresentation user = UserBuilder.create()
+ .username("invitedUser")
+ .email("inviteduser@email")
+ .enabled(true)
+ .build();
+ // User isn't created when we send the invite
+ OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
+ organization.members().inviteUser(user.getEmail()).close();
+
+ MimeMessage message = greenMail.getLastReceivedMessage();
+ Assert.assertNotNull(message);
+ String link = MailUtils.getPasswordResetEmailLink(message);
+ String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null);
+ Assert.assertNotNull(orgToken);
+ driver.navigate().to(link.trim());
+ Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
+ registerPage.assertCurrent();
+ driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS);
+ registerPage.register("firstName", "lastName", "invalid@email.com",
+ user.getUsername(), "password", "password", null, false, null);
+ Assert.assertTrue(driver.getPageSource().contains("Email does not match the invitation"));
+ List users = testRealm().users().searchByEmail(user.getEmail(), true);
+ Assert.assertTrue(users.isEmpty());
+ }
+
+ @Test
+ public void testLinkExpired() throws IOException {
+ UserRepresentation user = UserBuilder.create()
+ .username("invitedUser")
+ .email("inviteduser@email")
+ .enabled(true)
+ .build();
+ // User isn't created when we send the invite
+ OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
+ organization.members().inviteUser(user.getEmail()).close();
+
+ try {
+ setTimeOffset((int) TimeUnit.DAYS.toSeconds(1));
+ MimeMessage message = greenMail.getLastReceivedMessage();
+ Assert.assertNotNull(message);
+ String link = MailUtils.getPasswordResetEmailLink(message);
+ String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null);
+ Assert.assertNotNull(orgToken);
+ driver.navigate().to(link.trim());
+ Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
+ registerPage.assertCurrent();
+ driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS);
+ registerPage.register("firstName", "lastName", "invalid@email.com",
+ user.getUsername(), "password", "password", null, false, null);
+ Assert.assertTrue(driver.getPageSource().contains("The provided token is not valid or has expired."));
+ List users = testRealm().users().searchByEmail(user.getEmail(), true);
+ Assert.assertTrue(users.isEmpty());
+ } finally {
+ resetTimeOffset();
+ }
+ }
}
\ No newline at end of file