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 $ * @version $Revision: 1 $
*/ */
public interface RequiredActionContext { public interface RequiredActionContext {
public static enum Status { enum Status {
CHALLENGE, CHALLENGE,
SUCCESS, SUCCESS,
IGNORE, IGNORE,
FAILURE, FAILURE
CANCELED_AIA }
enum KcActionStatus {
SUCCESS,
CANCELLED,
ERROR
} }
/** /**
@ -139,11 +144,5 @@ public interface RequiredActionContext {
* *
*/ */
void ignore(); 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 KEY = "key";
public static final String KC_ACTION = "kc_action"; 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 int KC_ACTION_MAX_AGE = 300;
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST"; public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";

View file

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

View file

@ -66,7 +66,6 @@ import java.util.*;
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -180,7 +179,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("statusCode", status.getStatusCode()); 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); attributes.put("isAppInitiatedAction", true);
} }

View file

@ -203,6 +203,11 @@ public class OIDCLoginProtocol implements LoginProtocol {
String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM); String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM);
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, nonce); 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 // Standard or hybrid flow
String code = null; String code = null;
if (responseType.hasResponseType(OIDCResponseType.CODE)) { 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_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME";
public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL"; public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL";
private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID); 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) { public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) {
if (userSession == null) { if (userSession == null) {
@ -904,6 +902,11 @@ public class AuthenticationManager {
return authSession.getRequiredActions().iterator().next(); return authSession.getRequiredActions().iterator().next();
} }
String kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (kcAction != null) {
return kcAction;
}
if (client.isConsentRequired()) { if (client.isConsentRequired()) {
UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession); UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);
@ -1056,43 +1059,78 @@ public class AuthenticationManager {
List<RequiredActionProviderModel> sortedRequiredActions = sortRequiredActionsByPriority(realm, requiredActions); List<RequiredActionProviderModel> sortedRequiredActions = sortRequiredActionsByPriority(realm, requiredActions);
for (RequiredActionProviderModel model : sortedRequiredActions) { 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 (factory == null) { if (response != 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?");
}
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
RequiredActionProvider actionProvider = null;
try {
actionProvider = createRequiredAction(context);
} catch (AuthenticationFlowException e) {
if (e.getResponse() != null) {
return e.getResponse();
}
throw e;
}
actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getAuthenticationSession().getProtocol());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo())
.setEventBuilder(event);
Response response = protocol.sendError(context.getAuthenticationSession(), Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
return response; return response;
} }
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { }
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, model.getProviderId());
return context.getChallenge(); 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);
}
} }
else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success(); logger.debugv("Requested action {0} not configured for realm", kcAction);
// don't have to perform the same action twice, so remove it from both the user and session required actions setKcActionStatus(kcAction, RequiredActionContext.KcActionStatus.ERROR, authSession);
authSession.getAuthenticatedUser().removeRequiredAction(factory.getId()); }
authSession.removeRequiredAction(factory.getId());
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?");
}
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
RequiredActionProvider actionProvider = null;
try {
actionProvider = createRequiredAction(context);
} catch (AuthenticationFlowException e) {
if (e.getResponse() != null) {
return e.getResponse();
}
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) {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getAuthenticationSession().getProtocol());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo())
.setEventBuilder(event);
Response response = protocol.sendError(context.getAuthenticationSession(), Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
return response;
}
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, model.getProviderId());
return context.getChallenge();
}
else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success();
// 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; return null;
} }
@ -1143,37 +1181,11 @@ public class AuthenticationManager {
public void ignore() { public void ignore() {
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()"); 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); 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, 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) { boolean isCookie, String tokenString, HttpHeaders headers, Predicate<? super AccessToken>... additionalChecks) {
@ -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 java.util.Map;
import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -993,9 +991,10 @@ public class LoginActionsService {
Response response; Response response;
if (isCancelAppInitiatedAction(authSession, context)) { if (isCancelAppInitiatedAction(factory.getId(), authSession, context)) {
provider.initiatedActionCanceled(session, authSession); provider.initiatedActionCanceled(session, authSession);
context.cancelAIA(); AuthenticationManager.setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.CANCELLED, authSession);
context.success();
} else { } else {
provider.processAction(context); provider.processAction(context);
} }
@ -1011,16 +1010,13 @@ public class LoginActionsService {
authSession.removeRequiredAction(factory.getId()); authSession.removeRequiredAction(factory.getId());
authSession.getAuthenticatedUser().removeRequiredAction(factory.getId()); authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); 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); response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, session.getContext().getUri(), event);
} else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { } else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
response = context.getChallenge(); response = context.getChallenge();
} else if (context.getStatus() == RequiredActionContext.Status.FAILURE) { } else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
response = interruptionResponse(context, authSession, action, Error.CONSENT_DENIED); 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 { } else {
throw new RuntimeException("Unreachable"); throw new RuntimeException("Unreachable");
} }
@ -1041,21 +1037,13 @@ public class LoginActionsService {
return protocol.sendError(authSession, error); return protocol.sendError(authSession, error);
} }
private boolean isCancelAppInitiatedAction(AuthenticationSessionModel authSession, RequiredActionContextResult context) { private boolean isCancelAppInitiatedAction(String providerId, AuthenticationSessionModel authSession, RequiredActionContextResult context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); if (providerId.equals(authSession.getClientNote(Constants.KC_ACTION_EXECUTING))) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
boolean userRequestedCancelAIA = formData.getFirst(CANCEL_AIA) != null; boolean userRequestedCancelAIA = formData.getFirst(CANCEL_AIA) != null;
boolean isAIARequest = authSession.getClientNote(IS_AIA_REQUEST) != null; return userRequestedCancelAIA;
}
return isAIARequest && userRequestedCancelAIA; return false;
}
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;
} }
} }

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.testsuite.pages; package org.keycloak.testsuite.pages;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBy;
@ -82,4 +83,12 @@ public class LoginConfigTotpPage extends AbstractPage {
return loginErrorMessage.getText(); 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; package org.keycloak.testsuite.pages;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBy;
@ -68,4 +69,13 @@ public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
public String getFeedbackMessage() { public String getFeedbackMessage() {
return feedbackMessage.getText(); 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; package org.keycloak.testsuite.pages;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBy;
@ -91,4 +92,12 @@ public class LoginUpdateProfilePage extends AbstractPage {
throw new UnsupportedOperationException(); 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; 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.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; 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.pages.LoginPage;
import org.keycloak.testsuite.util.WaitUtils; 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 * @author Stan Silvert
*/ */
@ -50,13 +55,8 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
} }
protected void doAIA() { protected void doAIA() {
doAIA(false);
}
protected void doAIA(boolean silentCancel) {
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder()); UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
String uri = builder.queryParam("kc_action", this.aiaAction) String uri = builder.queryParam("kc_action", this.aiaAction)
.queryParam("silent_cancel", Boolean.toString(silentCancel))
.queryParam("response_type", "code") .queryParam("response_type", "code")
.queryParam("client_id", "test-app") .queryParam("client_id", "test-app")
.queryParam("scope", "openid") .queryParam("scope", "openid")
@ -65,15 +65,25 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
driver.navigate().to(uri); driver.navigate().to(uri);
WaitUtils.waitForPageToLoad(); WaitUtils.waitForPageToLoad();
} }
protected void assertRedirectSuccess() { protected void assertKcActionStatus(String expectedStatus) {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
URI url = null;
protected void assertCancelMessage() { try {
String url = this.driver.getCurrentUrl(); url = new URI(this.driver.getCurrentUrl());
Assert.assertTrue("Expected 'error=interaction_required' in url", url.contains("error=interaction_required")); } catch (URISyntaxException e) {
Assert.assertTrue("Expected 'error_description=User+cancelled+aplication-initiated+action.' in url", url.contains("error_description=User+cancelled+aplication-initiated+action.")); 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() { 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; package org.keycloak.testsuite.actions;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; 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.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.GreenMailRule;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/** /**
* @author Stan Silvert * @author Stan Silvert
*/ */
public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedActionTest { public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionResetPasswordTest() { public AppInitiatedActionResetPasswordTest() {
super("update_password"); super(UserModel.RequiredAction.UPDATE_PASSWORD.name());
} }
@Override @Override
@ -47,19 +55,28 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
@Page @Page
protected LoginPasswordUpdatePage changePasswordPage; protected LoginPasswordUpdatePage changePasswordPage;
@After
public void after() {
ApiUtil.resetUserPassword(testRealm().users().get(findUser("test-user@localhost").getId()), "password", false);
}
@Test @Test
public void tempPassword() throws Exception { public void resetPassword() throws Exception {
doAIA(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
doAIA();
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
assertTrue(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password"); changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().assertEvent(); EventRepresentation loginEvent = events.expectLogin().assertEvent();
@ -72,7 +89,31 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
events.expectLogin().assertEvent(); 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 @Test
public void cancelChangePassword() throws Exception { public void cancelChangePassword() throws Exception {
doAIA(); doAIA();
@ -82,8 +123,31 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
changePasswordPage.cancel(); changePasswordPage.cancel();
assertRedirectSuccess(); assertKcActionStatus("cancelled");
assertCancelMessage(); }
@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.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.HmacOTP; import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
@ -51,7 +52,7 @@ import static org.junit.Assert.assertTrue;
public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionTest { public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionTotpSetupTest() { public AppInitiatedActionTotpSetupTest() {
super("configure_totp"); super(UserModel.RequiredAction.CONFIGURE_TOTP.name());
} }
@Override @Override
@ -64,7 +65,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
for (AuthenticationExecutionInfoRepresentation execution : adminClient.realm("test").flows().getExecutions("browser")) { for (AuthenticationExecutionInfoRepresentation execution : adminClient.realm("test").flows().getExecutions("browser")) {
String providerId = execution.getProviderId(); String providerId = execution.getProviderId();
if ("auth-otp-form".equals(providerId)) { 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); adminClient.realm("test").flows().updateExecutions("browser", execution);
} }
} }
@ -111,8 +112,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
events.poll(); // skip to totp event events.poll(); // skip to totp event
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent() String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
.getDetails().get(Details.CODE_ID); .getDetails().get(Details.CODE_ID);
assertRedirectSuccess(); assertKcActionStatus("success");
events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent(); events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent();
} }
@ -125,9 +126,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
totpPage.assertCurrent(); totpPage.assertCurrent();
totpPage.cancel(); totpPage.cancel();
assertRedirectSuccess(); assertKcActionStatus("cancelled");
assertCancelMessage();
} }
@Test @Test
@ -297,8 +297,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
.getDetails().get(Details.CODE_ID); .getDetails().get(Details.CODE_ID);
assertRedirectSuccess(); assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent(); EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
oauth.openLogout(); oauth.openLogout();
@ -310,8 +310,6 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
loginTotpPage.login(totp.generateTOTP(totpSecret)); loginTotpPage.login(totp.generateTOTP(totpSecret));
assertRedirectSuccess();
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
} }
@ -333,7 +331,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
totpPage.configure(totp.generateTOTP(totpCode)); totpPage.configure(totp.generateTOTP(totpCode));
// After totp config, user should be on the app page // After totp config, user should be on the app page
assertRedirectSuccess(); assertKcActionStatus("success");
events.poll(); events.poll();
events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
@ -375,17 +373,6 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
// Try to login // Try to login
loginPage.open(); loginPage.open();
loginPage.login("setupTotp2", "password2"); 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 @Test
@ -416,7 +403,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
.getDetails().get(Details.CODE_ID); .getDetails().get(Details.CODE_ID);
assertRedirectSuccess(); assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
@ -431,7 +418,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
assertEquals(8, token.length()); assertEquals(8, token.length());
loginTotpPage.login(token); loginTotpPage.login(token);
assertRedirectSuccess(); assertKcActionStatus(null);
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
@ -469,7 +456,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
.getDetails().get(Details.CODE_ID); .getDetails().get(Details.CODE_ID);
//RequestType reqType = appPage.getRequestType(); //RequestType reqType = appPage.getRequestType();
assertRedirectSuccess(); assertKcActionStatus("success");
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
oauth.openLogout(); oauth.openLogout();
@ -481,7 +468,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String token = otpgen.generateHOTP(totpSecret, 1); String token = otpgen.generateHOTP(totpSecret, 1);
loginTotpPage.login(token); loginTotpPage.login(token);
assertRedirectSuccess(); assertKcActionStatus(null);
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
@ -506,7 +493,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.login(token); loginTotpPage.login(token);
assertRedirectSuccess(); assertKcActionStatus(null);
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();

View file

@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
@ -37,7 +38,7 @@ import org.keycloak.testsuite.util.UserBuilder;
public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedActionTest { public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionUpdateProfileTest() { public AppInitiatedActionUpdateProfileTest() {
super("update_profile"); super(UserModel.RequiredAction.UPDATE_PROFILE.name());
} }
@Page @Page
@ -85,7 +86,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
assertRedirectSuccess(); assertKcActionStatus("success");
// assert user is really updated in persistent store // assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); 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_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
assertRedirectSuccess(); assertKcActionStatus("success");
// assert user is really updated in persistent store // assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -132,8 +133,8 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.cancel(); updateProfilePage.cancel();
assertRedirectSuccess(); assertKcActionStatus("cancelled");
assertCancelMessage();
// assert nothing was updated in persistent store // assert nothing was updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -164,7 +165,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
.removeDetail(Details.CONSENT) .removeDetail(Details.CONSENT)
.assertEvent(); .assertEvent();
assertRedirectSuccess(); assertKcActionStatus("success");
events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).assertEvent(); 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.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.GreenMailRule; 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> * @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"); loginPage.login("test-user@localhost", "password");
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
assertFalse(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password"); changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); 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(); String userId = events.expectRegister("setupTotp", "email@mail.com").assertEvent().getUserId();
assertTrue(totpPage.isCurrent()); assertTrue(totpPage.isCurrent());
assertFalse(totpPage.isCancelDisplayed());
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); 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.pages.LoginUpdateProfileEditUsernameAllowedPage;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import static org.junit.Assert.assertFalse;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @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"); loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
assertFalse(updateProfilePage.isCancelDisplayed());
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");