From 335a10feadb087e7bee53fad1c3b86f4c2d6918c Mon Sep 17 00:00:00 2001 From: Marek Posolda Date: Thu, 4 Apr 2024 10:41:03 +0200 Subject: [PATCH] Handle 'You are already logged in' for expired authentication sessions (#27793) closes #24112 Signed-off-by: mposolda --- js/libs/keycloak-js/src/keycloak.js | 14 +- .../saml/SAML2ErrorResponseBuilder.java | 11 +- .../saml/v2/writers/SAMLResponseWriter.java | 1 + .../broker/provider/IdentityProvider.java | 16 + .../provider/util/IdentityBrokerState.java | 33 ++- .../java/org/keycloak/events/Details.java | 2 + .../main/java/org/keycloak/events/Errors.java | 1 + .../java/org/keycloak/models/Constants.java | 4 + .../org/keycloak/protocol/ClientData.java | 120 ++++++++ .../org/keycloak/protocol/LoginProtocol.java | 32 ++ .../util/IdentityBrokerStateTest.java | 45 ++- .../AuthenticationProcessor.java | 26 +- .../FormAuthenticationFlow.java | 1 + .../RequiredActionContextResult.java | 1 + .../actiontoken/ActionTokenContext.java | 8 +- .../ExecuteActionsActionTokenHandler.java | 3 +- ...dpVerifyAccountLinkActionTokenHandler.java | 2 +- .../VerifyEmailActionTokenHandler.java | 3 +- .../IdpEmailVerificationAuthenticator.java | 3 +- .../IdentityProviderAuthenticator.java | 3 +- .../requiredactions/UpdateEmail.java | 3 +- .../requiredactions/VerifyEmail.java | 2 +- .../oidc/AbstractOAuth2IdentityProvider.java | 6 +- .../oidc/KeycloakOIDCIdentityProvider.java | 1 + .../keycloak/broker/saml/SAMLEndpoint.java | 7 +- .../broker/saml/SAMLIdentityProvider.java | 6 + .../FreeMarkerLoginFormsProvider.java | 6 + .../forms/login/freemarker/model/UrlBean.java | 4 - .../protocol/docker/DockerAuthV2Protocol.java | 11 + .../protocol/oidc/OIDCLoginProtocol.java | 78 +++-- .../request/AuthorizationEndpointRequest.java | 9 + .../keycloak/protocol/saml/SamlProtocol.java | 58 +++- .../main/java/org/keycloak/services/Urls.java | 27 +- .../managers/AuthenticationManager.java | 1 + .../resources/IdentityBrokerService.java | 59 +++- .../resources/LoginActionsService.java | 91 +++--- .../resources/LogoutSessionCodeChecks.java | 4 +- .../services/resources/SessionCodeChecks.java | 58 +++- .../util/AuthenticationFlowURLHelper.java | 6 +- .../updaters/RealmAttributeUpdater.java | 5 + .../broker/AbstractFirstBrokerLoginTest.java | 15 +- .../broker/KcOidcMultipleTabsBrokerTest.java | 278 ++++++++++++++++++ .../broker/KcSamlBrokerConfiguration.java | 1 + .../broker/KcSamlBrokerDestinationTest.java | 13 +- .../broker/KcSamlBrokerFrontendUrlTest.java | 10 +- .../broker/KcSamlMultipleTabsBrokerTest.java | 217 ++++++++++++++ .../keycloak/testsuite/forms/LoginTest.java | 4 +- .../forms/MultipleTabsLoginTest.java | 248 +++++++++++++--- .../testsuite/forms/RestartCookieTest.java | 2 - 49 files changed, 1349 insertions(+), 210 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/protocol/ClientData.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcMultipleTabsBrokerTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlMultipleTabsBrokerTest.java diff --git a/js/libs/keycloak-js/src/keycloak.js b/js/libs/keycloak-js/src/keycloak.js index 35ff2d24de..ed10b1fe8d 100755 --- a/js/libs/keycloak-js/src/keycloak.js +++ b/js/libs/keycloak-js/src/keycloak.js @@ -408,7 +408,8 @@ function Keycloak (config) { var callbackState = { state: state, nonce: nonce, - redirectUri: encodeURIComponent(redirectUri) + redirectUri: encodeURIComponent(redirectUri), + loginOptions: options }; if (options && options.prompt) { @@ -752,9 +753,13 @@ function Keycloak (config) { if (error) { if (prompt != 'none') { - var errorData = { error: error, error_description: oauth.error_description }; - kc.onAuthError && kc.onAuthError(errorData); - promise && promise.setError(errorData); + if (oauth.error_description && oauth.error_description === "authentication_expired") { + kc.login(oauth.loginOptions); + } else { + var errorData = { error: error, error_description: oauth.error_description }; + kc.onAuthError && kc.onAuthError(errorData); + promise && promise.setError(errorData); + } } else { promise && promise.setSuccess(); } @@ -1062,6 +1067,7 @@ function Keycloak (config) { oauth.storedNonce = oauthState.nonce; oauth.prompt = oauthState.prompt; oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier; + oauth.loginOptions = oauthState.loginOptions; } return oauth; diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java index b42a42025a..f9844d61ff 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java +++ b/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java @@ -23,6 +23,7 @@ import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.protocol.ExtensionsType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusType; import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; @@ -39,6 +40,7 @@ import org.w3c.dom.Document; public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder { protected String status; + protected String statusMessage; protected String destination; protected NameIDType issuer; protected final List extensions = new LinkedList<>(); @@ -48,6 +50,11 @@ public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBui return this; } + public SAML2ErrorResponseBuilder statusMessage(String statusMessage) { + this.statusMessage = statusMessage; + return this; + } + public SAML2ErrorResponseBuilder destination(String destination) { this.destination = destination; return this; @@ -73,7 +80,9 @@ public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBui try { StatusResponseType statusResponse = new ResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant()); - statusResponse.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status)); + StatusType statusType = JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status); + statusType.setStatusMessage(statusMessage); + statusResponse.setStatus(statusType); statusResponse.setIssuer(issuer); statusResponse.setDestination(destination); diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java index 867eb28a71..5cab796d31 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java @@ -213,6 +213,7 @@ public class SAMLResponseWriter extends BaseWriter { String statusMessage = status.getStatusMessage(); if (StringUtil.isNotNull(statusMessage)) { StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.STATUS_MESSAGE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get()); + StaxUtil.writeCharacters(writer, statusMessage); StaxUtil.writeEndElement(writer); } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index e80750f9d6..a9b462e436 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -69,6 +69,15 @@ public interface IdentityProvider extends Provi */ Response cancelled(IdentityProviderModel idpConfig); + /** + * Indicates that login with the particular IDP should be retried + * + * @param identityProvider provider to retry login + * @param authSession authentication session + * @return see description + */ + Response retryLogin(IdentityProvider identityProvider, AuthenticationSessionModel authSession); + /** * Called when error happened on the IDP side. * Assumption is that authenticationSession is set in the {@link org.keycloak.models.KeycloakContext} when this method is called @@ -155,4 +164,11 @@ public interface IdentityProvider extends Provi default boolean reloadKeys() { return false; } + + /** + * @return true if identity provider supports long value of "state" parameter (or "RelayState" parameter), which can hold relatively big amount of context data + */ + default boolean supportsLongStateParameter() { + return true; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java index 524b45b48d..636da56a98 100644 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java @@ -17,13 +17,12 @@ package org.keycloak.broker.provider.util; -import org.keycloak.authorization.policy.evaluation.Realm; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; import org.keycloak.common.util.Base64Url; -import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.regex.Pattern; @@ -38,9 +37,10 @@ public class IdentityBrokerState { private static final Pattern DOT = Pattern.compile("\\."); - public static IdentityBrokerState decoded(String state, String clientId, String clientClientId, String tabId) { + public static IdentityBrokerState decoded(String state, String clientId, String clientClientId, String tabId, String clientData) { String clientIdEncoded = clientClientId; // Default use the client.clientId + boolean isUuid = false; if (clientId != null) { // According to (http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) there is a limit on the relaystate of 80 bytes. // in order to try to adher to the SAML specification we use an encoded value of the client.id (probably UUID) instead of the with @@ -52,22 +52,31 @@ public class IdentityBrokerState { bb.putLong(clientDbUuid.getLeastSignificantBits()); byte[] clientUuidBytes = bb.array(); clientIdEncoded = Base64Url.encode(clientUuidBytes); + isUuid = true; } catch (RuntimeException e) { // Ignore...the clientid in the database was not in UUID format. Just use as is. } } + if (!isUuid && clientIdEncoded != null) { + clientIdEncoded = Base64Url.encode(clientIdEncoded.getBytes(StandardCharsets.UTF_8)); + } String encodedState = state + "." + tabId + "." + clientIdEncoded; + if (clientData != null) { + encodedState = encodedState + "." + clientData; + } - return new IdentityBrokerState(state, clientClientId, tabId, encodedState); + return new IdentityBrokerState(state, clientClientId, tabId, clientData, encodedState); } public static IdentityBrokerState encoded(String encodedState, RealmModel realmModel) { - String[] decoded = DOT.split(encodedState, 3); + String[] decoded = DOT.split(encodedState, 4); String state =(decoded.length > 0) ? decoded[0] : null; String tabId = (decoded.length > 1) ? decoded[1] : null; String clientId = (decoded.length > 2) ? decoded[2] : null; + String clientData = (decoded.length > 3) ? decoded[3] : null; + boolean isUuid = false; if (clientId != null) { try { @@ -82,13 +91,17 @@ public class IdentityBrokerState { ClientModel clientModel = realmModel.getClientById(clientIdInDb); if (clientModel != null) { clientId = clientModel.getClientId(); + isUuid = true; } } catch (RuntimeException e) { // Ignore...the clientid was not in encoded uuid format. Just use as it is. } + if (!isUuid) { + clientId = new String(Base64Url.decode(clientId), StandardCharsets.UTF_8); + } } - return new IdentityBrokerState(state, clientId, tabId, encodedState); + return new IdentityBrokerState(state, clientId, tabId, clientData, encodedState); } @@ -96,14 +109,16 @@ public class IdentityBrokerState { private final String decodedState; private final String clientId; private final String tabId; + private final String clientData; // Encoded form of whole state private final String encoded; - private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String encoded) { + private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String clientData, String encoded) { this.decodedState = decodedStateParam; this.clientId = clientId; this.tabId = tabId; + this.clientData = clientData; this.encoded = encoded; } @@ -120,6 +135,10 @@ public class IdentityBrokerState { return tabId; } + public String getClientData() { + return clientData; + } + public String getEncoded() { return encoded; } diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java index 728f2dbd50..42e8f00e03 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Details.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java @@ -63,6 +63,8 @@ public interface Details { String REQUESTED_ISSUER = "requested_issuer"; String REQUESTED_SUBJECT = "requested_subject"; String RESTART_AFTER_TIMEOUT = "restart_after_timeout"; + String REDIRECTED_TO_CLIENT = "redirected_to_client"; + String LOGIN_RETRY = "login_retry"; String CONSENT = "consent"; String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java index f0d513e564..3f57742787 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java @@ -67,6 +67,7 @@ public interface Errors { String EXPIRED_CODE = "expired_code"; String INVALID_INPUT = "invalid_input"; String COOKIE_NOT_FOUND = "cookie_not_found"; + String ALREADY_LOGGED_IN = "already_logged_in"; String TOKEN_INTROSPECTION_FAILED = "token_introspection_failed"; diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index 4c5d535510..ee338e51ac 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -80,6 +80,7 @@ public final class Constants { public static final String EXECUTION = "execution"; public static final String CLIENT_ID = "client_id"; public static final String TAB_ID = "tab_id"; + public static final String CLIENT_DATA = "client_data"; public static final String SKIP_LOGOUT = "skip_logout"; public static final String KEY = "key"; @@ -164,4 +165,7 @@ public final class Constants { public static final String USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled"; public static final String TOTP_SECRET_KEY = "TOTP_SECRET_KEY"; + + // Sent to clients when authentication session expired, but user is already logged-in in current browser + public static final String AUTHENTICATION_EXPIRED_MESSAGE = "authentication_expired"; } diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/ClientData.java b/server-spi-private/src/main/java/org/keycloak/protocol/ClientData.java new file mode 100644 index 0000000000..8a6eb0edb3 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/protocol/ClientData.java @@ -0,0 +1,120 @@ +/* + * Copyright 2024 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.protocol; + +import java.io.IOException; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.ObjectUtil; +import org.keycloak.util.JsonSerialization; + +/** + * Encapsulates necessary data about client login request (OIDC or SAML request). Can be useful for cases when authenticationSession + * expired and we need to redirect back to the client with the error due to this. + * + * @author Marek Posolda + */ +public class ClientData { + + protected static final Logger logger = Logger.getLogger(ClientData.class); + + @JsonProperty("ru") + private String redirectUri; + + @JsonProperty("rt") + private String responseType; + + @JsonProperty("rm") + private String responseMode; + + @JsonProperty("st") + private String state; + + public ClientData() { + } + + public ClientData(String redirectUri, String responseType, String responseMode, String state) { + this.redirectUri = redirectUri; + this.responseType = responseType; + this.responseMode = responseMode; + this.state = state; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getResponseType() { + return responseType; + } + + public void setResponseType(String responseType) { + this.responseType = responseType; + } + + public String getResponseMode() { + return responseMode; + } + + public void setResponseMode(String responseMode) { + this.responseMode = responseMode; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @Override + public String toString() { + return String.format("ClientData [ redirectUri=%s, responseType=%s, responseMode=%s, state=%s ]", redirectUri, responseType, responseMode, state); + } + + public static ClientData decodeClientDataFromParameter(String clientDataParam) { + try { + if (ObjectUtil.isBlank(clientDataParam)) { + return null; + } else { + byte[] cdataJson = Base64Url.decode(clientDataParam); + return JsonSerialization.readValue(cdataJson, ClientData.class); + } + } catch (IOException ioe) { + logger.warnf("ClientData parameter in invalid format. ClientData parameter was %s", clientDataParam); + return null; + } + } + + public String encode() { + try { + return Base64Url.encode(JsonSerialization.writeValueAsBytes(this)); + } catch (IOException ioe) { + throw new RuntimeException("Not possible to serialize clientData"); + } + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java index d46adf7b64..4f5b650380 100755 --- a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java +++ b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java @@ -51,6 +51,12 @@ public interface LoginProtocol extends Provider { * Applications-initiated action was canceled by the user. Do not send error. */ CANCELLED_AIA_SILENT, + /** + * User is already logged-in and he has userSession in this browser. But authenticationSession is not valid anymore and hence could not continue authentication + * in proper way. Will need to redirect back to client, so client can retry authentication. Once client retries authentication, it will usually success automatically + * due SSO reauthentication. + */ + ALREADY_LOGGED_IN, /** * Consent denied by the user */ @@ -80,6 +86,32 @@ public interface LoginProtocol extends Provider { Response sendError(AuthenticationSessionModel authSession, Error error); + /** + * Returns client data, which will be wrapped in the "clientData" parameter sent within "authentication flow" requests. The purpose of clientData is to be able to send HTTP error + * response back to the client if authentication fails due some error and authenticationSession is not available anymore (was either expired or removed). So clientData need to contain + * all the data to be able to send such response. For instance redirect-uri, state in case of OIDC or RelayState in case of SAML etc. + * + * @param authSession session from which particular clientData can be retrieved + * @return client data, which will be wrapped in the "clientData" parameter sent within "authentication flow" requests + */ + ClientData getClientData(AuthenticationSessionModel authSession); + + /** + * Send the specified error to the specified client with the use of this protocol. ClientData can contain additional metadata about how to send error response to the + * client in a correct way for particular protocol. For instance redirect-uri where to send error, state to be used in OIDC authorization endpoint response etc. + * + * This method is usually used when we don't have authenticationSession anymore (it was removed or expired) as otherwise it is recommended to use {@link #sendError(AuthenticationSessionModel, Error)} + * + * NOTE: This method should also validate if provided clientData are valid according to given client (for instance if redirect-uri is valid) as clientData is request parameter, which + * can be injected to HTTP URLs by anyone. + * + * @param client client where to send error + * @param clientData clientData with additional protocol specific metadata needed for being able to properly send error with the use of this protocol + * @param error error to be used + * @return response if error was sent. Null if error was not sent. + */ + Response sendError(ClientModel client, ClientData clientData, Error error); + Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); diff --git a/server-spi-private/src/test/java/org/keycloak/broker/provider/util/IdentityBrokerStateTest.java b/server-spi-private/src/test/java/org/keycloak/broker/provider/util/IdentityBrokerStateTest.java index 282c4edd76..3bbf1065cf 100644 --- a/server-spi-private/src/test/java/org/keycloak/broker/provider/util/IdentityBrokerStateTest.java +++ b/server-spi-private/src/test/java/org/keycloak/broker/provider/util/IdentityBrokerStateTest.java @@ -2,7 +2,9 @@ package org.keycloak.broker.provider.util; import org.junit.Assert; import org.junit.Test; -import org.keycloak.models.*; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.ClientData; public class IdentityBrokerStateTest { @@ -17,13 +19,13 @@ public class IdentityBrokerStateTest { String tabId = "vpISZLVDAc0"; // When - IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId); + IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, null); // Then Assert.assertNotNull(encodedState); Assert.assertEquals(clientClientId, encodedState.getClientId()); Assert.assertEquals(tabId, encodedState.getTabId()); - Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.http://i.am.an.url", encodedState.getEncoded()); + Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs", encodedState.getEncoded()); } @Test @@ -36,7 +38,7 @@ public class IdentityBrokerStateTest { String tabId = "vpISZLVDAc0"; // When - IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId); + IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, null); // Then Assert.assertNotNull(encodedState); @@ -53,15 +55,21 @@ public class IdentityBrokerStateTest { String clientId = "c5ac1ea7-6c28-4be1-b7cd-d63a1ba57f78"; String clientClientId = "http://i.am.an.url"; String tabId = "vpISZLVDAc0"; + String clientDataParam = new ClientData("https://my-redirect-uri", "code", "query", "some-state").encode(); // When - IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId); + IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, clientDataParam); // Then Assert.assertNotNull(encodedState); Assert.assertEquals(clientClientId, encodedState.getClientId()); Assert.assertEquals(tabId, encodedState.getTabId()); - Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.xawep2woS-G3zdY6G6V_eA", encodedState.getEncoded()); + ClientData clientData = ClientData.decodeClientDataFromParameter(encodedState.getClientData()); + Assert.assertEquals("https://my-redirect-uri", clientData.getRedirectUri()); + Assert.assertEquals("code", clientData.getResponseType()); + Assert.assertEquals("query", clientData.getResponseMode()); + Assert.assertEquals("some-state", clientData.getState()); + Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.xawep2woS-G3zdY6G6V_eA.eyJydSI6Imh0dHBzOi8vbXktcmVkaXJlY3QtdXJpIiwicnQiOiJjb2RlIiwicm0iOiJxdWVyeSIsInN0Ijoic29tZS1zdGF0ZSJ9", encodedState.getEncoded()); } @Test @@ -84,7 +92,7 @@ public class IdentityBrokerStateTest { @Test public void testEncodedWithClientIdNotUUid() { // Given - String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.http://i.am.an.url"; + String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs"; String clientId = "http://i.am.an.url"; ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId); RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel); @@ -95,6 +103,29 @@ public class IdentityBrokerStateTest { // Then Assert.assertNotNull(decodedState); Assert.assertEquals("http://i.am.an.url", decodedState.getClientId()); + Assert.assertNull(ClientData.decodeClientDataFromParameter(decodedState.getClientData())); + } + + @Test + public void testEncodedWithClientData() { + // Given + String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs.eyJydSI6Imh0dHBzOi8vbXktcmVkaXJlY3QtdXJpIiwicnQiOiJjb2RlIiwicm0iOiJxdWVyeSIsInN0Ijoic29tZS1zdGF0ZSJ9"; + String clientId = "http://i.am.an.url"; + ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId); + RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel); + + // When + IdentityBrokerState decodedState = IdentityBrokerState.encoded(encoded, realmModel); + + // Then + Assert.assertNotNull(decodedState); + Assert.assertEquals("http://i.am.an.url", decodedState.getClientId()); + ClientData clientData = ClientData.decodeClientDataFromParameter(decodedState.getClientData()); + Assert.assertNotNull(clientData); + Assert.assertEquals("https://my-redirect-uri", clientData.getRedirectUri()); + Assert.assertEquals("code", clientData.getResponseType()); + Assert.assertEquals("query", clientData.getResponseMode()); + Assert.assertEquals("some-state", clientData.getState()); } } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 9640d926b5..bc6faff64d 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -40,6 +40,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; +import org.keycloak.protocol.ClientData; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.oidc.TokenManager; @@ -282,11 +283,22 @@ public class AuthenticationProcessor { getAuthenticationSession().setAuthenticatedUser(null); } + private String getClientData() { + return getClientData(getSession(), getAuthenticationSession()); + } + + public static String getClientData(KeycloakSession session, AuthenticationSessionModel authSession) { + LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol()); + ClientData clientData = protocol.getClientData(authSession); + return clientData.encode(); + } + public URI getRefreshUrl(boolean authSessionIdParam) { UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo()) .path(AuthenticationProcessor.this.flowPath) .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) - .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()); + .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()) + .queryParam(Constants.CLIENT_DATA, getClientData()); if (authSessionIdParam) { uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); } @@ -571,7 +583,8 @@ public class AuthenticationProcessor { .queryParam(LoginActionsService.SESSION_CODE, code) .queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) - .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()); + .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()) + .queryParam(Constants.CLIENT_DATA, getClientData()); if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) { uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); } @@ -585,7 +598,8 @@ public class AuthenticationProcessor { .queryParam(Constants.KEY, tokenString) .queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) - .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()); + .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()) + .queryParam(Constants.CLIENT_DATA, getClientData()); if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) { uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); } @@ -599,7 +613,8 @@ public class AuthenticationProcessor { .path(AuthenticationProcessor.this.flowPath) .queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) - .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()); + .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId()) + .queryParam(Constants.CLIENT_DATA, getClientData()); if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) { uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); } @@ -950,6 +965,9 @@ public class AuthenticationProcessor { } clone.setAuthNote(FORKED_FROM, authSession.getTabId()); + if (authSession.getAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS) != null) { + clone.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, authSession.getAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS)); + } logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s", clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId()); diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java index 2a0569f357..689f831290 100755 --- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java @@ -271,6 +271,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow { .queryParam(Constants.EXECUTION, executionId) .queryParam(Constants.CLIENT_ID, client.getClientId()) .queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId()) + .queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(processor.getSession(), processor.getAuthenticationSession())) .build(processor.getRealm().getName()); } diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index 25d8ec0af1..557885335a 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -149,6 +149,7 @@ public class RequiredActionContextResult implements RequiredActionContext { .queryParam(Constants.EXECUTION, getExecution()) .queryParam(Constants.CLIENT_ID, client.getClientId()) .queryParam(Constants.TAB_ID, authenticationSession.getTabId()) + .queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession)) .build(getRealm().getName()); } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java index 04fadf1880..6754478811 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java @@ -46,7 +46,7 @@ public class ActionTokenContext { @FunctionalInterface public interface ProcessBrokerFlow { - Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath); + Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath); }; private final KeycloakSession session; @@ -59,12 +59,13 @@ public class ActionTokenContext { private AuthenticationSessionModel authenticationSession; private boolean authenticationSessionFresh; private String executionId; + private String clientData; private final ProcessAuthenticateFlow processAuthenticateFlow; private final ProcessBrokerFlow processBrokerFlow; public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, HttpRequest request, - EventBuilder event, ActionTokenHandler handler, String executionId, + EventBuilder event, ActionTokenHandler handler, String executionId, String clientData, ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) { this.session = session; this.realm = realm; @@ -74,6 +75,7 @@ public class ActionTokenContext { this.event = event; this.handler = handler; this.executionId = executionId; + this.clientData = clientData; this.processAuthenticateFlow = processFlow; this.processBrokerFlow = processBrokerFlow; } @@ -162,6 +164,6 @@ public class ActionTokenContext { public Response brokerFlow(String authSessionId, String code, String flowPath) { ClientModel client = authenticationSession.getClient(); - return processBrokerFlow.brokerLoginFlow(authSessionId, code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath); + return processBrokerFlow.brokerLoginFlow(authSessionId, code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), clientData, flowPath); } } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java index 8486e78736..17bb665acc 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java @@ -18,6 +18,7 @@ package org.keycloak.authentication.actiontoken.execactions; import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.actiontoken.*; @@ -84,7 +85,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHandler String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); token.setCompoundAuthenticationSessionId(authSessionEncodedId); UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), - authSession.getClient().getClientId(), authSession.getTabId()); + authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession)); String confirmUri = builder.build(realm.getName()).toString(); return session.getProvider(LoginFormsProvider.class) diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java index 7ab2a0e558..324107a6d1 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java @@ -97,7 +97,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); token.setCompoundAuthenticationSessionId(authSessionEncodedId); UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), - authSession.getClient().getClientId(), authSession.getTabId()); + authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession)); String confirmUri = builder.build(realm.getName()).toString(); return session.getProvider(LoginFormsProvider.class) diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java index 1228e6ec7d..a51762ceb7 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java @@ -16,6 +16,7 @@ */ package org.keycloak.authentication.actiontoken.verifyemail; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler; import org.keycloak.TokenVerifier.Predicate; import org.keycloak.authentication.actiontoken.*; @@ -94,7 +95,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode(); String clientId = context.getAuthenticationSession().getClient().getClientId(); String tabId = context.getAuthenticationSession().getTabId(); - URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId); + String clientData = AuthenticationProcessor.getClientData(context.getSession(), context.getAuthenticationSession()); + URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId, clientData); Response response = Response.seeOther(location) .build(); // will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none. diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java index bad440e2c8..f884e6af0f 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java @@ -25,6 +25,7 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.UriInfo; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; @@ -128,7 +129,7 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor String link = Urls .actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo), - authenticationSession.getClient().getClientId(), authenticationSession.getTabId()) + authenticationSession.getClient().getClientId(), authenticationSession.getTabId(), AuthenticationProcessor.getClientData(session, authenticationSession)) .build(realm.getName()).toString(); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java index 46256989cb..df910c22b6 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java @@ -138,7 +138,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId()); UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), - authSession.getClient().getClientId(), authSession.getTabId()); + authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession)); String link = builder.build(realm.getName()).toString(); long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs); diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 9751a6da9e..4efbcb4d25 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -48,6 +48,7 @@ import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.jose.jwk.RSAPublicJWK; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; @@ -507,7 +508,8 @@ public abstract class AbstractOAuth2IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerAlias); - Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode)); + Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, providerAlias, clientSessionCode)); if (response != null) { if (isDebugEnabled()) { @@ -352,10 +352,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @Path("/{provider_alias}/login") public Response performPostLogin(@PathParam("provider_alias") String providerAlias, @QueryParam(LoginActionsService.SESSION_CODE) String code, - @QueryParam("client_id") String clientId, + @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId, @QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) { - return performLogin(providerAlias, code, clientId, tabId, loginHint); + return performLogin(providerAlias, code, clientId, tabId, clientData, loginHint); } @GET @@ -363,8 +364,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @Path("/{provider_alias}/login") public Response performLogin(@PathParam("provider_alias") String providerAlias, @QueryParam(LoginActionsService.SESSION_CODE) String code, - @QueryParam("client_id") String clientId, + @QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.TAB_ID) String tabId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) { this.event.detail(Details.IDENTITY_PROVIDER, providerAlias); @@ -373,7 +375,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } try { - AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId); + AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData); ClientSessionCode clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession); clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); @@ -392,11 +394,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal IdentityProvider identityProvider = providerFactory.create(session, identityProviderModel); - Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode)); + Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, providerAlias, clientSessionCode)); if (response != null) { if (isDebugEnabled()) { - logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider, response); + logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider.getConfig().getAlias(), response); } return response; } @@ -409,6 +411,25 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST); } + @Override + public Response retryLogin(IdentityProvider identityProvider, AuthenticationSessionModel authSession) { + ClientSessionCode clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession); + clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); + Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, identityProvider.getConfig().getAlias(), clientSessionCode)); + + if (response != null) { + event.detail(Details.IDENTITY_PROVIDER, identityProvider.getConfig().getAlias()) + .detail(Details.LOGIN_RETRY, "true") + .success(); + + if (isDebugEnabled()) { + logger.debugf("Identity provider [%s] is going to retry a login request [%s].", identityProvider.getConfig().getAlias(), response); + } + return response; + } + return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST); + } + @Path("{provider_alias}/endpoint") public Object getEndpoint(@PathParam("provider_alias") String providerAlias) { IdentityProvider identityProvider; @@ -600,6 +621,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal URI redirect = LoginActionsService.firstBrokerLoginProcessor(session.getContext().getUri()) .queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId()) .queryParam(Constants.TAB_ID, authenticationSession.getTabId()) + .queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession)) .build(realmModel.getName()); return Response.status(302).location(redirect).build(); @@ -640,9 +662,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @NoCache @Path("/after-first-broker-login") public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code, - @QueryParam("client_id") String clientId, + @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId); + AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData); return afterFirstBrokerLogin(authSession); } @@ -756,6 +779,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal URI redirect = LoginActionsService.postBrokerLoginProcessor(session.getContext().getUri()) .queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId()) .queryParam(Constants.TAB_ID, authSession.getTabId()) + .queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession)) .build(realmModel.getName()); return Response.status(302).location(redirect).build(); } @@ -767,9 +791,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @NoCache @Path("/after-post-broker-login") public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code, - @QueryParam("client_id") String clientId, + @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId); + AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId, clientData); try { SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); @@ -1064,20 +1089,21 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal String code = state.getDecodedState(); String clientId = state.getClientId(); String tabId = state.getTabId(); - return parseSessionCode(code, clientId, tabId); + String clientData = state.getClientData(); + return parseSessionCode(code, clientId, tabId, clientData); } /** * This method will throw JAX-RS exception in case it is not able to retrieve AuthenticationSessionModel. It never returns null */ - private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId) { + private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId, String clientData) { if (code == null || clientId == null || tabId == null) { logger.debugf("Invalid request. Authorization code, clientId or tabId was null. Code=%s, clientId=%s, tabID=%s", code, clientId, tabId); Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST); throw new WebApplicationException(staleCodeError); } - SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH); + SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, clientData, LoginActionsService.AUTHENTICATE_PATH); checks.initialVerify(); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { @@ -1144,14 +1170,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return null; } - private AuthenticationRequest createAuthenticationRequest(String providerAlias, ClientSessionCode clientSessionCode) { + private AuthenticationRequest createAuthenticationRequest(IdentityProvider identityProvider, String providerAlias, ClientSessionCode clientSessionCode) { AuthenticationSessionModel authSession = null; IdentityBrokerState encodedState = null; if (clientSessionCode != null) { authSession = clientSessionCode.getClientSession(); String relayState = clientSessionCode.getOrGenerateCode(); - encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId()); + String clientData = identityProvider.supportsLongStateParameter() ? AuthenticationProcessor.getClientData(session, authSession) : null; + encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId(), clientData); } return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.session.getContext().getUri(), encodedState, getRedirectUri(providerAlias)); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index bf177aa208..69e92fdfce 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -199,16 +199,17 @@ public class LoginActionsService { } } - private SessionCodeChecks checksForCode(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) { - SessionCodeChecks res = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, flowPath); + + private SessionCodeChecks checksForCode(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath) { + SessionCodeChecks res = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, clientData, flowPath); res.initialVerify(); return res; } - protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) { + protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId, String clientData) { return new AuthenticationFlowURLHelper(session, realm, session.getContext().getUri()) - .getLastExecutionUrl(flowPath, executionId, clientId, tabId); + .getLastExecutionUrl(flowPath, executionId, clientId, tabId, clientData); } @@ -222,9 +223,10 @@ public class LoginActionsService { public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead @QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.TAB_ID) String tabId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.SKIP_LOGOUT) String skipLogout) { event.event(EventType.RESTART_AUTHENTICATION); - SessionCodeChecks checks = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, null, null, clientId, tabId, null); + SessionCodeChecks checks = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, null, null, clientId, tabId, clientData, null); AuthenticationSessionModel authSession = checks.initialVerifyAuthSession(); if (authSession == null) { @@ -248,7 +250,7 @@ public class LoginActionsService { AuthenticationProcessor.resetFlow(authSession, flowPath); - URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId); + URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId, AuthenticationProcessor.getClientData(session, authSession)); logger.debugf("Flow restart requested. Redirecting to %s", redirectUri); return Response.status(Response.Status.FOUND).location(redirectUri).build(); } @@ -310,11 +312,12 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, - @QueryParam(Constants.TAB_ID) String tabId) { + @QueryParam(Constants.TAB_ID) String tabId, + @QueryParam(Constants.CLIENT_DATA) String clientData) { event.event(EventType.LOGIN); - SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, AUTHENTICATE_PATH); + SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, AUTHENTICATE_PATH); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { return checks.getResponse(); } @@ -388,8 +391,9 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, - @QueryParam(Constants.TAB_ID) String tabId) { - return authenticate(authSessionId, code, execution, clientId, tabId); + @QueryParam(Constants.TAB_ID) String tabId, + @QueryParam(Constants.CLIENT_DATA) String clientData) { + return authenticate(authSessionId, code, execution, clientId, tabId, clientData); } @Path(RESET_CREDENTIALS_PATH) @@ -399,14 +403,15 @@ public class LoginActionsService { @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.TAB_ID) String tabId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.KEY) String key) { if (key != null) { - return handleActionToken(key, execution, clientId, tabId); + return handleActionToken(key, execution, clientId, tabId, clientData); } event.event(EventType.RESET_PASSWORD); - return resetCredentials(authSessionId, code, execution, clientId, tabId); + return resetCredentials(authSessionId, code, execution, clientId, tabId, clientData); } /** @@ -424,13 +429,14 @@ public class LoginActionsService { @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, - @QueryParam(Constants.TAB_ID) String tabId) { + @QueryParam(Constants.TAB_ID) String tabId, + @QueryParam(Constants.CLIENT_DATA) String clientData) { ClientModel client = realm.getClientByClientId(clientId); AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId); processLocaleParam(authSession); // we allow applications to link to reset credentials without going through OAuth or SAML handshakes - if (authSession == null && code == null) { + if (authSession == null && code == null && clientData == null) { if (!realm.isResetPasswordAllowed()) { event.event(EventType.RESET_PASSWORD); event.error(Errors.NOT_ALLOWED); @@ -442,7 +448,7 @@ public class LoginActionsService { } event.event(EventType.RESET_PASSWORD); - return resetCredentials(authSessionId, code, execution, clientId, tabId); + return resetCredentials(authSessionId, code, execution, clientId, tabId, clientData); } AuthenticationSessionModel createAuthenticationSessionForClient(String clientID, String redirectUriParam) @@ -497,8 +503,8 @@ public class LoginActionsService { * @param execution * @return */ - protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId) { - SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, RESET_CREDENTIALS_PATH); + protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId, String clientData) { + SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, RESET_CREDENTIALS_PATH); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) { return checks.getResponse(); } @@ -527,11 +533,12 @@ public class LoginActionsService { @QueryParam(Constants.KEY) String key, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return handleActionToken(key, execution, clientId, tabId); + return handleActionToken(key, execution, clientId, tabId, clientData); } - protected Response handleActionToken(String tokenString, String execution, String clientId, String tabId) { + protected Response handleActionToken(String tokenString, String execution, String clientId, String tabId, String clientData) { T token; ActionTokenHandler handler; ActionTokenContext tokenContext; @@ -620,7 +627,7 @@ public class LoginActionsService { } // Now proceed with the verification and handle the token - tokenContext = new ActionTokenContext(session, realm, sessionContext.getUri(), clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow); + tokenContext = new ActionTokenContext(session, realm, sessionContext.getUri(), clientConnection, request, event, handler, execution, clientData, this::processFlow, this::brokerLoginFlow); try { String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext, authSession); @@ -737,8 +744,9 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return registerRequest(authSessionId, code, execution, clientId, tabId,false); + return registerRequest(authSessionId, code, execution, clientId, tabId,clientData); } @@ -754,19 +762,20 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return registerRequest(authSessionId, code, execution, clientId, tabId,true); + return registerRequest(authSessionId, code, execution, clientId, tabId,clientData); } - private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, boolean isPostRequest) { + private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, String clientData) { event.event(EventType.REGISTER); if (!realm.isRegistrationAllowed()) { event.error(Errors.REGISTRATION_DISABLED); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED); } - SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, REGISTRATION_PATH); + SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, REGISTRATION_PATH); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { return checks.getResponse(); } @@ -787,8 +796,9 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH); + return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, FIRST_BROKER_LOGIN_PATH); } @Path(FIRST_BROKER_LOGIN_PATH) @@ -797,8 +807,9 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH); + return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, FIRST_BROKER_LOGIN_PATH); } @Path(POST_BROKER_LOGIN_PATH) @@ -807,8 +818,9 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH); + return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, POST_BROKER_LOGIN_PATH); } @Path(POST_BROKER_LOGIN_PATH) @@ -817,18 +829,19 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) String code, @QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH); + return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, POST_BROKER_LOGIN_PATH); } - protected Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) { + protected Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath) { boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH); EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN; event.event(eventType); - SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, flowPath); + SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, flowPath); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { event.error("Failed to verify login action"); return checks.getResponse(); @@ -924,8 +937,9 @@ public class LoginActionsService { String clientId = authSession.getClient().getClientId(); String tabId = authSession.getTabId(); - URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) : - Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) ; + String clientData = AuthenticationProcessor.getClientData(session, authSession); + URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId, clientData) : + Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId, clientData) ; logger.debugf("Redirecting to '%s' ", redirect); return Response.status(302).location(redirect).build(); @@ -945,7 +959,8 @@ public class LoginActionsService { String code = formData.getFirst(SESSION_CODE); String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID); String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_ID); - SessionCodeChecks checks = checksForCode(null, code, null, clientId, tabId, REQUIRED_ACTION); + String clientData = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_DATA); + SessionCodeChecks checks = checksForCode(null, code, null, clientId, tabId, clientData, REQUIRED_ACTION); if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) { return checks.getResponse(); } @@ -1047,8 +1062,9 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) final String code, @QueryParam(Constants.EXECUTION) String action, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return processRequireAction(authSessionId, code, action, clientId, tabId); + return processRequireAction(authSessionId, code, action, clientId, tabId, clientData); } @Path(REQUIRED_ACTION) @@ -1057,14 +1073,15 @@ public class LoginActionsService { @QueryParam(SESSION_CODE) final String code, @QueryParam(Constants.EXECUTION) String action, @QueryParam(Constants.CLIENT_ID) String clientId, + @QueryParam(Constants.CLIENT_DATA) String clientData, @QueryParam(Constants.TAB_ID) String tabId) { - return processRequireAction(authSessionId, code, action, clientId, tabId); + return processRequireAction(authSessionId, code, action, clientId, tabId, clientData); } - private Response processRequireAction(final String authSessionId, final String code, String action, String clientId, String tabId) { + private Response processRequireAction(final String authSessionId, final String code, String action, String clientId, String tabId, String clientData) { event.event(EventType.CUSTOM_REQUIRED_ACTION); - SessionCodeChecks checks = checksForCode(authSessionId, code, action, clientId, tabId, REQUIRED_ACTION); + SessionCodeChecks checks = checksForCode(authSessionId, code, action, clientId, tabId, clientData, REQUIRED_ACTION); if (!checks.verifyRequiredAction(action)) { return checks.getResponse(); } diff --git a/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java index 6b9dc65348..048383a2eb 100644 --- a/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java @@ -18,8 +18,6 @@ package org.keycloak.services.resources; -import java.net.URI; - import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; @@ -42,7 +40,7 @@ public class LogoutSessionCodeChecks extends SessionCodeChecks { public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String clientId, String tabId) { - super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null); + super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null, null); } diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java index 84527a6094..b606ac8afd 100644 --- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -21,7 +21,6 @@ import static org.keycloak.services.managers.AuthenticationManager.authenticateI import java.net.URI; -import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; @@ -40,7 +39,11 @@ import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.protocol.ClientData; +import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.RestartLoginCookie; +import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; import org.keycloak.services.ErrorPage; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.AuthenticationManager; @@ -72,12 +75,14 @@ public class SessionCodeChecks { private final String code; private final String execution; private final String clientId; + private final ClientData clientData; private final String tabId; private final String flowPath; private final String authSessionId; + public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, - String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) { + String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath) { this.realm = realm; this.uriInfo = uriInfo; this.request = request; @@ -91,6 +96,7 @@ public class SessionCodeChecks { this.tabId = tabId; this.flowPath = flowPath; this.authSessionId = authSessionId; + this.clientData = ClientData.decodeClientDataFromParameter(clientData); } @@ -148,6 +154,7 @@ public class SessionCodeChecks { } if (client != null) { session.getContext().setClient(client); + setClientToEvent(client); } @@ -186,14 +193,32 @@ public class SessionCodeChecks { AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(session, realm, false); if (authResult != null && authResult.getSession() != null) { - LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession) - .setSuccess(Messages.ALREADY_LOGGED_IN); + response = null; - if (client == null) { - loginForm.setAttribute(Constants.SKIP_LINK, true); + if (client != null && clientData != null) { + LoginProtocol protocol = session.getProvider(LoginProtocol.class, client.getProtocol()); + protocol.setRealm(realm) + .setHttpHeaders(session.getContext().getRequestHeaders()) + .setUriInfo(session.getContext().getUri()) + .setEventBuilder(event); + response = protocol.sendError(client, clientData, LoginProtocol.Error.ALREADY_LOGGED_IN); + event.detail(Details.REDIRECTED_TO_CLIENT, "true"); } - response = loginForm.createInfoPage(); + if (response == null) { + LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession) + .setSuccess(Messages.ALREADY_LOGGED_IN); + + if (client == null) { + loginForm.setAttribute(Constants.SKIP_LINK, true); + } + + response = loginForm.createInfoPage(); + event.detail(Details.REDIRECTED_TO_CLIENT, "false"); + } + event.error(Errors.ALREADY_LOGGED_IN); + } else { + event.error(Errors.COOKIE_NOT_FOUND); } } @@ -280,7 +305,8 @@ public class SessionCodeChecks { if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) { String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); if (latestFlowPath != null) { - URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId); + String clientData = AuthenticationProcessor.getClientData(session, authSession); + URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId, clientData); logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri); authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION); @@ -342,7 +368,8 @@ public class SessionCodeChecks { authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT); - URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, tabId); + String clientData = AuthenticationProcessor.getClientData(session, authSession); + URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, tabId, clientData); logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri); response = Response.status(Response.Status.FOUND).location(redirectUri).build(); return false; @@ -387,7 +414,6 @@ public class SessionCodeChecks { String cook = RestartLoginCookie.getRestartCookie(session); if (cook == null) { - event.error(Errors.COOKIE_NOT_FOUND); return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.COOKIE_NOT_FOUND); } @@ -411,7 +437,8 @@ public class SessionCodeChecks { flowPath = LoginActionsService.AUTHENTICATE_PATH; } - URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getTabId()); + String clientData = AuthenticationProcessor.getClientData(session, authSession); + URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getTabId(), clientData); logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri); return Response.status(Response.Status.FOUND).location(redirectUri).build(); } else { @@ -431,17 +458,18 @@ public class SessionCodeChecks { } ClientModel client = authSession.getClient(); - uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()); - uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId()); + uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()) + .queryParam(Constants.TAB_ID, authSession.getTabId()) + .queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession)); URI redirect = uriBuilder.build(realm.getName()); return Response.status(302).location(redirect).build(); } - private URI getLastExecutionUrl(String flowPath, String executionId, String tabId) { + private URI getLastExecutionUrl(String flowPath, String executionId, String tabId, String clientData) { return new AuthenticationFlowURLHelper(session, realm, uriInfo) - .getLastExecutionUrl(flowPath, executionId, clientId, tabId); + .getLastExecutionUrl(flowPath, executionId, clientId, tabId, clientData); } diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java index 5b9007c960..7bdb336143 100644 --- a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java +++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java @@ -63,7 +63,7 @@ public class AuthenticationFlowURLHelper { } - public URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) { + public URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId, String clientData) { UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo) .path(flowPath); @@ -72,6 +72,7 @@ public class AuthenticationFlowURLHelper { } uriBuilder.queryParam(Constants.CLIENT_ID, clientId); uriBuilder.queryParam(Constants.TAB_ID, tabId); + uriBuilder.queryParam(Constants.CLIENT_DATA, clientData); return uriBuilder.build(realm.getName()); } @@ -89,7 +90,8 @@ public class AuthenticationFlowURLHelper { latestFlowPath = LoginActionsService.AUTHENTICATE_PATH; } - return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId(), authSession.getTabId()); + String clientData = AuthenticationProcessor.getClientData(session, authSession); + return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId(), authSession.getTabId(), clientData); } private String getExecutionId(AuthenticationSessionModel authSession) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java index 56ad131750..0473cea9bb 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java @@ -49,6 +49,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdaterMarek Posolda + */ +public class KcOidcMultipleTabsBrokerTest extends AbstractInitializedBaseBrokerTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Rule + public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); + + private String providerRealmId; + private String consumerRealmId; + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return KcOidcBrokerConfiguration.INSTANCE; + } + + // Similar to MultipleTabsLoginTest.multipleTabsParallelLogin but with IDP brokering test involved + @Test + public void testAuthenticationExpiredWithMoreBrowserTabs_clickIdpLoginInTab1AfterExpiration() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + getLogger().infof("URL in tab 1: %s", driver.getCurrentUrl()); + + // Open new tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + Assert.assertTrue(loginPage.isCurrent("consumer")); + getLogger().infof("URL in tab2: %s", driver.getCurrentUrl()); + + setTimeOffset(7200000); + + // Finish login in tab2 + loginPage.clickSocial(bc.getIDPAlias()); + Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning."); + logInWithBroker(bc); + + waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); + Assert.assertTrue("We must be on consumer realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname"); + appPage.assertCurrent(); + + // Go back to tab1 and click "login with IDP". Should be ideally logged-in automatically + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + loginPage.clickSocial(bc.getIDPAlias()); + + assertOnAppPageWithAlreadyLoggedInError(); + } + + } + + // Similar to MultipleTabsLoginTest.multipleTabsParallelLogin but with IDP brokering test involved + @Test + public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsumerAndProvider() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + // Open login page in tab1 and click "login with IDP" + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + loginPage.clickSocial(bc.getIDPAlias()); + + // Open login page in tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + Assert.assertTrue(loginPage.isCurrent("consumer")); + getLogger().infof("URL in tab2: %s", driver.getCurrentUrl()); + + setTimeOffset(7200000); + + // Finish login in tab2 + loginPage.clickSocial(bc.getIDPAlias()); + Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning."); + logInWithBroker(bc); + + waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); + Assert.assertTrue("We must be on consumer realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname"); + appPage.assertCurrent(); + events.clear(); + + // Login in provider realm will redirect back to consumer with "authentication_expired" error. + // The consumer has also expired authentication session, so that one will redirect straight to client due the "clientData" in IdentityBrokerState + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + + // Event for "already logged-in" in the provider realm + events.expectLogin().error(Errors.ALREADY_LOGGED_IN) + .realm(getProviderRealmId()) + .client("brokerapp") + .user((String) null) + .session((String) null) + .removeDetail(Details.CONSENT) + .removeDetail(Details.CODE_ID) + .detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint")) + .detail(Details.REDIRECTED_TO_CLIENT, "true") + .detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE) + .detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value()) + .assertEvent(); + + // Event for "already logged-in" in the consumer realm + events.expect(EventType.IDENTITY_PROVIDER_LOGIN).error(Errors.ALREADY_LOGGED_IN) + .realm(getConsumerRealmId()) + .client("broker-app") + .user((String) null) + .session((String) null) + .detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI)) + .detail(Details.REDIRECTED_TO_CLIENT, "true") + .detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE) + .detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value()) + .assertEvent(); + + assertOnAppPageWithAlreadyLoggedInError(); + + } + } + + @Test + public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInProvider() throws Exception { + // Testing the scenario when authenticationSession expired only in "provider" realm and "consumer" is able to handle it at IDP. + // So need to increase authSession timeout on "consumer" + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver); + AutoCloseable realmUpdater = new RealmAttributeUpdater(adminClient.realm(bc.consumerRealmName())) + .setAccessCodeLifespanLogin(7200) + .update() + ) { + // Open login page in tab1 and click "login with IDP" + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + loginPage.clickSocial(bc.getIDPAlias()); + + // Open login page in tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + Assert.assertTrue(loginPage.isCurrent("consumer")); + getLogger().infof("URL in tab2: %s", driver.getCurrentUrl()); + + setTimeOffset(3600); + + // Finish login in tab2 + logInWithBroker(bc); + + waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); + Assert.assertTrue("We must be on consumer realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname"); + appPage.assertCurrent(); + events.clear(); + + // Login in provider realm will redirect back to consumer with "authentication_expired" error. That one will handle the "authentication_expired" error and redirect back to "provider" + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + + // Event for "already logged-in" in the provider realm + events.expectLogin().error(Errors.ALREADY_LOGGED_IN) + .realm(getProviderRealmId()) + .client("brokerapp") + .user((String) null) + .session((String) null) + .removeDetail(Details.CONSENT) + .removeDetail(Details.CODE_ID) + .detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint")) + .detail(Details.REDIRECTED_TO_CLIENT, "true") + .detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE) + .detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value()) + .assertEvent(); + + // SAML IDP on "consumer" will retry IDP login on the "provider" + events.expect(EventType.IDENTITY_PROVIDER_LOGIN) + .realm(getConsumerRealmId()) + .client("broker-app") + .user((String) null) + .detail(Details.IDENTITY_PROVIDER, bc.getIDPAlias()) + .detail(Details.LOGIN_RETRY, "true") + .assertEvent(); + + // We were redirected back to IDP where user is asked to re-authenticate (due prompt=login being sent to OIDC IDP in authz request) + Assert.assertEquals("Please re-authenticate to continue", loginPage.getInfoMessage()); + Assert.assertTrue("We must be on provider realm right now",driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + loginPage.login(bc.getUserPassword()); + + // Login finished on IDP (provider) as well as on "consumer" realm after being redirected there from "provider" + events.expectLogin() + .realm(getProviderRealmId()) + .client("brokerapp") + .user(AssertEvents.isUUID()) + .detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint")) + .assertEvent(); + + Assert.assertEquals(EventType.CODE_TO_TOKEN.name(), events.poll().getType()); + Assert.assertEquals(EventType.USER_INFO_REQUEST.name(), events.poll().getType()); + + events.expectLogin() + .realm(getConsumerRealmId()) + .client("broker-app") + .user(AssertEvents.isUUID()) + .detail(Details.IDENTITY_PROVIDER, bc.getIDPAlias()) + .assertEvent(); + + // Being redirected back to consumer and then back to client right away. Authentication session on "consumer" realm is still valid, so no error here. + appPage.assertCurrent(); + OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + org.keycloak.testsuite.Assert.assertNotNull(authzResponse.getCode()); + org.keycloak.testsuite.Assert.assertNull(authzResponse.getError()); + } + } + + // Assert browser was redirected to the appPage with "error=temporarily_unavailable" and error_description corresponding to Constants.AUTHENTICATION_EXPIRED_MESSAGE + private void assertOnAppPageWithAlreadyLoggedInError() { + appPage.assertCurrent(); // Page "You are already logged in." should not be here + OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + org.keycloak.testsuite.Assert.assertEquals(OAuthErrorException.TEMPORARILY_UNAVAILABLE, authzResponse.getError()); + org.keycloak.testsuite.Assert.assertEquals(Constants.AUTHENTICATION_EXPIRED_MESSAGE, authzResponse.getErrorDescription()); + } + + private String getProviderRealmId() { + if (providerRealmId != null) return providerRealmId; + providerRealmId = adminClient.realm(bc.providerRealmName()).toRepresentation().getId(); + return providerRealmId; + } + + private String getConsumerRealmId() { + if (consumerRealmId != null) return consumerRealmId; + consumerRealmId = adminClient.realm(bc.consumerRealmName()).toRepresentation().getId(); + return consumerRealmId; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java index 7f04218dea..ebaad04d59 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java @@ -54,6 +54,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { realm.setEnabled(true); realm.setRealm(REALM_PROV_NAME); + realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue")); return realm; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerDestinationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerDestinationTest.java index eb39410f1c..b52a05c567 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerDestinationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerDestinationTest.java @@ -1,10 +1,13 @@ package org.keycloak.testsuite.broker; +import java.util.Collections; + import org.junit.Rule; import org.junit.Test; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.protocol.saml.SamlConfigAttributes; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.util.SamlClient; @@ -28,7 +31,15 @@ public class KcSamlBrokerDestinationTest extends AbstractBrokerTest { @Override protected BrokerConfiguration getBrokerConfiguration() { - return KcSamlBrokerConfiguration.INSTANCE; + return new KcSamlBrokerConfiguration() { + + @Override + public RealmRepresentation createProviderRealm() { + RealmRepresentation realm = super.createProviderRealm(); + realm.setEventsListeners(Collections.singletonList("jboss-logging")); + return realm; + } + }; } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerFrontendUrlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerFrontendUrlTest.java index 2a7eb489b9..d243834c6e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerFrontendUrlTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerFrontendUrlTest.java @@ -29,6 +29,7 @@ import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,7 +69,14 @@ public final class KcSamlBrokerFrontendUrlTest extends AbstractBrokerTest { return realm; } - @Override + @Override + public RealmRepresentation createProviderRealm() { + RealmRepresentation realm = super.createProviderRealm(); + realm.setEventsListeners(Collections.singletonList("jboss-logging")); + return realm; + } + + @Override public List createProviderClients() { List clients = super.createProviderClients(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlMultipleTabsBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlMultipleTabsBrokerTest.java new file mode 100644 index 0000000000..1166685ae5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlMultipleTabsBrokerTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2024 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.broker; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.util.BrowserTabUtil; +import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; +import org.keycloak.testsuite.util.OAuthClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; + +/** + * + * @author Marek Posolda + */ +public class KcSamlMultipleTabsBrokerTest extends AbstractInitializedBaseBrokerTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Rule + public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return KcSamlBrokerConfiguration.INSTANCE; + } + + private String providerRealmId; + private String consumerRealmId; + + // Similar to MultipleTabsLoginTest.multipleTabsParallelLogin but with IDP brokering test involved + @Test + public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsumerAndProvider() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + // Open login page in tab1 and click "login with IDP" + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + loginPage.clickSocial(bc.getIDPAlias()); + + // Open login page in tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + Assert.assertTrue(loginPage.isCurrent("consumer")); + getLogger().infof("URL in tab2: %s", driver.getCurrentUrl()); + + setTimeOffset(7200000); + + // Finish login in tab2 + loginPage.clickSocial(bc.getIDPAlias()); + Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning."); + logInWithBroker(bc); + + waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); + Assert.assertTrue("We must be on consumer realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname"); + appPage.assertCurrent(); + events.clear(); + + // Login in provider realm will redirect back to consumer with "authentication_expired" error. That one cannot redirect due the "clientData" missing in IdentityBrokerState for SAML brokers. + // Hence need to display "You are already logged in" here + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + + events.expectLogin().error(Errors.ALREADY_LOGGED_IN) + .realm(getProviderRealmId()) + .client(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName()) + .user((String) null) + .session((String) null) + .removeDetail(Details.CONSENT) + .removeDetail(Details.CODE_ID) + .detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint")) + .detail(Details.REDIRECTED_TO_CLIENT, "true") + .assertEvent(); + + // Event for "already logged-in" in the consumer realm + events.expect(EventType.IDENTITY_PROVIDER_LOGIN).error(Errors.ALREADY_LOGGED_IN) + .realm(getConsumerRealmId()) + .client("broker-app") + .user((String) null) + .session((String) null) + .removeDetail(Details.REDIRECT_URI) + .detail(Details.REDIRECTED_TO_CLIENT, "false") + .assertEvent(); + + // Being on "You are already logged-in" now. No way to redirect to client due "clientData" are null in RelayState of SAML IDP + loginPage.assertCurrent("consumer"); + Assert.assertEquals("You are already logged in.", loginPage.getInstruction()); + } + } + + @Test + public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInProvider() throws Exception { + // Testing the scenario when authenticationSession expired only in "provider" realm and "consumer" is able to handle it at IDP. + // So need to increase authSession timeout on "consumer" + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver); + AutoCloseable realmUpdater = new RealmAttributeUpdater(adminClient.realm(bc.consumerRealmName())) + .setAccessCodeLifespanLogin(7200) + .update() + ) { + // Open login page in tab1 and click "login with IDP" + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + loginPage.clickSocial(bc.getIDPAlias()); + + // Open login page in tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + Assert.assertTrue(loginPage.isCurrent("consumer")); + getLogger().infof("URL in tab2: %s", driver.getCurrentUrl()); + + setTimeOffset(3600); + + // Finish login in tab2 + logInWithBroker(bc); + + waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); + Assert.assertTrue("We must be on consumer realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname"); + appPage.assertCurrent(); + events.clear(); + + // Login in provider realm will redirect back to consumer with "authentication_expired" error. That one will redirect back to IDP (provider) as authenticationSession still exists on "consumer" + // Then automatic SSO login on provider and then being redirected right away to consumer and finally to client + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + + // Event 1: Already-logged-in on provider + events.expectLogin().error(Errors.ALREADY_LOGGED_IN) + .realm(getProviderRealmId()) + .client(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName()) + .user((String) null) + .session((String) null) + .removeDetail(Details.CONSENT) + .removeDetail(Details.CODE_ID) + .detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint")) + .detail(Details.REDIRECTED_TO_CLIENT, "true") + .assertEvent(); + + // Event 2: Consumer redirecting to "provider" IDP for retry login + events.expect(EventType.IDENTITY_PROVIDER_LOGIN) + .realm(getConsumerRealmId()) + .client("broker-app") + .user((String) null) + .detail(Details.IDENTITY_PROVIDER, bc.getIDPAlias()) + .detail(Details.LOGIN_RETRY, "true") + .assertEvent(); + + // Event 3: Successful SSO login on "provider", which then redirects back to "consumer" + events.expectLogin() + .realm(getProviderRealmId()) + .client(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName()) + .user(AssertEvents.isUUID()) + .detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint")) + .assertEvent(); + + // Event 4: Successful login on "consumer" + events.expectLogin() + .realm(getConsumerRealmId()) + .client("broker-app") + .user(AssertEvents.isUUID()) + .detail(Details.IDENTITY_PROVIDER, bc.getIDPAlias()) + .assertEvent(); + + // Authentication session on "consumer" realm is still valid, so no error here. + appPage.assertCurrent(); + OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + org.keycloak.testsuite.Assert.assertNotNull(authzResponse.getCode()); + org.keycloak.testsuite.Assert.assertNull(authzResponse.getError()); + } + } + + private String getProviderRealmId() { + if (providerRealmId != null) return providerRealmId; + providerRealmId = adminClient.realm(bc.providerRealmName()).toRepresentation().getId(); + return providerRealmId; + } + + private String getConsumerRealmId() { + if (consumerRealmId != null) return consumerRealmId; + consumerRealmId = adminClient.realm(bc.consumerRealmName()).toRepresentation().getId(); + return consumerRealmId; + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index c210faae05..454505e257 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -775,7 +775,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError()); setTimeOffset(0); - events.expectLogin().client((String) null).user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() + events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() .assertEvent(); } @@ -795,7 +795,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() .detail(Details.RESTART_AFTER_TIMEOUT, "true") - .client((String) null) .assertEvent(); } @@ -852,7 +851,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { events.expect(EventType.LOGIN_ERROR) .user(new UserRepresentation()) - .client(new ClientRepresentation()) .error(Errors.COOKIE_NOT_FOUND) .assertEvent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java index 8c706cb00e..2327277fff 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java @@ -18,11 +18,13 @@ package org.keycloak.testsuite.forms; import static org.hamcrest.MatcherAssert.assertThat; +import static org.keycloak.testsuite.AssertEvents.DEFAULT_REDIRECT_URI; import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; @@ -31,9 +33,15 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.utils.OIDCResponseMode; +import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -54,9 +62,11 @@ import org.keycloak.testsuite.pages.LoginUpdateProfilePage; import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.VerifyEmailPage; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.util.BrowserTabUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.WaitUtils; @@ -99,6 +109,9 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { @Rule public GreenMailRule greenMail = new GreenMailRule(); + @Rule + public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); + @Page protected AppPage appPage; @@ -154,13 +167,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { loginPage.assertCurrent(); // Login in tab2 - loginPage.login("login-test", "password"); - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); - appPage.assertCurrent(); + loginSuccessAndDoRequiredActions(); // Try to go back to tab 1. We should be logged-in automatically tabUtil.closeTab(1); @@ -177,18 +184,195 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { } } + // Simulating scenario described in https://github.com/keycloak/keycloak/issues/24112 + @Test + public void multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + multipleTabsParallelLogin(tabUtil); + events.clear(); + + loginPage.login("login-test", "password"); + assertOnAppPageWithAlreadyLoggedInError(EventType.LOGIN); + } + } + + @Test + public void multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle_badRedirectUri() throws Exception { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + multipleTabsParallelLogin(tabUtil); + + // Remove redirectUri from the client + try (ClientAttributeUpdater cap = ClientAttributeUpdater.forClient(adminClient, "test", "test-app") + .setRedirectUris(List.of("https://foo")) + .update()) { + + events.clear(); + loginPage.login("login-test", "password"); + events.expectLogin().user((String) null).session((String) null).error(Errors.INVALID_REDIRECT_URI) + .detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE) + .detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value()) + .removeDetail(Details.CONSENT) + .removeDetail(Details.CODE_ID) + .assertEvent(); + errorPage.assertCurrent(); // Page "You are already logged in." should not be here + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); + } + } + } + + private void multipleTabsParallelLogin(BrowserTabUtil tabUtil) { + assertThat(tabUtil.getCountOfTabs(), Matchers.is(1)); + oauth.openLoginForm(); + loginPage.assertCurrent(); + getLogger().info("URL in tab1: " + driver.getCurrentUrl()); + + // Open new tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + loginPage.assertCurrent(); + getLogger().info("URL in tab2: " + driver.getCurrentUrl()); + + // Wait until authentication session expires + setTimeOffset(7200000); + + // Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login + loginPage.login("login-test", "password"); + loginPage.assertCurrent(); + Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning."); + + loginSuccessAndDoRequiredActions(); + + // Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in") + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + } + + private void loginSuccessAndDoRequiredActions() { + loginPage.login("login-test", "password"); + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); + appPage.assertCurrent(); + } + + // Assert browser was redirected to the appPage with "error=temporarily_unavailable" and error_description corresponding to Constants.AUTHENTICATION_EXPIRED_MESSAGE + private void assertOnAppPageWithAlreadyLoggedInError(EventType expectedEventType) { + events.expect(expectedEventType) + .user((String) null).error(Errors.ALREADY_LOGGED_IN) + .detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI)) + .detail(Details.REDIRECTED_TO_CLIENT, "true") + .detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE) + .detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value()) + .assertEvent(); + appPage.assertCurrent(); // Page "You are already logged in." should not be here + OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + Assert.assertEquals(OAuthErrorException.TEMPORARILY_UNAVAILABLE, authzResponse.getError()); + Assert.assertEquals(Constants.AUTHENTICATION_EXPIRED_MESSAGE, authzResponse.getErrorDescription()); + } + + @Test + public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRegisterClick() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + multipleTabsParallelLogin(tabUtil); + events.clear(); + + loginPage.clickRegister(); + assertOnAppPageWithAlreadyLoggedInError(EventType.REGISTER); + } + } + + @Test + public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndResetPasswordClick() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + multipleTabsParallelLogin(tabUtil); + events.clear(); + + loginPage.resetPassword(); + assertOnAppPageWithAlreadyLoggedInError(EventType.RESET_PASSWORD); + } + } + + @Test + public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRequiredAction() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + // Go through login in tab1 until required actions are shown + assertThat(tabUtil.getCountOfTabs(), Matchers.is(1)); + oauth.openLoginForm(); + loginPage.assertCurrent(); + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + getLogger().info("URL in tab1: " + driver.getCurrentUrl()); + + // Open new tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + loginPage.assertCurrent(); + getLogger().info("URL in tab2: " + driver.getCurrentUrl()); + + // Wait until authentication session expires + setTimeOffset(7200000); + + // Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login + loginPage.login("login-test", "password"); + loginPage.assertCurrent(); + Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning."); + + loginSuccessAndDoRequiredActions(); + + // Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in") + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + events.clear(); + + updatePasswordPage.changePassword("password", "password"); + assertOnAppPageWithAlreadyLoggedInError(EventType.CUSTOM_REQUIRED_ACTION); + } + } + + @Test + public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRefreshInTab1() { + try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) { + // Go through login in tab1 and do unsuccessful login attempt (to make sure that "action URL" is shown in browser URL instead of OIDC authentication request URL) + assertThat(tabUtil.getCountOfTabs(), Matchers.is(1)); + oauth.openLoginForm(); + loginPage.assertCurrent(); + loginPage.login("login-test", "bad-password"); + loginPage.assertCurrent(); + getLogger().info("URL in tab1: " + driver.getCurrentUrl()); + + // Open new tab 2 + tabUtil.newTab(oauth.getLoginFormUrl()); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2)); + loginPage.assertCurrent(); + getLogger().info("URL in tab2: " + driver.getCurrentUrl()); + + // Wait until authentication session expires + setTimeOffset(7200000); + + // Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login + loginPage.login("login-test", "password"); + loginPage.assertCurrent(); + Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning."); + + loginSuccessAndDoRequiredActions(); + + // Go back to tab1 and refresh the page. Should be automatically authenticated here (previously it showed "You are already logged-in") + tabUtil.closeTab(1); + assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); + events.clear(); + + driver.navigate().refresh(); + assertOnAppPageWithAlreadyLoggedInError(EventType.LOGIN); + } + } + @Test public void testLoginAfterLogoutFromDifferentTab() { try (BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver)) { // login in the first tab oauth.openLoginForm(); - loginPage.login("login-test", "password"); - updatePasswordPage.assertCurrent(); String tab1WindowHandle = util.getActualWindowHandle(); - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); - appPage.assertCurrent(); + loginSuccessAndDoRequiredActions(); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken()); @@ -257,11 +441,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { loginPage.assertCurrent(); // Login success now - loginPage.login("login-test", "password"); - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); - appPage.assertCurrent(); + loginSuccessAndDoRequiredActions(); } @@ -282,11 +462,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError()); // Login success now - loginPage.login("login-test", "password"); - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); - appPage.assertCurrent(); + loginSuccessAndDoRequiredActions(); } @@ -374,13 +550,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Go back to tab1 and finish login here driver.navigate().to(tab1Url); - loginPage.login("login-test", "password"); - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); - - // Assert I am redirected to the appPage in tab1 - appPage.assertCurrent(); + loginSuccessAndDoRequiredActions(); // Go back to tab2 and finish login here. Should be on the root-url-client page driver.navigate().to(tab2Url); @@ -410,10 +580,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Go back to tab1 and finish login here driver.navigate().to(tab1Url); - loginPage.login("login-test", "password"); - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); + loginSuccessAndDoRequiredActions(); // Assert I am redirected to the appPage in tab1 and have state corresponding to tab1 appPage.assertCurrent(); @@ -444,10 +611,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { String tab2Url = driver.getCurrentUrl(); // Continue in tab2 and finish login here - loginPage.login("login-test", "password"); - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); + loginSuccessAndDoRequiredActions(); // Assert I am redirected to the appPage in tab2 and have state corresponding to tab2 appPage.assertCurrent(); @@ -490,13 +654,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { loginPage.assertCurrent(); // Login in tab2 - loginPage.login("login-test", "password"); - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword("password", "password"); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); - appPage.assertCurrent(); + loginSuccessAndDoRequiredActions(); // Try to go back to tab 1. We should be logged-in automatically tabUtil.closeTab(1); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java index 228abc1441..b9d783fc1a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java @@ -133,7 +133,6 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest { events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() .detail(Details.RESTART_AFTER_TIMEOUT, "true") - .client((String) null) .assertEvent(); } @@ -173,7 +172,6 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest { events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() .detail(Details.RESTART_AFTER_TIMEOUT, "true") - .client((String) null) .assertEvent(); } }