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:
parent
2c5eebc8d2
commit
335a10fead
49 changed files with 1349 additions and 210 deletions
|
@ -408,7 +408,8 @@ function Keycloak (config) {
|
||||||
var callbackState = {
|
var callbackState = {
|
||||||
state: state,
|
state: state,
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
redirectUri: encodeURIComponent(redirectUri)
|
redirectUri: encodeURIComponent(redirectUri),
|
||||||
|
loginOptions: options
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options && options.prompt) {
|
if (options && options.prompt) {
|
||||||
|
@ -752,9 +753,13 @@ function Keycloak (config) {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (prompt != 'none') {
|
if (prompt != 'none') {
|
||||||
|
if (oauth.error_description && oauth.error_description === "authentication_expired") {
|
||||||
|
kc.login(oauth.loginOptions);
|
||||||
|
} else {
|
||||||
var errorData = { error: error, error_description: oauth.error_description };
|
var errorData = { error: error, error_description: oauth.error_description };
|
||||||
kc.onAuthError && kc.onAuthError(errorData);
|
kc.onAuthError && kc.onAuthError(errorData);
|
||||||
promise && promise.setError(errorData);
|
promise && promise.setError(errorData);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
promise && promise.setSuccess();
|
promise && promise.setSuccess();
|
||||||
}
|
}
|
||||||
|
@ -1062,6 +1067,7 @@ function Keycloak (config) {
|
||||||
oauth.storedNonce = oauthState.nonce;
|
oauth.storedNonce = oauthState.nonce;
|
||||||
oauth.prompt = oauthState.prompt;
|
oauth.prompt = oauthState.prompt;
|
||||||
oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
|
oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
|
||||||
|
oauth.loginOptions = oauthState.loginOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
return oauth;
|
return oauth;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
|
@ -39,6 +40,7 @@ import org.w3c.dom.Document;
|
||||||
public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ErrorResponseBuilder> {
|
public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ErrorResponseBuilder> {
|
||||||
|
|
||||||
protected String status;
|
protected String status;
|
||||||
|
protected String statusMessage;
|
||||||
protected String destination;
|
protected String destination;
|
||||||
protected NameIDType issuer;
|
protected NameIDType issuer;
|
||||||
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
protected final List<NodeGenerator> extensions = new LinkedList<>();
|
||||||
|
@ -48,6 +50,11 @@ public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBui
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SAML2ErrorResponseBuilder statusMessage(String statusMessage) {
|
||||||
|
this.statusMessage = statusMessage;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SAML2ErrorResponseBuilder destination(String destination) {
|
public SAML2ErrorResponseBuilder destination(String destination) {
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
return this;
|
return this;
|
||||||
|
@ -73,7 +80,9 @@ public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBui
|
||||||
try {
|
try {
|
||||||
StatusResponseType statusResponse = new ResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
|
StatusResponseType statusResponse = new ResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
|
||||||
|
|
||||||
statusResponse.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status));
|
StatusType statusType = JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status);
|
||||||
|
statusType.setStatusMessage(statusMessage);
|
||||||
|
statusResponse.setStatus(statusType);
|
||||||
statusResponse.setIssuer(issuer);
|
statusResponse.setIssuer(issuer);
|
||||||
statusResponse.setDestination(destination);
|
statusResponse.setDestination(destination);
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,7 @@ public class SAMLResponseWriter extends BaseWriter {
|
||||||
String statusMessage = status.getStatusMessage();
|
String statusMessage = status.getStatusMessage();
|
||||||
if (StringUtil.isNotNull(statusMessage)) {
|
if (StringUtil.isNotNull(statusMessage)) {
|
||||||
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.STATUS_MESSAGE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get());
|
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.STATUS_MESSAGE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get());
|
||||||
|
StaxUtil.writeCharacters(writer, statusMessage);
|
||||||
StaxUtil.writeEndElement(writer);
|
StaxUtil.writeEndElement(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,15 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
||||||
*/
|
*/
|
||||||
Response cancelled(IdentityProviderModel idpConfig);
|
Response cancelled(IdentityProviderModel idpConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that login with the particular IDP should be retried
|
||||||
|
*
|
||||||
|
* @param identityProvider provider to retry login
|
||||||
|
* @param authSession authentication session
|
||||||
|
* @return see description
|
||||||
|
*/
|
||||||
|
Response retryLogin(IdentityProvider<?> identityProvider, AuthenticationSessionModel authSession);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when error happened on the IDP side.
|
* Called when error happened on the IDP side.
|
||||||
* Assumption is that authenticationSession is set in the {@link org.keycloak.models.KeycloakContext} when this method is called
|
* Assumption is that authenticationSession is set in the {@link org.keycloak.models.KeycloakContext} when this method is called
|
||||||
|
@ -155,4 +164,11 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
||||||
default boolean reloadKeys() {
|
default boolean reloadKeys() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if identity provider supports long value of "state" parameter (or "RelayState" parameter), which can hold relatively big amount of context data
|
||||||
|
*/
|
||||||
|
default boolean supportsLongStateParameter() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,12 @@
|
||||||
|
|
||||||
package org.keycloak.broker.provider.util;
|
package org.keycloak.broker.provider.util;
|
||||||
|
|
||||||
import org.keycloak.authorization.policy.evaluation.Realm;
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
|
||||||
import java.nio.BufferUnderflowException;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -38,9 +37,10 @@ public class IdentityBrokerState {
|
||||||
private static final Pattern DOT = Pattern.compile("\\.");
|
private static final Pattern DOT = Pattern.compile("\\.");
|
||||||
|
|
||||||
|
|
||||||
public static IdentityBrokerState decoded(String state, String clientId, String clientClientId, String tabId) {
|
public static IdentityBrokerState decoded(String state, String clientId, String clientClientId, String tabId, String clientData) {
|
||||||
|
|
||||||
String clientIdEncoded = clientClientId; // Default use the client.clientId
|
String clientIdEncoded = clientClientId; // Default use the client.clientId
|
||||||
|
boolean isUuid = false;
|
||||||
if (clientId != null) {
|
if (clientId != null) {
|
||||||
// According to (http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) there is a limit on the relaystate of 80 bytes.
|
// According to (http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) there is a limit on the relaystate of 80 bytes.
|
||||||
// in order to try to adher to the SAML specification we use an encoded value of the client.id (probably UUID) instead of the with
|
// in order to try to adher to the SAML specification we use an encoded value of the client.id (probably UUID) instead of the with
|
||||||
|
@ -52,22 +52,31 @@ public class IdentityBrokerState {
|
||||||
bb.putLong(clientDbUuid.getLeastSignificantBits());
|
bb.putLong(clientDbUuid.getLeastSignificantBits());
|
||||||
byte[] clientUuidBytes = bb.array();
|
byte[] clientUuidBytes = bb.array();
|
||||||
clientIdEncoded = Base64Url.encode(clientUuidBytes);
|
clientIdEncoded = Base64Url.encode(clientUuidBytes);
|
||||||
|
isUuid = true;
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
// Ignore...the clientid in the database was not in UUID format. Just use as is.
|
// Ignore...the clientid in the database was not in UUID format. Just use as is.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!isUuid && clientIdEncoded != null) {
|
||||||
|
clientIdEncoded = Base64Url.encode(clientIdEncoded.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
String encodedState = state + "." + tabId + "." + clientIdEncoded;
|
String encodedState = state + "." + tabId + "." + clientIdEncoded;
|
||||||
|
if (clientData != null) {
|
||||||
|
encodedState = encodedState + "." + clientData;
|
||||||
|
}
|
||||||
|
|
||||||
return new IdentityBrokerState(state, clientClientId, tabId, encodedState);
|
return new IdentityBrokerState(state, clientClientId, tabId, clientData, encodedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static IdentityBrokerState encoded(String encodedState, RealmModel realmModel) {
|
public static IdentityBrokerState encoded(String encodedState, RealmModel realmModel) {
|
||||||
String[] decoded = DOT.split(encodedState, 3);
|
String[] decoded = DOT.split(encodedState, 4);
|
||||||
|
|
||||||
String state =(decoded.length > 0) ? decoded[0] : null;
|
String state =(decoded.length > 0) ? decoded[0] : null;
|
||||||
String tabId = (decoded.length > 1) ? decoded[1] : null;
|
String tabId = (decoded.length > 1) ? decoded[1] : null;
|
||||||
String clientId = (decoded.length > 2) ? decoded[2] : null;
|
String clientId = (decoded.length > 2) ? decoded[2] : null;
|
||||||
|
String clientData = (decoded.length > 3) ? decoded[3] : null;
|
||||||
|
boolean isUuid = false;
|
||||||
|
|
||||||
if (clientId != null) {
|
if (clientId != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -82,13 +91,17 @@ public class IdentityBrokerState {
|
||||||
ClientModel clientModel = realmModel.getClientById(clientIdInDb);
|
ClientModel clientModel = realmModel.getClientById(clientIdInDb);
|
||||||
if (clientModel != null) {
|
if (clientModel != null) {
|
||||||
clientId = clientModel.getClientId();
|
clientId = clientModel.getClientId();
|
||||||
|
isUuid = true;
|
||||||
}
|
}
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
// Ignore...the clientid was not in encoded uuid format. Just use as it is.
|
// Ignore...the clientid was not in encoded uuid format. Just use as it is.
|
||||||
}
|
}
|
||||||
|
if (!isUuid) {
|
||||||
|
clientId = new String(Base64Url.decode(clientId), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new IdentityBrokerState(state, clientId, tabId, encodedState);
|
return new IdentityBrokerState(state, clientId, tabId, clientData, encodedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,14 +109,16 @@ public class IdentityBrokerState {
|
||||||
private final String decodedState;
|
private final String decodedState;
|
||||||
private final String clientId;
|
private final String clientId;
|
||||||
private final String tabId;
|
private final String tabId;
|
||||||
|
private final String clientData;
|
||||||
|
|
||||||
// Encoded form of whole state
|
// Encoded form of whole state
|
||||||
private final String encoded;
|
private final String encoded;
|
||||||
|
|
||||||
private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String encoded) {
|
private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String clientData, String encoded) {
|
||||||
this.decodedState = decodedStateParam;
|
this.decodedState = decodedStateParam;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.tabId = tabId;
|
this.tabId = tabId;
|
||||||
|
this.clientData = clientData;
|
||||||
this.encoded = encoded;
|
this.encoded = encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +135,10 @@ public class IdentityBrokerState {
|
||||||
return tabId;
|
return tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientData() {
|
||||||
|
return clientData;
|
||||||
|
}
|
||||||
|
|
||||||
public String getEncoded() {
|
public String getEncoded() {
|
||||||
return encoded;
|
return encoded;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ public interface Details {
|
||||||
String REQUESTED_ISSUER = "requested_issuer";
|
String REQUESTED_ISSUER = "requested_issuer";
|
||||||
String REQUESTED_SUBJECT = "requested_subject";
|
String REQUESTED_SUBJECT = "requested_subject";
|
||||||
String RESTART_AFTER_TIMEOUT = "restart_after_timeout";
|
String RESTART_AFTER_TIMEOUT = "restart_after_timeout";
|
||||||
|
String REDIRECTED_TO_CLIENT = "redirected_to_client";
|
||||||
|
String LOGIN_RETRY = "login_retry";
|
||||||
|
|
||||||
String CONSENT = "consent";
|
String CONSENT = "consent";
|
||||||
String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client
|
String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client
|
||||||
|
|
|
@ -67,6 +67,7 @@ public interface Errors {
|
||||||
String EXPIRED_CODE = "expired_code";
|
String EXPIRED_CODE = "expired_code";
|
||||||
String INVALID_INPUT = "invalid_input";
|
String INVALID_INPUT = "invalid_input";
|
||||||
String COOKIE_NOT_FOUND = "cookie_not_found";
|
String COOKIE_NOT_FOUND = "cookie_not_found";
|
||||||
|
String ALREADY_LOGGED_IN = "already_logged_in";
|
||||||
|
|
||||||
String TOKEN_INTROSPECTION_FAILED = "token_introspection_failed";
|
String TOKEN_INTROSPECTION_FAILED = "token_introspection_failed";
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,7 @@ public final class Constants {
|
||||||
public static final String EXECUTION = "execution";
|
public static final String EXECUTION = "execution";
|
||||||
public static final String CLIENT_ID = "client_id";
|
public static final String CLIENT_ID = "client_id";
|
||||||
public static final String TAB_ID = "tab_id";
|
public static final String TAB_ID = "tab_id";
|
||||||
|
public static final String CLIENT_DATA = "client_data";
|
||||||
|
|
||||||
public static final String SKIP_LOGOUT = "skip_logout";
|
public static final String SKIP_LOGOUT = "skip_logout";
|
||||||
public static final String KEY = "key";
|
public static final String KEY = "key";
|
||||||
|
@ -164,4 +165,7 @@ public final class Constants {
|
||||||
public static final String USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled";
|
public static final String USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled";
|
||||||
|
|
||||||
public static final String TOTP_SECRET_KEY = "TOTP_SECRET_KEY";
|
public static final String TOTP_SECRET_KEY = "TOTP_SECRET_KEY";
|
||||||
|
|
||||||
|
// Sent to clients when authentication session expired, but user is already logged-in in current browser
|
||||||
|
public static final String AUTHENTICATION_EXPIRED_MESSAGE = "authentication_expired";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,12 @@ public interface LoginProtocol extends Provider {
|
||||||
* Applications-initiated action was canceled by the user. Do not send error.
|
* Applications-initiated action was canceled by the user. Do not send error.
|
||||||
*/
|
*/
|
||||||
CANCELLED_AIA_SILENT,
|
CANCELLED_AIA_SILENT,
|
||||||
|
/**
|
||||||
|
* User is already logged-in and he has userSession in this browser. But authenticationSession is not valid anymore and hence could not continue authentication
|
||||||
|
* in proper way. Will need to redirect back to client, so client can retry authentication. Once client retries authentication, it will usually success automatically
|
||||||
|
* due SSO reauthentication.
|
||||||
|
*/
|
||||||
|
ALREADY_LOGGED_IN,
|
||||||
/**
|
/**
|
||||||
* Consent denied by the user
|
* Consent denied by the user
|
||||||
*/
|
*/
|
||||||
|
@ -80,6 +86,32 @@ public interface LoginProtocol extends Provider {
|
||||||
|
|
||||||
Response sendError(AuthenticationSessionModel authSession, Error error);
|
Response sendError(AuthenticationSessionModel authSession, Error error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns client data, which will be wrapped in the "clientData" parameter sent within "authentication flow" requests. The purpose of clientData is to be able to send HTTP error
|
||||||
|
* response back to the client if authentication fails due some error and authenticationSession is not available anymore (was either expired or removed). So clientData need to contain
|
||||||
|
* all the data to be able to send such response. For instance redirect-uri, state in case of OIDC or RelayState in case of SAML etc.
|
||||||
|
*
|
||||||
|
* @param authSession session from which particular clientData can be retrieved
|
||||||
|
* @return client data, which will be wrapped in the "clientData" parameter sent within "authentication flow" requests
|
||||||
|
*/
|
||||||
|
ClientData getClientData(AuthenticationSessionModel authSession);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the specified error to the specified client with the use of this protocol. ClientData can contain additional metadata about how to send error response to the
|
||||||
|
* client in a correct way for particular protocol. For instance redirect-uri where to send error, state to be used in OIDC authorization endpoint response etc.
|
||||||
|
*
|
||||||
|
* This method is usually used when we don't have authenticationSession anymore (it was removed or expired) as otherwise it is recommended to use {@link #sendError(AuthenticationSessionModel, Error)}
|
||||||
|
*
|
||||||
|
* NOTE: This method should also validate if provided clientData are valid according to given client (for instance if redirect-uri is valid) as clientData is request parameter, which
|
||||||
|
* can be injected to HTTP URLs by anyone.
|
||||||
|
*
|
||||||
|
* @param client client where to send error
|
||||||
|
* @param clientData clientData with additional protocol specific metadata needed for being able to properly send error with the use of this protocol
|
||||||
|
* @param error error to be used
|
||||||
|
* @return response if error was sent. Null if error was not sent.
|
||||||
|
*/
|
||||||
|
Response sendError(ClientModel client, ClientData clientData, Error error);
|
||||||
|
|
||||||
Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
|
Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
|
||||||
Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
|
Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,9 @@ package org.keycloak.broker.provider.util;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
|
|
||||||
|
|
||||||
public class IdentityBrokerStateTest {
|
public class IdentityBrokerStateTest {
|
||||||
|
@ -17,13 +19,13 @@ public class IdentityBrokerStateTest {
|
||||||
String tabId = "vpISZLVDAc0";
|
String tabId = "vpISZLVDAc0";
|
||||||
|
|
||||||
// When
|
// When
|
||||||
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId);
|
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, null);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.assertNotNull(encodedState);
|
Assert.assertNotNull(encodedState);
|
||||||
Assert.assertEquals(clientClientId, encodedState.getClientId());
|
Assert.assertEquals(clientClientId, encodedState.getClientId());
|
||||||
Assert.assertEquals(tabId, encodedState.getTabId());
|
Assert.assertEquals(tabId, encodedState.getTabId());
|
||||||
Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.http://i.am.an.url", encodedState.getEncoded());
|
Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs", encodedState.getEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -36,7 +38,7 @@ public class IdentityBrokerStateTest {
|
||||||
String tabId = "vpISZLVDAc0";
|
String tabId = "vpISZLVDAc0";
|
||||||
|
|
||||||
// When
|
// When
|
||||||
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId);
|
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, null);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.assertNotNull(encodedState);
|
Assert.assertNotNull(encodedState);
|
||||||
|
@ -53,15 +55,21 @@ public class IdentityBrokerStateTest {
|
||||||
String clientId = "c5ac1ea7-6c28-4be1-b7cd-d63a1ba57f78";
|
String clientId = "c5ac1ea7-6c28-4be1-b7cd-d63a1ba57f78";
|
||||||
String clientClientId = "http://i.am.an.url";
|
String clientClientId = "http://i.am.an.url";
|
||||||
String tabId = "vpISZLVDAc0";
|
String tabId = "vpISZLVDAc0";
|
||||||
|
String clientDataParam = new ClientData("https://my-redirect-uri", "code", "query", "some-state").encode();
|
||||||
|
|
||||||
// When
|
// When
|
||||||
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId);
|
IdentityBrokerState encodedState = IdentityBrokerState.decoded(state, clientId, clientClientId, tabId, clientDataParam);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.assertNotNull(encodedState);
|
Assert.assertNotNull(encodedState);
|
||||||
Assert.assertEquals(clientClientId, encodedState.getClientId());
|
Assert.assertEquals(clientClientId, encodedState.getClientId());
|
||||||
Assert.assertEquals(tabId, encodedState.getTabId());
|
Assert.assertEquals(tabId, encodedState.getTabId());
|
||||||
Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.xawep2woS-G3zdY6G6V_eA", encodedState.getEncoded());
|
ClientData clientData = ClientData.decodeClientDataFromParameter(encodedState.getClientData());
|
||||||
|
Assert.assertEquals("https://my-redirect-uri", clientData.getRedirectUri());
|
||||||
|
Assert.assertEquals("code", clientData.getResponseType());
|
||||||
|
Assert.assertEquals("query", clientData.getResponseMode());
|
||||||
|
Assert.assertEquals("some-state", clientData.getState());
|
||||||
|
Assert.assertEquals("gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.xawep2woS-G3zdY6G6V_eA.eyJydSI6Imh0dHBzOi8vbXktcmVkaXJlY3QtdXJpIiwicnQiOiJjb2RlIiwicm0iOiJxdWVyeSIsInN0Ijoic29tZS1zdGF0ZSJ9", encodedState.getEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -84,7 +92,7 @@ public class IdentityBrokerStateTest {
|
||||||
@Test
|
@Test
|
||||||
public void testEncodedWithClientIdNotUUid() {
|
public void testEncodedWithClientIdNotUUid() {
|
||||||
// Given
|
// Given
|
||||||
String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.http://i.am.an.url";
|
String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs";
|
||||||
String clientId = "http://i.am.an.url";
|
String clientId = "http://i.am.an.url";
|
||||||
ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId);
|
ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId);
|
||||||
RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel);
|
RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel);
|
||||||
|
@ -95,6 +103,29 @@ public class IdentityBrokerStateTest {
|
||||||
// Then
|
// Then
|
||||||
Assert.assertNotNull(decodedState);
|
Assert.assertNotNull(decodedState);
|
||||||
Assert.assertEquals("http://i.am.an.url", decodedState.getClientId());
|
Assert.assertEquals("http://i.am.an.url", decodedState.getClientId());
|
||||||
|
Assert.assertNull(ClientData.decodeClientDataFromParameter(decodedState.getClientData()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncodedWithClientData() {
|
||||||
|
// Given
|
||||||
|
String encoded = "gNrGamIDGKpKSI9yOrcFzYTKoFGH779_WNCacAelkhk.vpISZLVDAc0.aHR0cDovL2kuYW0uYW4udXJs.eyJydSI6Imh0dHBzOi8vbXktcmVkaXJlY3QtdXJpIiwicnQiOiJjb2RlIiwicm0iOiJxdWVyeSIsInN0Ijoic29tZS1zdGF0ZSJ9";
|
||||||
|
String clientId = "http://i.am.an.url";
|
||||||
|
ClientModel clientModel = new IdentityBrokerStateTestHelpers.TestClientModel(clientId, clientId);
|
||||||
|
RealmModel realmModel = new IdentityBrokerStateTestHelpers.TestRealmModel(clientId, clientId, clientModel);
|
||||||
|
|
||||||
|
// When
|
||||||
|
IdentityBrokerState decodedState = IdentityBrokerState.encoded(encoded, realmModel);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Assert.assertNotNull(decodedState);
|
||||||
|
Assert.assertEquals("http://i.am.an.url", decodedState.getClientId());
|
||||||
|
ClientData clientData = ClientData.decodeClientDataFromParameter(decodedState.getClientData());
|
||||||
|
Assert.assertNotNull(clientData);
|
||||||
|
Assert.assertEquals("https://my-redirect-uri", clientData.getRedirectUri());
|
||||||
|
Assert.assertEquals("code", clientData.getResponseType());
|
||||||
|
Assert.assertEquals("query", clientData.getResponseMode());
|
||||||
|
Assert.assertEquals("some-state", clientData.getState());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.light.LightweightUserAdapter;
|
import org.keycloak.models.light.LightweightUserAdapter;
|
||||||
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.LoginProtocol.Error;
|
import org.keycloak.protocol.LoginProtocol.Error;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
|
@ -282,11 +283,22 @@ public class AuthenticationProcessor {
|
||||||
getAuthenticationSession().setAuthenticatedUser(null);
|
getAuthenticationSession().setAuthenticatedUser(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getClientData() {
|
||||||
|
return getClientData(getSession(), getAuthenticationSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClientData(KeycloakSession session, AuthenticationSessionModel authSession) {
|
||||||
|
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol());
|
||||||
|
ClientData clientData = protocol.getClientData(authSession);
|
||||||
|
return clientData.encode();
|
||||||
|
}
|
||||||
|
|
||||||
public URI getRefreshUrl(boolean authSessionIdParam) {
|
public URI getRefreshUrl(boolean authSessionIdParam) {
|
||||||
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
|
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
|
||||||
.path(AuthenticationProcessor.this.flowPath)
|
.path(AuthenticationProcessor.this.flowPath)
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, getClientData());
|
||||||
if (authSessionIdParam) {
|
if (authSessionIdParam) {
|
||||||
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
||||||
}
|
}
|
||||||
|
@ -571,7 +583,8 @@ public class AuthenticationProcessor {
|
||||||
.queryParam(LoginActionsService.SESSION_CODE, code)
|
.queryParam(LoginActionsService.SESSION_CODE, code)
|
||||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, getClientData());
|
||||||
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
||||||
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
||||||
}
|
}
|
||||||
|
@ -585,7 +598,8 @@ public class AuthenticationProcessor {
|
||||||
.queryParam(Constants.KEY, tokenString)
|
.queryParam(Constants.KEY, tokenString)
|
||||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, getClientData());
|
||||||
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
||||||
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
||||||
}
|
}
|
||||||
|
@ -599,7 +613,8 @@ public class AuthenticationProcessor {
|
||||||
.path(AuthenticationProcessor.this.flowPath)
|
.path(AuthenticationProcessor.this.flowPath)
|
||||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, getClientData());
|
||||||
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
||||||
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
|
||||||
}
|
}
|
||||||
|
@ -950,6 +965,9 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
clone.setAuthNote(FORKED_FROM, authSession.getTabId());
|
clone.setAuthNote(FORKED_FROM, authSession.getTabId());
|
||||||
|
if (authSession.getAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS) != null) {
|
||||||
|
clone.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, authSession.getAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s",
|
logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s",
|
||||||
clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId());
|
clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId());
|
||||||
|
|
|
@ -271,6 +271,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
|
||||||
.queryParam(Constants.EXECUTION, executionId)
|
.queryParam(Constants.EXECUTION, executionId)
|
||||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||||
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
|
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(processor.getSession(), processor.getAuthenticationSession()))
|
||||||
.build(processor.getRealm().getName());
|
.build(processor.getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
||||||
.queryParam(Constants.EXECUTION, getExecution())
|
.queryParam(Constants.EXECUTION, getExecution())
|
||||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||||
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession))
|
||||||
.build(getRealm().getName());
|
.build(getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ProcessBrokerFlow {
|
public interface ProcessBrokerFlow {
|
||||||
Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath);
|
Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
@ -59,12 +59,13 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
private AuthenticationSessionModel authenticationSession;
|
private AuthenticationSessionModel authenticationSession;
|
||||||
private boolean authenticationSessionFresh;
|
private boolean authenticationSessionFresh;
|
||||||
private String executionId;
|
private String executionId;
|
||||||
|
private String clientData;
|
||||||
private final ProcessAuthenticateFlow processAuthenticateFlow;
|
private final ProcessAuthenticateFlow processAuthenticateFlow;
|
||||||
private final ProcessBrokerFlow processBrokerFlow;
|
private final ProcessBrokerFlow processBrokerFlow;
|
||||||
|
|
||||||
public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
|
public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
|
||||||
ClientConnection clientConnection, HttpRequest request,
|
ClientConnection clientConnection, HttpRequest request,
|
||||||
EventBuilder event, ActionTokenHandler<T> handler, String executionId,
|
EventBuilder event, ActionTokenHandler<T> handler, String executionId, String clientData,
|
||||||
ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
|
ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
|
@ -74,6 +75,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
this.event = event;
|
this.event = event;
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
this.executionId = executionId;
|
this.executionId = executionId;
|
||||||
|
this.clientData = clientData;
|
||||||
this.processAuthenticateFlow = processFlow;
|
this.processAuthenticateFlow = processFlow;
|
||||||
this.processBrokerFlow = processBrokerFlow;
|
this.processBrokerFlow = processBrokerFlow;
|
||||||
}
|
}
|
||||||
|
@ -162,6 +164,6 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
|
|
||||||
public Response brokerFlow(String authSessionId, String code, String flowPath) {
|
public Response brokerFlow(String authSessionId, String code, String flowPath) {
|
||||||
ClientModel client = authenticationSession.getClient();
|
ClientModel client = authenticationSession.getClient();
|
||||||
return processBrokerFlow.brokerLoginFlow(authSessionId, code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath);
|
return processBrokerFlow.brokerLoginFlow(authSessionId, code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), clientData, flowPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.authentication.actiontoken.execactions;
|
||||||
|
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
import org.keycloak.TokenVerifier.Predicate;
|
import org.keycloak.TokenVerifier.Predicate;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
import org.keycloak.authentication.actiontoken.*;
|
import org.keycloak.authentication.actiontoken.*;
|
||||||
|
@ -84,7 +85,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHandler
|
||||||
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
authSession.getClient().getClientId(), authSession.getTabId());
|
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
|
|
@ -97,7 +97,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
||||||
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
authSession.getClient().getClientId(), authSession.getTabId());
|
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authentication.actiontoken.verifyemail;
|
package org.keycloak.authentication.actiontoken.verifyemail;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;
|
import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;
|
||||||
import org.keycloak.TokenVerifier.Predicate;
|
import org.keycloak.TokenVerifier.Predicate;
|
||||||
import org.keycloak.authentication.actiontoken.*;
|
import org.keycloak.authentication.actiontoken.*;
|
||||||
|
@ -94,7 +95,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler<Ve
|
||||||
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
authSession.getClient().getClientId(), authSession.getTabId());
|
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.authentication.authenticators.broker;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.AuthenticationFlowError;
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
|
import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
|
||||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
|
@ -133,7 +134,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
||||||
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias(), authSession.getClient().getClientId()
|
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias(), authSession.getClient().getClientId()
|
||||||
);
|
);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
authSession.getClient().getClientId(), authSession.getTabId());
|
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
|
||||||
String link = builder
|
String link = builder
|
||||||
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
||||||
.build(realm.getName()).toString();
|
.build(realm.getName()).toString();
|
||||||
|
|
|
@ -83,7 +83,8 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
||||||
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
||||||
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
||||||
String tabId = context.getAuthenticationSession().getTabId();
|
String tabId = context.getAuthenticationSession().getTabId();
|
||||||
URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId);
|
String clientData = AuthenticationProcessor.getClientData(context.getSession(), context.getAuthenticationSession());
|
||||||
|
URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId, clientData);
|
||||||
Response response = Response.seeOther(location)
|
Response response = Response.seeOther(location)
|
||||||
.build();
|
.build();
|
||||||
// will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none.
|
// will forward the request to the IDP with prompt=none if the IDP accepts forwards with prompt=none.
|
||||||
|
|
|
@ -25,6 +25,7 @@ import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
import jakarta.ws.rs.core.UriInfo;
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.AuthenticatorUtil;
|
import org.keycloak.authentication.AuthenticatorUtil;
|
||||||
import org.keycloak.authentication.InitiatedActionSupport;
|
import org.keycloak.authentication.InitiatedActionSupport;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
@ -128,7 +129,7 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
|
|
||||||
String link = Urls
|
String link = Urls
|
||||||
.actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo),
|
.actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo),
|
||||||
authenticationSession.getClient().getClientId(), authenticationSession.getTabId())
|
authenticationSession.getClient().getClientId(), authenticationSession.getTabId(), AuthenticationProcessor.getClientData(session, authenticationSession))
|
||||||
|
|
||||||
.build(realm.getName()).toString();
|
.build(realm.getName()).toString();
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId());
|
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId());
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
authSession.getClient().getClientId(), authSession.getTabId());
|
authSession.getClient().getClientId(), authSession.getTabId(), AuthenticationProcessor.getClientData(session, authSession));
|
||||||
String link = builder.build(realm.getName()).toString();
|
String link = builder.build(realm.getName()).toString();
|
||||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ import org.keycloak.jose.jwk.JWKBuilder;
|
||||||
import org.keycloak.jose.jwk.RSAPublicJWK;
|
import org.keycloak.jose.jwk.RSAPublicJWK;
|
||||||
import org.keycloak.jose.jws.JWSBuilder;
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.KeycloakContext;
|
import org.keycloak.models.KeycloakContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -507,7 +508,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
@GET
|
@GET
|
||||||
public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_STATE) String state,
|
public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_STATE) String state,
|
||||||
@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_CODE) String authorizationCode,
|
@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_CODE) String authorizationCode,
|
||||||
@QueryParam(OAuth2Constants.ERROR) String error) {
|
@QueryParam(OAuth2Constants.ERROR) String error,
|
||||||
|
@QueryParam(OAuth2Constants.ERROR_DESCRIPTION) String errorDescription) {
|
||||||
OAuth2IdentityProviderConfig providerConfig = provider.getConfig();
|
OAuth2IdentityProviderConfig providerConfig = provider.getConfig();
|
||||||
|
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
|
@ -525,6 +527,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
return callback.cancelled(providerConfig);
|
return callback.cancelled(providerConfig);
|
||||||
} else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) {
|
} else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) {
|
||||||
return callback.error(error);
|
return callback.error(error);
|
||||||
|
} else if (error.equals(OAuthErrorException.TEMPORARILY_UNAVAILABLE) && Constants.AUTHENTICATION_EXPIRED_MESSAGE.equals(errorDescription)) {
|
||||||
|
return callback.retryLogin(this.provider, authSession);
|
||||||
} else {
|
} else {
|
||||||
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,7 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||||
.param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
|
.param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -42,6 +42,7 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeyManager;
|
import org.keycloak.models.KeyManager;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -447,8 +448,12 @@ public class SAMLEndpoint {
|
||||||
|
|
||||||
if (! isSuccessfulSamlResponse(responseType)) {
|
if (! isSuccessfulSamlResponse(responseType)) {
|
||||||
String statusMessage = responseType.getStatus() == null || responseType.getStatus().getStatusMessage() == null ? Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR : responseType.getStatus().getStatusMessage();
|
String statusMessage = responseType.getStatus() == null || responseType.getStatus().getStatusMessage() == null ? Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR : responseType.getStatus().getStatusMessage();
|
||||||
|
if (Constants.AUTHENTICATION_EXPIRED_MESSAGE.equals(statusMessage)) {
|
||||||
|
return callback.retryLogin(provider, authSession);
|
||||||
|
} else {
|
||||||
return callback.error(statusMessage);
|
return callback.error(statusMessage);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
|
if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
|
||||||
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||||
}
|
}
|
||||||
|
|
|
@ -519,4 +519,10 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsLongStateParameter() {
|
||||||
|
// SAML RelayState parameter has limits of 80 bytes per SAML specification
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import jakarta.ws.rs.core.UriInfo;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||||
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
|
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
|
||||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||||
|
@ -70,6 +71,7 @@ import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
import org.keycloak.theme.FreeMarkerException;
|
import org.keycloak.theme.FreeMarkerException;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
||||||
|
@ -367,6 +369,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
}
|
}
|
||||||
if (authenticationSession != null) {
|
if (authenticationSession != null) {
|
||||||
uriBuilder.queryParam(Constants.TAB_ID, authenticationSession.getTabId());
|
uriBuilder.queryParam(Constants.TAB_ID, authenticationSession.getTabId());
|
||||||
|
String authSessionAction = authenticationSession.getAction();
|
||||||
|
if (!AuthenticationSessionModel.Action.LOGGING_OUT.name().equals(authSessionAction) && !AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(authSessionAction)) {
|
||||||
|
uriBuilder.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return uriBuilder;
|
return uriBuilder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,10 +79,6 @@ public class UrlBean {
|
||||||
return Urls.realmRegisterPage(baseURI, realm).toString();
|
return Urls.realmRegisterPage(baseURI, realm).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLoginUpdateProfileUrl() {
|
|
||||||
return Urls.loginActionUpdateProfile(baseURI, realm).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLoginResetCredentialsUrl() {
|
public String getLoginResetCredentialsUrl() {
|
||||||
return Urls.loginResetCredentials(baseURI, realm).toString();
|
return Urls.loginResetCredentials(baseURI, realm).toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||||
import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
|
import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
|
||||||
|
@ -148,6 +149,16 @@ public class DockerAuthV2Protocol implements LoginProtocol {
|
||||||
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
|
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientData getClientData(AuthenticationSessionModel authSession) {
|
||||||
|
return new ClientData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response sendError(ClientModel client, ClientData clientData, Error error) {
|
||||||
|
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
|
public Response backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
|
||||||
return errorResponse(userSession, "backchannelLogout");
|
return errorResponse(userSession, "backchannelLogout");
|
||||||
|
|
|
@ -31,8 +31,6 @@ import org.keycloak.constants.AdapterConstants;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
|
||||||
import org.keycloak.headers.SecurityHeadersProvider;
|
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionContext;
|
import org.keycloak.models.ClientSessionContext;
|
||||||
|
@ -40,26 +38,26 @@ import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
|
||||||
|
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||||
import org.keycloak.protocol.oidc.utils.LogoutUtil;
|
import org.keycloak.protocol.oidc.utils.LogoutUtil;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||||
import org.keycloak.services.CorsErrorResponseException;
|
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
import org.keycloak.services.clientpolicy.context.ImplicitHybridTokenResponse;
|
import org.keycloak.services.clientpolicy.context.ImplicitHybridTokenResponse;
|
||||||
import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
|
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
||||||
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
||||||
import org.keycloak.services.managers.ResourceAdminManager;
|
import org.keycloak.services.managers.ResourceAdminManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
|
@ -321,13 +319,23 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
String redirect = authSession.getRedirectUri();
|
String redirect = authSession.getRedirectUri();
|
||||||
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
|
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
|
||||||
|
|
||||||
|
OIDCRedirectUriBuilder redirectUri = buildErrorRedirectUri(redirect, state, error);
|
||||||
|
|
||||||
|
// Remove authenticationSession from current tab
|
||||||
|
new AuthenticationSessionManager(session).removeTabIdInAuthenticationSession(realm, authSession);
|
||||||
|
|
||||||
|
return redirectUri.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OIDCRedirectUriBuilder buildErrorRedirectUri(String redirect, String state, Error error) {
|
||||||
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, null);
|
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode, session, null);
|
||||||
|
|
||||||
if (error != Error.CANCELLED_AIA_SILENT) {
|
OAuth2ErrorRepresentation oauthError = translateError(error);
|
||||||
redirectUri.addParam(OAuth2Constants.ERROR, translateError(error));
|
if (oauthError.getError() != null) {
|
||||||
|
redirectUri.addParam(OAuth2Constants.ERROR, oauthError.getError());
|
||||||
}
|
}
|
||||||
if (error == Error.CANCELLED_AIA) {
|
if (oauthError.getErrorDescription() != null) {
|
||||||
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, "User cancelled aplication-initiated action.");
|
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, oauthError.getErrorDescription());
|
||||||
}
|
}
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
redirectUri.addParam(OAuth2Constants.STATE, state);
|
redirectUri.addParam(OAuth2Constants.STATE, state);
|
||||||
|
@ -339,25 +347,59 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
redirectUri.addParam(OAuth2Constants.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
|
redirectUri.addParam(OAuth2Constants.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove authenticationSession from current tab
|
return redirectUri;
|
||||||
new AuthenticationSessionManager(session).removeTabIdInAuthenticationSession(realm, authSession);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientData getClientData(AuthenticationSessionModel authSession) {
|
||||||
|
return new ClientData(authSession.getRedirectUri(),
|
||||||
|
authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM),
|
||||||
|
authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM),
|
||||||
|
authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response sendError(ClientModel client, ClientData clientData, Error error) {
|
||||||
|
logger.tracef("Calling sendError with clientData when authenticating with client '%s' in realm '%s'. Error: %s", client.getClientId(), realm.getName(), error);
|
||||||
|
|
||||||
|
// Should check if clientData are valid for current client
|
||||||
|
AuthorizationEndpointRequest req = AuthorizationEndpointRequest.fromClientData(clientData);
|
||||||
|
AuthorizationEndpointChecker checker = new AuthorizationEndpointChecker()
|
||||||
|
.event(event)
|
||||||
|
.client(client)
|
||||||
|
.realm(realm)
|
||||||
|
.request(req)
|
||||||
|
.session(session);
|
||||||
|
try {
|
||||||
|
checker.checkResponseType();
|
||||||
|
checker.checkRedirectUri();
|
||||||
|
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
|
||||||
|
ex.throwAsErrorPageException(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupResponseTypeAndMode(clientData.getResponseType(), clientData.getResponseMode());
|
||||||
|
OIDCRedirectUriBuilder redirectUri = buildErrorRedirectUri(clientData.getRedirectUri(), clientData.getState(), error);
|
||||||
return redirectUri.build();
|
return redirectUri.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String translateError(Error error) {
|
private OAuth2ErrorRepresentation translateError(Error error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case CANCELLED_BY_USER:
|
case CANCELLED_AIA_SILENT:
|
||||||
|
return new OAuth2ErrorRepresentation(null, null);
|
||||||
case CANCELLED_AIA:
|
case CANCELLED_AIA:
|
||||||
|
return new OAuth2ErrorRepresentation(OAuthErrorException.ACCESS_DENIED, "User cancelled aplication-initiated action.");
|
||||||
|
case CANCELLED_BY_USER:
|
||||||
case CONSENT_DENIED:
|
case CONSENT_DENIED:
|
||||||
return OAuthErrorException.ACCESS_DENIED;
|
return new OAuth2ErrorRepresentation(OAuthErrorException.ACCESS_DENIED, null);
|
||||||
case PASSIVE_INTERACTION_REQUIRED:
|
case PASSIVE_INTERACTION_REQUIRED:
|
||||||
return OAuthErrorException.INTERACTION_REQUIRED;
|
return new OAuth2ErrorRepresentation(OAuthErrorException.INTERACTION_REQUIRED, null);
|
||||||
case PASSIVE_LOGIN_REQUIRED:
|
case PASSIVE_LOGIN_REQUIRED:
|
||||||
return OAuthErrorException.LOGIN_REQUIRED;
|
return new OAuth2ErrorRepresentation(OAuthErrorException.LOGIN_REQUIRED, null);
|
||||||
|
case ALREADY_LOGGED_IN:
|
||||||
|
return new OAuth2ErrorRepresentation(OAuthErrorException.TEMPORARILY_UNAVAILABLE, Constants.AUTHENTICATION_EXPIRED_MESSAGE);
|
||||||
default:
|
default:
|
||||||
ServicesLogger.LOGGER.untranslatedProtocol(error.name());
|
ServicesLogger.LOGGER.untranslatedProtocol(error.name());
|
||||||
return OAuthErrorException.SERVER_ERROR;
|
return new OAuth2ErrorRepresentation(OAuthErrorException.SERVER_ERROR, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.endpoints.request;
|
package org.keycloak.protocol.oidc.endpoints.request;
|
||||||
|
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
import org.keycloak.rar.AuthorizationRequestContext;
|
import org.keycloak.rar.AuthorizationRequestContext;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -66,6 +67,14 @@ public class AuthorizationEndpointRequest {
|
||||||
return redirectUriParam;
|
return redirectUriParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AuthorizationEndpointRequest fromClientData(ClientData cData) {
|
||||||
|
AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
|
||||||
|
request.responseType = cData.getResponseType();
|
||||||
|
request.responseMode = cData.getResponseMode();
|
||||||
|
request.redirectUriParam = cData.getRedirectUri();
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
public String getResponseType() {
|
public String getResponseType() {
|
||||||
return responseType;
|
return responseType;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,11 +38,13 @@ import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionContext;
|
import org.keycloak.models.ClientSessionContext;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeyManager;
|
import org.keycloak.models.KeyManager;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
|
@ -50,9 +52,11 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.SingleUseObjectProvider;
|
import org.keycloak.models.SingleUseObjectProvider;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.ProtocolMapper;
|
import org.keycloak.protocol.ProtocolMapper;
|
||||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||||
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.protocol.saml.mappers.NameIdMapperHelper;
|
import org.keycloak.protocol.saml.mappers.NameIdMapperHelper;
|
||||||
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
|
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
|
||||||
import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
|
import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
|
||||||
|
@ -78,6 +82,7 @@ import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
|
import org.keycloak.services.ErrorPageException;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.managers.ResourceAdminManager;
|
import org.keycloak.services.managers.ResourceAdminManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
@ -241,11 +246,41 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientData getClientData(AuthenticationSessionModel authSession) {
|
||||||
|
String responseMode = isPostBinding(authSession) ? SamlProtocol.SAML_POST_BINDING : SamlProtocol.SAML_REDIRECT_BINDING;
|
||||||
|
return new ClientData(authSession.getRedirectUri(),
|
||||||
|
null,
|
||||||
|
responseMode,
|
||||||
|
authSession.getClientNote(GeneralConstants.RELAY_STATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response sendError(ClientModel client, ClientData clientData, Error error) {
|
||||||
|
logger.tracef("Calling sendError with clientData when authenticating with client '%s' in realm '%s'. Error: %s", client.getClientId(), realm.getName(), error);
|
||||||
|
|
||||||
|
SamlClient samlClient = new SamlClient(client);
|
||||||
|
boolean postBinding = samlClient.forcePostBinding() || SamlProtocol.SAML_POST_BINDING.equals(clientData.getResponseMode());
|
||||||
|
event.detail(Details.REDIRECT_URI, clientData.getRedirectUri());
|
||||||
|
String validRedirectUri = RedirectUtils.verifyRedirectUri(session, clientData.getRedirectUri(), client);
|
||||||
|
if (validRedirectUri == null) {
|
||||||
|
event.error(Errors.INVALID_REDIRECT_URI);
|
||||||
|
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
return samlErrorMessage(
|
||||||
|
null, samlClient, postBinding,
|
||||||
|
validRedirectUri, translateErrorToSAMLStatus(error), clientData.getState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private Response samlErrorMessage(
|
private Response samlErrorMessage(
|
||||||
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
||||||
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
|
String destination, SAMLError samlError, String relayState) {
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
|
||||||
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get());
|
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm))
|
||||||
|
.status(samlError.error().get())
|
||||||
|
.statusMessage(samlError.errorDescription());
|
||||||
KeyManager keyManager = session.keys();
|
KeyManager keyManager = session.keys();
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (samlClient.requiresRealmSignature()) {
|
||||||
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
||||||
|
@ -276,18 +311,20 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
|
private SAMLError translateErrorToSAMLStatus(Error error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case CANCELLED_BY_USER:
|
case CANCELLED_BY_USER:
|
||||||
case CANCELLED_AIA:
|
case CANCELLED_AIA:
|
||||||
case CONSENT_DENIED:
|
case CONSENT_DENIED:
|
||||||
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
|
return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
|
||||||
case PASSIVE_INTERACTION_REQUIRED:
|
case PASSIVE_INTERACTION_REQUIRED:
|
||||||
case PASSIVE_LOGIN_REQUIRED:
|
case PASSIVE_LOGIN_REQUIRED:
|
||||||
return JBossSAMLURIConstants.STATUS_NO_PASSIVE;
|
return new SAMLError(JBossSAMLURIConstants.STATUS_NO_PASSIVE, null);
|
||||||
|
case ALREADY_LOGGED_IN:
|
||||||
|
return new SAMLError(JBossSAMLURIConstants.STATUS_AUTHNFAILED, Constants.AUTHENTICATION_EXPIRED_MESSAGE);
|
||||||
default:
|
default:
|
||||||
logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
|
logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
|
||||||
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
|
return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,7 +522,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
|
|
||||||
if (nameId == null) {
|
if (nameId == null) {
|
||||||
return samlErrorMessage(null, samlClient, isPostBinding(authSession), redirectUri,
|
return samlErrorMessage(null, samlClient, isPostBinding(authSession), redirectUri,
|
||||||
JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState);
|
new SAMLError(JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, null), relayState);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.nameIdentifier(nameIdFormat, nameId);
|
builder.nameIdentifier(nameIdFormat, nameId);
|
||||||
|
@ -1061,4 +1098,11 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
.header("Cache-Control", "no-cache, no-store").build();
|
.header("Cache-Control", "no-cache, no-store").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param error mandatory parameter
|
||||||
|
* @param errorDescription optional parameter
|
||||||
|
*/
|
||||||
|
private record SAMLError(JBossSAMLURIConstants error, String errorDescription) {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ public class Urls {
|
||||||
.build(realmName, providerAlias);
|
.build(realmName, providerAlias);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAuthnRequest(URI baseUri, String providerAlias, String realmName, String accessCode, String clientId, String tabId) {
|
public static URI identityProviderAuthnRequest(URI baseUri, String providerAlias, String realmName, String accessCode, String clientId, String tabId, String clientData) {
|
||||||
UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||||
.path(IdentityBrokerService.class, "performLogin");
|
.path(IdentityBrokerService.class, "performLogin");
|
||||||
|
|
||||||
|
@ -65,6 +65,9 @@ public class Urls {
|
||||||
if (tabId != null) {
|
if (tabId != null) {
|
||||||
uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId);
|
uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId);
|
||||||
}
|
}
|
||||||
|
if (clientData != null) {
|
||||||
|
uriBuilder.replaceQueryParam(Constants.CLIENT_DATA, clientData);
|
||||||
|
}
|
||||||
|
|
||||||
return uriBuilder.build(realmName, providerAlias);
|
return uriBuilder.build(realmName, providerAlias);
|
||||||
}
|
}
|
||||||
|
@ -84,23 +87,25 @@ public class Urls {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAuthnRequest(URI baseURI, String providerAlias, String realmName) {
|
public static URI identityProviderAuthnRequest(URI baseURI, String providerAlias, String realmName) {
|
||||||
return identityProviderAuthnRequest(baseURI, providerAlias, realmName, null, null, null);
|
return identityProviderAuthnRequest(baseURI, providerAlias, realmName, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
|
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId, String clientData) {
|
||||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||||
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
||||||
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
|
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
|
||||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||||
.replaceQueryParam(Constants.TAB_ID, tabId)
|
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||||
|
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
|
||||||
.build(realmName);
|
.build(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
|
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId, String clientData) {
|
||||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||||
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
|
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
|
||||||
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
|
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
|
||||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||||
|
.replaceQueryParam(Constants.CLIENT_DATA, clientData)
|
||||||
.replaceQueryParam(Constants.TAB_ID, tabId)
|
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||||
.build(realmName);
|
.build(realmName);
|
||||||
}
|
}
|
||||||
|
@ -117,24 +122,16 @@ public class Urls {
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "requiredAction");
|
return loginActionsBase(baseUri).path(LoginActionsService.class, "requiredAction");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static URI loginActionUpdateProfile(URI baseUri, String realmName) {
|
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) {
|
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "emailVerification");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static URI loginResetCredentials(URI baseUri, String realmName) {
|
public static URI loginResetCredentials(URI baseUri, String realmName) {
|
||||||
return loginResetCredentialsBuilder(baseUri).build(realmName);
|
return loginResetCredentialsBuilder(baseUri).build(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId, String tabId) {
|
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId, String tabId, String clientData) {
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
||||||
.queryParam(Constants.KEY, tokenString)
|
.queryParam(Constants.KEY, tokenString)
|
||||||
.queryParam(Constants.CLIENT_ID, clientId)
|
.queryParam(Constants.CLIENT_ID, clientId)
|
||||||
.queryParam(Constants.TAB_ID, tabId);
|
.queryParam(Constants.TAB_ID, tabId)
|
||||||
|
.queryParam(Constants.CLIENT_DATA, clientData);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -979,6 +979,7 @@ public class AuthenticationManager {
|
||||||
|
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
||||||
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
||||||
|
uriBuilder.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession));
|
||||||
|
|
||||||
if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
||||||
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());
|
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());
|
||||||
|
|
|
@ -329,7 +329,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
try {
|
try {
|
||||||
IdentityProvider<?> identityProvider = getIdentityProvider(session, realmModel, providerAlias);
|
IdentityProvider<?> identityProvider = getIdentityProvider(session, realmModel, providerAlias);
|
||||||
Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode));
|
Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, providerAlias, clientSessionCode));
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
if (isDebugEnabled()) {
|
if (isDebugEnabled()) {
|
||||||
|
@ -352,10 +352,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
@Path("/{provider_alias}/login")
|
@Path("/{provider_alias}/login")
|
||||||
public Response performPostLogin(@PathParam("provider_alias") String providerAlias,
|
public Response performPostLogin(@PathParam("provider_alias") String providerAlias,
|
||||||
@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
||||||
@QueryParam("client_id") String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId,
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
|
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
|
||||||
return performLogin(providerAlias, code, clientId, tabId, loginHint);
|
return performLogin(providerAlias, code, clientId, tabId, clientData, loginHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -363,8 +364,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
@Path("/{provider_alias}/login")
|
@Path("/{provider_alias}/login")
|
||||||
public Response performLogin(@PathParam("provider_alias") String providerAlias,
|
public Response performLogin(@PathParam("provider_alias") String providerAlias,
|
||||||
@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
||||||
@QueryParam("client_id") String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId,
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
|
@QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint) {
|
||||||
this.event.detail(Details.IDENTITY_PROVIDER, providerAlias);
|
this.event.detail(Details.IDENTITY_PROVIDER, providerAlias);
|
||||||
|
|
||||||
|
@ -373,7 +375,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId);
|
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData);
|
||||||
|
|
||||||
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
||||||
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
|
@ -392,11 +394,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
IdentityProvider<?> identityProvider = providerFactory.create(session, identityProviderModel);
|
IdentityProvider<?> identityProvider = providerFactory.create(session, identityProviderModel);
|
||||||
|
|
||||||
Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode));
|
Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, providerAlias, clientSessionCode));
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
if (isDebugEnabled()) {
|
if (isDebugEnabled()) {
|
||||||
logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider, response);
|
logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider.getConfig().getAlias(), response);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -409,6 +411,25 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST);
|
return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response retryLogin(IdentityProvider<?> identityProvider, AuthenticationSessionModel authSession) {
|
||||||
|
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
||||||
|
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
|
Response response = identityProvider.performLogin(createAuthenticationRequest(identityProvider, identityProvider.getConfig().getAlias(), clientSessionCode));
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
event.detail(Details.IDENTITY_PROVIDER, identityProvider.getConfig().getAlias())
|
||||||
|
.detail(Details.LOGIN_RETRY, "true")
|
||||||
|
.success();
|
||||||
|
|
||||||
|
if (isDebugEnabled()) {
|
||||||
|
logger.debugf("Identity provider [%s] is going to retry a login request [%s].", identityProvider.getConfig().getAlias(), response);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return redirectToErrorPage(Response.Status.INTERNAL_SERVER_ERROR, Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
@Path("{provider_alias}/endpoint")
|
@Path("{provider_alias}/endpoint")
|
||||||
public Object getEndpoint(@PathParam("provider_alias") String providerAlias) {
|
public Object getEndpoint(@PathParam("provider_alias") String providerAlias) {
|
||||||
IdentityProvider identityProvider;
|
IdentityProvider identityProvider;
|
||||||
|
@ -600,6 +621,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
URI redirect = LoginActionsService.firstBrokerLoginProcessor(session.getContext().getUri())
|
URI redirect = LoginActionsService.firstBrokerLoginProcessor(session.getContext().getUri())
|
||||||
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
|
||||||
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authenticationSession))
|
||||||
.build(realmModel.getName());
|
.build(realmModel.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
|
|
||||||
|
@ -640,9 +662,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
@NoCache
|
@NoCache
|
||||||
@Path("/after-first-broker-login")
|
@Path("/after-first-broker-login")
|
||||||
public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
||||||
@QueryParam("client_id") String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId);
|
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId, clientData);
|
||||||
return afterFirstBrokerLogin(authSession);
|
return afterFirstBrokerLogin(authSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,6 +779,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
URI redirect = LoginActionsService.postBrokerLoginProcessor(session.getContext().getUri())
|
URI redirect = LoginActionsService.postBrokerLoginProcessor(session.getContext().getUri())
|
||||||
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
|
||||||
.queryParam(Constants.TAB_ID, authSession.getTabId())
|
.queryParam(Constants.TAB_ID, authSession.getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession))
|
||||||
.build(realmModel.getName());
|
.build(realmModel.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
}
|
}
|
||||||
|
@ -767,9 +791,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
@NoCache
|
@NoCache
|
||||||
@Path("/after-post-broker-login")
|
@Path("/after-post-broker-login")
|
||||||
public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
||||||
@QueryParam("client_id") String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId);
|
AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId, clientData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
||||||
|
@ -1064,20 +1089,21 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
String code = state.getDecodedState();
|
String code = state.getDecodedState();
|
||||||
String clientId = state.getClientId();
|
String clientId = state.getClientId();
|
||||||
String tabId = state.getTabId();
|
String tabId = state.getTabId();
|
||||||
return parseSessionCode(code, clientId, tabId);
|
String clientData = state.getClientData();
|
||||||
|
return parseSessionCode(code, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method will throw JAX-RS exception in case it is not able to retrieve AuthenticationSessionModel. It never returns null
|
* This method will throw JAX-RS exception in case it is not able to retrieve AuthenticationSessionModel. It never returns null
|
||||||
*/
|
*/
|
||||||
private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId) {
|
private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId, String clientData) {
|
||||||
if (code == null || clientId == null || tabId == null) {
|
if (code == null || clientId == null || tabId == null) {
|
||||||
logger.debugf("Invalid request. Authorization code, clientId or tabId was null. Code=%s, clientId=%s, tabID=%s", code, clientId, tabId);
|
logger.debugf("Invalid request. Authorization code, clientId or tabId was null. Code=%s, clientId=%s, tabID=%s", code, clientId, tabId);
|
||||||
Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
throw new WebApplicationException(staleCodeError);
|
throw new WebApplicationException(staleCodeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
|
SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, clientData, LoginActionsService.AUTHENTICATE_PATH);
|
||||||
checks.initialVerify();
|
checks.initialVerify();
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
|
|
||||||
|
@ -1144,14 +1170,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthenticationRequest createAuthenticationRequest(String providerAlias, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
|
private AuthenticationRequest createAuthenticationRequest(IdentityProvider<?> identityProvider, String providerAlias, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
|
||||||
AuthenticationSessionModel authSession = null;
|
AuthenticationSessionModel authSession = null;
|
||||||
IdentityBrokerState encodedState = null;
|
IdentityBrokerState encodedState = null;
|
||||||
|
|
||||||
if (clientSessionCode != null) {
|
if (clientSessionCode != null) {
|
||||||
authSession = clientSessionCode.getClientSession();
|
authSession = clientSessionCode.getClientSession();
|
||||||
String relayState = clientSessionCode.getOrGenerateCode();
|
String relayState = clientSessionCode.getOrGenerateCode();
|
||||||
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId());
|
String clientData = identityProvider.supportsLongStateParameter() ? AuthenticationProcessor.getClientData(session, authSession) : null;
|
||||||
|
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId(), clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.session.getContext().getUri(), encodedState, getRedirectUri(providerAlias));
|
return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.session.getContext().getUri(), encodedState, getRedirectUri(providerAlias));
|
||||||
|
|
|
@ -199,16 +199,17 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SessionCodeChecks checksForCode(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
|
|
||||||
SessionCodeChecks res = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, flowPath);
|
private SessionCodeChecks checksForCode(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath) {
|
||||||
|
SessionCodeChecks res = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, clientData, flowPath);
|
||||||
res.initialVerify();
|
res.initialVerify();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) {
|
protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId, String clientData) {
|
||||||
return new AuthenticationFlowURLHelper(session, realm, session.getContext().getUri())
|
return new AuthenticationFlowURLHelper(session, realm, session.getContext().getUri())
|
||||||
.getLastExecutionUrl(flowPath, executionId, clientId, tabId);
|
.getLastExecutionUrl(flowPath, executionId, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -222,9 +223,10 @@ public class LoginActionsService {
|
||||||
public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
|
public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId,
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.SKIP_LOGOUT) String skipLogout) {
|
@QueryParam(Constants.SKIP_LOGOUT) String skipLogout) {
|
||||||
event.event(EventType.RESTART_AUTHENTICATION);
|
event.event(EventType.RESTART_AUTHENTICATION);
|
||||||
SessionCodeChecks checks = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, null, null, clientId, tabId, null);
|
SessionCodeChecks checks = new SessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, authSessionId, null, null, clientId, tabId, clientData, null);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
|
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
|
||||||
if (authSession == null) {
|
if (authSession == null) {
|
||||||
|
@ -248,7 +250,7 @@ public class LoginActionsService {
|
||||||
|
|
||||||
AuthenticationProcessor.resetFlow(authSession, flowPath);
|
AuthenticationProcessor.resetFlow(authSession, flowPath);
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId);
|
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId, AuthenticationProcessor.getClientData(session, authSession));
|
||||||
logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
|
logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
|
||||||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||||
}
|
}
|
||||||
|
@ -310,11 +312,12 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData) {
|
||||||
|
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, AUTHENTICATE_PATH);
|
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, AUTHENTICATE_PATH);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -388,8 +391,9 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
return authenticate(authSessionId, code, execution, clientId, tabId);
|
@QueryParam(Constants.CLIENT_DATA) String clientData) {
|
||||||
|
return authenticate(authSessionId, code, execution, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(RESET_CREDENTIALS_PATH)
|
@Path(RESET_CREDENTIALS_PATH)
|
||||||
|
@ -399,14 +403,15 @@ public class LoginActionsService {
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId,
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.KEY) String key) {
|
@QueryParam(Constants.KEY) String key) {
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
return handleActionToken(key, execution, clientId, tabId);
|
return handleActionToken(key, execution, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.event(EventType.RESET_PASSWORD);
|
event.event(EventType.RESET_PASSWORD);
|
||||||
|
|
||||||
return resetCredentials(authSessionId, code, execution, clientId, tabId);
|
return resetCredentials(authSessionId, code, execution, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -424,13 +429,14 @@ public class LoginActionsService {
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
|
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData) {
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
||||||
processLocaleParam(authSession);
|
processLocaleParam(authSession);
|
||||||
|
|
||||||
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
||||||
if (authSession == null && code == null) {
|
if (authSession == null && code == null && clientData == null) {
|
||||||
if (!realm.isResetPasswordAllowed()) {
|
if (!realm.isResetPasswordAllowed()) {
|
||||||
event.event(EventType.RESET_PASSWORD);
|
event.event(EventType.RESET_PASSWORD);
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
@ -442,7 +448,7 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
event.event(EventType.RESET_PASSWORD);
|
event.event(EventType.RESET_PASSWORD);
|
||||||
return resetCredentials(authSessionId, code, execution, clientId, tabId);
|
return resetCredentials(authSessionId, code, execution, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel createAuthenticationSessionForClient(String clientID, String redirectUriParam)
|
AuthenticationSessionModel createAuthenticationSessionForClient(String clientID, String redirectUriParam)
|
||||||
|
@ -497,8 +503,8 @@ public class LoginActionsService {
|
||||||
* @param execution
|
* @param execution
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId) {
|
protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId, String clientData) {
|
||||||
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
|
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, RESET_CREDENTIALS_PATH);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -527,11 +533,12 @@ public class LoginActionsService {
|
||||||
@QueryParam(Constants.KEY) String key,
|
@QueryParam(Constants.KEY) String key,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return handleActionToken(key, execution, clientId, tabId);
|
return handleActionToken(key, execution, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) {
|
protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId, String clientData) {
|
||||||
T token;
|
T token;
|
||||||
ActionTokenHandler<T> handler;
|
ActionTokenHandler<T> handler;
|
||||||
ActionTokenContext<T> tokenContext;
|
ActionTokenContext<T> tokenContext;
|
||||||
|
@ -620,7 +627,7 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now proceed with the verification and handle the token
|
// Now proceed with the verification and handle the token
|
||||||
tokenContext = new ActionTokenContext(session, realm, sessionContext.getUri(), clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
|
tokenContext = new ActionTokenContext(session, realm, sessionContext.getUri(), clientConnection, request, event, handler, execution, clientData, this::processFlow, this::brokerLoginFlow);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext, authSession);
|
String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext, authSession);
|
||||||
|
@ -737,8 +744,9 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return registerRequest(authSessionId, code, execution, clientId, tabId,false);
|
return registerRequest(authSessionId, code, execution, clientId, tabId,clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -754,19 +762,20 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return registerRequest(authSessionId, code, execution, clientId, tabId,true);
|
return registerRequest(authSessionId, code, execution, clientId, tabId,clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, boolean isPostRequest) {
|
private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, String clientData) {
|
||||||
event.event(EventType.REGISTER);
|
event.event(EventType.REGISTER);
|
||||||
if (!realm.isRegistrationAllowed()) {
|
if (!realm.isRegistrationAllowed()) {
|
||||||
event.error(Errors.REGISTRATION_DISABLED);
|
event.error(Errors.REGISTRATION_DISABLED);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, REGISTRATION_PATH);
|
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, REGISTRATION_PATH);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -787,8 +796,9 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
|
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, FIRST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||||
|
@ -797,8 +807,9 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
|
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, FIRST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(POST_BROKER_LOGIN_PATH)
|
@Path(POST_BROKER_LOGIN_PATH)
|
||||||
|
@ -807,8 +818,9 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
|
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, POST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(POST_BROKER_LOGIN_PATH)
|
@Path(POST_BROKER_LOGIN_PATH)
|
||||||
|
@ -817,18 +829,19 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) String code,
|
@QueryParam(SESSION_CODE) String code,
|
||||||
@QueryParam(Constants.EXECUTION) String execution,
|
@QueryParam(Constants.EXECUTION) String execution,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
|
return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, clientData, POST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
|
protected Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath) {
|
||||||
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
|
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
|
||||||
|
|
||||||
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
||||||
event.event(eventType);
|
event.event(eventType);
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, flowPath);
|
SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, flowPath);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
event.error("Failed to verify login action");
|
event.error("Failed to verify login action");
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
|
@ -924,8 +937,9 @@ public class LoginActionsService {
|
||||||
|
|
||||||
String clientId = authSession.getClient().getClientId();
|
String clientId = authSession.getClient().getClientId();
|
||||||
String tabId = authSession.getTabId();
|
String tabId = authSession.getTabId();
|
||||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) :
|
String clientData = AuthenticationProcessor.getClientData(session, authSession);
|
||||||
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) ;
|
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId, clientData) :
|
||||||
|
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId, clientData) ;
|
||||||
logger.debugf("Redirecting to '%s' ", redirect);
|
logger.debugf("Redirecting to '%s' ", redirect);
|
||||||
|
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
|
@ -945,7 +959,8 @@ public class LoginActionsService {
|
||||||
String code = formData.getFirst(SESSION_CODE);
|
String code = formData.getFirst(SESSION_CODE);
|
||||||
String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID);
|
String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID);
|
||||||
String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_ID);
|
String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_ID);
|
||||||
SessionCodeChecks checks = checksForCode(null, code, null, clientId, tabId, REQUIRED_ACTION);
|
String clientData = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_DATA);
|
||||||
|
SessionCodeChecks checks = checksForCode(null, code, null, clientId, tabId, clientData, REQUIRED_ACTION);
|
||||||
if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
|
if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -1047,8 +1062,9 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) final String code,
|
@QueryParam(SESSION_CODE) final String code,
|
||||||
@QueryParam(Constants.EXECUTION) String action,
|
@QueryParam(Constants.EXECUTION) String action,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return processRequireAction(authSessionId, code, action, clientId, tabId);
|
return processRequireAction(authSessionId, code, action, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(REQUIRED_ACTION)
|
@Path(REQUIRED_ACTION)
|
||||||
|
@ -1057,14 +1073,15 @@ public class LoginActionsService {
|
||||||
@QueryParam(SESSION_CODE) final String code,
|
@QueryParam(SESSION_CODE) final String code,
|
||||||
@QueryParam(Constants.EXECUTION) String action,
|
@QueryParam(Constants.EXECUTION) String action,
|
||||||
@QueryParam(Constants.CLIENT_ID) String clientId,
|
@QueryParam(Constants.CLIENT_ID) String clientId,
|
||||||
|
@QueryParam(Constants.CLIENT_DATA) String clientData,
|
||||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
return processRequireAction(authSessionId, code, action, clientId, tabId);
|
return processRequireAction(authSessionId, code, action, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response processRequireAction(final String authSessionId, final String code, String action, String clientId, String tabId) {
|
private Response processRequireAction(final String authSessionId, final String code, String action, String clientId, String tabId, String clientData) {
|
||||||
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(authSessionId, code, action, clientId, tabId, REQUIRED_ACTION);
|
SessionCodeChecks checks = checksForCode(authSessionId, code, action, clientId, tabId, clientData, REQUIRED_ACTION);
|
||||||
if (!checks.verifyRequiredAction(action)) {
|
if (!checks.verifyRequiredAction(action)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
package org.keycloak.services.resources;
|
package org.keycloak.services.resources;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.UriInfo;
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
|
@ -42,7 +40,7 @@ public class LogoutSessionCodeChecks extends SessionCodeChecks {
|
||||||
|
|
||||||
public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
|
public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
|
||||||
String code, String clientId, String tabId) {
|
String code, String clientId, String tabId) {
|
||||||
super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null);
|
super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ import static org.keycloak.services.managers.AuthenticationManager.authenticateI
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Cookie;
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.UriBuilder;
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
import jakarta.ws.rs.core.UriInfo;
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
@ -40,7 +39,11 @@ import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||||
|
import org.keycloak.protocol.ClientData;
|
||||||
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.RestartLoginCookie;
|
import org.keycloak.protocol.RestartLoginCookie;
|
||||||
|
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
|
||||||
|
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
@ -72,12 +75,14 @@ public class SessionCodeChecks {
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String execution;
|
private final String execution;
|
||||||
private final String clientId;
|
private final String clientId;
|
||||||
|
private final ClientData clientData;
|
||||||
private final String tabId;
|
private final String tabId;
|
||||||
private final String flowPath;
|
private final String flowPath;
|
||||||
private final String authSessionId;
|
private final String authSessionId;
|
||||||
|
|
||||||
|
|
||||||
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
|
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
|
||||||
String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
|
String authSessionId, String code, String execution, String clientId, String tabId, String clientData, String flowPath) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.uriInfo = uriInfo;
|
this.uriInfo = uriInfo;
|
||||||
this.request = request;
|
this.request = request;
|
||||||
|
@ -91,6 +96,7 @@ public class SessionCodeChecks {
|
||||||
this.tabId = tabId;
|
this.tabId = tabId;
|
||||||
this.flowPath = flowPath;
|
this.flowPath = flowPath;
|
||||||
this.authSessionId = authSessionId;
|
this.authSessionId = authSessionId;
|
||||||
|
this.clientData = ClientData.decodeClientDataFromParameter(clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,6 +154,7 @@ public class SessionCodeChecks {
|
||||||
}
|
}
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
session.getContext().setClient(client);
|
session.getContext().setClient(client);
|
||||||
|
setClientToEvent(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,6 +193,19 @@ public class SessionCodeChecks {
|
||||||
AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(session, realm, false);
|
AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(session, realm, false);
|
||||||
|
|
||||||
if (authResult != null && authResult.getSession() != null) {
|
if (authResult != null && authResult.getSession() != null) {
|
||||||
|
response = null;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession)
|
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession)
|
||||||
.setSuccess(Messages.ALREADY_LOGGED_IN);
|
.setSuccess(Messages.ALREADY_LOGGED_IN);
|
||||||
|
|
||||||
|
@ -194,6 +214,11 @@ public class SessionCodeChecks {
|
||||||
}
|
}
|
||||||
|
|
||||||
response = loginForm.createInfoPage();
|
response = loginForm.createInfoPage();
|
||||||
|
event.detail(Details.REDIRECTED_TO_CLIENT, "false");
|
||||||
|
}
|
||||||
|
event.error(Errors.ALREADY_LOGGED_IN);
|
||||||
|
} else {
|
||||||
|
event.error(Errors.COOKIE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +305,8 @@ public class SessionCodeChecks {
|
||||||
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
|
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
|
||||||
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
||||||
if (latestFlowPath != null) {
|
if (latestFlowPath != null) {
|
||||||
URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId);
|
String clientData = AuthenticationProcessor.getClientData(session, authSession);
|
||||||
|
URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId, clientData);
|
||||||
|
|
||||||
logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
|
logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
|
||||||
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
|
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
|
||||||
|
@ -342,7 +368,8 @@ public class SessionCodeChecks {
|
||||||
|
|
||||||
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT);
|
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT);
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, tabId);
|
String clientData = AuthenticationProcessor.getClientData(session, authSession);
|
||||||
|
URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, tabId, clientData);
|
||||||
logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
|
logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
|
||||||
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
|
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||||
return false;
|
return false;
|
||||||
|
@ -387,7 +414,6 @@ public class SessionCodeChecks {
|
||||||
|
|
||||||
String cook = RestartLoginCookie.getRestartCookie(session);
|
String cook = RestartLoginCookie.getRestartCookie(session);
|
||||||
if (cook == null) {
|
if (cook == null) {
|
||||||
event.error(Errors.COOKIE_NOT_FOUND);
|
|
||||||
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.COOKIE_NOT_FOUND);
|
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.COOKIE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,7 +437,8 @@ public class SessionCodeChecks {
|
||||||
flowPath = LoginActionsService.AUTHENTICATE_PATH;
|
flowPath = LoginActionsService.AUTHENTICATE_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getTabId());
|
String clientData = AuthenticationProcessor.getClientData(session, authSession);
|
||||||
|
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getTabId(), clientData);
|
||||||
logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
|
logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
|
||||||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||||
} else {
|
} else {
|
||||||
|
@ -431,17 +458,18 @@ public class SessionCodeChecks {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientModel client = authSession.getClient();
|
ClientModel client = authSession.getClient();
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||||
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
.queryParam(Constants.TAB_ID, authSession.getTabId())
|
||||||
|
.queryParam(Constants.CLIENT_DATA, AuthenticationProcessor.getClientData(session, authSession));
|
||||||
|
|
||||||
URI redirect = uriBuilder.build(realm.getName());
|
URI redirect = uriBuilder.build(realm.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private URI getLastExecutionUrl(String flowPath, String executionId, String tabId) {
|
private URI getLastExecutionUrl(String flowPath, String executionId, String tabId, String clientData) {
|
||||||
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
||||||
.getLastExecutionUrl(flowPath, executionId, clientId, tabId);
|
.getLastExecutionUrl(flowPath, executionId, clientId, tabId, clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class AuthenticationFlowURLHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) {
|
public URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId, String clientData) {
|
||||||
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
|
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
|
||||||
.path(flowPath);
|
.path(flowPath);
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ public class AuthenticationFlowURLHelper {
|
||||||
}
|
}
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, clientId);
|
uriBuilder.queryParam(Constants.CLIENT_ID, clientId);
|
||||||
uriBuilder.queryParam(Constants.TAB_ID, tabId);
|
uriBuilder.queryParam(Constants.TAB_ID, tabId);
|
||||||
|
uriBuilder.queryParam(Constants.CLIENT_DATA, clientData);
|
||||||
|
|
||||||
return uriBuilder.build(realm.getName());
|
return uriBuilder.build(realm.getName());
|
||||||
}
|
}
|
||||||
|
@ -89,7 +90,8 @@ public class AuthenticationFlowURLHelper {
|
||||||
latestFlowPath = LoginActionsService.AUTHENTICATE_PATH;
|
latestFlowPath = LoginActionsService.AUTHENTICATE_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId(), authSession.getTabId());
|
String clientData = AuthenticationProcessor.getClientData(session, authSession);
|
||||||
|
return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId(), authSession.getTabId(), clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getExecutionId(AuthenticationSessionModel authSession) {
|
private String getExecutionId(AuthenticationSessionModel authSession) {
|
||||||
|
|
|
@ -49,6 +49,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmAttributeUpdater setAccessCodeLifespanLogin(Integer accessCodeLifespanLogin) {
|
||||||
|
rep.setAccessCodeLifespanLogin(accessCodeLifespanLogin);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
|
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
|
||||||
rep.setSsoSessionIdleTimeout(timeout);
|
rep.setSsoSessionIdleTimeout(timeout);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -359,20 +359,23 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
|
||||||
|
|
||||||
// Click browser 'back' and then 'forward' and then continue
|
// Click browser 'back' and then 'forward' and then continue
|
||||||
driver.navigate().back();
|
driver.navigate().back();
|
||||||
assertTrue(driver.getPageSource().contains("You are already logged in."));
|
loginExpiredPage.assertCurrent();
|
||||||
driver.navigate().forward(); // here a new execution ID is added to the URL using JS, see below
|
driver.navigate().forward(); // here a new execution ID is added to the URL using JS, see below
|
||||||
idpConfirmLinkPage.assertCurrent();
|
idpConfirmLinkPage.assertCurrent();
|
||||||
|
|
||||||
// Click browser 'back' on review profile page
|
// Click browser 'back' on review profile page
|
||||||
idpConfirmLinkPage.clickReviewProfile();
|
idpConfirmLinkPage.clickReviewProfile();
|
||||||
|
// Need to confirm again with htmlUnit due the JS not working correctly
|
||||||
|
if (driver instanceof HtmlUnitDriver) {
|
||||||
|
idpConfirmLinkPage.assertCurrent();
|
||||||
|
idpConfirmLinkPage.clickReviewProfile();
|
||||||
|
}
|
||||||
waitForPage(driver, "update account information", false);
|
waitForPage(driver, "update account information", false);
|
||||||
updateAccountInformationPage.assertCurrent();
|
updateAccountInformationPage.assertCurrent();
|
||||||
driver.navigate().back();
|
driver.navigate().back();
|
||||||
// JS-capable browsers (i.e. all except HtmlUnit) add a new execution ID to the URL which then causes the login expire page to appear (because the old ID and new ID don't match)
|
|
||||||
if (!(driver instanceof HtmlUnitDriver)) {
|
|
||||||
loginExpiredPage.assertCurrent();
|
loginExpiredPage.assertCurrent();
|
||||||
loginExpiredPage.clickLoginContinueLink();
|
loginExpiredPage.clickLoginContinueLink();
|
||||||
}
|
|
||||||
waitForPage(driver, "update account information", false);
|
waitForPage(driver, "update account information", false);
|
||||||
updateAccountInformationPage.assertCurrent();
|
updateAccountInformationPage.assertCurrent();
|
||||||
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), "FirstName", "LastName");
|
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), "FirstName", "LastName");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
|
||||||
|
|
||||||
realm.setEnabled(true);
|
realm.setEnabled(true);
|
||||||
realm.setRealm(REALM_PROV_NAME);
|
realm.setRealm(REALM_PROV_NAME);
|
||||||
|
realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
|
||||||
|
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package org.keycloak.testsuite.broker;
|
package org.keycloak.testsuite.broker;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.SamlClient;
|
import org.keycloak.testsuite.util.SamlClient;
|
||||||
|
@ -28,7 +31,15 @@ public class KcSamlBrokerDestinationTest extends AbstractBrokerTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected BrokerConfiguration getBrokerConfiguration() {
|
protected BrokerConfiguration getBrokerConfiguration() {
|
||||||
return KcSamlBrokerConfiguration.INSTANCE;
|
return new KcSamlBrokerConfiguration() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RealmRepresentation createProviderRealm() {
|
||||||
|
RealmRepresentation realm = super.createProviderRealm();
|
||||||
|
realm.setEventsListeners(Collections.singletonList("jboss-logging"));
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.security.KeyManagementException;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -68,6 +69,13 @@ public final class KcSamlBrokerFrontendUrlTest extends AbstractBrokerTest {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RealmRepresentation createProviderRealm() {
|
||||||
|
RealmRepresentation realm = super.createProviderRealm();
|
||||||
|
realm.setEventsListeners(Collections.singletonList("jboss-logging"));
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ClientRepresentation> createProviderClients() {
|
public List<ClientRepresentation> createProviderClients() {
|
||||||
List<ClientRepresentation> clients = super.createProviderClients();
|
List<ClientRepresentation> clients = super.createProviderClients();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -775,7 +775,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
|
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
|
||||||
setTimeOffset(0);
|
setTimeOffset(0);
|
||||||
|
|
||||||
events.expectLogin().client((String) null).user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,7 +795,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||||
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
||||||
.client((String) null)
|
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -852,7 +851,6 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
events.expect(EventType.LOGIN_ERROR)
|
events.expect(EventType.LOGIN_ERROR)
|
||||||
.user(new UserRepresentation())
|
.user(new UserRepresentation())
|
||||||
.client(new ClientRepresentation())
|
|
||||||
.error(Errors.COOKIE_NOT_FOUND)
|
.error(Errors.COOKIE_NOT_FOUND)
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,13 @@
|
||||||
package org.keycloak.testsuite.forms;
|
package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.AssertEvents.DEFAULT_REDIRECT_URI;
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
||||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.hamcrest.MatcherAssert;
|
import org.hamcrest.MatcherAssert;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
@ -31,9 +33,15 @@ import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||||
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -54,9 +62,11 @@ import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
|
||||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||||
import org.keycloak.testsuite.pages.RegisterPage;
|
import org.keycloak.testsuite.pages.RegisterPage;
|
||||||
import org.keycloak.testsuite.pages.VerifyEmailPage;
|
import org.keycloak.testsuite.pages.VerifyEmailPage;
|
||||||
|
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.BrowserTabUtil;
|
import org.keycloak.testsuite.util.BrowserTabUtil;
|
||||||
import org.keycloak.testsuite.util.ClientBuilder;
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
import org.keycloak.testsuite.util.GreenMailRule;
|
import org.keycloak.testsuite.util.GreenMailRule;
|
||||||
|
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
@ -99,6 +109,9 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
@Rule
|
@Rule
|
||||||
public GreenMailRule greenMail = new GreenMailRule();
|
public GreenMailRule greenMail = new GreenMailRule();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected AppPage appPage;
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@ -154,13 +167,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
// Login in tab2
|
// Login in tab2
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.assertCurrent();
|
|
||||||
|
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
appPage.assertCurrent();
|
|
||||||
|
|
||||||
// Try to go back to tab 1. We should be logged-in automatically
|
// Try to go back to tab 1. We should be logged-in automatically
|
||||||
tabUtil.closeTab(1);
|
tabUtil.closeTab(1);
|
||||||
|
@ -177,18 +184,195 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simulating scenario described in https://github.com/keycloak/keycloak/issues/24112
|
||||||
|
@Test
|
||||||
|
public void multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle() {
|
||||||
|
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
|
multipleTabsParallelLogin(tabUtil);
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
assertOnAppPageWithAlreadyLoggedInError(EventType.LOGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle_badRedirectUri() throws Exception {
|
||||||
|
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
|
multipleTabsParallelLogin(tabUtil);
|
||||||
|
|
||||||
|
// Remove redirectUri from the client
|
||||||
|
try (ClientAttributeUpdater cap = ClientAttributeUpdater.forClient(adminClient, "test", "test-app")
|
||||||
|
.setRedirectUris(List.of("https://foo"))
|
||||||
|
.update()) {
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
events.expectLogin().user((String) null).session((String) null).error(Errors.INVALID_REDIRECT_URI)
|
||||||
|
.detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE)
|
||||||
|
.detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value())
|
||||||
|
.removeDetail(Details.CONSENT)
|
||||||
|
.removeDetail(Details.CODE_ID)
|
||||||
|
.assertEvent();
|
||||||
|
errorPage.assertCurrent(); // Page "You are already logged in." should not be here
|
||||||
|
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void multipleTabsParallelLogin(BrowserTabUtil tabUtil) {
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// Open new tab 2
|
||||||
|
tabUtil.newTab(oauth.getLoginFormUrl());
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// Wait until authentication session expires
|
||||||
|
setTimeOffset(7200000);
|
||||||
|
|
||||||
|
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
|
||||||
|
|
||||||
|
loginSuccessAndDoRequiredActions();
|
||||||
|
|
||||||
|
// Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in")
|
||||||
|
tabUtil.closeTab(1);
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loginSuccessAndDoRequiredActions() {
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
updatePasswordPage.changePassword("password", "password");
|
||||||
|
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
||||||
|
.email("john@doe3.com").submit();
|
||||||
|
appPage.assertCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert browser was redirected to the appPage with "error=temporarily_unavailable" and error_description corresponding to Constants.AUTHENTICATION_EXPIRED_MESSAGE
|
||||||
|
private void assertOnAppPageWithAlreadyLoggedInError(EventType expectedEventType) {
|
||||||
|
events.expect(expectedEventType)
|
||||||
|
.user((String) null).error(Errors.ALREADY_LOGGED_IN)
|
||||||
|
.detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI))
|
||||||
|
.detail(Details.REDIRECTED_TO_CLIENT, "true")
|
||||||
|
.detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE)
|
||||||
|
.detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value())
|
||||||
|
.assertEvent();
|
||||||
|
appPage.assertCurrent(); // Page "You are already logged in." should not be here
|
||||||
|
OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
|
||||||
|
Assert.assertEquals(OAuthErrorException.TEMPORARILY_UNAVAILABLE, authzResponse.getError());
|
||||||
|
Assert.assertEquals(Constants.AUTHENTICATION_EXPIRED_MESSAGE, authzResponse.getErrorDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRegisterClick() {
|
||||||
|
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
|
multipleTabsParallelLogin(tabUtil);
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
loginPage.clickRegister();
|
||||||
|
assertOnAppPageWithAlreadyLoggedInError(EventType.REGISTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndResetPasswordClick() {
|
||||||
|
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
|
multipleTabsParallelLogin(tabUtil);
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
loginPage.resetPassword();
|
||||||
|
assertOnAppPageWithAlreadyLoggedInError(EventType.RESET_PASSWORD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRequiredAction() {
|
||||||
|
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
|
// Go through login in tab1 until required actions are shown
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
updatePasswordPage.assertCurrent();
|
||||||
|
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// Open new tab 2
|
||||||
|
tabUtil.newTab(oauth.getLoginFormUrl());
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// Wait until authentication session expires
|
||||||
|
setTimeOffset(7200000);
|
||||||
|
|
||||||
|
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
|
||||||
|
|
||||||
|
loginSuccessAndDoRequiredActions();
|
||||||
|
|
||||||
|
// Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in")
|
||||||
|
tabUtil.closeTab(1);
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
updatePasswordPage.changePassword("password", "password");
|
||||||
|
assertOnAppPageWithAlreadyLoggedInError(EventType.CUSTOM_REQUIRED_ACTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleTabsParallelLoginTestWithAuthSessionExpiredAndRefreshInTab1() {
|
||||||
|
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
|
// Go through login in tab1 and do unsuccessful login attempt (to make sure that "action URL" is shown in browser URL instead of OIDC authentication request URL)
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test", "bad-password");
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// Open new tab 2
|
||||||
|
tabUtil.newTab(oauth.getLoginFormUrl());
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// Wait until authentication session expires
|
||||||
|
setTimeOffset(7200000);
|
||||||
|
|
||||||
|
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
|
||||||
|
|
||||||
|
loginSuccessAndDoRequiredActions();
|
||||||
|
|
||||||
|
// Go back to tab1 and refresh the page. Should be automatically authenticated here (previously it showed "You are already logged-in")
|
||||||
|
tabUtil.closeTab(1);
|
||||||
|
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
driver.navigate().refresh();
|
||||||
|
assertOnAppPageWithAlreadyLoggedInError(EventType.LOGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLoginAfterLogoutFromDifferentTab() {
|
public void testLoginAfterLogoutFromDifferentTab() {
|
||||||
try (BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
try (BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||||
// login in the first tab
|
// login in the first tab
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
loginPage.login("login-test", "password");
|
|
||||||
updatePasswordPage.assertCurrent();
|
|
||||||
String tab1WindowHandle = util.getActualWindowHandle();
|
String tab1WindowHandle = util.getActualWindowHandle();
|
||||||
updatePasswordPage.changePassword("password", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
appPage.assertCurrent();
|
|
||||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
|
AccessToken accessToken = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||||
|
@ -257,11 +441,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
// Login success now
|
// Login success now
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
appPage.assertCurrent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -282,11 +462,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError());
|
Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError());
|
||||||
|
|
||||||
// Login success now
|
// Login success now
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
appPage.assertCurrent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -374,13 +550,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
// Go back to tab1 and finish login here
|
// Go back to tab1 and finish login here
|
||||||
driver.navigate().to(tab1Url);
|
driver.navigate().to(tab1Url);
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
|
|
||||||
// Assert I am redirected to the appPage in tab1
|
|
||||||
appPage.assertCurrent();
|
|
||||||
|
|
||||||
// Go back to tab2 and finish login here. Should be on the root-url-client page
|
// Go back to tab2 and finish login here. Should be on the root-url-client page
|
||||||
driver.navigate().to(tab2Url);
|
driver.navigate().to(tab2Url);
|
||||||
|
@ -410,10 +580,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
// Go back to tab1 and finish login here
|
// Go back to tab1 and finish login here
|
||||||
driver.navigate().to(tab1Url);
|
driver.navigate().to(tab1Url);
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
|
|
||||||
// Assert I am redirected to the appPage in tab1 and have state corresponding to tab1
|
// Assert I am redirected to the appPage in tab1 and have state corresponding to tab1
|
||||||
appPage.assertCurrent();
|
appPage.assertCurrent();
|
||||||
|
@ -444,10 +611,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
String tab2Url = driver.getCurrentUrl();
|
String tab2Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
// Continue in tab2 and finish login here
|
// Continue in tab2 and finish login here
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
|
|
||||||
// Assert I am redirected to the appPage in tab2 and have state corresponding to tab2
|
// Assert I am redirected to the appPage in tab2 and have state corresponding to tab2
|
||||||
appPage.assertCurrent();
|
appPage.assertCurrent();
|
||||||
|
@ -490,13 +654,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
// Login in tab2
|
// Login in tab2
|
||||||
loginPage.login("login-test", "password");
|
loginSuccessAndDoRequiredActions();
|
||||||
updatePasswordPage.assertCurrent();
|
|
||||||
|
|
||||||
updatePasswordPage.changePassword("password", "password");
|
|
||||||
updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3")
|
|
||||||
.email("john@doe3.com").submit();
|
|
||||||
appPage.assertCurrent();
|
|
||||||
|
|
||||||
// Try to go back to tab 1. We should be logged-in automatically
|
// Try to go back to tab 1. We should be logged-in automatically
|
||||||
tabUtil.closeTab(1);
|
tabUtil.closeTab(1);
|
||||||
|
|
|
@ -133,7 +133,6 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||||
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
||||||
.client((String) null)
|
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +172,6 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
|
||||||
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
|
||||||
.client((String) null)
|
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue