Improve invitation messages and flow
Closes #29945 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
2c521bd64d
commit
320f8eb1b4
14 changed files with 141 additions and 34 deletions
|
@ -1,13 +1,13 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
* and other contributors as indicated by the @author tags.
|
* and other contributors as indicated by the @author tags.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
package org.keycloak.authentication.actiontoken.inviteorg;
|
package org.keycloak.authentication.actiontoken.inviteorg;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import jakarta.ws.rs.core.UriBuilder;
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
import jakarta.ws.rs.core.UriInfo;
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
import org.keycloak.TokenVerifier.Predicate;
|
import org.keycloak.TokenVerifier.Predicate;
|
||||||
|
@ -43,7 +44,7 @@ import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
import java.util.List;
|
import java.net.URI;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,9 +113,9 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
.setAuthenticationSession(authSession)
|
.setAuthenticationSession(authSession)
|
||||||
.setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS)
|
.setSuccess(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP, organization.getName())
|
||||||
|
.setAttribute("messageHeader", Messages.CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE)
|
||||||
.setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
|
.setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
|
||||||
.setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, List.of(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP))
|
|
||||||
.setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
|
.setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
|
||||||
.createInfoPage();
|
.createInfoPage();
|
||||||
}
|
}
|
||||||
|
@ -135,6 +136,17 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
||||||
tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN));
|
tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN));
|
||||||
|
|
||||||
String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getRequest(), event);
|
String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getRequest(), event);
|
||||||
|
|
||||||
|
if (nextAction == null) {
|
||||||
|
// do not show account updated page
|
||||||
|
authSession.removeAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS);
|
||||||
|
|
||||||
|
if (redirectUri != null) {
|
||||||
|
// always redirect to the expected URI if provided
|
||||||
|
return Response.status(Status.FOUND).location(URI.create(redirectUri)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
|
return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,17 +17,27 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.forms;
|
package org.keycloak.authentication.forms;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.FormAuthenticator;
|
import org.keycloak.authentication.FormAuthenticator;
|
||||||
import org.keycloak.authentication.FormAuthenticatorFactory;
|
import org.keycloak.authentication.FormAuthenticatorFactory;
|
||||||
import org.keycloak.authentication.FormContext;
|
import org.keycloak.authentication.FormContext;
|
||||||
|
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.organization.utils.Organizations;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +57,27 @@ public class RegistrationPage implements FormAuthenticator, FormAuthenticatorFac
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response render(FormContext context, LoginFormsProvider form) {
|
public Response render(FormContext context, LoginFormsProvider form) {
|
||||||
|
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
|
try {
|
||||||
|
InviteOrgActionToken token = Organizations.parseInvitationToken(context.getHttpRequest());
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
|
OrganizationModel organization = provider.getById(token.getOrgId());
|
||||||
|
|
||||||
|
if (organization == null || !organization.isEnabled()) {
|
||||||
|
return form.setError(Messages.EXPIRED_ACTION).createErrorPage(Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setAttribute("messageHeader", Messages.REGISTER_ORGANIZATION_MEMBER);
|
||||||
|
form.setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName());
|
||||||
|
}
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
return form.setError(Messages.EXPIRED_ACTION).createErrorPage(Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return form.createRegistration();
|
return form.createRegistration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.keycloak.authentication.forms;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.MultivaluedHashMap;
|
import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.TokenVerifier;
|
|
||||||
import org.keycloak.authentication.AuthenticationFlowError;
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
import org.keycloak.authentication.AuthenticationFlowException;
|
import org.keycloak.authentication.AuthenticationFlowException;
|
||||||
import org.keycloak.authentication.FormAction;
|
import org.keycloak.authentication.FormAction;
|
||||||
|
@ -37,7 +36,6 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.Constants;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
@ -46,6 +44,7 @@ import org.keycloak.models.RequiredActionProviderModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.organization.utils.Organizations;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
@ -290,29 +289,24 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
||||||
|
|
||||||
private boolean validateOrganizationInvitation(ValidationContext context, MultivaluedMap<String, String> formData, String email) {
|
private boolean validateOrganizationInvitation(ValidationContext context, MultivaluedMap<String, String> formData, String email) {
|
||||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
MultivaluedMap<String, String> queryParameters = context.getHttpRequest().getUri().getQueryParameters();
|
|
||||||
String tokenFromQuery = queryParameters.getFirst(Constants.TOKEN);
|
|
||||||
|
|
||||||
if (tokenFromQuery == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Consumer<List<FormMessage>> error = messages -> {
|
Consumer<List<FormMessage>> error = messages -> {
|
||||||
context.getEvent().detail(Messages.INVALID_ORG_INVITE, tokenFromQuery);
|
|
||||||
context.error(Errors.INVALID_TOKEN);
|
context.error(Errors.INVALID_TOKEN);
|
||||||
context.validationError(formData, messages);
|
context.validationError(formData, messages);
|
||||||
};
|
};
|
||||||
|
|
||||||
TokenVerifier<InviteOrgActionToken> tokenVerifier = TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class);
|
|
||||||
InviteOrgActionToken token;
|
InviteOrgActionToken token;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
token = tokenVerifier.getToken();
|
token = Organizations.parseInvitationToken(context.getHttpRequest());
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
error.accept(List.of(new FormMessage("Unexpected error parsing the invitation token")));
|
error.accept(List.of(new FormMessage("Unexpected error parsing the invitation token")));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
KeycloakSession session = context.getSession();
|
KeycloakSession session = context.getSession();
|
||||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
OrganizationModel organization = provider.getById(token.getOrgId());
|
OrganizationModel organization = provider.getById(token.getOrgId());
|
||||||
|
|
|
@ -83,8 +83,13 @@ public class OrganizationInvitationResource {
|
||||||
return sendInvitation(user);
|
return sendInvitation(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!realm.isRegistrationAllowed()) {
|
||||||
|
throw ErrorResponse.error("Realm does not allow self-registration", Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
user = new InMemoryUserAdapter(session, realm, null);
|
user = new InMemoryUserAdapter(session, realm, null);
|
||||||
user.setEmail(email);
|
user.setEmail(email);
|
||||||
|
|
||||||
if (firstName != null && lastName != null) {
|
if (firstName != null && lastName != null) {
|
||||||
user.setFirstName(firstName);
|
user.setFirstName(firstName);
|
||||||
user.setLastName(lastName);
|
user.setLastName(lastName);
|
||||||
|
|
|
@ -19,14 +19,21 @@ package org.keycloak.organization.utils;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.TokenVerifier;
|
||||||
|
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.http.HttpRequest;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
@ -180,4 +187,15 @@ public class Organizations {
|
||||||
public static OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) {
|
public static OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) {
|
||||||
return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified());
|
return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static InviteOrgActionToken parseInvitationToken(HttpRequest request) throws VerificationException {
|
||||||
|
MultivaluedMap<String, String> queryParameters = request.getUri().getQueryParameters();
|
||||||
|
String tokenFromQuery = queryParameters.getFirst(Constants.TOKEN);
|
||||||
|
|
||||||
|
if (tokenFromQuery == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class).getToken();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,7 +192,7 @@ public class Messages {
|
||||||
public static final String STALE_INVITE_ORG_LINK = "staleInviteOrgLink";
|
public static final String STALE_INVITE_ORG_LINK = "staleInviteOrgLink";
|
||||||
|
|
||||||
public static final String IDENTITY_PROVIDER_UNEXPECTED_ERROR = "identityProviderUnexpectedErrorMessage";
|
public static final String IDENTITY_PROVIDER_UNEXPECTED_ERROR = "identityProviderUnexpectedErrorMessage";
|
||||||
|
|
||||||
public static final String IDENTITY_PROVIDER_UNMATCHED_ESSENTIAL_CLAIM_ERROR = "federatedIdentityUnmatchedEssentialClaimMessage";
|
public static final String IDENTITY_PROVIDER_UNMATCHED_ESSENTIAL_CLAIM_ERROR = "federatedIdentityUnmatchedEssentialClaimMessage";
|
||||||
|
|
||||||
public static final String IDENTITY_PROVIDER_MISSING_STATE_ERROR = "identityProviderMissingStateMessage";
|
public static final String IDENTITY_PROVIDER_MISSING_STATE_ERROR = "identityProviderMissingStateMessage";
|
||||||
|
@ -321,4 +321,6 @@ public class Messages {
|
||||||
public static final String OAUTH2_DEVICE_CONSENT_DENIED = "oauth2DeviceConsentDeniedMessage";
|
public static final String OAUTH2_DEVICE_CONSENT_DENIED = "oauth2DeviceConsentDeniedMessage";
|
||||||
|
|
||||||
public static final String CONFIRM_ORGANIZATION_MEMBERSHIP = "organization.confirm-membership";
|
public static final String CONFIRM_ORGANIZATION_MEMBERSHIP = "organization.confirm-membership";
|
||||||
|
public static final String CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE = "organization.confirm-membership.title";
|
||||||
|
public static final String REGISTER_ORGANIZATION_MEMBER = "organization.member.register.title";
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,10 @@ public abstract class AbstractPage {
|
||||||
|
|
||||||
abstract public boolean isCurrent();
|
abstract public boolean isCurrent();
|
||||||
|
|
||||||
|
public boolean isCurrent(String expectedTitle) {
|
||||||
|
return PageUtils.getPageTitle(driver).equals(expectedTitle);
|
||||||
|
}
|
||||||
|
|
||||||
abstract public void open() throws Exception;
|
abstract public void open() throws Exception;
|
||||||
|
|
||||||
public WebDriver getDriver() {
|
public WebDriver getDriver() {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.Constants;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.testsuite.auth.page.AccountFields;
|
import org.keycloak.testsuite.auth.page.AccountFields;
|
||||||
import org.keycloak.testsuite.auth.page.PasswordFields;
|
import org.keycloak.testsuite.auth.page.PasswordFields;
|
||||||
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.keycloak.testsuite.util.UIUtils;
|
import org.keycloak.testsuite.util.UIUtils;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
|
@ -245,7 +246,7 @@ public class RegisterPage extends AbstractPage {
|
||||||
|
|
||||||
|
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return PageUtils.getPageTitle(driver).equals("Register");
|
return isCurrent("Register");
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountFields.AccountErrors getInputAccountErrors(){
|
public AccountFields.AccountErrors getInputAccountErrors(){
|
||||||
|
@ -267,4 +268,9 @@ public class RegisterPage extends AbstractPage {
|
||||||
assertCurrent();
|
assertCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void assertCurrent(String orgName) {
|
||||||
|
String name = getClass().getSimpleName();
|
||||||
|
Assert.assertTrue("Expected " + name + " but was " + DroneUtils.getCurrentDriver().getTitle() + " (" + DroneUtils.getCurrentDriver().getCurrentUrl() + ")",
|
||||||
|
isCurrent("Create an account to join the " + orgName + " organization"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -29,6 +30,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
import jakarta.mail.MessagingException;
|
import jakarta.mail.MessagingException;
|
||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
@ -37,6 +39,7 @@ import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.common.util.UriUtils;
|
import org.keycloak.common.util.UriUtils;
|
||||||
import org.keycloak.cookie.CookieType;
|
import org.keycloak.cookie.CookieType;
|
||||||
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
|
@ -135,7 +138,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
Assert.assertNotNull(orgToken);
|
Assert.assertNotNull(orgToken);
|
||||||
driver.navigate().to(link.trim());
|
driver.navigate().to(link.trim());
|
||||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||||
registerPage.assertCurrent();
|
registerPage.assertCurrent(organizationName);
|
||||||
registerPage.register("firstName", "lastName", user.getEmail(),
|
registerPage.register("firstName", "lastName", user.getEmail(),
|
||||||
user.getUsername(), "password", "password", null, false, null);
|
user.getUsername(), "password", "password", null, false, null);
|
||||||
List<UserRepresentation> users = testRealm().users().searchByEmail(user.getEmail(), true);
|
List<UserRepresentation> users = testRealm().users().searchByEmail(user.getEmail(), true);
|
||||||
|
@ -149,6 +152,27 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
Assert.assertNotNull(driver.manage().getCookieNamed(CookieType.IDENTITY.getName()));
|
Assert.assertNotNull(driver.manage().getCookieNamed(CookieType.IDENTITY.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailRegistrationNotEnabledWhenInvitingNewUser() throws IOException, MessagingException {
|
||||||
|
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());
|
||||||
|
RealmRepresentation realm = testRealm().toRepresentation();
|
||||||
|
realm.setRegistrationAllowed(false);
|
||||||
|
testRealm().update(realm);
|
||||||
|
try (Response response = organization.members().inviteUser(user.getEmail(), null, null)) {
|
||||||
|
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
||||||
|
assertEquals("Realm does not allow self-registration", response.readEntity(ErrorRepresentation.class).getErrorMessage());
|
||||||
|
} finally {
|
||||||
|
realm.setRegistrationAllowed(true);
|
||||||
|
testRealm().update(realm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEmailDoesNotChangeOnRegistration() throws IOException {
|
public void testEmailDoesNotChangeOnRegistration() throws IOException {
|
||||||
UserRepresentation user = UserBuilder.create()
|
UserRepresentation user = UserBuilder.create()
|
||||||
|
@ -168,8 +192,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
Assert.assertNotNull(orgToken);
|
Assert.assertNotNull(orgToken);
|
||||||
driver.navigate().to(link.trim());
|
driver.navigate().to(link.trim());
|
||||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||||
registerPage.assertCurrent();
|
registerPage.assertCurrent(organizationName);
|
||||||
driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS);
|
|
||||||
registerPage.register("firstName", "lastName", "invalid@email.com",
|
registerPage.register("firstName", "lastName", "invalid@email.com",
|
||||||
user.getUsername(), "password", "password", null, false, null);
|
user.getUsername(), "password", "password", null, false, null);
|
||||||
Assert.assertTrue(driver.getPageSource().contains("Email does not match the invitation"));
|
Assert.assertTrue(driver.getPageSource().contains("Email does not match the invitation"));
|
||||||
|
@ -198,7 +221,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
Assert.assertNotNull(orgToken);
|
Assert.assertNotNull(orgToken);
|
||||||
driver.navigate().to(link.trim());
|
driver.navigate().to(link.trim());
|
||||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||||
registerPage.assertCurrent();
|
registerPage.assertCurrent(organizationName);
|
||||||
driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS);
|
driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS);
|
||||||
registerPage.register("firstName", "lastName", "invalid@email.com",
|
registerPage.register("firstName", "lastName", "invalid@email.com",
|
||||||
user.getUsername(), "password", "password", null, false, null);
|
user.getUsername(), "password", "password", null, false, null);
|
||||||
|
@ -216,17 +239,19 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject());
|
Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject());
|
||||||
EmailBody body = MailUtils.getBody(message);
|
EmailBody body = MailUtils.getBody(message);
|
||||||
if (user.getFirstName() != null && user.getLastName() != null) {
|
if (user.getFirstName() != null && user.getLastName() != null) {
|
||||||
assertThat(body.getText(), Matchers.containsString(user.getFirstName() + " " + user.getLastName()));
|
assertThat(body.getText(), Matchers.containsString("Hi, " + user.getFirstName() + " " + user.getLastName() + "."));
|
||||||
}
|
}
|
||||||
String link = MailUtils.getLink(body.getHtml());
|
String link = MailUtils.getLink(body.getHtml());
|
||||||
driver.navigate().to(link.trim());
|
driver.navigate().to(link.trim());
|
||||||
// not yet a member
|
// not yet a member
|
||||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||||
// confirm the intent of membership
|
// confirm the intent of membership
|
||||||
assertThat(infoPage.getInfo(), containsString("You are about to join organization " + organizationName));
|
assertThat(driver.getPageSource(), containsString("You are about to join organization " + organizationName));
|
||||||
|
assertThat(infoPage.getInfo(), containsString("By clicking on the link below, you will become a member of the " + organizationName + " organization:"));
|
||||||
infoPage.clickToContinue();
|
infoPage.clickToContinue();
|
||||||
assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
|
// redirect to the account console and eventually force the user to authenticate if not already
|
||||||
|
assertThat(driver.getTitle(), containsString("Account Management"));
|
||||||
// now a member
|
// now a member
|
||||||
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
|
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ emailVerificationBodyHtml=<p>Someone has created a {2} account with this email a
|
||||||
orgInviteSubject=Invitation to join the {0} organization
|
orgInviteSubject=Invitation to join the {0} organization
|
||||||
orgInviteBody=You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
orgInviteBody=You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
||||||
orgInviteBodyHtml=<p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
orgInviteBodyHtml=<p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
||||||
orgInviteBodyPersonalized="{5}" "{6}"\n\n You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
orgInviteBodyPersonalized=Hi, "{5}" "{6}".\n\n You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
||||||
orgInviteBodyPersonalizedHtml=<p>{5} {6}</p><p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
orgInviteBodyPersonalizedHtml=<p>Hi, {5} {6}.</p><p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
||||||
emailUpdateConfirmationSubject=Verify new email
|
emailUpdateConfirmationSubject=Verify new email
|
||||||
emailUpdateConfirmationBody=To update your {2} account with email address {1}, click the link below\n\n{0}\n\nThis link will expire within {3}.\n\nIf you don''t want to proceed with this modification, just ignore this message.
|
emailUpdateConfirmationBody=To update your {2} account with email address {1}, click the link below\n\n{0}\n\nThis link will expire within {3}.\n\nIf you don''t want to proceed with this modification, just ignore this message.
|
||||||
emailUpdateConfirmationBodyHtml=<p>To update your {2} account with email address {1}, click the link below</p><p><a href="{0}">{0}</a></p><p>This link will expire within {3}.</p><p>If you don''t want to proceed with this modification, just ignore this message.</p>
|
emailUpdateConfirmationBodyHtml=<p>To update your {2} account with email address {1}, click the link below</p><p><a href="{0}">{0}</a></p><p>This link will expire within {3}.</p><p>If you don''t want to proceed with this modification, just ignore this message.</p>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<@layout.registrationLayout displayMessage=false; section>
|
<@layout.registrationLayout displayMessage=false; section>
|
||||||
<#if section = "header">
|
<#if section = "header">
|
||||||
<#if messageHeader??>
|
<#if messageHeader??>
|
||||||
${messageHeader}
|
${kcSanitize(msg("${messageHeader}"))?no_esc}
|
||||||
<#else>
|
<#else>
|
||||||
${message.summary}
|
${message.summary}
|
||||||
</#if>
|
</#if>
|
||||||
|
|
|
@ -518,4 +518,6 @@ doLogout=Logout
|
||||||
readOnlyUsernameMessage=You can''t update your username as it is read-only.
|
readOnlyUsernameMessage=You can''t update your username as it is read-only.
|
||||||
error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} value(s).
|
error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} value(s).
|
||||||
|
|
||||||
requiredAction.organization.confirm-membership=You are about to join organization ${kc.org.name}
|
organization.confirm-membership.title=You are about to join organization ${kc.org.name}
|
||||||
|
organization.confirm-membership=By clicking on the link below, you will become a member of the {0} organization:
|
||||||
|
organization.member.register.title=Create an account to join the ${kc.org.name} organization
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<#import "register-commons.ftl" as registerCommons>
|
<#import "register-commons.ftl" as registerCommons>
|
||||||
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
|
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
|
||||||
<#if section = "header">
|
<#if section = "header">
|
||||||
${msg("registerTitle")}
|
<#if messageHeader??>
|
||||||
|
${kcSanitize(msg("${messageHeader}"))?no_esc}
|
||||||
|
<#else>
|
||||||
|
${msg("registerTitle")}
|
||||||
|
</#if>
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
|
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<#import "register-commons.ftl" as registerCommons>
|
<#import "register-commons.ftl" as registerCommons>
|
||||||
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
|
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
|
||||||
<#if section = "header">
|
<#if section = "header">
|
||||||
${msg("registerTitle")}
|
<#if messageHeader??>
|
||||||
|
${kcSanitize(msg("${messageHeader}"))?no_esc}
|
||||||
|
<#else>
|
||||||
|
${msg("registerTitle")}
|
||||||
|
</#if>
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
|
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue