From f83756b177c3abc2381b5e247015fe8b982f8eba Mon Sep 17 00:00:00 2001 From: Lex Cao Date: Sat, 13 Jan 2024 23:47:50 +0800 Subject: [PATCH] Error handle for the Json request in createErrorPage Closes #13368 These changes introduce a new error handler for building error based on the media type. - It should create error form response when it is valid HTML request - It could create error response with JSON if content type matches Signed-off-by: Lex Cao --- .../UserSessionLimitsAuthenticator.java | 9 +- .../FreeMarkerLoginFormsProvider.java | 138 ++++++++++-------- .../oidc/endpoints/TokenEndpoint.java | 1 + .../org/keycloak/utils/MediaTypeMatcher.java | 14 +- .../keycloak/testsuite/util/OAuthClient.java | 3 + .../forms/AllowDenyAuthenticatorTest.java | 45 ++++++ .../testsuite/forms/DirectGrantFlowTest.java | 23 +++ .../sessionlimits/UserSessionLimitsTest.java | 10 +- 8 files changed, 167 insertions(+), 76 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java index 2544b4c910..df86cc630c 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java @@ -152,14 +152,7 @@ public class UserSessionLimitsAuthenticator implements Authenticator { .orElse(SESSION_LIMIT_EXCEEDED); context.getEvent().error(Errors.GENERIC_AUTHENTICATION_ERROR); - Response challenge = null; - if(context.getFlowPath() == null) { - OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(Errors.GENERIC_AUTHENTICATION_ERROR, errorMessage); - challenge = Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build(); - } - else { - challenge = context.form().setError(errorMessage).createErrorPage(Response.Status.FORBIDDEN); - } + Response challenge = context.form().setError(errorMessage).createErrorPage(Response.Status.FORBIDDEN); context.failure(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, challenge, eventDetails, errorMessage); break; diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index 2a1e742a5d..c11d41dea3 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -16,19 +16,6 @@ */ package org.keycloak.forms.login.freemarker; -import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD; - -import java.io.IOException; -import java.net.URI; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; @@ -45,21 +32,22 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.common.util.ObjectUtil; import org.keycloak.forms.login.LoginFormsPages; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.forms.login.MessageType; import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean; import org.keycloak.forms.login.freemarker.model.AuthenticationSessionBean; -import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodeInputLoginBean; -import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodesBean; import org.keycloak.forms.login.freemarker.model.ClientBean; import org.keycloak.forms.login.freemarker.model.CodeBean; import org.keycloak.forms.login.freemarker.model.EmailBean; +import org.keycloak.forms.login.freemarker.model.FrontChannelLogoutBean; import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean; import org.keycloak.forms.login.freemarker.model.LoginBean; -import org.keycloak.forms.login.freemarker.model.FrontChannelLogoutBean; import org.keycloak.forms.login.freemarker.model.LogoutConfirmBean; import org.keycloak.forms.login.freemarker.model.OAuthGrantBean; import org.keycloak.forms.login.freemarker.model.ProfileBean; import org.keycloak.forms.login.freemarker.model.RealmBean; +import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodeInputLoginBean; +import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodesBean; import org.keycloak.forms.login.freemarker.model.RegisterBean; import org.keycloak.forms.login.freemarker.model.RequiredActionUrlFormatterMethod; import org.keycloak.forms.login.freemarker.model.SAMLPostFormBean; @@ -77,6 +65,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.rar.AuthorizationDetails; +import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; @@ -87,11 +76,25 @@ import org.keycloak.theme.beans.AdvancedMessageFormatterMethod; import org.keycloak.theme.beans.LocaleBean; import org.keycloak.theme.beans.MessageBean; import org.keycloak.theme.beans.MessageFormatterMethod; -import org.keycloak.forms.login.MessageType; import org.keycloak.theme.beans.MessagesPerFieldBean; import org.keycloak.theme.freemarker.FreeMarkerProvider; import org.keycloak.userprofile.UserProfileContext; import org.keycloak.utils.MediaType; +import org.keycloak.utils.MediaTypeMatcher; + +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD; /** * @author Stian Thorgersen @@ -115,7 +118,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { protected boolean detachedAuthSession = false; protected KeycloakSession session; - /** authenticationSession can be null for some renderings, mainly error pages */ + /** + * authenticationSession can be null for some renderings, mainly error pages + */ protected AuthenticationSessionModel authenticationSession; protected RealmModel realm; protected ClientModel client; @@ -164,8 +169,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { page = LoginFormsPages.LOGIN_UPDATE_PROFILE; break; case UPDATE_EMAIL: - UpdateProfileContext updateEmailContext = new UserUpdateProfileContext(realm,user); - attributes.put("user",new ProfileBean(updateEmailContext,formData)); + UpdateProfileContext updateEmailContext = new UserUpdateProfileContext(realm, user); + attributes.put("user", new ProfileBean(updateEmailContext, formData)); actionMessage = Messages.UPDATE_EMAIL; page = LoginFormsPages.UPDATE_EMAIL; break; @@ -175,8 +180,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { page = LoginFormsPages.LOGIN_UPDATE_PASSWORD; break; case VERIFY_EMAIL: - UpdateProfileContext userBasedContext1 = new UserUpdateProfileContext(realm,user); - attributes.put("user",new ProfileBean(userBasedContext1,formData)); + UpdateProfileContext userBasedContext1 = new UserUpdateProfileContext(realm, user); + attributes.put("user", new ProfileBean(userBasedContext1, formData)); actionMessage = Messages.VERIFY_EMAIL; page = LoginFormsPages.LOGIN_VERIFY_EMAIL; break; @@ -225,8 +230,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { if (!isDetachedAuthenticationSession()) { if ((AuthenticationSessionModel.Action.AUTHENTICATE.name().equals(authenticationSession.getAction())) || - (AuthenticationSessionModel.Action.REQUIRED_ACTIONS.name().equals(authenticationSession.getAction())) || - (AuthenticationSessionModel.Action.OAUTH_GRANT.name().equals(authenticationSession.getAction()))) { + (AuthenticationSessionModel.Action.REQUIRED_ACTIONS.name().equals(authenticationSession.getAction())) || + (AuthenticationSessionModel.Action.OAUTH_GRANT.name().equals(authenticationSession.getAction()))) { setAttribute("authenticationSession", new AuthenticationSessionBean(authenticationSession.getParentSession().getId(), authenticationSession.getTabId())); } } @@ -273,7 +278,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { attributes.put("configuredOtpCredentials", new TotpLoginBean(session, realm, user, (String) this.attributes.get(OTPFormAuthenticator.SELECTED_OTP_CREDENTIAL_ID))); break; case REGISTER: - RegisterBean rb = new RegisterBean(formData,session); + RegisterBean rb = new RegisterBean(formData, session); //legacy bean for static template attributes.put("register", rb); //bean for dynamic template @@ -308,7 +313,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return processTemplate(theme, Templates.getTemplate(page), locale); } - + /** * Get sure that correct hostname and path is used for totp form. * Relevant when running in proxy mode. @@ -344,9 +349,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { /** * Prepare base uri builder for later use - * + * * @param resetRequestUriParams - for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param, so we have to reset them for some pages - * @return base uri builder + * @return base uri builder */ protected UriBuilder prepareBaseUriBuilder(boolean resetRequestUriParams) { String requestURI = uriInfo.getBaseUri().getPath(); @@ -366,7 +371,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { /** * Get Theme used for page rendering. - * + * * @return theme for page rendering, never null * @throws IOException in case of Theme loading problem */ @@ -376,8 +381,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { /** * Load message bundle and place it into msg template attribute. Also load Theme properties and place them into properties template attribute. - * - * @param theme actual Theme to load bundle from + * + * @param theme actual Theme to load bundle from * @param locale to load bundle for * @return message bundle for other use */ @@ -403,8 +408,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { /** * Handle messages to be shown on the page - set them to template attributes - * - * @param locale to be used for message text loading + * + * @param locale to be used for message text loading * @param messagesBundle to be used for message text loading * @see #messageType * @see #messages @@ -429,29 +434,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { @Override public String getMessage(String message) { - Theme theme; - try { - theme = getTheme(); - } catch (IOException e) { - logger.error("Failed to create theme", e); - throw new RuntimeException("Failed to create theme"); - } - - Locale locale = session.getContext().resolveLocale(user); - Properties messagesBundle = handleThemeResources(theme, locale); - FormMessage msg = new FormMessage(null, message); - return formatMessage(msg, messagesBundle, locale); + return formatMessage(new FormMessage(null, message)); } /** * Create common attributes used in all templates. - * - * @param theme actual Theme used (provided by getTheme()) - * @param locale actual locale + * + * @param theme actual Theme used (provided by getTheme()) + * @param locale actual locale * @param messagesBundle actual message bundle (provided by handleThemeResources()) * @param baseUriBuilder actual base uri builder (provided by prepareBaseUriBuilder()) - * @param page in case if common page is rendered, is null if called from createForm() - * + * @param page in case if common page is rendered, is null if called from createForm() */ protected void createCommonAttributes(Theme theme, Locale locale, Properties messagesBundle, UriBuilder baseUriBuilder, LoginFormsPages page) { URI baseUri = baseUriBuilder.build(); @@ -538,10 +531,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { /** * Process FreeMarker template and prepare Response. Some fields are used for rendering also. - * - * @param theme to be used (provided by getTheme()) + * + * @param theme to be used (provided by getTheme()) * @param templateName name of the template to be rendered - * @param locale to be used + * @param locale to be used * @return Response object to be returned to the browser, never null */ protected Response processTemplate(Theme theme, String templateName, Locale locale) { @@ -563,11 +556,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return createResponse(LoginFormsPages.LOGIN); } - public Response createLoginUsername(){ + public Response createLoginUsername() { return createResponse(LoginFormsPages.LOGIN_USERNAME); - }; + } - public Response createLoginPassword(){ + ; + + public Response createLoginPassword() { return createResponse(LoginFormsPages.LOGIN_PASSWORD); } @@ -580,7 +575,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD); } - @Override public Response createOtpReset() { + @Override + public Response createOtpReset() { return createResponse(LoginFormsPages.LOGIN_RESET_OTP); } @@ -606,7 +602,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { if (this.formData == null) { this.formData = new MultivaluedHashMap<>(); } - if(this.realm.isRegistrationEmailAsUsername()) { + if (this.realm.isRegistrationEmailAsUsername()) { String value = this.formData.getFirst("email"); if (value == null || value.trim().isEmpty()) { this.formData.putSingle("email", loginHint); @@ -670,6 +666,16 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { @Override public Response createErrorPage(Response.Status status) { + if (MediaTypeMatcher.isJsonRequest(session.getContext().getRequestHeaders())) { + OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(); + errorRep.setError(formatMessage(getFirstMessage())); + + return Response.status(status) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorRep) + .build(); + } + this.status = status; return createResponse(LoginFormsPages.ERROR); } @@ -738,6 +744,20 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return formMessage == null ? null : formMessage.getMessage(); } + protected String formatMessage(FormMessage message) { + Theme theme; + try { + theme = getTheme(); + } catch (IOException e) { + logger.error("Failed to create theme", e); + throw new RuntimeException("Failed to create theme"); + } + + Locale locale = session.getContext().resolveLocale(user); + Properties messagesBundle = handleThemeResources(theme, locale); + return formatMessage(message, messagesBundle, locale); + } + protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) { if (message == null) return null; @@ -875,7 +895,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } - public LoginFormsProvider setAuthContext(AuthenticationFlowContext context){ + public LoginFormsProvider setAuthContext(AuthenticationFlowContext context) { this.context = context; return this; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index a733c43472..f43d2934e0 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -664,6 +664,7 @@ public class TokenEndpoint { AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setAuthenticationSession(authSession) .setFlowId(flowId) + .setFlowPath("token") .setConnection(clientConnection) .setEventBuilder(event) .setRealm(realm) diff --git a/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java b/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java index c76c93b481..6b1e0a3b2d 100644 --- a/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java +++ b/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java @@ -1,16 +1,24 @@ package org.keycloak.utils; import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; public class MediaTypeMatcher { public static boolean isHtmlRequest(HttpHeaders headers) { - for (jakarta.ws.rs.core.MediaType m : headers.getAcceptableMediaTypes()) { - if (!m.isWildcardType() && m.isCompatible(jakarta.ws.rs.core.MediaType.TEXT_HTML_TYPE)) { + return isAcceptMediaType(headers, MediaType.TEXT_HTML_TYPE); + } + + public static boolean isJsonRequest(HttpHeaders headers) { + return isAcceptMediaType(headers, MediaType.APPLICATION_JSON_TYPE); + } + + private static boolean isAcceptMediaType(HttpHeaders headers, MediaType textHtmlType) { + for (MediaType m : headers.getAcceptableMediaTypes()) { + if (!m.isWildcardType() && m.isCompatible(textHtmlType)) { return true; } } return false; } - } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 0f25d188da..bc8a73a838 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -76,6 +76,7 @@ import org.keycloak.testsuite.runonserver.RunOnServerException; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; +import org.keycloak.utils.MediaType; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -604,6 +605,8 @@ public class OAuthClient { try (CloseableHttpClient client = httpClient.get()) { HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm)); + post.addHeader("Accept", MediaType.APPLICATION_JSON); + if (requestHeaders != null) { for (Map.Entry header : requestHeaders.entrySet()) { post.addHeader(header.getKey(), header.getValue()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AllowDenyAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AllowDenyAuthenticatorTest.java index 1691d5328b..cd52101e87 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AllowDenyAuthenticatorTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AllowDenyAuthenticatorTest.java @@ -8,6 +8,8 @@ import org.keycloak.authentication.authenticators.access.DenyAccessAuthenticator import org.keycloak.authentication.authenticators.browser.PasswordFormFactory; import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; import org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthenticatorFactory; +import org.keycloak.authentication.authenticators.directgrant.ValidatePassword; +import org.keycloak.authentication.authenticators.directgrant.ValidateUsername; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.models.AuthenticationExecutionModel; @@ -19,12 +21,15 @@ import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginUsernameOnlyPage; import org.keycloak.testsuite.pages.PasswordPage; import org.keycloak.testsuite.util.FlowUtil; +import org.keycloak.testsuite.util.OAuthClient; import java.util.HashMap; import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.keycloak.testsuite.forms.BrowserFlowTest.revertFlows; /** @@ -329,6 +334,25 @@ public class AllowDenyAuthenticatorTest extends AbstractTestRealmKeycloakTest { } } + @Test + public void testDenyAccessInDirectGrantFlow() throws Exception { + String flowAlias = "direct grant - deny defaultMessage"; + String user = "test-user@localhost"; + String clientId = "direct-grant"; + + configureDirectGrantFlowWithDenyAccess(flowAlias, new HashMap<>()); + + try { + oauth.clientId(clientId); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", user, "password"); + assertEquals(401, response.getStatusCode()); + assertEquals("Access denied", response.getError()); + assertNull(response.getErrorDescription()); + } finally { + DirectGrantFlowTest.revertFlows(testRealm(), flowAlias); + } + } + /** * This flow contains: * UsernameForm REQUIRED @@ -412,4 +436,25 @@ public class AllowDenyAuthenticatorTest extends AbstractTestRealmKeycloakTest { .defineAsBrowserFlow() // Activate this new flow ); } + + /** + * This flow contains: + * Validate Username REQUIRED + * Validate Password REQUIRED + * Deny Access REQUIRED + * + * @param newFlowAlias + * @param denyConfig + */ + private void configureDirectGrantFlowWithDenyAccess(String newFlowAlias, Map denyConfig) { + testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyDirectGrantFlow(newFlowAlias)); + testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session) + .selectFlow(newFlowAlias) + .clear() + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ValidateUsername.PROVIDER_ID) + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ValidatePassword.PROVIDER_ID) + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, DenyAccessAuthenticatorFactory.PROVIDER_ID, config -> config.setConfig(denyConfig)) + .defineAsDirectGrantFlow() // Activate this new flow + ); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/DirectGrantFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/DirectGrantFlowTest.java index a36a91262b..99e72ee071 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/DirectGrantFlowTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/DirectGrantFlowTest.java @@ -22,17 +22,23 @@ import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.Rule; import org.junit.Test; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory; import org.keycloak.authentication.authenticators.directgrant.ValidatePassword; import org.keycloak.authentication.authenticators.directgrant.ValidateUsername; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest; import org.keycloak.testsuite.util.FlowUtil; import org.keycloak.testsuite.util.OAuthClient; import org.openqa.selenium.WebDriver; +import java.util.List; + import static org.junit.Assert.assertEquals; /** @@ -85,5 +91,22 @@ public class DirectGrantFlowTest extends AbstractTestRealmKeycloakTest { assertEquals("Account is not fully set up", response.getErrorDescription()); } + public static void revertFlows(RealmResource realmResource, String flowToDeleteAlias) { + List flows = realmResource.flows().getFlows(); + // Set default direct grant flow + RealmRepresentation realm = realmResource.toRepresentation(); + realm.setDirectGrantFlow(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW); + realmResource.update(realm); + + AuthenticationFlowRepresentation flowRepresentation = AbstractAuthenticationTest.findFlowByAlias(flowToDeleteAlias, flows); + + // Throw error if flow doesn't exist to ensure we did not accidentally use different alias of non-existing flow when + // calling this method + if (flowRepresentation == null) { + throw new IllegalArgumentException("The flow with alias " + flowToDeleteAlias + " did not exist"); + } + + realmResource.flows().deleteFlow(flowRepresentation.getId()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java index 3a666d4c4c..c851c0e32f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java @@ -211,9 +211,8 @@ public class UserSessionLimitsTest extends AbstractTestRealmKeycloakTest { assertEquals(200, response.getStatusCode()); response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); - assertEquals(401, response.getStatusCode()); - assertEquals(Errors.GENERIC_AUTHENTICATION_ERROR, response.getError()); - assertEquals(ERROR_TO_DISPLAY, response.getErrorDescription()); + assertEquals(403, response.getStatusCode()); + assertEquals(ERROR_TO_DISPLAY, response.getError()); } @Test @@ -247,9 +246,8 @@ public class UserSessionLimitsTest extends AbstractTestRealmKeycloakTest { assertEquals(200, response.getStatusCode()); response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); - assertEquals(401, response.getStatusCode()); - assertEquals(Errors.GENERIC_AUTHENTICATION_ERROR, response.getError()); - assertEquals(ERROR_TO_DISPLAY, response.getErrorDescription()); + assertEquals(403, response.getStatusCode()); + assertEquals(ERROR_TO_DISPLAY, response.getError()); } finally { setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.USER_REALM_LIMIT, "0"); setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.USER_CLIENT_LIMIT, "1");