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

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

View file

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

View file

@ -69,6 +69,15 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/
Response cancelled(IdentityProviderModel idpConfig);
/**
* Indicates that login with the particular IDP should be retried
*
* @param identityProvider provider to retry login
* @param authSession authentication session
* @return see description
*/
Response retryLogin(IdentityProvider<?> identityProvider, AuthenticationSessionModel authSession);
/**
* Called when error happened on the IDP side.
* Assumption is that authenticationSession is set in the {@link org.keycloak.models.KeycloakContext} when this method is called
@ -155,4 +164,11 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
default boolean reloadKeys() {
return false;
}
/**
* @return true if identity provider supports long value of "state" parameter (or "RelayState" parameter), which can hold relatively big amount of context data
*/
default boolean supportsLongStateParameter() {
return true;
}
}

View file

@ -17,13 +17,12 @@
package org.keycloak.broker.provider.util;
import org.keycloak.authorization.policy.evaluation.Realm;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.common.util.Base64Url;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
@ -38,9 +37,10 @@ public class IdentityBrokerState {
private static final Pattern DOT = Pattern.compile("\\.");
public static IdentityBrokerState decoded(String state, String clientId, String clientClientId, String tabId) {
public static IdentityBrokerState decoded(String state, String clientId, String clientClientId, String tabId, String clientData) {
String clientIdEncoded = clientClientId; // Default use the client.clientId
boolean isUuid = false;
if (clientId != null) {
// According to (http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) there is a limit on the relaystate of 80 bytes.
// in order to try to adher to the SAML specification we use an encoded value of the client.id (probably UUID) instead of the with
@ -52,22 +52,31 @@ public class IdentityBrokerState {
bb.putLong(clientDbUuid.getLeastSignificantBits());
byte[] clientUuidBytes = bb.array();
clientIdEncoded = Base64Url.encode(clientUuidBytes);
isUuid = true;
} catch (RuntimeException e) {
// Ignore...the clientid in the database was not in UUID format. Just use as is.
}
}
if (!isUuid && clientIdEncoded != null) {
clientIdEncoded = Base64Url.encode(clientIdEncoded.getBytes(StandardCharsets.UTF_8));
}
String encodedState = state + "." + tabId + "." + clientIdEncoded;
if (clientData != null) {
encodedState = encodedState + "." + clientData;
}
return new IdentityBrokerState(state, clientClientId, tabId, encodedState);
return new IdentityBrokerState(state, clientClientId, tabId, clientData, encodedState);
}
public static IdentityBrokerState encoded(String encodedState, RealmModel realmModel) {
String[] decoded = DOT.split(encodedState, 3);
String[] decoded = DOT.split(encodedState, 4);
String state =(decoded.length > 0) ? decoded[0] : null;
String tabId = (decoded.length > 1) ? decoded[1] : null;
String clientId = (decoded.length > 2) ? decoded[2] : null;
String clientData = (decoded.length > 3) ? decoded[3] : null;
boolean isUuid = false;
if (clientId != null) {
try {
@ -82,13 +91,17 @@ public class IdentityBrokerState {
ClientModel clientModel = realmModel.getClientById(clientIdInDb);
if (clientModel != null) {
clientId = clientModel.getClientId();
isUuid = true;
}
} catch (RuntimeException e) {
// Ignore...the clientid was not in encoded uuid format. Just use as it is.
}
if (!isUuid) {
clientId = new String(Base64Url.decode(clientId), StandardCharsets.UTF_8);
}
}
return new IdentityBrokerState(state, clientId, tabId, encodedState);
return new IdentityBrokerState(state, clientId, tabId, clientData, encodedState);
}
@ -96,14 +109,16 @@ public class IdentityBrokerState {
private final String decodedState;
private final String clientId;
private final String tabId;
private final String clientData;
// Encoded form of whole state
private final String encoded;
private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String encoded) {
private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String clientData, String encoded) {
this.decodedState = decodedStateParam;
this.clientId = clientId;
this.tabId = tabId;
this.clientData = clientData;
this.encoded = encoded;
}
@ -120,6 +135,10 @@ public class IdentityBrokerState {
return tabId;
}
public String getClientData() {
return clientData;
}
public String getEncoded() {
return encoded;
}

View file

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

View file

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

View file

@ -80,6 +80,7 @@ public final class Constants {
public static final String EXECUTION = "execution";
public static final String CLIENT_ID = "client_id";
public static final String TAB_ID = "tab_id";
public static final String CLIENT_DATA = "client_data";
public static final String SKIP_LOGOUT = "skip_logout";
public static final String KEY = "key";
@ -164,4 +165,7 @@ public final class Constants {
public static final String USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled";
public static final String TOTP_SECRET_KEY = "TOTP_SECRET_KEY";
// Sent to clients when authentication session expired, but user is already logged-in in current browser
public static final String AUTHENTICATION_EXPIRED_MESSAGE = "authentication_expired";
}

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.
*/
CANCELLED_AIA_SILENT,
/**
* User is already logged-in and he has userSession in this browser. But authenticationSession is not valid anymore and hence could not continue authentication
* in proper way. Will need to redirect back to client, so client can retry authentication. Once client retries authentication, it will usually success automatically
* due SSO reauthentication.
*/
ALREADY_LOGGED_IN,
/**
* Consent denied by the user
*/
@ -80,6 +86,32 @@ public interface LoginProtocol extends Provider {
Response sendError(AuthenticationSessionModel authSession, Error error);
/**
* Returns client data, which will be wrapped in the "clientData" parameter sent within "authentication flow" requests. The purpose of clientData is to be able to send HTTP error
* response back to the client if authentication fails due some error and authenticationSession is not available anymore (was either expired or removed). So clientData need to contain
* all the data to be able to send such response. For instance redirect-uri, state in case of OIDC or RelayState in case of SAML etc.
*
* @param authSession session from which particular clientData can be retrieved
* @return client data, which will be wrapped in the "clientData" parameter sent within "authentication flow" requests
*/
ClientData getClientData(AuthenticationSessionModel authSession);
/**
* Send the specified error to the specified client with the use of this protocol. ClientData can contain additional metadata about how to send error response to the
* client in a correct way for particular protocol. For instance redirect-uri where to send error, state to be used in OIDC authorization endpoint response etc.
*
* This method is usually used when we don't have authenticationSession anymore (it was removed or expired) as otherwise it is recommended to use {@link #sendError(AuthenticationSessionModel, Error)}
*
* NOTE: This method should also validate if provided clientData are valid according to given client (for instance if redirect-uri is valid) as clientData is request parameter, which
* can be injected to HTTP URLs by anyone.
*
* @param client client where to send error
* @param clientData clientData with additional protocol specific metadata needed for being able to properly send error with the use of this protocol
* @param error error to be used
* @return response if error was sent. Null if error was not sent.
*/
Response sendError(ClientModel client, ClientData clientData, Error error);
Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);

View file

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

View file

@ -40,6 +40,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.light.LightweightUserAdapter;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.TokenManager;
@ -282,11 +283,22 @@ public class AuthenticationProcessor {
getAuthenticationSession().setAuthenticatedUser(null);
}
private String getClientData() {
return getClientData(getSession(), getAuthenticationSession());
}
public static String getClientData(KeycloakSession session, AuthenticationSessionModel authSession) {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol());
ClientData clientData = protocol.getClientData(authSession);
return clientData.encode();
}
public URI getRefreshUrl(boolean authSessionIdParam) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath)
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
.queryParam(Constants.CLIENT_DATA, getClientData());
if (authSessionIdParam) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
}
@ -571,7 +583,8 @@ public class AuthenticationProcessor {
.queryParam(LoginActionsService.SESSION_CODE, code)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
.queryParam(Constants.CLIENT_DATA, getClientData());
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
}
@ -585,7 +598,8 @@ public class AuthenticationProcessor {
.queryParam(Constants.KEY, tokenString)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
.queryParam(Constants.CLIENT_DATA, getClientData());
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
}
@ -599,7 +613,8 @@ public class AuthenticationProcessor {
.path(AuthenticationProcessor.this.flowPath)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
.queryParam(Constants.CLIENT_DATA, getClientData());
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
}
@ -950,6 +965,9 @@ public class AuthenticationProcessor {
}
clone.setAuthNote(FORKED_FROM, authSession.getTabId());
if (authSession.getAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS) != null) {
clone.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, authSession.getAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS));
}
logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s",
clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId());

View file

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

View file

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

View file

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

View file

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

View file

@ -97,7 +97,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
authSession.getClient().getClientId(), authSession.getTabId());
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
String confirmUri = builder.build(realm.getName()).toString();
return session.getProvider(LoginFormsProvider.class)

View file

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

View file

@ -20,6 +20,7 @@ package org.keycloak.authentication.authenticators.broker;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
@ -133,7 +134,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias(), authSession.getClient().getClientId()
);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
authSession.getClient().getClientId(), authSession.getTabId());
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
String link = builder
.queryParam(Constants.EXECUTION, context.getExecution().getId())
.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 clientId = context.getAuthenticationSession().getClient().getClientId();
String tabId = context.getAuthenticationSession().getTabId();
URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId);
String clientData = AuthenticationProcessor.getClientData(context.getSession(), context.getAuthenticationSession());
URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId, clientData);
Response response = Response.seeOther(location)
.build();
// will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none.

View file

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

View file

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

View file

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

View file

@ -42,6 +42,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -447,7 +448,11 @@ public class SAMLEndpoint {
if (! isSuccessfulSamlResponse(responseType)) {
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()) {
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);

View file

@ -519,4 +519,10 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
}
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.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
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.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme;
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
@ -367,6 +369,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
if (authenticationSession != null) {
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;
}

View file

@ -79,10 +79,6 @@ public class UrlBean {
return Urls.realmRegisterPage(baseURI, realm).toString();
}
public String getLoginUpdateProfileUrl() {
return Urls.loginActionUpdateProfile(baseURI, realm).toString();
}
public String getLoginResetCredentialsUrl() {
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.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapperUtils;
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();
}
@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
public Response backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
return errorResponse(userSession, "backchannelLogout");

View file

@ -31,8 +31,6 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
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.ClientModel;
import org.keycloak.models.ClientSessionContext;
@ -40,26 +38,26 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ClientData;
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.OIDCRedirectUriBuilder;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessTokenResponse;
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.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException;
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.AuthenticationSessionManager;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@ -321,13 +319,23 @@ public class OIDCLoginProtocol implements LoginProtocol {
String redirect = authSession.getRedirectUri();
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);
if (error != Error.CANCELLED_AIA_SILENT) {
redirectUri.addParam(OAuth2Constants.ERROR, translateError(error));
OAuth2ErrorRepresentation oauthError = translateError(error);
if (oauthError.getError() != null) {
redirectUri.addParam(OAuth2Constants.ERROR, oauthError.getError());
}
if (error == Error.CANCELLED_AIA) {
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, "User cancelled aplication-initiated action.");
if (oauthError.getErrorDescription() != null) {
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, oauthError.getErrorDescription());
}
if (state != null) {
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()));
}
// Remove authenticationSession from current tab
new AuthenticationSessionManager(session).removeTabIdInAuthenticationSession(realm, authSession);
return redirectUri;
}
@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();
}
private String translateError(Error error) {
private OAuth2ErrorRepresentation translateError(Error error) {
switch (error) {
case CANCELLED_BY_USER:
case CANCELLED_AIA_SILENT:
return new OAuth2ErrorRepresentation(null, null);
case CANCELLED_AIA:
return new OAuth2ErrorRepresentation(OAuthErrorException.ACCESS_DENIED, "User cancelled aplication-initiated action.");
case CANCELLED_BY_USER:
case CONSENT_DENIED:
return OAuthErrorException.ACCESS_DENIED;
return new OAuth2ErrorRepresentation(OAuthErrorException.ACCESS_DENIED, null);
case PASSIVE_INTERACTION_REQUIRED:
return OAuthErrorException.INTERACTION_REQUIRED;
return new OAuth2ErrorRepresentation(OAuthErrorException.INTERACTION_REQUIRED, null);
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:
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;
import org.keycloak.protocol.ClientData;
import org.keycloak.rar.AuthorizationRequestContext;
import java.util.HashMap;
@ -66,6 +67,14 @@ public class AuthorizationEndpointRequest {
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() {
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.StatusResponseType;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
@ -50,9 +52,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapper;
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.SAMLAttributeStatementMapper;
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.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ResourceAdminManager;
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(
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);
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();
if (samlClient.requiresRealmSignature()) {
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) {
case CANCELLED_BY_USER:
case CANCELLED_AIA:
case CONSENT_DENIED:
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
case PASSIVE_INTERACTION_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:
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) {
return samlErrorMessage(null, samlClient, isPostBinding(authSession), redirectUri,
JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState);
new SAMLError(JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, null), relayState);
}
builder.nameIdentifier(nameIdFormat, nameId);
@ -1061,4 +1098,11 @@ public class SamlProtocol implements LoginProtocol {
.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);
}
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")
.path(IdentityBrokerService.class, "performLogin");
@ -65,6 +65,9 @@ public class Urls {
if (tabId != null) {
uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId);
}
if (clientData != null) {
uriBuilder.replaceQueryParam(Constants.CLIENT_DATA, clientData);
}
return uriBuilder.build(realmName, providerAlias);
}
@ -84,23 +87,25 @@ public class Urls {
}
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")
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
.replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.TAB_ID, tabId)
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
.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")
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
.replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
.replaceQueryParam(Constants.TAB_ID, tabId)
.build(realmName);
}
@ -117,24 +122,16 @@ public class Urls {
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) {
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")
.queryParam(Constants.KEY, tokenString)
.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.TAB_ID, authSession.getTabId());
uriBuilder.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession));
if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());

View file

@ -329,7 +329,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
try {
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 (isDebugEnabled()) {
@ -352,10 +352,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Path("/{provider_alias}/login")
public Response performPostLogin(@PathParam("provider_alias") String providerAlias,
@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId,
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
return performLogin(providerAlias, code, clientId, tabId, loginHint);
return performLogin(providerAlias, code, clientId, tabId, clientData, loginHint);
}
@GET
@ -363,8 +364,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Path("/{provider_alias}/login")
public Response performLogin(@PathParam("provider_alias") String providerAlias,
@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.TAB_ID) String tabId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
this.event.detail(Details.IDENTITY_PROVIDER, providerAlias);
@ -373,7 +375,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
try {
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId);
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData);
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
@ -392,11 +394,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
IdentityProvider<?> identityProvider = providerFactory.create(session, identityProviderModel);
Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode));
Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, providerAlias, clientSessionCode));
if (response != null) {
if (isDebugEnabled()) {
logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider, response);
logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider.getConfig().getAlias(), response);
}
return response;
}
@ -409,6 +411,25 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST);
}
@Override
public Response retryLogin(IdentityProvider<?> identityProvider, AuthenticationSessionModel authSession) {
ClientSessionCode<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")
public Object getEndpoint(@PathParam("provider_alias") String providerAlias) {
IdentityProvider identityProvider;
@ -600,6 +621,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
URI redirect = LoginActionsService.firstBrokerLoginProcessor(session.getContext().getUri())
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession))
.build(realmModel.getName());
return Response.status(302).location(redirect).build();
@ -640,9 +662,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@NoCache
@Path("/after-first-broker-login")
public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) {
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId);
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData);
return afterFirstBrokerLogin(authSession);
}
@ -756,6 +779,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
URI redirect = LoginActionsService.postBrokerLoginProcessor(session.getContext().getUri())
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
.queryParam(Constants.TAB_ID, authSession.getTabId())
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession))
.build(realmModel.getName());
return Response.status(302).location(redirect).build();
}
@ -767,9 +791,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@NoCache
@Path("/after-post-broker-login")
public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.CLIENT_ID) String clientId,
@QueryParam(Constants.CLIENT_DATA) String clientData,
@QueryParam(Constants.TAB_ID) String tabId) {
AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId);
AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId, clientData);
try {
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
@ -1064,20 +1089,21 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
String code = state.getDecodedState();
String clientId = state.getClientId();
String tabId = state.getTabId();
return parseSessionCode(code, clientId, tabId);
String clientData = state.getClientData();
return parseSessionCode(code, clientId, tabId, clientData);
}
/**
* This method will throw JAX-RS exception in case it is not able to retrieve AuthenticationSessionModel. It never returns null
*/
private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId) {
private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId, String clientData) {
if (code == null || clientId == null || tabId == null) {
logger.debugf("Invalid request. Authorization code, clientId or tabId was null. Code=%s, clientId=%s, tabID=%s", code, clientId, tabId);
Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
throw new WebApplicationException(staleCodeError);
}
SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, clientData, LoginActionsService.AUTHENTICATE_PATH);
checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
@ -1144,14 +1170,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return null;
}
private AuthenticationRequest createAuthenticationRequest(String providerAlias, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
private AuthenticationRequest createAuthenticationRequest(IdentityProvider<?> identityProvider, String providerAlias, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
AuthenticationSessionModel authSession = null;
IdentityBrokerState encodedState = null;
if (clientSessionCode != null) {
authSession = clientSessionCode.getClientSession();
String relayState = clientSessionCode.getOrGenerateCode();
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId());
String clientData = identityProvider.supportsLongStateParameter() ? AuthenticationProcessor.getClientData(session, authSession) : null;
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId(), clientData);
}
return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.session.getContext().getUri(), encodedState, getRedirectUri(providerAlias));

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

View file

@ -18,8 +18,6 @@
package org.keycloak.services.resources;
import java.net.URI;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
@ -42,7 +40,7 @@ public class LogoutSessionCodeChecks extends SessionCodeChecks {
public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
String code, String clientId, String tabId) {
super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null);
super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null, null);
}

View file

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

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

View file

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

View file

@ -359,20 +359,23 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
// Click browser 'back' and then 'forward' and then continue
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
idpConfirmLinkPage.assertCurrent();
// Click browser 'back' on review profile page
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);
updateAccountInformationPage.assertCurrent();
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.clickLoginContinueLink();
}
loginExpiredPage.assertCurrent();
loginExpiredPage.clickLoginContinueLink();
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
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.setRealm(REALM_PROV_NAME);
realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
return realm;
}

View file

@ -1,10 +1,13 @@
package org.keycloak.testsuite.broker;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.SamlClient;
@ -28,7 +31,15 @@ public class KcSamlBrokerDestinationTest extends AbstractBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcSamlBrokerConfiguration.INSTANCE;
return new KcSamlBrokerConfiguration() {
@Override
public RealmRepresentation createProviderRealm() {
RealmRepresentation realm = super.createProviderRealm();
realm.setEventsListeners(Collections.singletonList("jboss-logging"));
return realm;
}
};
}
@Test

View file

@ -29,6 +29,7 @@ import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -68,6 +69,13 @@ public final class KcSamlBrokerFrontendUrlTest extends AbstractBrokerTest {
return realm;
}
@Override
public RealmRepresentation createProviderRealm() {
RealmRepresentation realm = super.createProviderRealm();
realm.setEventsListeners(Collections.singletonList("jboss-logging"));
return realm;
}
@Override
public List<ClientRepresentation> 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());
setTimeOffset(0);
events.expectLogin().client((String) null).user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.assertEvent();
}
@ -795,7 +795,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent();
}
@ -852,7 +851,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
events.expect(EventType.LOGIN_ERROR)
.user(new UserRepresentation())
.client(new ClientRepresentation())
.error(Errors.COOKIE_NOT_FOUND)
.assertEvent();

View file

@ -18,11 +18,13 @@
package org.keycloak.testsuite.forms;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.AssertEvents.DEFAULT_REDIRECT_URI;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
@ -31,9 +33,15 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -54,9 +62,11 @@ import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.BrowserTabUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
@ -99,6 +109,9 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Page
protected AppPage appPage;
@ -154,13 +167,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent();
// Login in tab2
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
loginSuccessAndDoRequiredActions();
// Try to go back to tab 1. We should be logged-in automatically
tabUtil.closeTab(1);
@ -177,18 +184,195 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
}
}
// Simulating scenario described in https://github.com/keycloak/keycloak/issues/24112
@Test
public void multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle() {
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
multipleTabsParallelLogin(tabUtil);
events.clear();
loginPage.login("login-test", "password");
assertOnAppPageWithAlreadyLoggedInError(EventType.LOGIN);
}
}
@Test
public void multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle_badRedirectUri() throws Exception {
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
multipleTabsParallelLogin(tabUtil);
// Remove redirectUri from the client
try (ClientAttributeUpdater cap = ClientAttributeUpdater.forClient(adminClient, "test", "test-app")
.setRedirectUris(List.of("https://foo"))
.update()) {
events.clear();
loginPage.login("login-test", "password");
events.expectLogin().user((String) null).session((String) null).error(Errors.INVALID_REDIRECT_URI)
.detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE)
.detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value())
.removeDetail(Details.CONSENT)
.removeDetail(Details.CODE_ID)
.assertEvent();
errorPage.assertCurrent(); // Page "You are already logged in." should not be here
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
}
}
}
private void multipleTabsParallelLogin(BrowserTabUtil tabUtil) {
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
oauth.openLoginForm();
loginPage.assertCurrent();
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
// Open new tab 2
tabUtil.newTab(oauth.getLoginFormUrl());
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
loginPage.assertCurrent();
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
// Wait until authentication session expires
setTimeOffset(7200000);
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
loginPage.login("login-test", "password");
loginPage.assertCurrent();
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
loginSuccessAndDoRequiredActions();
// Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in")
tabUtil.closeTab(1);
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
}
private void loginSuccessAndDoRequiredActions() {
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
}
// Assert browser was redirected to the appPage with "error=temporarily_unavailable" and error_description corresponding to Constants.AUTHENTICATION_EXPIRED_MESSAGE
private void assertOnAppPageWithAlreadyLoggedInError(EventType expectedEventType) {
events.expect(expectedEventType)
.user((String) null).error(Errors.ALREADY_LOGGED_IN)
.detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI))
.detail(Details.REDIRECTED_TO_CLIENT, "true")
.detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE)
.detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value())
.assertEvent();
appPage.assertCurrent(); // Page "You are already logged in." should not be here
OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
Assert.assertEquals(OAuthErrorException.TEMPORARILY_UNAVAILABLE, authzResponse.getError());
Assert.assertEquals(Constants.AUTHENTICATION_EXPIRED_MESSAGE, authzResponse.getErrorDescription());
}
@Test
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRegisterClick() {
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
multipleTabsParallelLogin(tabUtil);
events.clear();
loginPage.clickRegister();
assertOnAppPageWithAlreadyLoggedInError(EventType.REGISTER);
}
}
@Test
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndResetPasswordClick() {
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
multipleTabsParallelLogin(tabUtil);
events.clear();
loginPage.resetPassword();
assertOnAppPageWithAlreadyLoggedInError(EventType.RESET_PASSWORD);
}
}
@Test
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRequiredAction() {
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
// Go through login in tab1 until required actions are shown
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
oauth.openLoginForm();
loginPage.assertCurrent();
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
// Open new tab 2
tabUtil.newTab(oauth.getLoginFormUrl());
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
loginPage.assertCurrent();
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
// Wait until authentication session expires
setTimeOffset(7200000);
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
loginPage.login("login-test", "password");
loginPage.assertCurrent();
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
loginSuccessAndDoRequiredActions();
// Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in")
tabUtil.closeTab(1);
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
events.clear();
updatePasswordPage.changePassword("password", "password");
assertOnAppPageWithAlreadyLoggedInError(EventType.CUSTOM_REQUIRED_ACTION);
}
}
@Test
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRefreshInTab1() {
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
// Go through login in tab1 and do unsuccessful login attempt (to make sure that "action URL" is shown in browser URL instead of OIDC authentication request URL)
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
oauth.openLoginForm();
loginPage.assertCurrent();
loginPage.login("login-test", "bad-password");
loginPage.assertCurrent();
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
// Open new tab 2
tabUtil.newTab(oauth.getLoginFormUrl());
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
loginPage.assertCurrent();
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
// Wait until authentication session expires
setTimeOffset(7200000);
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
loginPage.login("login-test", "password");
loginPage.assertCurrent();
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
loginSuccessAndDoRequiredActions();
// Go back to tab1 and refresh the page. Should be automatically authenticated here (previously it showed "You are already logged-in")
tabUtil.closeTab(1);
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
events.clear();
driver.navigate().refresh();
assertOnAppPageWithAlreadyLoggedInError(EventType.LOGIN);
}
}
@Test
public void testLoginAfterLogoutFromDifferentTab() {
try (BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
// login in the first tab
oauth.openLoginForm();
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
String tab1WindowHandle = util.getActualWindowHandle();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
loginSuccessAndDoRequiredActions();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
@ -257,11 +441,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent();
// Login success now
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
loginSuccessAndDoRequiredActions();
}
@ -282,11 +462,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError());
// Login success now
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
loginSuccessAndDoRequiredActions();
}
@ -374,13 +550,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
// Go back to tab1 and finish login here
driver.navigate().to(tab1Url);
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
// Assert I am redirected to the appPage in tab1
appPage.assertCurrent();
loginSuccessAndDoRequiredActions();
// Go back to tab2 and finish login here. Should be on the root-url-client page
driver.navigate().to(tab2Url);
@ -410,10 +580,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
// Go back to tab1 and finish login here
driver.navigate().to(tab1Url);
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
loginSuccessAndDoRequiredActions();
// Assert I am redirected to the appPage in tab1 and have state corresponding to tab1
appPage.assertCurrent();
@ -444,10 +611,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
String tab2Url = driver.getCurrentUrl();
// Continue in tab2 and finish login here
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
loginSuccessAndDoRequiredActions();
// Assert I am redirected to the appPage in tab2 and have state corresponding to tab2
appPage.assertCurrent();
@ -490,13 +654,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
loginPage.assertCurrent();
// Login in tab2
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
.email("john@doe3.com").submit();
appPage.assertCurrent();
loginSuccessAndDoRequiredActions();
// Try to go back to tab 1. We should be logged-in automatically
tabUtil.closeTab(1);

View file

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