KEYCLOAK-11898 Refactor AIA implementation

This commit is contained in:
stianst 2019-11-04 12:53:29 +01:00 committed by Bruno Oliveira da Silva
parent 63abebd993
commit 062841a059
19 changed files with 263 additions and 290 deletions

View file

@ -37,12 +37,17 @@ import java.net.URI;
* @version $Revision: 1 $
*/
public interface RequiredActionContext {
public static enum Status {
enum Status {
CHALLENGE,
SUCCESS,
IGNORE,
FAILURE,
CANCELED_AIA
FAILURE
}
enum KcActionStatus {
SUCCESS,
CANCELLED,
ERROR
}
/**
@ -140,10 +145,4 @@ public interface RequiredActionContext {
*/
void ignore();
/**
* Mark application-initiated action as canceled by the user.
*
*/
void cancelAIA();
}

View file

@ -69,6 +69,8 @@ public final class Constants {
public static final String KEY = "key";
public static final String KC_ACTION = "kc_action";
public static final String KC_ACTION_STATUS = "kc_action_status";
public static final String KC_ACTION_EXECUTING = "kc_action_executing";
public static final int KC_ACTION_MAX_AGE = 300;
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";

View file

@ -137,11 +137,6 @@ public class RequiredActionContextResult implements RequiredActionContext {
status = Status.IGNORE;
}
@Override
public void cancelAIA() {
status = Status.CANCELED_AIA;
}
@Override
public URI getActionUrl(String code) {
ClientModel client = authenticationSession.getClient();

View file

@ -66,7 +66,6 @@ import java.util.*;
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
import static org.keycloak.services.managers.AuthenticationManager.IS_AIA_REQUEST;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -180,7 +179,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("statusCode", status.getStatusCode());
}
if (authenticationSession != null && authenticationSession.getClientNote(IS_AIA_REQUEST) != null) {
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) {
attributes.put("isAppInitiatedAction", true);
}

View file

@ -203,6 +203,11 @@ public class OIDCLoginProtocol implements LoginProtocol {
String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM);
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, nonce);
String kcActionStatus = authSession.getClientNote(Constants.KC_ACTION_STATUS);
if (kcActionStatus != null) {
redirectUri.addParam(Constants.KC_ACTION_STATUS, kcActionStatus);
}
// Standard or hybrid flow
String code = null;
if (responseType.hasResponseType(OIDCResponseType.CODE)) {

View file

@ -129,8 +129,6 @@ public class AuthenticationManager {
public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME";
public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL";
private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID);
public static final String IS_AIA_REQUEST = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.IS_AIA_REQUEST;
public static final String IS_SILENT_CANCEL = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.AIA_SILENT_CANCEL;
public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) {
if (userSession == null) {
@ -904,6 +902,11 @@ public class AuthenticationManager {
return authSession.getRequiredActions().iterator().next();
}
String kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (kcAction != null) {
return kcAction;
}
if (client.isConsentRequired()) {
UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);
@ -1056,7 +1059,30 @@ public class AuthenticationManager {
List<RequiredActionProviderModel> sortedRequiredActions = sortRequiredActionsByPriority(realm, requiredActions);
for (RequiredActionProviderModel model : sortedRequiredActions) {
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
Response response = executeAction(session, authSession, model, request, event, realm, user, false);
if (response != null) {
return response;
}
}
String kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (kcAction != null) {
for (RequiredActionProviderModel m : realm.getRequiredActionProviders()) {
if (m.getProviderId().equals(kcAction)) {
return executeAction(session, authSession, m, request, event, realm, user, true);
}
}
logger.debugv("Requested action {0} not configured for realm", kcAction);
setKcActionStatus(kcAction, RequiredActionContext.KcActionStatus.ERROR, authSession);
}
return null;
}
private static Response executeAction(KeycloakSession session, AuthenticationSessionModel authSession, RequiredActionProviderModel model,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user, boolean kcActionExecution) {
RequiredActionFactory factory = (RequiredActionFactory) session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
if (factory == null) {
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
}
@ -1070,6 +1096,17 @@ public class AuthenticationManager {
}
throw e;
}
if (kcActionExecution) {
if (actionProvider.initiatedActionSupport() == InitiatedActionSupport.NOT_SUPPORTED) {
logger.debugv("Requested action {0} does not support being invoked with kc_action", factory.getId());
setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.ERROR, authSession);
return null;
} else {
authSession.setClientNote(Constants.KC_ACTION_EXECUTING, factory.getId());
}
}
actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
@ -1091,8 +1128,9 @@ public class AuthenticationManager {
// don't have to perform the same action twice, so remove it from both the user and session required actions
authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
authSession.removeRequiredAction(factory.getId());
setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.SUCCESS, authSession);
}
}
return null;
}
@ -1143,38 +1181,12 @@ public class AuthenticationManager {
public void ignore() {
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
}
@Override
public void cancelAIA() {
throw new RuntimeException("Not allowed to call cancelAIA() within evaluateTriggers()");
}
};
evaluateApplicationInitiatedActionTrigger(session, provider, model, authSession);
provider.evaluateTriggers(result);
}
}
// Determine if provider is being requested as an Application-Initiated Action
// If so, add it to the authSession.
private static void evaluateApplicationInitiatedActionTrigger(final KeycloakSession session,
final RequiredActionProvider provider,
final RequiredActionProviderModel model,
final AuthenticationSessionModel authSession
) {
if (provider.initiatedActionSupport() == InitiatedActionSupport.NOT_SUPPORTED) return;
String aia = authSession.getClientNote(Constants.KC_ACTION);
if (aia == null) return;
// make sure you are evaluating the action that was requested
if (!aia.equalsIgnoreCase(model.getProviderId())) return;
authSession.addRequiredAction(model.getProviderId());
authSession.removeClientNote(Constants.KC_ACTION); // keep this from being executed twice
authSession.setClientNote(IS_AIA_REQUEST, "true");
}
public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers, Predicate<? super AccessToken>... additionalChecks) {
try {
@ -1266,4 +1278,12 @@ public class AuthenticationManager {
}
}
public static void setKcActionStatus(String executedProviderId, RequiredActionContext.KcActionStatus status, AuthenticationSessionModel authSession) {
if (executedProviderId.equals(authSession.getClientNote(Constants.KC_ACTION))) {
authSession.setClientNote(Constants.KC_ACTION_STATUS, status.name().toLowerCase());
authSession.removeClientNote(Constants.KC_ACTION);
authSession.removeClientNote(Constants.KC_ACTION_EXECUTING);
}
}
}

View file

@ -101,8 +101,6 @@ import java.net.URI;
import java.util.Map;
import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS;
import static org.keycloak.services.managers.AuthenticationManager.IS_AIA_REQUEST;
import static org.keycloak.services.managers.AuthenticationManager.IS_SILENT_CANCEL;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -993,9 +991,10 @@ public class LoginActionsService {
Response response;
if (isCancelAppInitiatedAction(authSession, context)) {
if (isCancelAppInitiatedAction(factory.getId(), authSession, context)) {
provider.initiatedActionCanceled(session, authSession);
context.cancelAIA();
AuthenticationManager.setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.CANCELLED, authSession);
context.success();
} else {
provider.processAction(context);
}
@ -1011,16 +1010,13 @@ public class LoginActionsService {
authSession.removeRequiredAction(factory.getId());
authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
AuthenticationManager.setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.SUCCESS, authSession);
response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, session.getContext().getUri(), event);
} else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
response = context.getChallenge();
} else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
response = interruptionResponse(context, authSession, action, Error.CONSENT_DENIED);
} else if (isSilentAIACancel(authSession, context)) {
response = interruptionResponse(context, authSession, action, Error.CANCELLED_AIA_SILENT);
} else if (context.getStatus() == RequiredActionContext.Status.CANCELED_AIA) {
response = interruptionResponse(context, authSession, action, Error.CANCELLED_AIA);
} else {
throw new RuntimeException("Unreachable");
}
@ -1041,21 +1037,13 @@ public class LoginActionsService {
return protocol.sendError(authSession, error);
}
private boolean isCancelAppInitiatedAction(AuthenticationSessionModel authSession, RequiredActionContextResult context) {
private boolean isCancelAppInitiatedAction(String providerId, AuthenticationSessionModel authSession, RequiredActionContextResult context) {
if (providerId.equals(authSession.getClientNote(Constants.KC_ACTION_EXECUTING))) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
boolean userRequestedCancelAIA = formData.getFirst(CANCEL_AIA) != null;
boolean isAIARequest = authSession.getClientNote(IS_AIA_REQUEST) != null;
return isAIARequest && userRequestedCancelAIA;
return userRequestedCancelAIA;
}
private boolean isSilentAIACancel(AuthenticationSessionModel authSession, RequiredActionContextResult context) {
String silentCancel = authSession.getClientNote(IS_SILENT_CANCEL);
boolean isSilentCancel = "true".equalsIgnoreCase(silentCancel);
boolean isAIACancel = isCancelAppInitiatedAction(authSession, context);
return isSilentCancel && isAIACancel;
return false;
}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.pages;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@ -82,4 +83,12 @@ public class LoginConfigTotpPage extends AbstractPage {
return loginErrorMessage.getText();
}
public boolean isCancelDisplayed() {
try {
return cancelAIAButton.isDisplayed();
} catch (NoSuchElementException e) {
return false;
}
}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.pages;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@ -68,4 +69,13 @@ public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
public String getFeedbackMessage() {
return feedbackMessage.getText();
}
public boolean isCancelDisplayed() {
try {
return cancelAIAButton.isDisplayed();
} catch (NoSuchElementException e) {
return false;
}
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.pages;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@ -91,4 +92,12 @@ public class LoginUpdateProfilePage extends AbstractPage {
throw new UnsupportedOperationException();
}
public boolean isCancelDisplayed() {
try {
return cancelAIAButton.isDisplayed();
} catch (NoSuchElementException e) {
return false;
}
}
}

View file

@ -16,11 +16,11 @@
*/
package org.keycloak.testsuite.actions;
import javax.ws.rs.core.UriBuilder;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Rule;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
@ -29,6 +29,11 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.WaitUtils;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
/**
* @author Stan Silvert
*/
@ -50,13 +55,8 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
}
protected void doAIA() {
doAIA(false);
}
protected void doAIA(boolean silentCancel) {
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
String uri = builder.queryParam("kc_action", this.aiaAction)
.queryParam("silent_cancel", Boolean.toString(silentCancel))
.queryParam("response_type", "code")
.queryParam("client_id", "test-app")
.queryParam("scope", "openid")
@ -66,14 +66,24 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
WaitUtils.waitForPageToLoad();
}
protected void assertRedirectSuccess() {
protected void assertKcActionStatus(String expectedStatus) {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
protected void assertCancelMessage() {
String url = this.driver.getCurrentUrl();
Assert.assertTrue("Expected 'error=interaction_required' in url", url.contains("error=interaction_required"));
Assert.assertTrue("Expected 'error_description=User+cancelled+aplication-initiated+action.' in url", url.contains("error_description=User+cancelled+aplication-initiated+action."));
URI url = null;
try {
url = new URI(this.driver.getCurrentUrl());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
List<NameValuePair> pairs = URLEncodedUtils.parse(url, "UTF-8");
String kcActionStatus = null;
for (NameValuePair p : pairs) {
if (p.getName().equals("kc_action_status")) {
kcActionStatus = p.getValue();
break;
}
}
Assert.assertEquals(expectedStatus, kcActionStatus);
}
protected void assertSilentCancelMessage() {

View file

@ -1,72 +0,0 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.actions;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
/**
* Test makes sure that sending a cancel signal does not remove a non-AIA
* required action
*
* @author Stan Silvert
*/
public class AppInitiatedActionCancelTest extends AbstractAppInitiatedActionTest {
@Page
protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage;
public AppInitiatedActionCancelTest() {
super("update_profile");
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
ActionUtil.addRequiredActionForUser(testRealm, "test-user@localhost", UserModel.RequiredAction.UPDATE_PROFILE.name());
}
@Test
// Verify that sending a "cancel" does not remove the required action.
public void cancelUpdateProfile() {
doAIA();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.cancel();
assertRedirectSuccess();
assertCancelMessage();
appPage.logout();
loginPage.open();
loginPage.assertCurrent();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
}
@Test
public void silentCancelUpdateProfile() {
doAIA(true);
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.cancel();
assertRedirectSuccess();
assertSilentCancelMessage();
}
}

View file

@ -17,23 +17,31 @@
package org.keycloak.testsuite.actions;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.EventType;
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.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.GreenMailRule;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author Stan Silvert
*/
public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionResetPasswordTest() {
super("update_password");
super(UserModel.RequiredAction.UPDATE_PASSWORD.name());
}
@Override
@ -47,19 +55,28 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
@Page
protected LoginPasswordUpdatePage changePasswordPage;
@After
public void after() {
ApiUtil.resetUserPassword(testRealm().users().get(findUser("test-user@localhost").getId()), "password", false);
}
@Test
public void tempPassword() throws Exception {
doAIA();
public void resetPassword() throws Exception {
loginPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
doAIA();
changePasswordPage.assertCurrent();
assertTrue(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
@ -73,6 +90,30 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
events.expectLogin().assertEvent();
}
@Test
public void resetPasswordRequiresReAuth() throws Exception {
loginPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
setTimeOffset(350);
// Should prompt for re-authentication
doAIA();
loginPage.assertCurrent();
loginPage.login("test-user@localhost", "password");
changePasswordPage.assertCurrent();
assertTrue(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
}
@Test
public void cancelChangePassword() throws Exception {
doAIA();
@ -82,8 +123,31 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
changePasswordPage.assertCurrent();
changePasswordPage.cancel();
assertRedirectSuccess();
assertCancelMessage();
assertKcActionStatus("cancelled");
}
@Test
public void resetPasswordUserHasUpdatePasswordRequiredAction() throws Exception {
loginPage.open();
loginPage.login("test-user@localhost", "password");
UserResource userResource = testRealm().users().get(findUser("test-user@localhost").getId());
UserRepresentation userRep = userResource.toRepresentation();
userRep.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
userResource.update(userRep);
events.expectLogin().assertEvent();
doAIA();
changePasswordPage.assertCurrent();
assertFalse(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
}
}

View file

@ -1,62 +0,0 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.actions;
import java.util.List;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.models.AccountRoles;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testsuite.pages.ErrorPage;
/**
*
* @author Stan Silvert
*/
public class AppInitiatedActionRoleTest extends AbstractAppInitiatedActionTest {
@Page
protected ErrorPage errorPage;
public AppInitiatedActionRoleTest() {
super("update_profile");
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
List<RoleRepresentation> roleList = testRealm.getRoles().getClient().get("test-app");
RoleRepresentation manageAccountRole = null;
for (RoleRepresentation role : roleList) {
if (role.getName().equals(AccountRoles.MANAGE_ACCOUNT)) {
manageAccountRole = role;
break;
}
}
roleList.remove(manageAccountRole);
}
@Test
public void roleNotSetTest() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
doAIA();
errorPage.assertCurrent();
}
}

View file

@ -25,6 +25,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
@ -51,7 +52,7 @@ import static org.junit.Assert.assertTrue;
public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionTotpSetupTest() {
super("configure_totp");
super(UserModel.RequiredAction.CONFIGURE_TOTP.name());
}
@Override
@ -64,7 +65,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
for (AuthenticationExecutionInfoRepresentation execution : adminClient.realm("test").flows().getExecutions("browser")) {
String providerId = execution.getProviderId();
if ("auth-otp-form".equals(providerId)) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL.name());
adminClient.realm("test").flows().updateExecutions("browser", execution);
}
}
@ -112,7 +113,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
.getDetails().get(Details.CODE_ID);
assertRedirectSuccess();
assertKcActionStatus("success");
events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent();
}
@ -126,8 +127,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
totpPage.assertCurrent();
totpPage.cancel();
assertRedirectSuccess();
assertCancelMessage();
assertKcActionStatus("cancelled");
}
@Test
@ -297,7 +297,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
.getDetails().get(Details.CODE_ID);
assertRedirectSuccess();
assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
@ -310,8 +310,6 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
loginTotpPage.login(totp.generateTOTP(totpSecret));
assertRedirectSuccess();
events.expectLogin().assertEvent();
}
@ -333,7 +331,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
totpPage.configure(totp.generateTOTP(totpCode));
// After totp config, user should be on the app page
assertRedirectSuccess();
assertKcActionStatus("success");
events.poll();
events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
@ -375,17 +373,6 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
// Try to login
loginPage.open();
loginPage.login("setupTotp2", "password2");
// Since the authentificator was removed, it has to be set up again
totpPage.assertCurrent();
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent()
.getDetails().get(Details.CODE_ID);
assertRedirectSuccess();
events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent();
}
@Test
@ -416,7 +403,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
.getDetails().get(Details.CODE_ID);
assertRedirectSuccess();
assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
@ -431,7 +418,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
assertEquals(8, token.length());
loginTotpPage.login(token);
assertRedirectSuccess();
assertKcActionStatus(null);
events.expectLogin().assertEvent();
@ -469,7 +456,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
.getDetails().get(Details.CODE_ID);
//RequestType reqType = appPage.getRequestType();
assertRedirectSuccess();
assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
oauth.openLogout();
@ -481,7 +468,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String token = otpgen.generateHOTP(totpSecret, 1);
loginTotpPage.login(token);
assertRedirectSuccess();
assertKcActionStatus(null);
events.expectLogin().assertEvent();
@ -506,7 +493,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
loginTotpPage.assertCurrent();
loginTotpPage.login(token);
assertRedirectSuccess();
assertKcActionStatus(null);
events.expectLogin().assertEvent();

View file

@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -37,7 +38,7 @@ import org.keycloak.testsuite.util.UserBuilder;
public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionUpdateProfileTest() {
super("update_profile");
super(UserModel.RequiredAction.UPDATE_PROFILE.name());
}
@Page
@ -85,7 +86,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
events.expectLogin().assertEvent();
assertRedirectSuccess();
assertKcActionStatus("success");
// assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -113,7 +114,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
assertRedirectSuccess();
assertKcActionStatus("success");
// assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -132,8 +133,8 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
updateProfilePage.assertCurrent();
updateProfilePage.cancel();
assertRedirectSuccess();
assertCancelMessage();
assertKcActionStatus("cancelled");
// assert nothing was updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -164,7 +165,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
.removeDetail(Details.CONSENT)
.assertEvent();
assertRedirectSuccess();
assertKcActionStatus("success");
events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).assertEvent();

View file

@ -33,6 +33,9 @@ import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.GreenMailRule;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -66,6 +69,8 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe
loginPage.login("test-user@localhost", "password");
changePasswordPage.assertCurrent();
assertFalse(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();

View file

@ -131,6 +131,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
String userId = events.expectRegister("setupTotp", "email@mail.com").assertEvent().getUserId();
assertTrue(totpPage.isCurrent());
assertFalse(totpPage.isCancelDisplayed());
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));

View file

@ -38,6 +38,8 @@ import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
import org.keycloak.testsuite.util.UserBuilder;
import static org.junit.Assert.assertFalse;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -92,6 +94,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
assertFalse(updateProfilePage.isCancelDisplayed());
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");