[KEYCLOAK-2052] Allows independently set timeouts for e-mail verification link and rest e.g. forgot password link
Co-authored-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
parent
925d5e1dea
commit
03d0488335
20 changed files with 695 additions and 59 deletions
|
@ -454,6 +454,12 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setAccessCodeLifespanUserAction(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getUserActionTokenLifespans() {
|
||||
if (isUpdated()) return updated.getUserActionTokenLifespans();
|
||||
return cached.getUserActionTokenLifespans();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessCodeLifespanLogin() {
|
||||
if (isUpdated()) return updated.getAccessCodeLifespanLogin();
|
||||
|
@ -490,6 +496,20 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setActionTokenGeneratedByUserLifespan(seconds);
|
||||
}
|
||||
|
||||
@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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -473,6 +473,19 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
em.flush();
|
||||
}
|
||||
|
||||
@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() {
|
||||
|
@ -1954,4 +1978,5 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
if (c == null) return null;
|
||||
return entityToModel(c);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
@ -448,6 +449,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();
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -514,6 +679,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");
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,8 +71,6 @@ public abstract class AbstractConsoleTest extends AbstractAuthTest {
|
|||
if (!testContext.isAdminLoggedIn()) {
|
||||
loginToMasterRealmAdminConsoleAs(adminUser);
|
||||
testContext.setAdminLoggedIn(true);
|
||||
} else {
|
||||
// adminConsoleRealmPage.navigateTo();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
};
|
||||
|
@ -1110,6 +1124,13 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
});
|
||||
};
|
||||
|
||||
$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();
|
||||
};
|
||||
|
|
|
@ -187,6 +187,32 @@
|
|||
</kc-tooltip>
|
||||
</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>
|
||||
|
|
Loading…
Reference in a new issue