keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 21(+20 -1)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java 6(+5 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 96(+93 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java 312(+272 -40)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AssertAdminEvents.java 3(+1 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserActionTokenBuilder.java 38(+38 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/realm/TokenSettings.java 38(+38 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java 2(+0 -2)
Details
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 52a81de..e854002 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -143,6 +143,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected Map<String, String> attributes;
+ private Map<String, Integer> userActionTokenLifespans;
+
public CachedRealm(Long revision, RealmModel model) {
super(revision, model.getId());
name = model.getName();
@@ -192,6 +194,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
emailTheme = model.getEmailTheme();
requiredCredentials = model.getRequiredCredentials();
+ userActionTokenLifespans = Collections.unmodifiableMap(new HashMap<>(model.getUserActionTokenLifespans()));
this.identityProviders = new ArrayList<>();
@@ -407,6 +410,11 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public int getAccessCodeLifespanUserAction() {
return accessCodeLifespanUserAction;
}
+
+ public Map<String, Integer> getUserActionTokenLifespans() {
+ return userActionTokenLifespans;
+ }
+
public int getAccessCodeLifespanLogin() {
return accessCodeLifespanLogin;
}
@@ -419,6 +427,18 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return actionTokenGeneratedByUserLifespan;
}
+ /**
+ * This method is supposed to return user lifespan based on the action token ID
+ * provided. If nothing is provided, it will return the default lifespan.
+ * @param actionTokenId
+ * @return lifespan
+ */
+ public int getActionTokenGeneratedByUserLifespan(String actionTokenId) {
+ if (actionTokenId == null || this.userActionTokenLifespans.get(actionTokenId) == null)
+ return getActionTokenGeneratedByUserLifespan();
+ return this.userActionTokenLifespans.get(actionTokenId);
+ }
+
public List<RequiredCredentialModel> getRequiredCredentials() {
return requiredCredentials;
}
@@ -609,5 +629,4 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public Map<String, String> getAttributes() {
return attributes;
}
-
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index f3ae325..bfa00e0 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -455,6 +455,12 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public Map<String, Integer> getUserActionTokenLifespans() {
+ if (isUpdated()) return updated.getUserActionTokenLifespans();
+ return cached.getUserActionTokenLifespans();
+ }
+
+ @Override
public int getAccessCodeLifespanLogin() {
if (isUpdated()) return updated.getAccessCodeLifespanLogin();
return cached.getAccessCodeLifespanLogin();
@@ -491,6 +497,20 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public int getActionTokenGeneratedByUserLifespan(String actionTokenId) {
+ if (isUpdated()) return updated.getActionTokenGeneratedByUserLifespan(actionTokenId);
+ return cached.getActionTokenGeneratedByUserLifespan(actionTokenId);
+ }
+
+ @Override
+ public void setActionTokenGeneratedByUserLifespan(String actionTokenId, Integer seconds) {
+ if (seconds != null) {
+ getDelegateForUpdate();
+ updated.setActionTokenGeneratedByUserLifespan(actionTokenId, seconds);
+ }
+ }
+
+ @Override
public List<RequiredCredentialModel> getRequiredCredentials() {
if (isUpdated()) return updated.getRequiredCredentials();
return cached.getRequiredCredentials();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 862db6c..d82144d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -474,6 +474,19 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
+ public Map<String, Integer> getUserActionTokenLifespans() {
+
+ Map<String, Integer> userActionTokens = new HashMap<>();
+
+ getAttributes().entrySet().stream()
+ .filter(Objects::nonNull)
+ .filter(entry -> entry.getKey().startsWith(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "."))
+ .forEach(entry -> userActionTokens.put(entry.getKey().substring(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN.length() + 1), Integer.valueOf(entry.getValue())));
+
+ return Collections.unmodifiableMap(userActionTokens);
+ }
+
+ @Override
public int getAccessCodeLifespanLogin() {
return realm.getAccessCodeLifespanLogin();
}
@@ -504,6 +517,17 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
setAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN, actionTokenGeneratedByUserLifespan);
}
+ @Override
+ public int getActionTokenGeneratedByUserLifespan(String actionTokenId) {
+ return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "." + actionTokenId, getAccessCodeLifespanUserAction());
+ }
+
+ @Override
+ public void setActionTokenGeneratedByUserLifespan(String actionTokenId, Integer actionTokenGeneratedByUserLifespan) {
+ if (actionTokenGeneratedByUserLifespan != null)
+ setAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "." + actionTokenId, actionTokenGeneratedByUserLifespan);
+ }
+
protected RequiredCredentialModel initRequiredCredentialModel(String type) {
RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
if (model == null) {
@@ -647,7 +671,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
entities.remove(entity);
}
em.flush();
- }
+ }
@Override
public List<GroupModel> getDefaultGroups() {
@@ -1802,7 +1826,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
/**
* This just exists for testing purposes
- *
+ *
*/
public static final String COMPONENT_PROVIDER_EXISTS_DISABLED = "component.provider.exists.disabled";
@@ -1954,4 +1978,5 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
if (c == null) return null;
return entityToModel(c);
}
+
}
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 4d7090e..6d48425 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -186,6 +186,13 @@ public interface RealmModel extends RoleContainerModel {
void setAccessCodeLifespanUserAction(int seconds);
+ /**
+ * This method will return a map with all the lifespans available
+ * or an empty map, but never null.
+ * @return map with user action token lifespans
+ */
+ Map<String, Integer> getUserActionTokenLifespans();
+
int getAccessCodeLifespanLogin();
void setAccessCodeLifespanLogin(int seconds);
@@ -196,6 +203,9 @@ public interface RealmModel extends RoleContainerModel {
int getActionTokenGeneratedByUserLifespan();
void setActionTokenGeneratedByUserLifespan(int seconds);
+ int getActionTokenGeneratedByUserLifespan(String actionTokenType);
+ void setActionTokenGeneratedByUserLifespan(String actionTokenType, Integer seconds);
+
List<RequiredCredentialModel> getRequiredCredentials();
void addRequiredCredential(String cred);
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index d118634..093fbb7 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -116,7 +116,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
UriInfo uriInfo = session.getContext().getUri();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
- int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
+ int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(IdpVerifyAccountLinkActionToken.TOKEN_TYPE);
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
index 4ac9bff..a61d4db 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
@@ -85,7 +85,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return;
}
- int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan();
+ int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan(ResetCredentialsActionToken.TOKEN_TYPE);
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
// We send the secret in the email in a link as a query param.
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index baa3c4e..4208308 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -131,7 +131,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
RealmModel realm = session.getContext().getRealm();
UriInfo uriInfo = session.getContext().getUri();
- int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
+ int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getId(), user.getEmail());
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java
index 69aa142..add2a89 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java
@@ -56,7 +56,11 @@ public class MailUtils {
assertEquals("text/html; charset=UTF-8", htmlContentType);
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
- final String htmlChangePwdUrl = getLink(htmlBody);
+ // .replace() accounts for escaping the ampersand
+ // It's not escaped in the html version because html retrieved from a
+ // message bundle is considered safe and it must be unescaped to display
+ // properly.
+ final String htmlChangePwdUrl = MailUtils.getLink(htmlBody).replace("&", "&");
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index f4c7452..55dd453 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -27,14 +27,12 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
-import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ProceedPage;
@@ -45,16 +43,19 @@ import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
+import org.keycloak.testsuite.util.UserActionTokenBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import org.hamcrest.Matchers;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -449,6 +450,95 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
}
@Test
+ public void verifyEmailExpiredCodedPerActionLifespan() throws IOException, MessagingException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ realmRep.setAttributes(UserActionTokenBuilder.create().verifyEmailLifespan(60).build());
+ testRealm().update(realmRep);
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ verifyEmailPage.assertCurrent();
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getLastReceivedMessage();
+
+ String verificationUrl = getPasswordResetEmailLink(message);
+
+ events.poll();
+
+ try {
+ setTimeOffset(70);
+
+ driver.navigate().to(verificationUrl.trim());
+
+ loginPage.assertCurrent();
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
+ .error(Errors.EXPIRED_CODE)
+ .client((String)null)
+ .user(testUserId)
+ .session((String)null)
+ .clearDetails()
+ .detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE)
+ .assertEvent();
+ } finally {
+ setTimeOffset(0);
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
+ @Test
+ public void verifyEmailExpiredCodedPerActionMultipleTimeouts() throws IOException, MessagingException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ //Make sure that one attribute settings won't affect the other
+ realmRep.setAttributes(UserActionTokenBuilder.create().verifyEmailLifespan(60).resetCredentialsLifespan(300).build());
+ testRealm().update(realmRep);
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ verifyEmailPage.assertCurrent();
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getLastReceivedMessage();
+
+ String verificationUrl = getPasswordResetEmailLink(message);
+
+ events.poll();
+
+ try {
+ setTimeOffset(70);
+
+ driver.navigate().to(verificationUrl.trim());
+
+ loginPage.assertCurrent();
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
+ .error(Errors.EXPIRED_CODE)
+ .client((String)null)
+ .user(testUserId)
+ .session((String)null)
+ .clearDetails()
+ .detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE)
+ .assertEvent();
+ } finally {
+ setTimeOffset(0);
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
+ @Test
public void verifyEmailExpiredCodeAndExpiredSession() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
index da54a72..15477fe 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
@@ -47,6 +47,7 @@ import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
@@ -337,7 +338,7 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
// Receive email
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
- String changePasswordUrl = ResetPasswordTest.getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index bc4379e..a009820 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -38,12 +38,13 @@ import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.UserActionTokenBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import javax.mail.MessagingException;
-import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -131,7 +132,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@@ -251,7 +252,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@@ -295,7 +296,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@@ -362,7 +363,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
try {
setTimeOffset(1800 + 23);
@@ -399,7 +400,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
setTimeOffset(70);
@@ -418,6 +419,84 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
}
+ @Test
+ public void resetPasswordExpiredCodeShortPerActionLifespan() throws IOException, MessagingException, InterruptedException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ realmRep.setAttributes(UserActionTokenBuilder.create().resetCredentialsLifespan(60).build());
+ testRealm().update(realmRep);
+
+ try {
+ initiateResetPasswordFromResetPasswordPage("login-test");
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
+
+ setTimeOffset(70);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ loginPage.assertCurrent();
+
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
+ @Test
+ public void resetPasswordExpiredCodeShortPerActionMultipleTimeouts() throws IOException, MessagingException, InterruptedException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ //Make sure that one attribute settings won't affect the other
+ realmRep.setAttributes(UserActionTokenBuilder.create().resetCredentialsLifespan(60).verifyEmailLifespan(300).build());
+
+ testRealm().update(realmRep);
+
+ try {
+ initiateResetPasswordFromResetPasswordPage("login-test");
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
+
+ setTimeOffset(70);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ loginPage.assertCurrent();
+
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
// KEYCLOAK-4016
@Test
public void resetPasswordExpiredCodeAndAuthSession() throws IOException, MessagingException, InterruptedException {
@@ -439,7 +518,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message).replace("&", "&");
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message).replace("&", "&");
setTimeOffset(70);
@@ -463,6 +542,92 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
}
+ @Test
+ public void resetPasswordExpiredCodeAndAuthSessionPerActionLifespan() throws IOException, MessagingException, InterruptedException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ realmRep.setAttributes(UserActionTokenBuilder.create().resetCredentialsLifespan(60).build());
+ testRealm().update(realmRep);
+
+ try {
+ initiateResetPasswordFromResetPasswordPage("login-test");
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message).replace("&", "&");
+
+ setTimeOffset(70);
+
+ log.debug("Going to reset password URI.");
+ driver.navigate().to(oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials"); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
+ log.debug("Removing cookies.");
+ driver.manage().deleteAllCookies();
+ driver.navigate().to(changePasswordUrl.trim());
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Action expired.", errorPage.getError());
+ String backToAppLink = errorPage.getBackToApplicationLink();
+ Assert.assertTrue(backToAppLink.endsWith("/app/auth"));
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
+ @Test
+ public void resetPasswordExpiredCodeAndAuthSessionPerActionMultipleTimeouts() throws IOException, MessagingException, InterruptedException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ //Make sure that one attribute settings won't affect the other
+ realmRep.setAttributes(UserActionTokenBuilder.create().resetCredentialsLifespan(60).verifyEmailLifespan(300).build());
+ testRealm().update(realmRep);
+
+ try {
+ initiateResetPasswordFromResetPasswordPage("login-test");
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message).replace("&", "&");
+
+ setTimeOffset(70);
+
+ log.debug("Going to reset password URI.");
+ driver.navigate().to(oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials"); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
+ log.debug("Removing cookies.");
+ driver.manage().deleteAllCookies();
+ driver.navigate().to(changePasswordUrl.trim());
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Action expired.", errorPage.getError());
+ String backToAppLink = errorPage.getBackToApplicationLink();
+ Assert.assertTrue(backToAppLink.endsWith("/app/auth"));
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
// KEYCLOAK-5061
@Test
@@ -495,7 +660,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
setTimeOffset(70);
@@ -515,6 +680,103 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
@Test
+ public void resetPasswordExpiredCodeForgotPasswordFlowPerActionLifespan() throws IOException, MessagingException, InterruptedException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ realmRep.setAttributes(UserActionTokenBuilder.create().resetCredentialsLifespan(60).build());
+ testRealm().update(realmRep);
+
+ try {
+ // Redirect directly to KC "forgot password" endpoint instead of "authenticate" endpoint
+ String loginUrl = oauth.getLoginFormUrl();
+ String forgotPasswordUrl = loginUrl.replace("/auth?", "/forgot-credentials?"); // Workaround, but works
+
+ driver.navigate().to(forgotPasswordUrl);
+ resetPasswordPage.assertCurrent();
+ resetPasswordPage.changePassword("login-test");
+
+ loginPage.assertCurrent();
+ assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ expectedMessagesCount++;
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
+
+ setTimeOffset(70);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ resetPasswordPage.assertCurrent();
+
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
+ @Test
+ public void resetPasswordExpiredCodeForgotPasswordFlowPerActionMultipleTimeouts() throws IOException, MessagingException, InterruptedException {
+ RealmRepresentation realmRep = testRealm().toRepresentation();
+ Map<String, String> originalAttributes = Collections.unmodifiableMap(new HashMap<>(realmRep.getAttributes()));
+
+ //Make sure that one attribute settings won't affect the other
+ realmRep.setAttributes(UserActionTokenBuilder.create().resetCredentialsLifespan(60).verifyEmailLifespan(300).build());
+ testRealm().update(realmRep);
+
+ try {
+ // Redirect directly to KC "forgot password" endpoint instead of "authenticate" endpoint
+ String loginUrl = oauth.getLoginFormUrl();
+ String forgotPasswordUrl = loginUrl.replace("/auth?", "/forgot-credentials?"); // Workaround, but works
+
+ driver.navigate().to(forgotPasswordUrl);
+ resetPasswordPage.assertCurrent();
+ resetPasswordPage.changePassword("login-test");
+
+ loginPage.assertCurrent();
+ assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ expectedMessagesCount++;
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
+
+ setTimeOffset(70);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ resetPasswordPage.assertCurrent();
+
+ assertEquals("Action expired. Please start again.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setAttributes(originalAttributes);
+ testRealm().update(realmRep);
+ }
+ }
+
+ @Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
UserRepresentation user = findUser("login-test");
try {
@@ -608,7 +870,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).session((String)null).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
@@ -701,7 +963,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
log.debug("Going to reset password URI.");
driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
@@ -710,8 +972,6 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
log.debug("Going to URI from e-mail.");
driver.navigate().to(changePasswordUrl.trim());
-// System.out.println(driver.getPageSource());
-
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("resetPassword", "resetPassword");
@@ -719,32 +979,4 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
infoPage.assertCurrent();
assertEquals("Your account has been updated.", infoPage.getInfo());
}
-
- public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
- Multipart multipart = (Multipart) message.getContent();
-
- final String textContentType = multipart.getBodyPart(0).getContentType();
-
- assertEquals("text/plain; charset=UTF-8", textContentType);
-
- final String textBody = (String) multipart.getBodyPart(0).getContent();
- final String textChangePwdUrl = MailUtils.getLink(textBody);
-
- final String htmlContentType = multipart.getBodyPart(1).getContentType();
-
- assertEquals("text/html; charset=UTF-8", htmlContentType);
-
- final String htmlBody = (String) multipart.getBodyPart(1).getContent();
-
- // .replace() accounts for escaping the ampersand
- // It's not escaped in the html version because html retrieved from a
- // message bundle is considered safe and it must be unescaped to display
- // properly.
- final String htmlChangePwdUrl = MailUtils.getLink(htmlBody).replace("&", "&");
-
- assertEquals(htmlChangePwdUrl, textChangePwdUrl);
-
- return htmlChangePwdUrl;
- }
-
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AssertAdminEvents.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AssertAdminEvents.java
index ec1156a..fc82162 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AssertAdminEvents.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AssertAdminEvents.java
@@ -39,7 +39,6 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.util.JsonSerialization;
-import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
@@ -251,7 +250,7 @@ public class AssertAdminEvents implements TestRule {
// Reflection-based comparing for other types - compare the non-null fields of "expected" representation with the "actual" representation from the event
for (Method method : Reflections.getAllDeclaredMethods(expectedRep.getClass())) {
- if (method.getName().startsWith("get") || method.getName().startsWith("is")) {
+ if (method.getParameterCount() == 0 && (method.getName().startsWith("get") || method.getName().startsWith("is"))) {
Object expectedValue = Reflections.invokeMethod(method, expectedRep);
if (expectedValue != null) {
Object actualValue = Reflections.invokeMethod(method, actualRep);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserActionTokenBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserActionTokenBuilder.java
new file mode 100644
index 0000000..43685cf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserActionTokenBuilder.java
@@ -0,0 +1,38 @@
+package org.keycloak.testsuite.util;
+
+import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken;
+import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
+ */
+public class UserActionTokenBuilder {
+
+ private final Map<String, String> realmAttributes;
+ private static final String ATTR_PREFIX = "actionTokenGeneratedByUserLifespan.";
+
+ private UserActionTokenBuilder(HashMap<String, String> attr) {
+ realmAttributes = attr;
+ }
+
+ public static UserActionTokenBuilder create() {
+ return new UserActionTokenBuilder(new HashMap<>());
+ }
+
+ public UserActionTokenBuilder resetCredentialsLifespan(int lifespan) {
+ realmAttributes.put(ATTR_PREFIX + ResetCredentialsActionToken.TOKEN_TYPE, String.valueOf(lifespan));
+ return this;
+ }
+
+ public UserActionTokenBuilder verifyEmailLifespan(int lifespan) {
+ realmAttributes.put(ATTR_PREFIX + VerifyEmailActionToken.TOKEN_TYPE, String.valueOf(lifespan));
+ return this;
+ }
+
+ public Map<String, String> build() {
+ return realmAttributes;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/realm/TokenSettings.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/realm/TokenSettings.java
index e93c34f..ff131be 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/realm/TokenSettings.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/realm/TokenSettings.java
@@ -61,6 +61,18 @@ public class TokenSettings extends RealmSettings {
@FindBy(name = "ssoSessionMaxLifespanUnit")
private Select sessionLifespanTimeoutUnit;
+ @FindBy(name = "actionTokenAttributeSelect")
+ private Select actionTokenAttributeSelect;
+
+ @FindBy(name = "actionTokenAttributeUnit")
+ private Select actionTokenAttributeUnit;
+
+ @FindBy(id = "actionTokenAttributeTime")
+ private WebElement actionTokenAttributeTime;
+
+ @FindBy(xpath = "//button[@data-ng-click='resetToDefaultToken(actionTokenId)']")
+ private WebElement resetButton;
+
public void setSessionTimeout(int timeout, TimeUnit unit) {
setTimeout(sessionTimeoutUnit, sessionTimeout, timeout, unit);
}
@@ -69,6 +81,12 @@ public class TokenSettings extends RealmSettings {
setTimeout(sessionLifespanTimeoutUnit, sessionLifespanTimeout, time, unit);
}
+ public void setOperation(String tokenType, int time, TimeUnit unit) {
+ waitUntilElement(sessionTimeout).is().present();
+ actionTokenAttributeSelect.selectByValue(tokenType.toLowerCase());
+ setTimeout(actionTokenAttributeUnit, actionTokenAttributeTime, time, unit);
+ }
+
private void setTimeout(Select timeoutElement, WebElement unitElement,
int timeout, TimeUnit unit) {
waitUntilElement(sessionTimeout).is().present();
@@ -77,5 +95,25 @@ public class TokenSettings extends RealmSettings {
unitElement.sendKeys(valueOf(timeout));
}
+ public boolean isOperationEquals(String tokenType, int timeout, TimeUnit unit) {
+ selectOperation(tokenType);
+
+ waitUntilElement(sessionTimeout).is().present();
+ actionTokenAttributeSelect.selectByValue(tokenType.toLowerCase());
+
+ return actionTokenAttributeTime.getAttribute("value").equals(Integer.toString(timeout)) &&
+ actionTokenAttributeUnit.getFirstSelectedOption().getText().equals(capitalize(unit.name().toLowerCase()));
+ }
+
+ public void resetActionToken(String tokenType) {
+ selectOperation(tokenType);
+ waitUntilElement(resetButton).is().visible();
+ resetButton.click();
+ }
+
+ public void selectOperation(String tokenType) {
+ waitUntilElement(sessionTimeout).is().present();
+ actionTokenAttributeSelect.selectByValue(tokenType.toLowerCase());
+ }
}
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java
index 0b02193..d01e4a5 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java
@@ -71,8 +71,6 @@ public abstract class AbstractConsoleTest extends AbstractAuthTest {
if (!testContext.isAdminLoggedIn()) {
loginToMasterRealmAdminConsoleAs(adminUser);
testContext.setAdminLoggedIn(true);
- } else {
-// adminConsoleRealmPage.navigateTo();
}
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java
index b022d13..cd7d309 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java
@@ -17,13 +17,27 @@
*/
package org.keycloak.testsuite.console.realm;
+import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
+import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken;
+import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
+import org.keycloak.models.jpa.entities.RealmAttributes;
+import org.keycloak.testsuite.auth.page.account.Account;
import org.keycloak.testsuite.console.page.realm.TokenSettings;
+import org.keycloak.testsuite.console.page.users.UserAttributes;
+import org.keycloak.testsuite.pages.VerifyEmailPage;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.TimeUnit;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
@@ -36,13 +50,20 @@ public class TokensTest extends AbstractRealmTest {
@Page
private TokenSettings tokenSettingsPage;
+ @Page
+ private UserAttributes userAttributesPage;
+
+ @Page
+ protected VerifyEmailPage verifyEmailPage;
+
+ @Page
+ private Account testRealmAccountPage;
+
private static final int TIMEOUT = 1;
private static final TimeUnit TIME_UNIT = TimeUnit.MINUTES;
@Before
public void beforeTokensTest() {
-// configure().realmSettings();
-// tabs().tokens();
tokenSettingsPage.navigateTo();
}
@@ -78,10 +99,92 @@ public class TokensTest extends AbstractRealmTest {
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); // assert logged out (lifespan exceeded)
}
+ @Test
+ public void testLifespanOfVerifyEmailActionTokenPropagated() throws InterruptedException {
+ tokenSettingsPage.form().setOperation(VerifyEmailActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.DAYS);
+ tokenSettingsPage.form().save();
+ assertAlertSuccess();
+
+ loginToTestRealmConsoleAs(testUser);
+ driver.navigate().refresh();
+
+ tokenSettingsPage.navigateTo();
+ tokenSettingsPage.form().selectOperation(VerifyEmailActionToken.TOKEN_TYPE);
+
+ assertTrue("User action token for verify e-mail expected",
+ tokenSettingsPage.form().isOperationEquals(VerifyEmailActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.DAYS));
+
+ }
+
+ @Test
+ public void testLifespanActionTokenPropagatedForVerifyEmailAndResetPassword() throws InterruptedException {
+ tokenSettingsPage.form().setOperation(VerifyEmailActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.DAYS);
+ tokenSettingsPage.form().setOperation(ResetCredentialsActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.HOURS);
+ tokenSettingsPage.form().save();
+ assertAlertSuccess();
+
+ loginToTestRealmConsoleAs(testUser);
+ driver.navigate().refresh();
+
+ tokenSettingsPage.navigateTo();
+ assertTrue("User action token for verify e-mail expected",
+ tokenSettingsPage.form().isOperationEquals(VerifyEmailActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.DAYS));
+
+ assertTrue("User action token for reset credentials expected",
+ tokenSettingsPage.form().isOperationEquals(ResetCredentialsActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.HOURS));
+
+ //Verify if values were properly propagated
+ Map<String, Integer> userActionTokens = getUserActionTokens();
+
+ assertThat("Action Token attributes list should contain 2 items", userActionTokens.entrySet(), Matchers.hasSize(2));
+ assertThat(userActionTokens, Matchers.hasEntry(VerifyEmailActionToken.TOKEN_TYPE, Long.toString(TimeUnit.DAYS.toSeconds(TIMEOUT))));
+ assertThat(userActionTokens, Matchers.hasEntry(ResetCredentialsActionToken.TOKEN_TYPE, Long.toString(TimeUnit.HOURS.toSeconds(TIMEOUT))));
+
+ }
+
+ @Test
+ public void testLifespanActionTokenResetForVerifyEmail() throws InterruptedException {
+ tokenSettingsPage.form().setOperation(VerifyEmailActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.DAYS);
+ tokenSettingsPage.form().setOperation(ResetCredentialsActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.HOURS);
+ tokenSettingsPage.form().save();
+ assertAlertSuccess();
+
+ loginToTestRealmConsoleAs(testUser);
+ driver.navigate().refresh();
+
+ tokenSettingsPage.navigateTo();
+ assertTrue("User action token for verify e-mail expected",
+ tokenSettingsPage.form().isOperationEquals(VerifyEmailActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.DAYS));
+
+ assertTrue("User action token for reset credentials expected",
+ tokenSettingsPage.form().isOperationEquals(ResetCredentialsActionToken.TOKEN_TYPE, TIMEOUT, TimeUnit.HOURS));
+
+ //Remove VerifyEmailActionToken and reset attribute
+ tokenSettingsPage.form().resetActionToken(VerifyEmailActionToken.TOKEN_TYPE);
+ tokenSettingsPage.form().save();
+
+ //Verify if values were properly propagated
+ Map<String, Integer> userActionTokens = getUserActionTokens();
+
+ assertTrue("Action Token attributes list should contain 1 item", userActionTokens.size() == 1);
+ assertNull("VerifyEmailActionToken should not exist", userActionTokens.get(VerifyEmailActionToken.TOKEN_TYPE));
+ assertEquals("ResetCredentialsActionToken expected to be propagated",
+ userActionTokens.get(ResetCredentialsActionToken.TOKEN_TYPE).longValue(), TimeUnit.HOURS.toSeconds(TIMEOUT));
+
+ }
+
+ private Map<String, Integer> getUserActionTokens() {
+ Map<String, Integer> userActionTokens = new HashMap<>();
+ adminClient.realm(testRealmPage.getAuthRealm()).toRepresentation().getAttributes().entrySet().stream()
+ .filter(Objects::nonNull)
+ .filter(entry -> entry.getKey().startsWith(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "."))
+ .forEach(entry -> userActionTokens.put(entry.getKey().substring(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN.length() + 1), Integer.valueOf(entry.getValue())));
+ return userActionTokens;
+ }
+
private void waitForTimeout (int timeout) throws InterruptedException {
log.info("Wait for timeout: " + timeout + " " + TIME_UNIT);
TIME_UNIT.sleep(timeout);
log.info("Timeout reached");
}
-
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 4a6798d..66b1943 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -114,6 +114,15 @@ action-token-generated-by-admin-lifespan=Default Admin-Initiated Action Lifespan
action-token-generated-by-admin-lifespan.tooltip=Maximum time before an action permit sent to a user by admin is expired. This value is recommended to be long to allow admins send e-mails for users that are currently offline. The default timeout can be overridden right before issuing the token.
action-token-generated-by-user-lifespan=User-Initiated Action Lifespan
action-token-generated-by-user-lifespan.tooltip=Maximum time before an action permit sent by a user (e.g. forgot password e-mail) is expired. This value is recommended to be short because it is expected that the user would react to self-created action quickly.
+
+action-token-generated-by-user.execute-actions=Execute Actions
+action-token-generated-by-user.idp-verify-account-via-email=IdP Account E-mail Verification
+action-token-generated-by-user.reset-credentials=Forgot Password
+action-token-generated-by-user.verify-email=E-mail Verification
+action-token-generated-by-user.tooltip=Override default settings of maximum time before an action permit sent by a user (e.g. forgot password e-mail) is expired for specific action. This value is recommended to be short because it is expected that the user would react to self-created action quickly.
+action-token-generated-by-user.reset=Reset
+action-token-generated-by-user.operation=Override User-Initiated Action Lifespan
+
client-login-timeout=Client login timeout
client-login-timeout.tooltip=Max time an client has to finish the access token protocol. This should normally be 1 minute.
login-timeout=Login timeout
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index 936b26b..540f236 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -195,6 +195,9 @@ module.config([ '$routeProvider', function($routeProvider) {
.when('/realms/:realm/token-settings', {
templateUrl : resourceUrl + '/partials/realm-tokens.html',
resolve : {
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
+ },
realm : function(RealmLoader) {
return RealmLoader();
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index ca16f91..db3fd4b 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1065,8 +1065,10 @@ module.controller('RealmIdentityProviderExportCtrl', function(realm, identityPro
}
});
-module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit, TimeUnit2) {
+module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit, TimeUnit2, serverInfo) {
$scope.realm = realm;
+ $scope.serverInfo = serverInfo;
+ $scope.actionTokenProviders = $scope.serverInfo.providers.actionTokenHandler.providers;
$scope.realm.accessTokenLifespan = TimeUnit2.asUnit(realm.accessTokenLifespan);
$scope.realm.accessTokenLifespanForImplicitFlow = TimeUnit2.asUnit(realm.accessTokenLifespanForImplicitFlow);
@@ -1078,6 +1080,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
$scope.realm.actionTokenGeneratedByAdminLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan);
$scope.realm.actionTokenGeneratedByUserLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByUserLifespan);
+ $scope.realm.attributes = realm.attributes
var oldCopy = angular.copy($scope.realm);
$scope.changed = false;
@@ -1088,6 +1091,17 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
}
}, true);
+ $scope.$watch('actionLifespanId', function () {
+ $scope.actionTokenAttribute = TimeUnit2.asUnit($scope.realm.attributes['actionTokenGeneratedByUserLifespan.' + $scope.actionLifespanId]);
+ }, true);
+
+ $scope.$watch('actionTokenAttribute', function () {
+ if ($scope.actionLifespanId != null && $scope.actionTokenAttribute != null) {
+ $scope.changed = true;
+ $scope.realm.attributes['actionTokenGeneratedByUserLifespan.' + $scope.actionLifespanId] = $scope.actionTokenAttribute.toSeconds();
+ }
+ }, true);
+
$scope.changeRevokeRefreshToken = function() {
};
@@ -1109,6 +1123,13 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
Notifications.success("The changes have been saved to the realm.");
});
};
+
+ $scope.resetToDefaultToken = function (actionTokenId) {
+ $scope.actionTokenAttribute = {};
+ delete $scope.realm.attributes['actionTokenGeneratedByUserLifespan.' + $scope.actionLifespanId];
+ //Only for UI effects, resets to the original state
+ $scope.actionTokenAttribute.unit = 'Minutes';
+ }
$scope.reset = function() {
$route.reload();
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index 81374d6..3b05fdb 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -188,6 +188,32 @@
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="actionTokenAttributeSelect" class="two-lines">
+ {{:: 'action-token-generated-by-user.operation' | translate }} </label>
+ <div class="form-inline col-md-6 time-selector">
+ <select class="form-control" name="actionTokenAttributeSelect" id="actionTokenAttributeSelect"
+ ng-model="actionLifespanId">
+ <option value="" disabled selected>{{:: 'select-one.placeholder' | translate}}</option>
+ <option ng-repeat="(actionTokenId, value) in actionTokenProviders" value="{{actionTokenId}}">
+ {{:: 'action-token-generated-by-user.' + actionTokenId | translate }}
+ </option>
+ </select>
+ <input class="form-control" type="number" min="1" max="31536000" data-ng-model="actionTokenAttribute.time"
+ id="actionTokenAttributeTime" name="actionTokenAttributeTime">
+ <select class="form-control" name="actionTokenAttributeUnit"
+ data-ng-model="actionTokenAttribute.unit">
+ <option value="Minutes" ng-selected="true">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ <button data-ng-click="resetToDefaultToken(actionTokenId)">{{:: 'action-token-generated-by-user.reset' | translate}}</button>
+ </div>
+ <kc-tooltip>
+ {{:: 'action-token-generated-by-user.tooltip' | translate}}
+ </kc-tooltip>
+ </div>
+
+ <div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>