Handle 'You are already logged in' for expired authentication sessions (#27793)

closes #24112

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
Marek Posolda 2024-04-04 10:41:03 +02:00 committed by GitHub
parent 2c5eebc8d2
commit 335a10fead
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1349 additions and 210 deletions

View file

@ -408,7 +408,8 @@ function Keycloak (config) {
var callbackState = { var callbackState = {
state: state, state: state,
nonce: nonce, nonce: nonce,
redirectUri: encodeURIComponent(redirectUri) redirectUri: encodeURIComponent(redirectUri),
loginOptions: options
}; };
if (options && options.prompt) { if (options && options.prompt) {
@ -752,9 +753,13 @@ function Keycloak (config) {
if (error) { if (error) {
if (prompt != 'none') { if (prompt != 'none') {
var errorData = { error: error, error_description: oauth.error_description }; if (oauth.error_description && oauth.error_description === "authentication_expired") {
kc.onAuthError && kc.onAuthError(errorData); kc.login(oauth.loginOptions);
promise && promise.setError(errorData); } else {
var errorData = { error: error, error_description: oauth.error_description };
kc.onAuthError && kc.onAuthError(errorData);
promise && promise.setError(errorData);
}
} else { } else {
promise && promise.setSuccess(); promise && promise.setSuccess();
} }
@ -1062,6 +1067,7 @@ function Keycloak (config) {
oauth.storedNonce = oauthState.nonce; oauth.storedNonce = oauthState.nonce;
oauth.prompt = oauthState.prompt; oauth.prompt = oauthState.prompt;
oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier; oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
oauth.loginOptions = oauthState.loginOptions;
} }
return oauth; return oauth;

View file

@ -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.ExtensionsType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType; 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.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.exceptions.ProcessingException;
@ -39,6 +40,7 @@ import org.w3c.dom.Document;
public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ErrorResponseBuilder> { public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ErrorResponseBuilder> {
protected String status; protected String status;
protected String statusMessage;
protected String destination; protected String destination;
protected NameIDType issuer; protected NameIDType issuer;
protected final List<NodeGenerator> extensions = new LinkedList<>(); protected final List<NodeGenerator> extensions = new LinkedList<>();
@ -48,6 +50,11 @@ public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBui
return this; return this;
} }
public SAML2ErrorResponseBuilder statusMessage(String statusMessage) {
this.statusMessage = statusMessage;
return this;
}
public SAML2ErrorResponseBuilder destination(String destination) { public SAML2ErrorResponseBuilder destination(String destination) {
this.destination = destination; this.destination = destination;
return this; return this;
@ -73,7 +80,9 @@ public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBui
try { try {
StatusResponseType statusResponse = new ResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant()); 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.setIssuer(issuer);
statusResponse.setDestination(destination); statusResponse.setDestination(destination);

View file

@ -213,6 +213,7 @@ public class SAMLResponseWriter extends BaseWriter {
String statusMessage = status.getStatusMessage(); String statusMessage = status.getStatusMessage();
if (StringUtil.isNotNull(statusMessage)) { if (StringUtil.isNotNull(statusMessage)) {
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.STATUS_MESSAGE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get()); StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.STATUS_MESSAGE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get());
StaxUtil.writeCharacters(writer, statusMessage);
StaxUtil.writeEndElement(writer); StaxUtil.writeEndElement(writer);
} }

View file

@ -69,6 +69,15 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/ */
Response cancelled(IdentityProviderModel idpConfig); 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. * 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 * 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<C extends IdentityProviderModel> extends Provi
default boolean reloadKeys() { default boolean reloadKeys() {
return false; 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;
}
} }

View file

@ -17,13 +17,12 @@
package org.keycloak.broker.provider.util; package org.keycloak.broker.provider.util;
import org.keycloak.authorization.policy.evaluation.Realm;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -38,9 +37,10 @@ public class IdentityBrokerState {
private static final Pattern DOT = Pattern.compile("\\."); 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 String clientIdEncoded = clientClientId; // Default use the client.clientId
boolean isUuid = false;
if (clientId != null) { 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. // 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 // 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()); bb.putLong(clientDbUuid.getLeastSignificantBits());
byte[] clientUuidBytes = bb.array(); byte[] clientUuidBytes = bb.array();
clientIdEncoded = Base64Url.encode(clientUuidBytes); clientIdEncoded = Base64Url.encode(clientUuidBytes);
isUuid = true;
} catch (RuntimeException e) { } catch (RuntimeException e) {
// Ignore...the clientid in the database was not in UUID format. Just use as is. // 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; 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) { 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 state =(decoded.length > 0) ? decoded[0] : null;
String tabId = (decoded.length > 1) ? decoded[1] : null; String tabId = (decoded.length > 1) ? decoded[1] : null;
String clientId = (decoded.length > 2) ? decoded[2] : null; String clientId = (decoded.length > 2) ? decoded[2] : null;
String clientData = (decoded.length > 3) ? decoded[3] : null;
boolean isUuid = false;
if (clientId != null) { if (clientId != null) {
try { try {
@ -82,13 +91,17 @@ public class IdentityBrokerState {
ClientModel clientModel = realmModel.getClientById(clientIdInDb); ClientModel clientModel = realmModel.getClientById(clientIdInDb);
if (clientModel != null) { if (clientModel != null) {
clientId = clientModel.getClientId(); clientId = clientModel.getClientId();
isUuid = true;
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
// Ignore...the clientid was not in encoded uuid format. Just use as it is. // 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 decodedState;
private final String clientId; private final String clientId;
private final String tabId; private final String tabId;
private final String clientData;
// Encoded form of whole state // Encoded form of whole state
private final String encoded; 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.decodedState = decodedStateParam;
this.clientId = clientId; this.clientId = clientId;
this.tabId = tabId; this.tabId = tabId;
this.clientData = clientData;
this.encoded = encoded; this.encoded = encoded;
} }
@ -120,6 +135,10 @@ public class IdentityBrokerState {
return tabId; return tabId;
} }
public String getClientData() {
return clientData;
}
public String getEncoded() { public String getEncoded() {
return encoded; return encoded;
} }

View file

@ -63,6 +63,8 @@ public interface Details {
String REQUESTED_ISSUER = "requested_issuer"; String REQUESTED_ISSUER = "requested_issuer";
String REQUESTED_SUBJECT = "requested_subject"; String REQUESTED_SUBJECT = "requested_subject";
String RESTART_AFTER_TIMEOUT = "restart_after_timeout"; String RESTART_AFTER_TIMEOUT = "restart_after_timeout";
String REDIRECTED_TO_CLIENT = "redirected_to_client";
String LOGIN_RETRY = "login_retry";
String CONSENT = "consent"; String CONSENT = "consent";
String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client

View file

@ -67,6 +67,7 @@ public interface Errors {
String EXPIRED_CODE = "expired_code"; String EXPIRED_CODE = "expired_code";
String INVALID_INPUT = "invalid_input"; String INVALID_INPUT = "invalid_input";
String COOKIE_NOT_FOUND = "cookie_not_found"; String COOKIE_NOT_FOUND = "cookie_not_found";
String ALREADY_LOGGED_IN = "already_logged_in";
String TOKEN_INTROSPECTION_FAILED = "token_introspection_failed"; String TOKEN_INTROSPECTION_FAILED = "token_introspection_failed";

View file

@ -80,6 +80,7 @@ public final class Constants {
public static final String EXECUTION = "execution"; public static final String EXECUTION = "execution";
public static final String CLIENT_ID = "client_id"; public static final String CLIENT_ID = "client_id";
public static final String TAB_ID = "tab_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 SKIP_LOGOUT = "skip_logout";
public static final String KEY = "key"; 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 USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled";
public static final String TOTP_SECRET_KEY = "TOTP_SECRET_KEY"; 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";
} }

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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");
}
}
}

View file

@ -51,6 +51,12 @@ public interface LoginProtocol extends Provider {
* Applications-initiated action was canceled by the user. Do not send error. * Applications-initiated action was canceled by the user. Do not send error.
*/ */
CANCELLED_AIA_SILENT, 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 * Consent denied by the user
*/ */
@ -80,6 +86,32 @@ public interface LoginProtocol extends Provider {
Response sendError(AuthenticationSessionModel authSession, Error error); 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 backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);

View file

@ -2,7 +2,9 @@ package org.keycloak.broker.provider.util;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; 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 { public class IdentityBrokerStateTest {
@ -17,13 +19,13 @@ public class IdentityBrokerStateTest {
String tabId = "vpISZLVDAc0"; String tabId = "vpISZLVDAc0";
// When // When
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId); IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, null);
// Then // Then
Assert.assertNotNull(encodedState); Assert.assertNotNull(encodedState);
Assert.assertEquals(clientClientId, encodedState.getClientId()); Assert.assertEquals(clientClientId, encodedState.getClientId());
Assert.assertEquals(tabId, encodedState.getTabId()); 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 @Test
@ -36,7 +38,7 @@ public class IdentityBrokerStateTest {
String tabId = "vpISZLVDAc0"; String tabId = "vpISZLVDAc0";
// When // When
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId); IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, null);
// Then // Then
Assert.assertNotNull(encodedState); Assert.assertNotNull(encodedState);
@ -53,15 +55,21 @@ public class IdentityBrokerStateTest {
String clientId = "c5ac1ea7-6c28-4be1-b7cd-d63a1ba57f78"; String clientId = "c5ac1ea7-6c28-4be1-b7cd-d63a1ba57f78";
String clientClientId = "http://i.am.an.url"; String clientClientId = "http://i.am.an.url";
String tabId = "vpISZLVDAc0"; String tabId = "vpISZLVDAc0";
String clientDataParam = new ClientData("https://my-redirect-uri", "code", "query", "some-state").encode();
// When // When
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId); IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, clientDataParam);
// Then // Then
Assert.assertNotNull(encodedState); Assert.assertNotNull(encodedState);
Assert.assertEquals(clientClientId, encodedState.getClientId()); Assert.assertEquals(clientClientId, encodedState.getClientId());
Assert.assertEquals(tabId, encodedState.getTabId()); 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 @Test
@ -84,7 +92,7 @@ public class IdentityBrokerStateTest {
@Test @Test
public void testEncodedWithClientIdNotUUid() { public void testEncodedWithClientIdNotUUid() {
// Given // Given
String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.http://i.am.an.url"; String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs";
String clientId = "http://i.am.an.url"; String clientId = "http://i.am.an.url";
ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId); ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId);
RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel); RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel);
@ -95,6 +103,29 @@ public class IdentityBrokerStateTest {
// Then // Then
Assert.assertNotNull(decodedState); Assert.assertNotNull(decodedState);
Assert.assertEquals("http://i.am.an.url", decodedState.getClientId()); 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());
} }
} }

View file

@ -40,6 +40,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.models.light.LightweightUserAdapter;
import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
@ -282,11 +283,22 @@ public class AuthenticationProcessor {
getAuthenticationSession().setAuthenticatedUser(null); 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) { public URI getRefreshUrl(boolean authSessionIdParam) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo()) UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath) .path(AuthenticationProcessor.this.flowPath)
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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) { if (authSessionIdParam) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
} }
@ -571,7 +583,8 @@ public class AuthenticationProcessor {
.queryParam(LoginActionsService.SESSION_CODE, code) .queryParam(LoginActionsService.SESSION_CODE, code)
.queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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)) { if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
} }
@ -585,7 +598,8 @@ public class AuthenticationProcessor {
.queryParam(Constants.KEY, tokenString) .queryParam(Constants.KEY, tokenString)
.queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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)) { if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
} }
@ -599,7 +613,8 @@ public class AuthenticationProcessor {
.path(AuthenticationProcessor.this.flowPath) .path(AuthenticationProcessor.this.flowPath)
.queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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)) { if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()); uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
} }
@ -950,6 +965,9 @@ public class AuthenticationProcessor {
} }
clone.setAuthNote(FORKED_FROM, authSession.getTabId()); 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", logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s",
clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId()); clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId());

View file

@ -271,6 +271,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
.queryParam(Constants.EXECUTION, executionId) .queryParam(Constants.EXECUTION, executionId)
.queryParam(Constants.CLIENT_ID, client.getClientId()) .queryParam(Constants.CLIENT_ID, client.getClientId())
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId()) .queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(processor.getSession(), processor.getAuthenticationSession()))
.build(processor.getRealm().getName()); .build(processor.getRealm().getName());
} }

View file

@ -149,6 +149,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
.queryParam(Constants.EXECUTION, getExecution()) .queryParam(Constants.EXECUTION, getExecution())
.queryParam(Constants.CLIENT_ID, client.getClientId()) .queryParam(Constants.CLIENT_ID, client.getClientId())
.queryParam(Constants.TAB_ID, authenticationSession.getTabId()) .queryParam(Constants.TAB_ID, authenticationSession.getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession))
.build(getRealm().getName()); .build(getRealm().getName());
} }

View file

@ -46,7 +46,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
@FunctionalInterface @FunctionalInterface
public interface ProcessBrokerFlow { 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; private final KeycloakSession session;
@ -59,12 +59,13 @@ public class ActionTokenContext<T extends JsonWebToken> {
private AuthenticationSessionModel authenticationSession; private AuthenticationSessionModel authenticationSession;
private boolean authenticationSessionFresh; private boolean authenticationSessionFresh;
private String executionId; private String executionId;
private String clientData;
private final ProcessAuthenticateFlow processAuthenticateFlow; private final ProcessAuthenticateFlow processAuthenticateFlow;
private final ProcessBrokerFlow processBrokerFlow; private final ProcessBrokerFlow processBrokerFlow;
public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo, public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
ClientConnection clientConnection, HttpRequest request, ClientConnection clientConnection, HttpRequest request,
EventBuilder event, ActionTokenHandler<T> handler, String executionId, EventBuilder event, ActionTokenHandler<T> handler, String executionId, String clientData,
ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) { ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
this.session = session; this.session = session;
this.realm = realm; this.realm = realm;
@ -74,6 +75,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
this.event = event; this.event = event;
this.handler = handler; this.handler = handler;
this.executionId = executionId; this.executionId = executionId;
this.clientData = clientData;
this.processAuthenticateFlow = processFlow; this.processAuthenticateFlow = processFlow;
this.processBrokerFlow = processBrokerFlow; this.processBrokerFlow = processBrokerFlow;
} }
@ -162,6 +164,6 @@ public class ActionTokenContext<T extends JsonWebToken> {
public Response brokerFlow(String authSessionId, String code, String flowPath) { public Response brokerFlow(String authSessionId, String code, String flowPath) {
ClientModel client = authenticationSession.getClient(); 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);
} }
} }

View file

@ -18,6 +18,7 @@ package org.keycloak.authentication.actiontoken.execactions;
import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier;
import org.keycloak.TokenVerifier.Predicate; import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.actiontoken.*; import org.keycloak.authentication.actiontoken.*;
@ -84,7 +85,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHandler
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
token.setCompoundAuthenticationSessionId(authSessionEncodedId); token.setCompoundAuthenticationSessionId(authSessionEncodedId);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), 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(); String confirmUri = builder.build(realm.getName()).toString();
return session.getProvider(LoginFormsProvider.class) return session.getProvider(LoginFormsProvider.class)

View file

@ -97,7 +97,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
token.setCompoundAuthenticationSessionId(authSessionEncodedId); token.setCompoundAuthenticationSessionId(authSessionEncodedId);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), 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(); String confirmUri = builder.build(realm.getName()).toString();
return session.getProvider(LoginFormsProvider.class) return session.getProvider(LoginFormsProvider.class)

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.authentication.actiontoken.verifyemail; package org.keycloak.authentication.actiontoken.verifyemail;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler; import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;
import org.keycloak.TokenVerifier.Predicate; import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.authentication.actiontoken.*; import org.keycloak.authentication.actiontoken.*;
@ -94,7 +95,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler<Ve
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
token.setCompoundAuthenticationSessionId(authSessionEncodedId); token.setCompoundAuthenticationSessionId(authSessionEncodedId);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), 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(); String confirmUri = builder.build(realm.getName()).toString();
return session.getProvider(LoginFormsProvider.class) return session.getProvider(LoginFormsProvider.class)

View file

@ -20,6 +20,7 @@ package org.keycloak.authentication.authenticators.broker;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken; import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
@ -133,7 +134,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias(), authSession.getClient().getClientId() brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias(), authSession.getClient().getClientId()
); );
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), 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 String link = builder
.queryParam(Constants.EXECUTION, context.getExecution().getId()) .queryParam(Constants.EXECUTION, context.getExecution().getId())
.build(realm.getName()).toString(); .build(realm.getName()).toString();

View file

@ -83,7 +83,8 @@ public class IdentityProviderAuthenticator implements Authenticator {
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode(); String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
String clientId = context.getAuthenticationSession().getClient().getClientId(); String clientId = context.getAuthenticationSession().getClient().getClientId();
String tabId = context.getAuthenticationSession().getTabId(); 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) Response response = Response.seeOther(location)
.build(); .build();
// will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none. // will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none.

View file

@ -25,6 +25,7 @@ import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
@ -128,7 +129,7 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor
String link = Urls String link = Urls
.actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo), .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(); .build(realm.getName()).toString();

View file

@ -138,7 +138,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId()); VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId());
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), 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(); String link = builder.build(realm.getName()).toString();
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs); long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);

View file

@ -48,6 +48,7 @@ import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.jose.jwk.RSAPublicJWK; import org.keycloak.jose.jwk.RSAPublicJWK;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -507,7 +508,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
@GET @GET
public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_STATE) String state, public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_STATE) String state,
@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_CODE) String authorizationCode, @QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_CODE) String authorizationCode,
@QueryParam(OAuth2Constants.ERROR) String error) { @QueryParam(OAuth2Constants.ERROR) String error,
@QueryParam(OAuth2Constants.ERROR_DESCRIPTION) String errorDescription) {
OAuth2IdentityProviderConfig providerConfig = provider.getConfig(); OAuth2IdentityProviderConfig providerConfig = provider.getConfig();
if (state == null) { if (state == null) {
@ -525,6 +527,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return callback.cancelled(providerConfig); return callback.cancelled(providerConfig);
} else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) { } else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) {
return callback.error(error); return callback.error(error);
} else if (error.equals(OAuthErrorException.TEMPORARILY_UNAVAILABLE) && Constants.AUTHENTICATION_EXPIRED_MESSAGE.equals(errorDescription)) {
return callback.retryLogin(this.provider, authSession);
} else { } else {
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
} }

View file

@ -134,6 +134,7 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
.param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work .param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
} }
} }
@Override @Override

View file

@ -42,6 +42,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeyManager; import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -447,7 +448,11 @@ public class SAMLEndpoint {
if (! isSuccessfulSamlResponse(responseType)) { if (! isSuccessfulSamlResponse(responseType)) {
String statusMessage = responseType.getStatus() == null || responseType.getStatus().getStatusMessage() == null ? Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR : responseType.getStatus().getStatusMessage(); String statusMessage = responseType.getStatus() == null || responseType.getStatus().getStatusMessage() == null ? Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR : responseType.getStatus().getStatusMessage();
return callback.error(statusMessage); if (Constants.AUTHENTICATION_EXPIRED_MESSAGE.equals(statusMessage)) {
return callback.retryLogin(provider, authSession);
} else {
return callback.error(statusMessage);
}
} }
if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) { if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);

View file

@ -519,4 +519,10 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
} }
return false; return false;
} }
@Override
public boolean supportsLongStateParameter() {
// SAML RelayState parameter has limits of 80 bytes per SAML specification
return false;
}
} }

View file

@ -24,6 +24,7 @@ import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext; import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
@ -70,6 +71,7 @@ import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod; import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
@ -367,6 +369,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
} }
if (authenticationSession != null) { if (authenticationSession != null) {
uriBuilder.queryParam(Constants.TAB_ID, authenticationSession.getTabId()); uriBuilder.queryParam(Constants.TAB_ID, authenticationSession.getTabId());
String authSessionAction = authenticationSession.getAction();
if (!AuthenticationSessionModel.Action.LOGGING_OUT.name().equals(authSessionAction) && !AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(authSessionAction)) {
uriBuilder.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession));
}
} }
return uriBuilder; return uriBuilder;
} }

View file

@ -79,10 +79,6 @@ public class UrlBean {
return Urls.realmRegisterPage(baseURI, realm).toString(); return Urls.realmRegisterPage(baseURI, realm).toString();
} }
public String getLoginUpdateProfileUrl() {
return Urls.loginActionUpdateProfile(baseURI, realm).toString();
}
public String getLoginResetCredentialsUrl() { public String getLoginResetCredentialsUrl() {
return Urls.loginResetCredentials(baseURI, realm).toString(); return Urls.loginResetCredentials(baseURI, realm).toString();
} }

View file

@ -13,6 +13,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper; import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
@ -148,6 +149,16 @@ public class DockerAuthV2Protocol implements LoginProtocol {
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build(); return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
} }
@Override
public ClientData getClientData(AuthenticationSessionModel authSession) {
return new ClientData();
}
@Override
public Response sendError(ClientModel client, ClientData clientData, Error error) {
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
@Override @Override
public Response backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) { public Response backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
return errorResponse(userSession, "backchannelLogout"); return errorResponse(userSession, "backchannelLogout");

View file

@ -31,8 +31,6 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
@ -40,26 +38,26 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.utils.LogoutUtil; import org.keycloak.protocol.oidc.utils.LogoutUtil;
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder; import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.ImplicitHybridTokenResponse; import org.keycloak.services.clientpolicy.context.ImplicitHybridTokenResponse;
import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.protocol.oidc.utils.OAuth2Code; import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser; import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
@ -321,13 +319,23 @@ public class OIDCLoginProtocol implements LoginProtocol {
String redirect = authSession.getRedirectUri(); String redirect = authSession.getRedirectUri();
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM); String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
OIDCRedirectUriBuilder redirectUri = buildErrorRedirectUri(redirect, state, error);
// Remove authenticationSession from current tab
new AuthenticationSessionManager(session).removeTabIdInAuthenticationSession(realm, authSession);
return redirectUri.build();
}
private OIDCRedirectUriBuilder buildErrorRedirectUri(String redirect, String state, Error error) {
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, null); OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, null);
if (error != Error.CANCELLED_AIA_SILENT) { OAuth2ErrorRepresentation oauthError = translateError(error);
redirectUri.addParam(OAuth2Constants.ERROR, translateError(error)); if (oauthError.getError() != null) {
redirectUri.addParam(OAuth2Constants.ERROR, oauthError.getError());
} }
if (error == Error.CANCELLED_AIA) { if (oauthError.getErrorDescription() != null) {
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, "User cancelled aplication-initiated action."); redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, oauthError.getErrorDescription());
} }
if (state != null) { if (state != null) {
redirectUri.addParam(OAuth2Constants.STATE, state); redirectUri.addParam(OAuth2Constants.STATE, state);
@ -339,25 +347,59 @@ public class OIDCLoginProtocol implements LoginProtocol {
redirectUri.addParam(OAuth2Constants.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())); redirectUri.addParam(OAuth2Constants.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
} }
// Remove authenticationSession from current tab return redirectUri;
new AuthenticationSessionManager(session).removeTabIdInAuthenticationSession(realm, authSession); }
@Override
public ClientData getClientData(AuthenticationSessionModel authSession) {
return new ClientData(authSession.getRedirectUri(),
authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM),
authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM),
authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM));
}
@Override
public Response sendError(ClientModel client, ClientData clientData, Error error) {
logger.tracef("Calling sendError with clientData when authenticating with client '%s' in realm '%s'. Error: %s", client.getClientId(), realm.getName(), error);
// Should check if clientData are valid for current client
AuthorizationEndpointRequest req = AuthorizationEndpointRequest.fromClientData(clientData);
AuthorizationEndpointChecker checker = new AuthorizationEndpointChecker()
.event(event)
.client(client)
.realm(realm)
.request(req)
.session(session);
try {
checker.checkResponseType();
checker.checkRedirectUri();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
ex.throwAsErrorPageException(null);
}
setupResponseTypeAndMode(clientData.getResponseType(), clientData.getResponseMode());
OIDCRedirectUriBuilder redirectUri = buildErrorRedirectUri(clientData.getRedirectUri(), clientData.getState(), error);
return redirectUri.build(); return redirectUri.build();
} }
private String translateError(Error error) { private OAuth2ErrorRepresentation translateError(Error error) {
switch (error) { switch (error) {
case CANCELLED_BY_USER: case CANCELLED_AIA_SILENT:
return new OAuth2ErrorRepresentation(null, null);
case CANCELLED_AIA: case CANCELLED_AIA:
return new OAuth2ErrorRepresentation(OAuthErrorException.ACCESS_DENIED, "User cancelled aplication-initiated action.");
case CANCELLED_BY_USER:
case CONSENT_DENIED: case CONSENT_DENIED:
return OAuthErrorException.ACCESS_DENIED; return new OAuth2ErrorRepresentation(OAuthErrorException.ACCESS_DENIED, null);
case PASSIVE_INTERACTION_REQUIRED: case PASSIVE_INTERACTION_REQUIRED:
return OAuthErrorException.INTERACTION_REQUIRED; return new OAuth2ErrorRepresentation(OAuthErrorException.INTERACTION_REQUIRED, null);
case PASSIVE_LOGIN_REQUIRED: case PASSIVE_LOGIN_REQUIRED:
return OAuthErrorException.LOGIN_REQUIRED; return new OAuth2ErrorRepresentation(OAuthErrorException.LOGIN_REQUIRED, null);
case ALREADY_LOGGED_IN:
return new OAuth2ErrorRepresentation(OAuthErrorException.TEMPORARILY_UNAVAILABLE, Constants.AUTHENTICATION_EXPIRED_MESSAGE);
default: default:
ServicesLogger.LOGGER.untranslatedProtocol(error.name()); ServicesLogger.LOGGER.untranslatedProtocol(error.name());
return OAuthErrorException.SERVER_ERROR; return new OAuth2ErrorRepresentation(OAuthErrorException.SERVER_ERROR, null);
} }
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.protocol.oidc.endpoints.request; package org.keycloak.protocol.oidc.endpoints.request;
import org.keycloak.protocol.ClientData;
import org.keycloak.rar.AuthorizationRequestContext; import org.keycloak.rar.AuthorizationRequestContext;
import java.util.HashMap; import java.util.HashMap;
@ -66,6 +67,14 @@ public class AuthorizationEndpointRequest {
return redirectUriParam; return redirectUriParam;
} }
public static AuthorizationEndpointRequest fromClientData(ClientData cData) {
AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
request.responseType = cData.getResponseType();
request.responseMode = cData.getResponseMode();
request.redirectUriParam = cData.getRedirectUri();
return request;
}
public String getResponseType() { public String getResponseType() {
return responseType; return responseType;
} }

View file

@ -38,11 +38,13 @@ import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeyManager; import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
@ -50,9 +52,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.mappers.NameIdMapperHelper; import org.keycloak.protocol.saml.mappers.NameIdMapperHelper;
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper; import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
@ -78,6 +82,7 @@ import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
@ -241,11 +246,41 @@ public class SamlProtocol implements LoginProtocol {
} }
} }
@Override
public ClientData getClientData(AuthenticationSessionModel authSession) {
String responseMode = isPostBinding(authSession) ? SamlProtocol.SAML_POST_BINDING : SamlProtocol.SAML_REDIRECT_BINDING;
return new ClientData(authSession.getRedirectUri(),
null,
responseMode,
authSession.getClientNote(GeneralConstants.RELAY_STATE));
}
@Override
public Response sendError(ClientModel client, ClientData clientData, Error error) {
logger.tracef("Calling sendError with clientData when authenticating with client '%s' in realm '%s'. Error: %s", client.getClientId(), realm.getName(), error);
SamlClient samlClient = new SamlClient(client);
boolean postBinding = samlClient.forcePostBinding() || SamlProtocol.SAML_POST_BINDING.equals(clientData.getResponseMode());
event.detail(Details.REDIRECT_URI, clientData.getRedirectUri());
String validRedirectUri = RedirectUtils.verifyRedirectUri(session, clientData.getRedirectUri(), client);
if (validRedirectUri == null) {
event.error(Errors.INVALID_REDIRECT_URI);
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
}
return samlErrorMessage(
null, samlClient, postBinding,
validRedirectUri, translateErrorToSAMLStatus(error), clientData.getState()
);
}
private Response samlErrorMessage( private Response samlErrorMessage(
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding, AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
String destination, JBossSAMLURIConstants statusDetail, String relayState) { String destination, SAMLError samlError, String relayState) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState); JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get()); SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm))
.status(samlError.error().get())
.statusMessage(samlError.errorDescription());
KeyManager keyManager = session.keys(); KeyManager keyManager = session.keys();
if (samlClient.requiresRealmSignature()) { if (samlClient.requiresRealmSignature()) {
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm); KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
@ -276,18 +311,20 @@ public class SamlProtocol implements LoginProtocol {
} }
} }
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) { private SAMLError translateErrorToSAMLStatus(Error error) {
switch (error) { switch (error) {
case CANCELLED_BY_USER: case CANCELLED_BY_USER:
case CANCELLED_AIA: case CANCELLED_AIA:
case CONSENT_DENIED: case CONSENT_DENIED:
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED; return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
case PASSIVE_INTERACTION_REQUIRED: case PASSIVE_INTERACTION_REQUIRED:
case PASSIVE_LOGIN_REQUIRED: case PASSIVE_LOGIN_REQUIRED:
return JBossSAMLURIConstants.STATUS_NO_PASSIVE; return new SAMLError(JBossSAMLURIConstants.STATUS_NO_PASSIVE, null);
case ALREADY_LOGGED_IN:
return new SAMLError(JBossSAMLURIConstants.STATUS_AUTHNFAILED, Constants.AUTHENTICATION_EXPIRED_MESSAGE);
default: default:
logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error"); logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED; return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
} }
} }
@ -485,7 +522,7 @@ public class SamlProtocol implements LoginProtocol {
if (nameId == null) { if (nameId == null) {
return samlErrorMessage(null, samlClient, isPostBinding(authSession), redirectUri, return samlErrorMessage(null, samlClient, isPostBinding(authSession), redirectUri,
JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState); new SAMLError(JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, null), relayState);
} }
builder.nameIdentifier(nameIdFormat, nameId); builder.nameIdentifier(nameIdFormat, nameId);
@ -1061,4 +1098,11 @@ public class SamlProtocol implements LoginProtocol {
.header("Cache-Control", "no-cache, no-store").build(); .header("Cache-Control", "no-cache, no-store").build();
} }
/**
* @param error mandatory parameter
* @param errorDescription optional parameter
*/
private record SAMLError(JBossSAMLURIConstants error, String errorDescription) {
}
} }

View file

@ -52,7 +52,7 @@ public class Urls {
.build(realmName, providerAlias); .build(realmName, providerAlias);
} }
public static URI identityProviderAuthnRequest(URI baseUri, String providerAlias, String realmName, String accessCode, String clientId, String tabId) { public static URI identityProviderAuthnRequest(URI baseUri, String providerAlias, String realmName, String accessCode, String clientId, String tabId, String clientData) {
UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService") UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
.path(IdentityBrokerService.class, "performLogin"); .path(IdentityBrokerService.class, "performLogin");
@ -65,6 +65,9 @@ public class Urls {
if (tabId != null) { if (tabId != null) {
uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId); uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId);
} }
if (clientData != null) {
uriBuilder.replaceQueryParam(Constants.CLIENT_DATA, clientData);
}
return uriBuilder.build(realmName, providerAlias); return uriBuilder.build(realmName, providerAlias);
} }
@ -84,23 +87,25 @@ public class Urls {
} }
public static URI identityProviderAuthnRequest(URI baseURI, String providerAlias, String realmName) { public static URI identityProviderAuthnRequest(URI baseURI, String providerAlias, String realmName) {
return identityProviderAuthnRequest(baseURI, providerAlias, realmName, null, null, null); return identityProviderAuthnRequest(baseURI, providerAlias, realmName, null, null, null, null);
} }
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) { public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId, String clientData) {
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
.path(IdentityBrokerService.class, "afterFirstBrokerLogin") .path(IdentityBrokerService.class, "afterFirstBrokerLogin")
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode) .replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
.replaceQueryParam(Constants.CLIENT_ID, clientId) .replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.TAB_ID, tabId) .replaceQueryParam(Constants.TAB_ID, tabId)
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
.build(realmName); .build(realmName);
} }
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) { public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId, String clientData) {
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow") .path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode) .replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
.replaceQueryParam(Constants.CLIENT_ID, clientId) .replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
.replaceQueryParam(Constants.TAB_ID, tabId) .replaceQueryParam(Constants.TAB_ID, tabId)
.build(realmName); .build(realmName);
} }
@ -117,24 +122,16 @@ public class Urls {
return loginActionsBase(baseUri).path(LoginActionsService.class, "requiredAction"); return loginActionsBase(baseUri).path(LoginActionsService.class, "requiredAction");
} }
public static URI loginActionUpdateProfile(URI baseUri, String realmName) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmName);
}
public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "emailVerification");
}
public static URI loginResetCredentials(URI baseUri, String realmName) { public static URI loginResetCredentials(URI baseUri, String realmName) {
return loginResetCredentialsBuilder(baseUri).build(realmName); return loginResetCredentialsBuilder(baseUri).build(realmName);
} }
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId, String tabId) { public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId, String tabId, String clientData) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken") return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
.queryParam(Constants.KEY, tokenString) .queryParam(Constants.KEY, tokenString)
.queryParam(Constants.CLIENT_ID, clientId) .queryParam(Constants.CLIENT_ID, clientId)
.queryParam(Constants.TAB_ID, tabId); .queryParam(Constants.TAB_ID, tabId)
.queryParam(Constants.CLIENT_DATA, clientData);
} }

View file

@ -979,6 +979,7 @@ public class AuthenticationManager {
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId()); uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId()); uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
uriBuilder.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession));
if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) { if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId()); uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());

View file

@ -329,7 +329,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
try { try {
IdentityProvider<?> identityProvider = getIdentityProvider(session, realmModel, providerAlias); IdentityProvider<?> identityProvider = getIdentityProvider(session, realmModel, providerAlias);
Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode)); Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, providerAlias, clientSessionCode));
if (response != null) { if (response != null) {
if (isDebugEnabled()) { if (isDebugEnabled()) {
@ -352,10 +352,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Path("/{provider_alias}/login") @Path("/{provider_alias}/login")
public Response performPostLogin(@PathParam("provider_alias") String providerAlias, public Response performPostLogin(@PathParam("provider_alias") String providerAlias,
@QueryParam(LoginActionsService.SESSION_CODE) String code, @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(Constants.TAB_ID) String tabId,
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) { @QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
return performLogin(providerAlias, code, clientId, tabId, loginHint); return performLogin(providerAlias, code, clientId, tabId, clientData, loginHint);
} }
@GET @GET
@ -363,8 +364,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Path("/{provider_alias}/login") @Path("/{provider_alias}/login")
public Response performLogin(@PathParam("provider_alias") String providerAlias, public Response performLogin(@PathParam("provider_alias") String providerAlias,
@QueryParam(LoginActionsService.SESSION_CODE) String code, @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.TAB_ID) String tabId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) { @QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
this.event.detail(Details.IDENTITY_PROVIDER, providerAlias); this.event.detail(Details.IDENTITY_PROVIDER, providerAlias);
@ -373,7 +375,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
} }
try { try {
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId); AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData);
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession); ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
@ -392,11 +394,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
IdentityProvider<?> identityProvider = providerFactory.create(session, identityProviderModel); 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 (response != null) {
if (isDebugEnabled()) { 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; 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); return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST);
} }
@Override
public Response retryLogin(IdentityProvider<?> identityProvider, AuthenticationSessionModel authSession) {
ClientSessionCode<AuthenticationSessionModel> 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") @Path("{provider_alias}/endpoint")
public Object getEndpoint(@PathParam("provider_alias") String providerAlias) { public Object getEndpoint(@PathParam("provider_alias") String providerAlias) {
IdentityProvider identityProvider; IdentityProvider identityProvider;
@ -600,6 +621,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
URI redirect = LoginActionsService.firstBrokerLoginProcessor(session.getContext().getUri()) URI redirect = LoginActionsService.firstBrokerLoginProcessor(session.getContext().getUri())
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId()) .queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
.queryParam(Constants.TAB_ID, authenticationSession.getTabId()) .queryParam(Constants.TAB_ID, authenticationSession.getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession))
.build(realmModel.getName()); .build(realmModel.getName());
return Response.status(302).location(redirect).build(); return Response.status(302).location(redirect).build();
@ -640,9 +662,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@NoCache @NoCache
@Path("/after-first-broker-login") @Path("/after-first-broker-login")
public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code, 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) { @QueryParam(Constants.TAB_ID) String tabId) {
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId); AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData);
return afterFirstBrokerLogin(authSession); return afterFirstBrokerLogin(authSession);
} }
@ -756,6 +779,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
URI redirect = LoginActionsService.postBrokerLoginProcessor(session.getContext().getUri()) URI redirect = LoginActionsService.postBrokerLoginProcessor(session.getContext().getUri())
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId()) .queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
.queryParam(Constants.TAB_ID, authSession.getTabId()) .queryParam(Constants.TAB_ID, authSession.getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession))
.build(realmModel.getName()); .build(realmModel.getName());
return Response.status(302).location(redirect).build(); return Response.status(302).location(redirect).build();
} }
@ -767,9 +791,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@NoCache @NoCache
@Path("/after-post-broker-login") @Path("/after-post-broker-login")
public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code, 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) { @QueryParam(Constants.TAB_ID) String tabId) {
AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId); AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId, clientData);
try { try {
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); 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 code = state.getDecodedState();
String clientId = state.getClientId(); String clientId = state.getClientId();
String tabId = state.getTabId(); 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 * 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) { 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); 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); Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
throw new WebApplicationException(staleCodeError); 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(); checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
@ -1144,14 +1170,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return null; return null;
} }
private AuthenticationRequest createAuthenticationRequest(String providerAlias, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) { private AuthenticationRequest createAuthenticationRequest(IdentityProvider<?> identityProvider, String providerAlias, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
AuthenticationSessionModel authSession = null; AuthenticationSessionModel authSession = null;
IdentityBrokerState encodedState = null; IdentityBrokerState encodedState = null;
if (clientSessionCode != null) { if (clientSessionCode != null) {
authSession = clientSessionCode.getClientSession(); authSession = clientSessionCode.getClientSession();
String relayState = clientSessionCode.getOrGenerateCode(); 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)); return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.session.getContext().getUri(), encodedState, getRedirectUri(providerAlias));

View file

@ -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(); res.initialVerify();
return res; 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()) 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 public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.TAB_ID) String tabId, @QueryParam(Constants.TAB_ID) String tabId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.SKIP_LOGOUT) String skipLogout) { @QueryParam(Constants.SKIP_LOGOUT) String skipLogout) {
event.event(EventType.RESTART_AUTHENTICATION); 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(); AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
if (authSession == null) { if (authSession == null) {
@ -248,7 +250,7 @@ public class LoginActionsService {
AuthenticationProcessor.resetFlow(authSession, flowPath); 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); logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build(); return Response.status(Response.Status.FOUND).location(redirectUri).build();
} }
@ -310,11 +312,12 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @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); 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)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -388,8 +391,9 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId,
return authenticate(authSessionId, code, execution, clientId, tabId); @QueryParam(Constants.CLIENT_DATA) String clientData) {
return authenticate(authSessionId, code, execution, clientId, tabId, clientData);
} }
@Path(RESET_CREDENTIALS_PATH) @Path(RESET_CREDENTIALS_PATH)
@ -399,14 +403,15 @@ public class LoginActionsService {
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.TAB_ID) String tabId, @QueryParam(Constants.TAB_ID) String tabId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.KEY) String key) { @QueryParam(Constants.KEY) String key) {
if (key != null) { if (key != null) {
return handleActionToken(key, execution, clientId, tabId); return handleActionToken(key, execution, clientId, tabId, clientData);
} }
event.event(EventType.RESET_PASSWORD); 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.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @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); ClientModel client = realm.getClientByClientId(clientId);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId); AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
processLocaleParam(authSession); processLocaleParam(authSession);
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes // 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()) { if (!realm.isResetPasswordAllowed()) {
event.event(EventType.RESET_PASSWORD); event.event(EventType.RESET_PASSWORD);
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
@ -442,7 +448,7 @@ public class LoginActionsService {
} }
event.event(EventType.RESET_PASSWORD); 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) AuthenticationSessionModel createAuthenticationSessionForClient(String clientID, String redirectUriParam)
@ -497,8 +503,8 @@ public class LoginActionsService {
* @param execution * @param execution
* @return * @return
*/ */
protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId) { protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId, String clientData) {
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, RESET_CREDENTIALS_PATH); SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, RESET_CREDENTIALS_PATH);
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -527,11 +533,12 @@ public class LoginActionsService {
@QueryParam(Constants.KEY) String key, @QueryParam(Constants.KEY) String key,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return handleActionToken(key, execution, clientId, tabId); return handleActionToken(key, execution, clientId, tabId, clientData);
} }
protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) { protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId, String clientData) {
T token; T token;
ActionTokenHandler<T> handler; ActionTokenHandler<T> handler;
ActionTokenContext<T> tokenContext; ActionTokenContext<T> tokenContext;
@ -620,7 +627,7 @@ public class LoginActionsService {
} }
// Now proceed with the verification and handle the token // 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 { try {
String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext, authSession); String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext, authSession);
@ -737,8 +744,9 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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); event.event(EventType.REGISTER);
if (!realm.isRegistrationAllowed()) { if (!realm.isRegistrationAllowed()) {
event.error(Errors.REGISTRATION_DISABLED); event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED); 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)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -787,8 +796,9 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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) @Path(FIRST_BROKER_LOGIN_PATH)
@ -797,8 +807,9 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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) @Path(POST_BROKER_LOGIN_PATH)
@ -807,8 +818,9 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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) @Path(POST_BROKER_LOGIN_PATH)
@ -817,18 +829,19 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) String code, @QueryParam(SESSION_CODE) String code,
@QueryParam(Constants.EXECUTION) String execution, @QueryParam(Constants.EXECUTION) String execution,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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); boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN; EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType); 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)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
event.error("Failed to verify login action"); event.error("Failed to verify login action");
return checks.getResponse(); return checks.getResponse();
@ -924,8 +937,9 @@ public class LoginActionsService {
String clientId = authSession.getClient().getClientId(); String clientId = authSession.getClient().getClientId();
String tabId = authSession.getTabId(); String tabId = authSession.getTabId();
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) : String clientData = AuthenticationProcessor.getClientData(session, authSession);
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) ; 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); logger.debugf("Redirecting to '%s' ", redirect);
return Response.status(302).location(redirect).build(); return Response.status(302).location(redirect).build();
@ -945,7 +959,8 @@ public class LoginActionsService {
String code = formData.getFirst(SESSION_CODE); String code = formData.getFirst(SESSION_CODE);
String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID); String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID);
String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_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())) { if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
return checks.getResponse(); return checks.getResponse();
} }
@ -1047,8 +1062,9 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) final String code, @QueryParam(SESSION_CODE) final String code,
@QueryParam(Constants.EXECUTION) String action, @QueryParam(Constants.EXECUTION) String action,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return processRequireAction(authSessionId, code, action, clientId, tabId); return processRequireAction(authSessionId, code, action, clientId, tabId, clientData);
} }
@Path(REQUIRED_ACTION) @Path(REQUIRED_ACTION)
@ -1057,14 +1073,15 @@ public class LoginActionsService {
@QueryParam(SESSION_CODE) final String code, @QueryParam(SESSION_CODE) final String code,
@QueryParam(Constants.EXECUTION) String action, @QueryParam(Constants.EXECUTION) String action,
@QueryParam(Constants.CLIENT_ID) String clientId, @QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) { @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); 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)) { if (!checks.verifyRequiredAction(action)) {
return checks.getResponse(); return checks.getResponse();
} }

View file

@ -18,8 +18,6 @@
package org.keycloak.services.resources; package org.keycloak.services.resources;
import java.net.URI;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo; 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, public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
String code, String clientId, String tabId) { 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);
} }

View file

@ -21,7 +21,6 @@ import static org.keycloak.services.managers.AuthenticationManager.authenticateI
import java.net.URI; import java.net.URI;
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
@ -40,7 +39,11 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie; 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.ErrorPage;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -72,12 +75,14 @@ public class SessionCodeChecks {
private final String code; private final String code;
private final String execution; private final String execution;
private final String clientId; private final String clientId;
private final ClientData clientData;
private final String tabId; private final String tabId;
private final String flowPath; private final String flowPath;
private final String authSessionId; private final String authSessionId;
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, 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.realm = realm;
this.uriInfo = uriInfo; this.uriInfo = uriInfo;
this.request = request; this.request = request;
@ -91,6 +96,7 @@ public class SessionCodeChecks {
this.tabId = tabId; this.tabId = tabId;
this.flowPath = flowPath; this.flowPath = flowPath;
this.authSessionId = authSessionId; this.authSessionId = authSessionId;
this.clientData = ClientData.decodeClientDataFromParameter(clientData);
} }
@ -148,6 +154,7 @@ public class SessionCodeChecks {
} }
if (client != null) { if (client != null) {
session.getContext().setClient(client); session.getContext().setClient(client);
setClientToEvent(client);
} }
@ -186,14 +193,32 @@ public class SessionCodeChecks {
AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(session, realm, false); AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(session, realm, false);
if (authResult != null && authResult.getSession() != null) { if (authResult != null && authResult.getSession() != null) {
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession) response = null;
.setSuccess(Messages.ALREADY_LOGGED_IN);
if (client == null) { if (client != null && clientData != null) {
loginForm.setAttribute(Constants.SKIP_LINK, true); 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))) { if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
if (latestFlowPath != null) { 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); logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION); 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); 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); logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
response = Response.status(Response.Status.FOUND).location(redirectUri).build(); response = Response.status(Response.Status.FOUND).location(redirectUri).build();
return false; return false;
@ -387,7 +414,6 @@ public class SessionCodeChecks {
String cook = RestartLoginCookie.getRestartCookie(session); String cook = RestartLoginCookie.getRestartCookie(session);
if (cook == null) { if (cook == null) {
event.error(Errors.COOKIE_NOT_FOUND);
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.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; 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); logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build(); return Response.status(Response.Status.FOUND).location(redirectUri).build();
} else { } else {
@ -431,17 +458,18 @@ public class SessionCodeChecks {
} }
ClientModel client = authSession.getClient(); ClientModel client = authSession.getClient();
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()); uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId())
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId()); .queryParam(Constants.TAB_ID, authSession.getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession));
URI redirect = uriBuilder.build(realm.getName()); URI redirect = uriBuilder.build(realm.getName());
return Response.status(302).location(redirect).build(); 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) return new AuthenticationFlowURLHelper(session, realm, uriInfo)
.getLastExecutionUrl(flowPath, executionId, clientId, tabId); .getLastExecutionUrl(flowPath, executionId, clientId, tabId, clientData);
} }

View file

@ -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) UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(flowPath); .path(flowPath);
@ -72,6 +72,7 @@ public class AuthenticationFlowURLHelper {
} }
uriBuilder.queryParam(Constants.CLIENT_ID, clientId); uriBuilder.queryParam(Constants.CLIENT_ID, clientId);
uriBuilder.queryParam(Constants.TAB_ID, tabId); uriBuilder.queryParam(Constants.TAB_ID, tabId);
uriBuilder.queryParam(Constants.CLIENT_DATA, clientData);
return uriBuilder.build(realm.getName()); return uriBuilder.build(realm.getName());
} }
@ -89,7 +90,8 @@ public class AuthenticationFlowURLHelper {
latestFlowPath = LoginActionsService.AUTHENTICATE_PATH; 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) { private String getExecutionId(AuthenticationSessionModel authSession) {

View file

@ -49,6 +49,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
return this; return this;
} }
public RealmAttributeUpdater setAccessCodeLifespanLogin(Integer accessCodeLifespanLogin) {
rep.setAccessCodeLifespanLogin(accessCodeLifespanLogin);
return this;
}
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) { public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
rep.setSsoSessionIdleTimeout(timeout); rep.setSsoSessionIdleTimeout(timeout);
return this; return this;

View file

@ -359,20 +359,23 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
// Click browser 'back' and then 'forward' and then continue // Click browser 'back' and then 'forward' and then continue
driver.navigate().back(); driver.navigate().back();
assertTrue(driver.getPageSource().contains("You are already logged in.")); loginExpiredPage.assertCurrent();
driver.navigate().forward(); // here a new execution ID is added to the URL using JS, see below driver.navigate().forward(); // here a new execution ID is added to the URL using JS, see below
idpConfirmLinkPage.assertCurrent(); idpConfirmLinkPage.assertCurrent();
// Click browser 'back' on review profile page // Click browser 'back' on review profile page
idpConfirmLinkPage.clickReviewProfile(); idpConfirmLinkPage.clickReviewProfile();
// Need to confirm again with htmlUnit due the JS not working correctly
if (driver instanceof HtmlUnitDriver) {
idpConfirmLinkPage.assertCurrent();
idpConfirmLinkPage.clickReviewProfile();
}
waitForPage(driver, "update account information", false); waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent(); updateAccountInformationPage.assertCurrent();
driver.navigate().back(); driver.navigate().back();
// JS-capable browsers (i.e. all except HtmlUnit) add a new execution ID to the URL which then causes the login expire page to appear (because the old ID and new ID don't match)
if (!(driver instanceof HtmlUnitDriver)) { loginExpiredPage.assertCurrent();
loginExpiredPage.assertCurrent(); loginExpiredPage.clickLoginContinueLink();
loginExpiredPage.clickLoginContinueLink();
}
waitForPage(driver, "update account information", false); waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent(); updateAccountInformationPage.assertCurrent();
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), "FirstName", "LastName"); updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), "FirstName", "LastName");

View file

@ -0,0 +1,278 @@
/*
* 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.OAuthErrorException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
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.AssertEvents.DEFAULT_REDIRECT_URI;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -54,6 +54,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
realm.setEnabled(true); realm.setEnabled(true);
realm.setRealm(REALM_PROV_NAME); realm.setRealm(REALM_PROV_NAME);
realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
return realm; return realm;
} }

View file

@ -1,10 +1,13 @@
package org.keycloak.testsuite.broker; package org.keycloak.testsuite.broker;
import java.util.Collections;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient;
@ -28,7 +31,15 @@ public class KcSamlBrokerDestinationTest extends AbstractBrokerTest {
@Override @Override
protected BrokerConfiguration getBrokerConfiguration() { 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 @Test

View file

@ -29,6 +29,7 @@ import java.security.KeyManagementException;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -68,6 +69,13 @@ public final class KcSamlBrokerFrontendUrlTest extends AbstractBrokerTest {
return realm; return realm;
} }
@Override
public RealmRepresentation createProviderRealm() {
RealmRepresentation realm = super.createProviderRealm();
realm.setEventsListeners(Collections.singletonList("jboss-logging"));
return realm;
}
@Override @Override
public List<ClientRepresentation> createProviderClients() { public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> clients = super.createProviderClients(); List<ClientRepresentation> clients = super.createProviderClients();

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -775,7 +775,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError()); Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
setTimeOffset(0); 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(); .assertEvent();
} }
@ -795,7 +795,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true") .detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent(); .assertEvent();
} }
@ -852,7 +851,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
events.expect(EventType.LOGIN_ERROR) events.expect(EventType.LOGIN_ERROR)
.user(new UserRepresentation()) .user(new UserRepresentation())
.client(new ClientRepresentation())
.error(Errors.COOKIE_NOT_FOUND) .error(Errors.COOKIE_NOT_FOUND)
.assertEvent(); .assertEvent();

View file

@ -18,11 +18,13 @@
package org.keycloak.testsuite.forms; package org.keycloak.testsuite.forms;
import static org.hamcrest.MatcherAssert.assertThat; 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.ServerURLs.getAuthServerContextRoot;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.List;
import org.hamcrest.MatcherAssert; import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
@ -31,9 +33,15 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; 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.Constants;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils; 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.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; 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.OAuthGrantPage;
import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage; import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.BrowserTabUtil; import org.keycloak.testsuite.util.BrowserTabUtil;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
@ -99,6 +109,9 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
@Rule @Rule
public GreenMailRule greenMail = new GreenMailRule(); public GreenMailRule greenMail = new GreenMailRule();
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Page @Page
protected AppPage appPage; protected AppPage appPage;
@ -154,13 +167,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent(); loginPage.assertCurrent();
// Login in tab2 // Login in tab2
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
// Try to go back to tab 1. We should be logged-in automatically // Try to go back to tab 1. We should be logged-in automatically
tabUtil.closeTab(1); 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 @Test
public void testLoginAfterLogoutFromDifferentTab() { public void testLoginAfterLogoutFromDifferentTab() {
try (BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver)) { try (BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
// login in the first tab // login in the first tab
oauth.openLoginForm(); oauth.openLoginForm();
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
String tab1WindowHandle = util.getActualWindowHandle(); String tab1WindowHandle = util.getActualWindowHandle();
updatePasswordPage.changePassword("password", "password"); loginSuccessAndDoRequiredActions();
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken()); AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
@ -257,11 +441,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent(); loginPage.assertCurrent();
// Login success now // Login success now
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
} }
@ -282,11 +462,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError()); Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError());
// Login success now // Login success now
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
} }
@ -374,13 +550,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
// Go back to tab1 and finish login here // Go back to tab1 and finish login here
driver.navigate().to(tab1Url); driver.navigate().to(tab1Url);
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
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();
// Go back to tab2 and finish login here. Should be on the root-url-client page // Go back to tab2 and finish login here. Should be on the root-url-client page
driver.navigate().to(tab2Url); driver.navigate().to(tab2Url);
@ -410,10 +580,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
// Go back to tab1 and finish login here // Go back to tab1 and finish login here
driver.navigate().to(tab1Url); driver.navigate().to(tab1Url);
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
// Assert I am redirected to the appPage in tab1 and have state corresponding to tab1 // Assert I am redirected to the appPage in tab1 and have state corresponding to tab1
appPage.assertCurrent(); appPage.assertCurrent();
@ -444,10 +611,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
String tab2Url = driver.getCurrentUrl(); String tab2Url = driver.getCurrentUrl();
// Continue in tab2 and finish login here // Continue in tab2 and finish login here
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
// Assert I am redirected to the appPage in tab2 and have state corresponding to tab2 // Assert I am redirected to the appPage in tab2 and have state corresponding to tab2
appPage.assertCurrent(); appPage.assertCurrent();
@ -490,13 +654,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent(); loginPage.assertCurrent();
// Login in tab2 // Login in tab2
loginPage.login("login-test", "password"); loginSuccessAndDoRequiredActions();
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
// Try to go back to tab 1. We should be logged-in automatically // Try to go back to tab 1. We should be logged-in automatically
tabUtil.closeTab(1); tabUtil.closeTab(1);

View file

@ -133,7 +133,6 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true") .detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent(); .assertEvent();
} }
@ -173,7 +172,6 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true") .detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent(); .assertEvent();
} }
} }