diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
index d53960a82e..b94ed2787f 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
@@ -63,7 +63,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
sendRequest(deployment);
lastRequestTime = currentTime;
} else {
- log.debug("Won't send request to realm jwks url. Last request time was " + lastRequestTime);
+ log.debugf("Won't send request to realm jwks url. Last request time was %d. Current time is %d.", lastRequestTime, currentTime);
}
return lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
@@ -76,6 +76,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
synchronized (this) {
sendRequest(deployment);
lastRequestTime = Time.currentTime();
+ log.debugf("Reset time offset to %d.", lastRequestTime);
}
}
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index e188f6ea75..6b2be22202 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -268,7 +268,7 @@ public class KeycloakInstalled {
// pass the id_token_hint so that sessions is invalidated for this particular session
String logoutUrl = deployment.getLogoutUrl().clone()
- .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
+ .queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri)
.queryParam("id_token_hint", idTokenString)
.build().toString();
diff --git a/adapters/oidc/js/src/keycloak.js b/adapters/oidc/js/src/keycloak.js
index 2f8e59c27b..2e4aadce08 100755
--- a/adapters/oidc/js/src/keycloak.js
+++ b/adapters/oidc/js/src/keycloak.js
@@ -478,7 +478,8 @@ function Keycloak (config) {
kc.createLogoutUrl = function(options) {
var url = kc.endpoints.logout()
- + '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
+ + '?post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false))
+ + '&id_token_hint=' + encodeURIComponent(kc.idToken);
return url;
}
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 79d4393910..c4dbac7e2a 100755
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -36,6 +36,10 @@ public interface OAuth2Constants {
String REDIRECT_URI = "redirect_uri";
+ String POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri";
+
+ String ID_TOKEN_HINT = "id_token_hint";
+
String DISPLAY = "display";
String SCOPE = "scope";
diff --git a/examples/kerberos/src/main/webapp/index.jsp b/examples/kerberos/src/main/webapp/index.jsp
index 5bb95b0c68..62c39567ef 100644
--- a/examples/kerberos/src/main/webapp/index.jsp
+++ b/examples/kerberos/src/main/webapp/index.jsp
@@ -17,7 +17,7 @@
<%
String logoutUri = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
- .queryParam("redirect_uri", "/kerberos-portal").build("kerberos-demo").toString();
+ .build("kerberos-demo").toString();
%>
Details about user from LDAP | Logout
diff --git a/examples/ldap/src/main/webapp/index.jsp b/examples/ldap/src/main/webapp/index.jsp
index 683e5b1652..72b8c2e8bb 100644
--- a/examples/ldap/src/main/webapp/index.jsp
+++ b/examples/ldap/src/main/webapp/index.jsp
@@ -21,7 +21,7 @@
<%
String logoutUri = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
- .queryParam("redirect_uri", "/ldap-portal").build("ldap-demo").toString();
+ .build("ldap-demo").toString();
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
IDToken idToken = securityContext.getIdToken();
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
index 1568f18d2e..6714454b34 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
@@ -65,6 +65,8 @@ public interface AccountProvider extends Provider {
AccountProvider setStateChecker(String stateChecker);
+ AccountProvider setIdTokenHint(String idTokenHint);
+
AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported, boolean authorizationSupported);
AccountProvider setAttribute(String key, String value);
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java
index b29d8c27c7..d0ff6db193 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java
@@ -28,6 +28,6 @@ public enum LoginFormsPages {
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE,
LOGIN_RECOVERY_AUTHN_CODES_INPUT, LOGIN_RECOVERY_AUTHN_CODES_CONFIG,
- FRONTCHANNEL_LOGOUT;
+ FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index 8106019e07..9a2487ee00 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -102,6 +102,8 @@ public interface LoginFormsProvider extends Provider {
Response createFrontChannelLogoutPage();
+ Response createLogoutConfirmPage();
+
LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession);
LoginFormsProvider setClientSessionCode(String accessCode);
diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java
index ca977f801b..6d5955a039 100755
--- a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java
+++ b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java
@@ -82,7 +82,15 @@ public interface LoginProtocol extends Provider {
Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
- Response finishLogout(UserSessionModel userSession);
+
+ /**
+ * This method is called when browser logout is going to be finished. It is not triggered during backchannel logout
+ *
+ * @param userSession user session, which was logged out
+ * @param logoutSession authentication session, which was used during logout to track the logout state
+ * @return response to be sent to the client
+ */
+ Response finishBrowserLogout(UserSessionModel userSession, AuthenticationSessionModel logoutSession);
/**
* @param userSession
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
index fd916084f6..173a59ec04 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
@@ -79,6 +79,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
protected String[] referrer;
protected List events;
protected String stateChecker;
+ protected String idTokenHint;
protected List sessions;
protected boolean identityProviderEnabled;
protected boolean eventsEnabled;
@@ -151,7 +152,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
attributes.put("realm", new RealmBean(realm));
}
- attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), stateChecker));
+ attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), idTokenHint));
if (realm.isInternationalizationEnabled()) {
UriBuilder b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath());
@@ -369,6 +370,12 @@ public class FreeMarkerAccountProvider implements AccountProvider {
return this;
}
+ @Override
+ public AccountProvider setIdTokenHint(String idTokenHint) {
+ this.idTokenHint = idTokenHint;
+ return this;
+ }
+
@Override
public AccountProvider setFeatures(boolean identityProviderEnabled, boolean eventsEnabled, boolean passwordUpdateSupported, boolean authorizationSupported) {
this.identityProviderEnabled = identityProviderEnabled;
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java
index 6e4c529b13..529f339d58 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java
@@ -33,13 +33,15 @@ public class UrlBean {
private URI baseURI;
private URI baseQueryURI;
private URI currentURI;
+ private String idTokenHint;
- public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI baseQueryURI, URI currentURI, String stateChecker) {
+ public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI baseQueryURI, URI currentURI, String idTokenHint) {
this.realm = realm.getName();
this.theme = theme;
this.baseURI = baseURI;
this.baseQueryURI = baseQueryURI;
this.currentURI = currentURI;
+ this.idTokenHint = idTokenHint;
}
public String getApplicationsUrl() {
@@ -71,7 +73,7 @@ public class UrlBean {
}
public String getLogoutUrl() {
- return Urls.accountLogout(baseQueryURI, currentURI, realm).toString();
+ return Urls.accountLogout(baseQueryURI, currentURI, realm, idTokenHint).toString();
}
public String getResourceUrl() {
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index e6ebac0cda..ad2ec949ec 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -36,6 +36,7 @@ import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean;
import org.keycloak.forms.login.freemarker.model.LoginBean;
import org.keycloak.forms.login.freemarker.model.FrontChannelLogoutBean;
+import org.keycloak.forms.login.freemarker.model.LogoutConfirmBean;
import org.keycloak.forms.login.freemarker.model.OAuthGrantBean;
import org.keycloak.forms.login.freemarker.model.ProfileBean;
import org.keycloak.forms.login.freemarker.model.RealmBean;
@@ -286,6 +287,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case FRONTCHANNEL_LOGOUT:
attributes.put("logout", new FrontChannelLogoutBean(session));
break;
+ case LOGOUT_CONFIRM:
+ attributes.put("logoutConfirm", new LogoutConfirmBean(accessCode, authenticationSession));
+ break;
}
return processTemplate(theme, Templates.getTemplate(page), locale);
@@ -681,6 +685,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return createResponse(LoginFormsPages.FRONTCHANNEL_LOGOUT);
}
+ @Override
+ public Response createLogoutConfirmPage() {
+ return createResponse(LoginFormsPages.LOGOUT_CONFIRM);
+ }
+
protected void setMessage(MessageType type, String message, Object... parameters) {
messageType = type;
messages = new ArrayList<>();
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
index 8c639864a4..828c474aef 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
@@ -84,6 +84,8 @@ public class Templates {
return "idp-review-user-profile.ftl";
case FRONTCHANNEL_LOGOUT:
return "frontchannel-logout.ftl";
+ case LOGOUT_CONFIRM:
+ return "logout-confirm.ftl";
default:
throw new IllegalArgumentException();
}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/LogoutConfirmBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/LogoutConfirmBean.java
new file mode 100644
index 0000000000..936affd559
--- /dev/null
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/LogoutConfirmBean.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 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.forms.login.freemarker.model;
+
+import org.keycloak.models.utils.SystemClientUtil;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ * @author Marek Posolda
+ */
+public class LogoutConfirmBean {
+
+ private final String code;
+ private final AuthenticationSessionModel logoutSession;
+
+ public LogoutConfirmBean(String code, AuthenticationSessionModel logoutSession) {
+ this.code = code;
+ this.logoutSession = logoutSession;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public boolean isSkipLink() {
+ return logoutSession == null || logoutSession.getClient().equals(SystemClientUtil.getSystemClient(logoutSession.getRealm()));
+ }
+}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
index ad33f03a36..df51b59272 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
@@ -95,6 +95,10 @@ public class UrlBean {
return Urls.firstBrokerLoginProcessor(baseURI, realm).toString();
}
+ public String getLogoutConfirmAction() {
+ return Urls.logoutConfirm(baseURI, realm).toString();
+ }
+
public String getResourcesUrl() {
return Urls.themeRoot(baseURI).toString() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName();
}
diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
index d5fb8b6d5d..1c01f3d4a7 100644
--- a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
+++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java
@@ -160,7 +160,7 @@ public class DockerAuthV2Protocol implements LoginProtocol {
}
@Override
- public Response finishLogout(final UserSessionModel userSession) {
+ public Response finishBrowserLogout(final UserSessionModel userSession, AuthenticationSessionModel logoutSession) {
return errorResponse(userSession, "finishLogout");
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index ccf878a043..5446a29ad2 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -41,6 +41,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
+import org.keycloak.protocol.oidc.utils.LogoutUtil;
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@@ -52,6 +54,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@@ -73,12 +76,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String LOGIN_PROTOCOL = "openid-connect";
public static final String STATE_PARAM = "state";
- public static final String LOGOUT_STATE_PARAM = "OIDC_LOGOUT_STATE_PARAM";
public static final String SCOPE_PARAM = "scope";
public static final String CODE_PARAM = "code";
public static final String RESPONSE_TYPE_PARAM = "response_type";
public static final String GRANT_TYPE_PARAM = "grant_type";
public static final String REDIRECT_URI_PARAM = "redirect_uri";
+ public static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri";
public static final String CLIENT_ID_PARAM = "client_id";
public static final String NONCE_PARAM = "nonce";
public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
@@ -91,7 +94,11 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String ACR_PARAM = "acr_values";
public static final String ID_TOKEN_HINT = "id_token_hint";
+ public static final String LOGOUT_STATE_PARAM = "OIDC_LOGOUT_STATE_PARAM";
public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
+ public static final String LOGOUT_VALIDATED_ID_TOKEN_SESSION_STATE = "OIDC_LOGOUT_VALIDATED_ID_TOKEN_SESSION_STATE";
+ public static final String LOGOUT_VALIDATED_ID_TOKEN_ISSUED_AT = "OIDC_LOGOUT_VALIDATED_ID_TOKEN_ISSUED_AT";
+
public static final String ISSUER = "iss";
public static final String RESPONSE_MODE_PARAM = "response_mode";
@@ -350,28 +357,21 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
@Override
- public Response finishLogout(UserSessionModel userSession) {
- String redirectUri = userSession.getNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
- String state = userSession.getNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM);
+ public Response finishBrowserLogout(UserSessionModel userSession, AuthenticationSessionModel logoutSession) {
event.event(EventType.LOGOUT);
+
+ String redirectUri = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
if (redirectUri != null) {
event.detail(Details.REDIRECT_URI, redirectUri);
}
event.user(userSession.getUser()).session(userSession).success();
FrontChannelLogoutHandler frontChannelLogoutHandler = FrontChannelLogoutHandler.current(session);
if (frontChannelLogoutHandler != null) {
- return frontChannelLogoutHandler.renderLogoutPage(redirectUri);
- }
- if (redirectUri != null) {
- UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
- if (state != null)
- uriBuilder.queryParam(STATE_PARAM, state);
- return Response.status(302).location(uriBuilder.build()).build();
- } else {
- // TODO Empty content with ok makes no sense. Should it display a page? Or use noContent?
- session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType();
- return Response.ok().build();
+ String finalRedirectUri = redirectUri == null ? null : LogoutUtil.getRedirectUriWithAttachedState(redirectUri, logoutSession).toString();
+ return frontChannelLogoutHandler.renderLogoutPage(finalRedirectUri);
}
+
+ return LogoutUtil.sendResponseAfterLogoutFinished(session, logoutSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
index e5b89db6aa..dbf01d8d37 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
@@ -17,6 +17,7 @@
package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger;
+import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.common.constants.KerberosConstants;
@@ -102,6 +103,17 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
public static final String OFFLINE_ACCESS_SCOPE_CONSENT_TEXT = Constants.OFFLINE_ACCESS_SCOPE_CONSENT_TEXT;
public static final String ROLES_SCOPE_CONSENT_TEXT = "${rolesScopeConsentText}";
+ public static final String CONFIG_LEGACY_LOGOUT_REDIRECT_URI = "legacy-logout-redirect-uri";
+
+ private OIDCProviderConfig providerConfig;
+
+ @Override
+ public void init(Config.Scope config) {
+ this.providerConfig = new OIDCProviderConfig(config);
+ if (providerConfig.isLegacyLogoutRedirectUri()) {
+ logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
+ }
+ }
@Override
public LoginProtocol create(KeycloakSession session) {
@@ -379,7 +391,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
@Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event) {
- return new OIDCLoginProtocolService(realm, event);
+ return new OIDCLoginProtocolService(realm, event, providerConfig);
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index b404eeb9a7..ff17107f72 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -25,6 +25,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.Config;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
import org.keycloak.crypto.KeyType;
@@ -78,9 +79,10 @@ public class OIDCLoginProtocolService {
private static final Logger logger = Logger.getLogger(OIDCLoginProtocolService.class);
- private RealmModel realm;
- private TokenManager tokenManager;
- private EventBuilder event;
+ private final RealmModel realm;
+ private final TokenManager tokenManager;
+ private final EventBuilder event;
+ private final OIDCProviderConfig providerConfig;
@Context
private KeycloakSession session;
@@ -94,10 +96,11 @@ public class OIDCLoginProtocolService {
@Context
private ClientConnection clientConnection;
- public OIDCLoginProtocolService(RealmModel realm, EventBuilder event) {
+ public OIDCLoginProtocolService(RealmModel realm, EventBuilder event, OIDCProviderConfig providerConfig) {
this.realm = realm;
this.tokenManager = new TokenManager();
this.event = event;
+ this.providerConfig = providerConfig;
}
public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) {
@@ -261,7 +264,7 @@ public class OIDCLoginProtocolService {
* https://issues.redhat.com/browse/KEYCLOAK-2940 */
@Path("logout")
public Object logout() {
- LogoutEndpoint endpoint = new LogoutEndpoint(tokenManager, realm, event);
+ LogoutEndpoint endpoint = new LogoutEndpoint(tokenManager, realm, event, providerConfig);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
return endpoint;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCProviderConfig.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCProviderConfig.java
new file mode 100644
index 0000000000..d450032c5f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCProviderConfig.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 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.oidc;
+
+import org.keycloak.Config;
+
+/**
+ * @author Marek Posolda
+ */
+public class OIDCProviderConfig {
+
+ private final boolean legacyLogoutRedirectUri;
+
+ public OIDCProviderConfig(Config.Scope config) {
+ this.legacyLogoutRedirectUri = config.getBoolean(OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, false);
+ }
+
+ public boolean isLegacyLogoutRedirectUri() {
+ return legacyLogoutRedirectUri;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 66b8a4a08d..29634d34fe 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -30,16 +30,24 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.headers.SecurityHeadersProvider;
+import org.keycloak.locale.LocaleSelectorProvider;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.SystemClientUtil;
import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
import org.keycloak.protocol.oidc.LogoutTokenValidationCode;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
+import org.keycloak.protocol.oidc.OIDCProviderConfig;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
+import org.keycloak.protocol.oidc.utils.LogoutUtil;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.LogoutToken;
@@ -50,10 +58,16 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.LogoutRequestContext;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
+import org.keycloak.services.resources.LogoutSessionCodeChecks;
+import org.keycloak.services.resources.SessionCodeChecks;
import org.keycloak.services.util.MtlsHoKTokenUtil;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.Consumes;
@@ -67,13 +81,13 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.UserSessionModel.State.LOGGED_OUT;
import static org.keycloak.models.UserSessionModel.State.LOGGING_OUT;
+import static org.keycloak.services.resources.LoginActionsService.SESSION_CODE;
/**
* @author Stian Thorgersen
@@ -93,19 +107,21 @@ public class LogoutEndpoint {
@Context
private HttpHeaders headers;
- private TokenManager tokenManager;
- private RealmModel realm;
- private EventBuilder event;
+ private final TokenManager tokenManager;
+ private final RealmModel realm;
+ private final EventBuilder event;
+ private final OIDCProviderConfig providerConfig;
// When enabled we cannot search offline sessions by brokerSessionId. We need to search by federated userId and then filter by brokerSessionId.
- private boolean offlineSessionsLazyLoadingEnabled;
+ private final boolean offlineSessionsLazyLoadingEnabled;
private Cors cors;
- public LogoutEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
+ public LogoutEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event, OIDCProviderConfig providerConfig) {
this.tokenManager = tokenManager;
this.realm = realm;
this.event = event;
+ this.providerConfig = providerConfig;
this.offlineSessionsLazyLoadingEnabled = !Config.scope("userSessions").scope("infinispan").getBoolean("preloadOfflineSessionsFromDatabase", false);
}
@@ -121,18 +137,42 @@ public class LogoutEndpoint {
* When the logout is initiated by a remote idp, the parameter "initiating_idp" can be supplied. This param will
* prevent upstream logout (since the logout procedure has already been started in the remote idp).
*
- * @param redirectUri
+ * This endpoint is aligned with OpenID Connect RP-Initiated Logout specification https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
+ *
+ * All parameters are optional. Some combinations of parameters are invalid as described in the specification
+ *
+ * @param deprecatedRedirectUri Parameter "redirect_uri" is not supported by the specification. It is here just for the backwards compatibility
+ * @param encodedIdToken Parameter "id_token_hint" as described in the specification.
+ * @param postLogoutRedirectUri Parameter "post_logout_redirect_uri" as described in the specification with the URL to redirect after logout.
+ * @param state Parameter "state" as described in the specification. Will be used to send "state" when redirecting back to the application after the logout
+ * @param uiLocales Parameter "ui_locales" as described in the specification. Can be used by the client to display pages in specified locale (if any pages are going to be displayed to the user during logout)
* @param initiatingIdp The alias of the idp initiating the logout.
* @return
*/
@GET
@NoCache
- public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, // deprecated
- @QueryParam("id_token_hint") String encodedIdToken,
- @QueryParam("post_logout_redirect_uri") String postLogoutRedirectUri,
- @QueryParam("state") String state,
- @QueryParam("initiating_idp") String initiatingIdp) {
- String redirect = postLogoutRedirectUri != null ? postLogoutRedirectUri : redirectUri;
+ public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String deprecatedRedirectUri, // deprecated
+ @QueryParam(OIDCLoginProtocol.ID_TOKEN_HINT) String encodedIdToken,
+ @QueryParam(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM) String postLogoutRedirectUri,
+ @QueryParam(OIDCLoginProtocol.STATE_PARAM) String state,
+ @QueryParam(OIDCLoginProtocol.UI_LOCALES_PARAM) String uiLocales,
+ @QueryParam(AuthenticationManager.INITIATING_IDP_PARAM) String initiatingIdp) {
+
+ if (deprecatedRedirectUri != null && !providerConfig.isLegacyLogoutRedirectUri()) {
+ event.event(EventType.LOGOUT);
+ event.error(Errors.INVALID_REQUEST);
+ logger.warnf("Parameter 'redirect_uri' no longer supported. Please use 'post_logout_redirect_uri' with 'id_token_hint' for this endpoint. Alternatively you can enable backwards compatibility option '%s' of oidc login protocol in the server configuration.",
+ OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
+ }
+
+ if (postLogoutRedirectUri != null && encodedIdToken == null) {
+ event.event(EventType.LOGOUT);
+ event.error(Errors.INVALID_REQUEST);
+ logger.warnf("Parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.");
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER, OIDCLoginProtocol.ID_TOKEN_HINT);
+ }
+
IDToken idToken = null;
if (encodedIdToken != null) {
try {
@@ -141,34 +181,117 @@ public class LogoutEndpoint {
} catch (OAuthErrorException | VerificationException e) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_TOKEN);
- return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.ID_TOKEN_HINT);
}
}
- if (redirect != null) {
- String validatedUri;
- ClientModel client = (idToken == null || idToken.getIssuedFor() == null) ? null : realm.getClientByClientId(idToken.getIssuedFor());
+ ClientModel client = (idToken == null || idToken.getIssuedFor() == null) ? null : realm.getClientByClientId(idToken.getIssuedFor());
+ if (client != null) {
+ session.getContext().setClient(client);
+ }
+
+ String validatedRedirectUri = null;
+ if (postLogoutRedirectUri != null || deprecatedRedirectUri != null) {
+ String redirectUri = postLogoutRedirectUri != null ? postLogoutRedirectUri : deprecatedRedirectUri;
if (client != null) {
- validatedUri = RedirectUtils.verifyRedirectUri(session, redirect, client);
- } else {
- validatedUri = RedirectUtils.verifyRealmRedirectUri(session, redirect);
+ validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, redirectUri, client);
+ } else if (providerConfig.isLegacyLogoutRedirectUri()) {
+ validatedRedirectUri = RedirectUtils.verifyRealmRedirectUri(session, deprecatedRedirectUri);
}
- if (validatedUri == null) {
+
+ if (validatedRedirectUri == null) {
event.event(EventType.LOGOUT);
- event.detail(Details.REDIRECT_URI, redirect);
+ event.detail(Details.REDIRECT_URI, redirectUri);
event.error(Errors.INVALID_REDIRECT_URI);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
}
- redirect = validatedUri;
}
- UserSessionModel userSession = null;
+ AuthenticationSessionModel logoutSession = AuthenticationManager.createOrJoinLogoutSession(session, realm, new AuthenticationSessionManager(session), null, true);
+ session.getContext().setAuthenticationSession(logoutSession);
+ if (uiLocales != null) {
+ logoutSession.setAuthNote(LocaleSelectorProvider.CLIENT_REQUEST_LOCALE, uiLocales);
+ }
+ if (validatedRedirectUri != null) {
+ logoutSession.setAuthNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, validatedRedirectUri);
+ }
+ if (state != null) {
+ logoutSession.setAuthNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM, state);
+ }
+ if (initiatingIdp != null) {
+ logoutSession.setAuthNote(AuthenticationManager.LOGOUT_INITIATING_IDP, initiatingIdp);
+ }
if (idToken != null) {
+ logoutSession.setAuthNote(OIDCLoginProtocol.LOGOUT_VALIDATED_ID_TOKEN_SESSION_STATE, idToken.getSessionState());
+ logoutSession.setAuthNote(OIDCLoginProtocol.LOGOUT_VALIDATED_ID_TOKEN_ISSUED_AT, String.valueOf(idToken.getIat()));
+ }
+
+ LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
+ .setAuthenticationSession(logoutSession);
+
+ // Client was not sent in id_token_hint or has consentRequired. Logout confirmation screen will be displayed to the user in this case
+ if (client == null || client.isConsentRequired()) {
+ return displayLogoutConfirmationScreen(loginForm, logoutSession);
+ } else {
+ return doBrowserLogout(logoutSession);
+ }
+ }
+
+ private Response displayLogoutConfirmationScreen(LoginFormsProvider loginForm, AuthenticationSessionModel authSession) {
+ ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession);
+ accessCode.setAction(AuthenticatedClientSessionModel.Action.LOGGING_OUT.name());
+
+ return loginForm
+ .setClientSessionCode(accessCode.getOrGenerateCode())
+ .createLogoutConfirmPage();
+ }
+
+ @Path("/logout-confirm")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response logoutConfirmAction() {
+ MultivaluedMap formData = request.getDecodedFormParameters();
+ event.event(EventType.LOGOUT);
+ String code = formData.getFirst(SESSION_CODE);
+ String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID);
+ String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_ID);
+
+ logger.tracef("Logout confirmed. sessionCode=%s, clientId=%s, tabId=%s", code, clientId, tabId);
+
+ SessionCodeChecks checks = new LogoutSessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, code, clientId, tabId);
+ checks.initialVerify();
+ if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.LOGGING_OUT.name(), ClientSessionCode.ActionType.USER) || !formData.containsKey("confirmLogout")) {
+ AuthenticationSessionModel logoutSession = checks.getAuthenticationSession();
+ logger.debugf("Failed verification during logout. logoutSessionId=%s, clientId=%s, tabId=%s",
+ logoutSession != null ? logoutSession.getParentSession().getId() : "unknown", clientId, tabId);
+
+ if (logoutSession == null || logoutSession.getClient().equals(SystemClientUtil.getSystemClient(logoutSession.getRealm()))) {
+ // Cleanup system client URL to avoid links to account management
+ session.getProvider(LoginFormsProvider.class).setAttribute(Constants.SKIP_LINK, true);
+ }
+
+ return ErrorPage.error(session, logoutSession, Response.Status.BAD_REQUEST, Messages.FAILED_LOGOUT);
+ }
+
+ AuthenticationSessionModel logoutSession = checks.getAuthenticationSession();
+ logger.tracef("Logout code successfully verified. Logout Session is '%s'. Client ID is '%s'.", logoutSession.getParentSession().getId(),
+ logoutSession.getClient().getClientId());
+ return doBrowserLogout(logoutSession);
+ }
+
+
+ // Method triggered after user eventually confirmed that he wants to logout and all other checks were done
+ private Response doBrowserLogout(AuthenticationSessionModel logoutSession) {
+ UserSessionModel userSession = null;
+ String userSessionIdFromIdToken = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_VALIDATED_ID_TOKEN_SESSION_STATE);
+ String idTokenIssuedAtStr = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_VALIDATED_ID_TOKEN_ISSUED_AT);
+ if (userSessionIdFromIdToken != null && idTokenIssuedAtStr != null) {
try {
- userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
+ userSession = session.sessions().getUserSession(realm, userSessionIdFromIdToken);
if (userSession != null) {
- checkTokenIssuedAt(idToken, userSession);
+ Integer idTokenIssuedAt = Integer.parseInt(idTokenIssuedAtStr);
+ checkTokenIssuedAt(idTokenIssuedAt, userSession);
}
} catch (OAuthErrorException e) {
event.event(EventType.LOGOUT);
@@ -181,12 +304,11 @@ public class LogoutEndpoint {
AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
userSession = userSession != null ? userSession : authResult.getSession();
- return initiateBrowserLogout(userSession, redirect, state, initiatingIdp);
- }
- else if (userSession != null) {
+ return initiateBrowserLogout(userSession);
+ } else if (userSession != null) {
// identity cookie is missing but there's valid id_token_hint which matches session cookie => continue with browser logout
- if (idToken != null && idToken.getSessionState().equals(AuthenticationManager.getSessionIdFromSessionCookie(session))) {
- return initiateBrowserLogout(userSession, redirect, state, initiatingIdp);
+ if (userSessionIdFromIdToken.equals(AuthenticationManager.getSessionIdFromSessionCookie(session))) {
+ return initiateBrowserLogout(userSession);
}
// check if the user session is not logging out or already logged out
// this might happen when a backChannelLogout is already initiated from AuthenticationManager.authenticateIdentityCookie
@@ -194,21 +316,22 @@ public class LogoutEndpoint {
// non browser logout
event.event(EventType.LOGOUT);
AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true);
+
+ String redirectUri = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
+ if (redirectUri != null) {
+ event.detail(Details.REDIRECT_URI, redirectUri);
+ }
event.user(userSession.getUser()).session(userSession).success();
}
}
- if (redirect != null) {
- UriBuilder uriBuilder = UriBuilder.fromUri(redirect);
- if (state != null) uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
- return Response.status(302).location(uriBuilder.build()).build();
- } else {
- // TODO Empty content with ok makes no sense. Should it display a page? Or use noContent?
- session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType();
- return Response.ok().build();
- }
+ logger.tracef("Removing logout session '%s' used during logout.", logoutSession.getParentSession().getId());
+ RootAuthenticationSessionModel rootAuthSession = logoutSession.getParentSession();
+ rootAuthSession.removeAuthenticationSessionByTabId(logoutSession.getTabId());
+ return LogoutUtil.sendResponseAfterLogoutFinished(session, logoutSession);
}
+
/**
* Logout a session via a non-browser invocation. Similar signature to refresh token except there is no grant_type.
* You must pass in the refresh token and
@@ -262,7 +385,7 @@ public class LogoutEndpoint {
}
if (userSessionModel != null) {
- checkTokenIssuedAt(token, userSessionModel);
+ checkTokenIssuedAt(token.getIssuedAt(), userSessionModel);
logout(userSessionModel, offline);
}
} catch (OAuthErrorException e) {
@@ -476,19 +599,17 @@ public class LogoutEndpoint {
}
}
- private void checkTokenIssuedAt(IDToken token, UserSessionModel userSession) throws OAuthErrorException {
- if (token.getIssuedAt() + 1 < userSession.getStarted()) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh toked issued before the user session started");
+ private void checkTokenIssuedAt(int idTokenIssuedAt, UserSessionModel userSession) throws OAuthErrorException {
+ if (idTokenIssuedAt + 1 < userSession.getStarted()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Toked issued before the user session started");
}
}
- private Response initiateBrowserLogout(UserSessionModel userSession, String redirect, String state, String initiatingIdp ) {
- if (redirect != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirect);
- if (state != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM, state);
+ private Response initiateBrowserLogout(UserSessionModel userSession) {
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, OIDCLoginProtocol.LOGIN_PROTOCOL);
- logger.debug("Initiating OIDC browser logout");
- Response response = AuthenticationManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, initiatingIdp);
- logger.debug("finishing OIDC browser logout");
+ logger.tracef("Calling initiateBrowserLogout for user session '%s'", userSession.getId());
+ Response response = AuthenticationManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
+ logger.tracef("Finished call of initiateBrowserLogout for user session '%s'", userSession.getId());
return response;
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/LogoutUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/LogoutUtil.java
new file mode 100644
index 0000000000..27198d9bf7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/LogoutUtil.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 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.oidc.utils;
+
+import java.net.URI;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.utils.SystemClientUtil;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ * Utilities for OIDC logout
+ *
+ * @author Marek Posolda
+ */
+public class LogoutUtil {
+
+ public static Response sendResponseAfterLogoutFinished(KeycloakSession session, AuthenticationSessionModel logoutSession) {
+ String redirectUri = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
+ if (redirectUri != null) {
+ URI finalRedirectUri = getRedirectUriWithAttachedState(redirectUri, logoutSession);
+ return Response.status(302).location(finalRedirectUri).build();
+ }
+
+ LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setSuccess(Messages.SUCCESS_LOGOUT);
+ boolean usedSystemClient = logoutSession.getClient().equals(SystemClientUtil.getSystemClient(logoutSession.getRealm()));
+ if (usedSystemClient) {
+ loginForm.setAttribute(Constants.SKIP_LINK, true);
+ }
+ return loginForm.createInfoPage();
+ }
+
+
+ public static URI getRedirectUriWithAttachedState(String redirectUri, AuthenticationSessionModel logoutSession) {
+ if (redirectUri == null) return null;
+ String state = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM);
+
+ UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
+ if (state != null) {
+ uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
+ }
+ return uriBuilder.build();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
index da0f11af76..2c7f53be77 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
@@ -41,6 +41,12 @@ public class RedirectUtils {
private static final Logger logger = Logger.getLogger(RedirectUtils.class);
+ /**
+ * This method is deprecated for performance and security reasons and it is available just for the
+ * backwards compatibility. It is recommended to use some other methods of this class where the client is given as an argument
+ * to the method, so we know the client, which redirect-uri we are trying to resolve.
+ */
+ @Deprecated
public static String verifyRealmRedirectUri(KeycloakSession session, String redirectUri) {
Set validRedirects = getValidateRedirectUris(session);
return verifyRedirectUri(session, null, redirectUri, validRedirects, true);
@@ -71,6 +77,7 @@ public class RedirectUtils {
return resolveValidRedirects;
}
+ @Deprecated
private static Set getValidateRedirectUris(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
return session.clientStorageManager().getAllRedirectUrisOfEnabledClients(realm).entrySet().stream()
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index fb51b1a556..44b813d9f2 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -672,7 +672,7 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
- public Response finishLogout(UserSessionModel userSession) {
+ public Response finishBrowserLogout(UserSessionModel userSession, AuthenticationSessionModel logoutSession) {
logger.debug("finishLogout");
String logoutBindingUri = userSession.getNote(SAML_LOGOUT_BINDING_URI);
if (logoutBindingUri == null) {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 39b7c6f380..c91b7334ca 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -255,7 +255,7 @@ public class SamlService extends AuthorizationEndpointBase {
session.getContext().setClient(client);
logger.debug("logout response");
- Response response = authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, null);
+ Response response = authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
event.success();
return response;
}
@@ -580,7 +580,7 @@ public class SamlService extends AuthorizationEndpointBase {
}
logger.debug("browser Logout");
- return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, null);
+ return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
} else if (logoutRequest.getSessionIndex() != null) {
for (String sessionIndex : logoutRequest.getSessionIndex()) {
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index fb71147268..8381bf94b5 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -16,10 +16,12 @@
*/
package org.keycloak.services;
+import org.keycloak.OAuth2Constants;
import org.keycloak.common.Version;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.services.resources.account.AccountFormService;
import org.keycloak.services.resources.IdentityBrokerService;
@@ -139,8 +141,12 @@ public class Urls {
return accountBase(baseUri).path(AccountFormService.class, "sessionsPage").build(realmName);
}
- public static URI accountLogout(URI baseUri, URI redirectUri, String realmName) {
- return realmLogout(baseUri).queryParam("redirect_uri", redirectUri).build(realmName);
+ public static URI accountLogout(URI baseUri, URI redirectUri, String realmName, String idTokenHint) {
+ return realmLogout(baseUri).queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri).queryParam(OAuth2Constants.ID_TOKEN_HINT, idTokenHint).build(realmName);
+ }
+
+ public static URI logoutConfirm(URI baseUri, String realmName) {
+ return realmLogout(baseUri).path(LogoutEndpoint.class, "logoutConfirmAction").build(realmName);
}
public static URI accountResourcesPage(URI baseUri, String realmName) {
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index f2878a4dab..5e7b2e496f 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -84,7 +84,6 @@ import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.AuthorizationContextUtil;
import org.keycloak.services.util.CookieHelper;
-import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.P3PHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
@@ -154,7 +153,18 @@ public class AuthenticationManager {
// used solely to determine is user is logged in
public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION";
public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME";
+
+ // ** Logout related notes **/
+ // Flag in the logout session to specify if we use "system" client or real client
+ public static final String LOGOUT_WITH_SYSTEM_CLIENT = "LOGOUT_WITH_SYSTEM_CLIENT";
+ // Protocol of the client, which initiated logout
public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL";
+ // Filled in case that logout was triggered with "initiating idp"
+ public static final String LOGOUT_INITIATING_IDP = "LOGOUT_INITIATING_IDP";
+
+ // Parameter of LogoutEndpoint
+ public static final String INITIATING_IDP_PARAM = "initiating_idp";
+
private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID);
public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) {
@@ -287,6 +297,7 @@ public class AuthenticationManager {
userSessionOnlyHasLoggedOutClients =
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
} finally {
+ logger.tracef("Removing logout session '%s' after backchannel logout", logoutAuthSession.getParentSession().getId());
RootAuthenticationSessionModel rootAuthSession = logoutAuthSession.getParentSession();
rootAuthSession.removeAuthenticationSessionByTabId(logoutAuthSession.getTabId());
}
@@ -313,9 +324,17 @@ public class AuthenticationManager {
return backchannelLogoutResponse;
}
- private static AuthenticationSessionModel createOrJoinLogoutSession(KeycloakSession session, RealmModel realm, final AuthenticationSessionManager asm, UserSessionModel userSession, boolean browserCookie) {
- // Account management client is used as a placeholder
- ClientModel client = SystemClientUtil.getSystemClient(realm);
+ public static AuthenticationSessionModel createOrJoinLogoutSession(KeycloakSession session, RealmModel realm, final AuthenticationSessionManager asm, UserSessionModel userSession, boolean browserCookie) {
+ AuthenticationSessionModel logoutSession = session.getContext().getAuthenticationSession();
+ if (logoutSession != null && AuthenticationSessionModel.Action.LOGGING_OUT.name().equals(logoutSession.getAction())) {
+ return logoutSession;
+ }
+
+ ClientModel client = session.getContext().getClient();
+ if (client == null) {
+ // Account management client is used as a placeholder
+ client = SystemClientUtil.getSystemClient(realm);
+ }
String authSessionId;
RootAuthenticationSessionModel rootLogoutSession = null;
@@ -328,9 +347,11 @@ public class AuthenticationManager {
if (rootLogoutSession != null) {
authSessionId = rootLogoutSession.getId();
browserCookiePresent = true;
- } else {
+ } else if (userSession != null) {
authSessionId = userSession.getId();
rootLogoutSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
+ } else {
+ authSessionId = KeycloakModelUtils.generateId();
}
if (rootLogoutSession == null) {
@@ -342,15 +363,22 @@ public class AuthenticationManager {
}
// See if we have logoutAuthSession inside current rootSession. Create new if not
- Optional found = rootLogoutSession.getAuthenticationSessions().values().stream().filter((AuthenticationSessionModel authSession) -> {
- return client.equals(authSession.getClient()) && Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), authSession.getAction());
+ Optional found = rootLogoutSession.getAuthenticationSessions().values().stream()
+ .filter( authSession -> AuthenticationSessionModel.Action.LOGGING_OUT.name().equals(authSession.getAction()))
+ .findFirst();
- }).findFirst();
-
- AuthenticationSessionModel logoutAuthSession = found.isPresent() ? found.get() : rootLogoutSession.createAuthenticationSession(client);
+ AuthenticationSessionModel logoutAuthSession;
+ if (found.isPresent()) {
+ logoutAuthSession = found.get();
+ logger.tracef("Found existing logout session for client '%s'. Authentication session id: %s", client.getClientId(), rootLogoutSession.getId());
+ } else {
+ logoutAuthSession = rootLogoutSession.createAuthenticationSession(client);
+ logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
+ session.getContext().setClient(client);
+ logger.tracef("Creating logout session for client '%s'. Authentication session id: %s", client.getClientId(), rootLogoutSession.getId());
+ }
session.getContext().setAuthenticationSession(logoutAuthSession);
- logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
return logoutAuthSession;
}
@@ -593,8 +621,7 @@ public class AuthenticationManager {
UserSessionModel userSession,
UriInfo uriInfo,
ClientConnection connection,
- HttpHeaders headers,
- String initiatingIdp) {
+ HttpHeaders headers) {
if (userSession == null) return null;
if (logger.isDebugEnabled()) {
@@ -615,6 +642,7 @@ public class AuthenticationManager {
}
String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
+ String initiatingIdp = logoutAuthSession.getAuthNote(AuthenticationManager.LOGOUT_INITIATING_IDP);
if (brokerId != null && !brokerId.equals(initiatingIdp)) {
IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId);
response = identityProvider.keycloakInitiatedBrowserLogout(session, userSession, uriInfo, realm);
@@ -666,7 +694,7 @@ public class AuthenticationManager {
.setEventBuilder(event);
- Response response = protocol.finishLogout(userSession);
+ Response response = protocol.finishBrowserLogout(userSession, logoutAuthSession);
// It may be possible that there are some client sessions that are still in LOGGING_OUT state
long numberOfUnconfirmedSessions = userSession.getAuthenticatedClientSessions().values().stream()
@@ -691,6 +719,7 @@ public class AuthenticationManager {
session.sessions().removeUserSession(realm, userSession);
}
+ logger.tracef("Removing logout session '%s'.", logoutAuthSession.getParentSession().getId());
session.authenticationSessions().removeRootAuthenticationSession(realm, logoutAuthSession.getParentSession());
return response;
@@ -1412,7 +1441,7 @@ public class AuthenticationManager {
AccessToken token = verifier.verify().getToken();
if (checkActive) {
if (!token.isActive() || token.getIssuedAt() < realm.getNotBefore()) {
- logger.debug("Identity cookie expired");
+ logger.debugf("Identity cookie expired. Token expiration: %d, Current Time: %d. token issued at: %d, realm not before: %d", token.getExp(), Time.currentTime(), token.getIssuedAt(), realm.getNotBefore());
return null;
}
}
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 0cb6bd4d81..9ebf8d03b6 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -235,6 +235,8 @@ public class Messages {
public static final String INSUFFICIENT_LEVEL_OF_AUTHENTICATION = "insufficientLevelOfAuthentication";
+ public static final String SUCCESS_LOGOUT = "successLogout";
+
public static final String FAILED_LOGOUT = "failedLogout";
public static final String CONSENT_DENIED="consentDenied";
diff --git a/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java
new file mode 100644
index 0000000000..21eb09da1e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/LogoutSessionCodeChecks.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 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.services.resources;
+
+import java.net.URI;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.ErrorPage;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
+/**
+ * @author Marek Posolda
+ */
+public class LogoutSessionCodeChecks extends SessionCodeChecks {
+
+ public LogoutSessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
+ String code, String clientId, String tabId) {
+ super(realm, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, null);
+ }
+
+
+ @Override
+ protected void setClientToEvent(ClientModel client) {
+ // Skip sending client to logout event
+ }
+
+ @Override
+ protected Response restartAuthenticationSessionFromCookie(RootAuthenticationSessionModel existingRootSession) {
+ // Skip restarting authentication session from KC_RESTART cookie during logout
+ getEvent().error(Errors.SESSION_EXPIRED);
+ return ErrorPage.error(getSession(), null, Response.Status.BAD_REQUEST, Messages.FAILED_LOGOUT);
+ }
+
+ @Override
+ protected boolean isActionActive(ClientSessionCode.ActionType actionType) {
+ if (!getClientCode().isActionActive(actionType)) {
+ getEvent().clone().error(Errors.EXPIRED_CODE);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
index 963017d50a..5c8db8e8a9 100644
--- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
@@ -201,6 +201,7 @@ public class SessionCodeChecks {
if (authSession == null) {
return false;
}
+ session.getContext().setAuthenticationSession(authSession);
// Check cached response from previous action request
response = BrowserHistoryHelper.getInstance().loadSavedResponse(session, authSession);
@@ -218,7 +219,7 @@ public class SessionCodeChecks {
return false;
}
- event.client(client);
+ setClientToEvent(client);
session.getContext().setClient(client);
if (!client.isEnabled()) {
@@ -270,15 +271,18 @@ public class SessionCodeChecks {
// In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
- URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId);
+ if (latestFlowPath != null) {
+ URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId);
- logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
- authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
- response = Response.status(Response.Status.FOUND).location(redirectUri).build();
- } else {
- response = showPageExpired(authSession);
+ logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
+ authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
+ response = Response.status(Response.Status.FOUND).location(redirectUri).build();
+ return false;
+ }
}
+ response = showPageExpired(authSession);
return false;
+
}
@@ -290,6 +294,11 @@ public class SessionCodeChecks {
}
}
+ // Client is not null
+ protected void setClientToEvent(ClientModel client) {
+ event.client(client);
+ }
+
public boolean verifyActiveAndValidAction(String expectedAction, ClientSessionCode.ActionType actionType) {
if (failed()) {
@@ -317,7 +326,7 @@ public class SessionCodeChecks {
}
- private boolean isActionActive(ClientSessionCode.ActionType actionType) {
+ protected boolean isActionActive(ClientSessionCode.ActionType actionType) {
if (!clientCode.isActionActive(actionType)) {
event.clone().error(Errors.EXPIRED_CODE);
@@ -364,7 +373,7 @@ public class SessionCodeChecks {
}
- private Response restartAuthenticationSessionFromCookie(RootAuthenticationSessionModel existingRootSession) {
+ protected Response restartAuthenticationSessionFromCookie(RootAuthenticationSessionModel existingRootSession) {
logger.debug("Authentication session not found. Trying to restart from cookie.");
AuthenticationSessionModel authSession = null;
@@ -432,4 +441,12 @@ public class SessionCodeChecks {
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
.showPageExpired(authSession);
}
+
+ protected KeycloakSession getSession() {
+ return session;
+ }
+
+ protected EventBuilder getEvent() {
+ return event;
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
index a9b607df91..c570cde41c 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
@@ -44,6 +44,7 @@ import org.keycloak.locale.LocaleUpdaterProvider;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
@@ -56,7 +57,9 @@ import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.representations.IDToken;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.ServicesLogger;
@@ -69,6 +72,7 @@ import org.keycloak.services.managers.UserConsentManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AbstractSecuredLocalService;
import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionModel;
@@ -147,6 +151,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
}
public void init() {
+ session.getContext().setClient(client);
eventStore = session.getProvider(EventStoreProvider.class);
account = session.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(session.getContext().getUri()).setHttpHeaders(headers);
@@ -183,6 +188,11 @@ public class AccountFormService extends AbstractSecuredLocalService {
}
account.setUser(auth.getUser());
+
+ ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(auth.getClientSession(), session);
+ IDToken idToken = new TokenManager().responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(authResult.getToken()).generateIDToken().getIdToken();
+ idToken.issuedFor(client.getClientId());
+ account.setIdTokenHint(session.tokens().encodeAndEncrypt(idToken));
}
account.setFeatures(realm.isIdentityFederationEnabled(), eventStore != null && realm.isEventsEnabled(), true, Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION));
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index 480e1b540f..b7c30141ab 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
@@ -298,7 +298,7 @@ public class AdminConsole {
URI redirect = AdminRoot.adminConsoleUrl(session.getContext().getUri(UrlType.ADMIN)).build(realm.getName());
return Response.status(302).location(
- OIDCLoginProtocolService.logoutUrl(session.getContext().getUri(UrlType.ADMIN)).queryParam("redirect_uri", redirect.toString()).build(realm.getName())
+ OIDCLoginProtocolService.logoutUrl(session.getContext().getUri(UrlType.ADMIN)).queryParam("post_logout_redirect_uri", redirect.toString()).build(realm.getName())
).build();
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index 28ea55c909..3ce96c92b1 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.rest;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.util.HtmlUtils;
import org.keycloak.common.util.Time;
@@ -49,6 +50,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
+import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthDetailsRepresentation;
@@ -966,6 +968,16 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
}
+ @GET
+ @Path("/reinitialize-provider-factory-with-system-properties-scope")
+ @Consumes(MediaType.TEXT_HTML_UTF_8)
+ public void reinitializeProviderFactoryWithSystemPropertiesScope(@QueryParam("provider-type") String providerType, @QueryParam("provider-id") String providerId,
+ @QueryParam("system-properties-prefix") String systemPropertiesPrefix) throws Exception {
+ Class extends Provider> providerClass = (Class extends Provider>) Class.forName(providerType);
+ ProviderFactory factory = session.getKeycloakSessionFactory().getProviderFactory(providerClass, providerId);
+ factory.init(new Config.SystemPropertiesScope(systemPropertiesPrefix));
+ }
+
/**
* This will send POST request to specified URL with specified form parameters. It's not easily possible to "trick" web driver to send POST
* request with custom parameters, which are not directly available in the form.
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp
index eedc4e9731..6a33abc38b 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp
@@ -15,4 +15,4 @@
String authUri = authScheme + "://" + authHost + ":" + authPort + "/auth";
%>
\ No newline at end of file
+ .build("servlet-authz").toString()%>">Sign Out
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp
index 006e0bf1c7..5da96b59c3 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp
+++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp
@@ -15,4 +15,4 @@
String authUri = authScheme + "://" + authHost + ":" + authPort + "/auth";
%>
\ No newline at end of file
+ .build("servlet-policy-enforcer-authz").toString()%>">Sign Out
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index 6becdc9d49..3b5376d26c 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -350,6 +350,23 @@ public interface TestingResource {
@Consumes(MediaType.TEXT_HTML_UTF_8)
void setSystemPropertyOnServer(@QueryParam("property-name") String propertyName, @QueryParam("property-value") String propertyValue);
+ /**
+ * Re-initialize specified provider factory with system properties scope. This will allow to change providerConfig in runtime with {@link #setSystemPropertyOnServer}
+ *
+ * This works just for the provider factories, which can be re-initialized without any side-effects (EG. some functionality already dependent
+ * on the previously initialized properties, which cannot be easily changed in runtime)
+ *
+ * @param providerType fully qualified class name of provider (subclass of org.keycloak.provider.Provider)
+ * @param providerId provider Id
+ * @param systemPropertiesPrefix prefix to be used for system properties
+ */
+ @GET
+ @Path("/reinitialize-provider-factory-with-system-properties-scope")
+ @Consumes(MediaType.TEXT_HTML_UTF_8)
+ @NoCache
+ void reinitializeProviderFactoryWithSystemPropertiesScope(@QueryParam("provider-type") String providerType, @QueryParam("provider-id") String providerId,
+ @QueryParam("system-properties-prefix") String systemPropertiesPrefix);
+
/**
* This method is here just to have all endpoints from TestingResourceProvider available here.
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AppPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AppPage.java
index 4292b501f2..5d5141391b 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AppPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AppPage.java
@@ -57,10 +57,8 @@ public class AppPage extends AbstractPage {
AUTH_RESPONSE, LOGOUT_REQUEST, APP_REQUEST
}
- public void logout() {
- String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(oauth.AUTH_SERVER_ROOT))
- .queryParam(OAuth2Constants.REDIRECT_URI, oauth.APP_AUTH_ROOT).build("test").toString();
- driver.navigate().to(logoutUri);
+ public void logout(String idTokenHint) {
+ oauth.idTokenHint(idTokenHint).openLogout();
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java
index 562ad0d0d4..3f80376e79 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java
@@ -40,6 +40,9 @@ public class InfoPage extends LanguageComboboxAwarePage {
@FindBy(linkText = "» Klicken Sie hier um fortzufahren")
private WebElement clickToContinueDe;
+ @FindBy(linkText = "« Zpět na aplikaci")
+ private WebElement backToApplicationCs;
+
public String getInfo() {
return infoMessage.getText();
}
@@ -62,4 +65,8 @@ public class InfoPage extends LanguageComboboxAwarePage {
clickToContinueDe.click();
}
+ public void clickBackToApplicationLinkCs() {
+ backToApplicationCs.click();
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LogoutConfirmPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LogoutConfirmPage.java
new file mode 100644
index 0000000000..271bcfa367
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LogoutConfirmPage.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 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.pages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author Marek Posolda
+ */
+public class LogoutConfirmPage extends LanguageComboboxAwarePage {
+
+ @FindBy(css = "input[type=\"submit\"]")
+ private WebElement confirmLogoutButton;
+
+ @FindBy(linkText = "« Back to Application")
+ private WebElement backToApplicationLink;
+
+ @Override
+ public boolean isCurrent() {
+ return isCurrent(driver);
+ }
+
+ public boolean isCurrent(WebDriver driver1) {
+ return "Logging out".equals(PageUtils.getPageTitle(driver1));
+ }
+
+
+ @Override
+ public void open() throws Exception {
+ throw new UnsupportedOperationException("Not supported to directly open logout confirmation page");
+ }
+
+ public void confirmLogout() {
+ confirmLogoutButton.click();
+ }
+
+ public void confirmLogout(WebDriver driver) {
+ driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
+ }
+
+ public void clickBackToApplicationLink() {
+ backToApplicationLink.click();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java
index 7dc86bff2e..4b57a52b88 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java
@@ -98,6 +98,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdater {
return KeycloakModelUtils.generateId();
};
@@ -1361,8 +1384,14 @@ public class OAuthClient {
public void openLogout() {
UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl));
- if (redirectUri != null) {
- b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
+ if (postLogoutRedirectUri != null) {
+ b.queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, postLogoutRedirectUri);
+ }
+ if (idTokenHint != null) {
+ b.queryParam(OAuth2Constants.ID_TOKEN_HINT, idTokenHint);
+ }
+ if(initiatingIDP != null) {
+ b.queryParam(AuthenticationManager.INITIATING_IDP_PARAM, initiatingIDP);
}
driver.navigate().to(b.build(realm).toString());
}
@@ -1581,6 +1610,21 @@ public class OAuthClient {
this.redirectUri = redirectUri;
return this;
}
+
+ public OAuthClient postLogoutRedirectUri(String postLogoutRedirectUri) {
+ this.postLogoutRedirectUri = postLogoutRedirectUri;
+ return this;
+ }
+
+ public OAuthClient idTokenHint(String idTokenHint) {
+ this.idTokenHint = idTokenHint;
+ return this;
+ }
+
+ public OAuthClient initiatingIDP(String initiatingIDP) {
+ this.initiatingIDP = initiatingIDP;
+ return this;
+ }
public OAuthClient kcAction(String kcAction) {
this.kcAction = kcAction;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java
index a7f7cd6651..3b1db81fb6 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java
@@ -1,7 +1,10 @@
package org.keycloak.testsuite.util.javascript;
+import org.jboss.logging.Logger;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
@@ -27,6 +30,8 @@ public class JavascriptTestExecutor {
private OIDCLogin loginPage;
protected boolean configured;
+ private static final Logger logger = Logger.getLogger(JavascriptTestExecutor.class);
+
public static JavascriptTestExecutor create(WebDriver driver, OIDCLogin loginPage) {
return new JavascriptTestExecutor(driver, loginPage);
}
@@ -125,7 +130,23 @@ public class JavascriptTestExecutor {
}
public JavascriptTestExecutor logout(JavascriptStateValidator validator) {
+ return logout(validator, null);
+ }
+
+ public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage) {
jsExecutor.executeScript("keycloak.logout()");
+
+ try {
+ // simple check if we are at the logout confirm page, if so just click 'Yes'
+ if (logoutConfirmPage != null && logoutConfirmPage.isCurrent(jsDriver)) {
+ logoutConfirmPage.confirmLogout(jsDriver);
+ waitForPageToLoad();
+ }
+ } catch (Exception ex) {
+ // ignore errors when checking logoutConfirm page, if an error tests will also fail
+ logger.error("Exception during checking logout confirmation page", ex);
+ }
+
if (validator != null) {
validator.validate(jsDriver, output, events);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java
index fbe0496794..be32cecc65 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java
@@ -102,19 +102,25 @@ public abstract class AbstractTestRealmKeycloakTest extends AbstractKeycloakTest
protected OAuthClient.AccessTokenResponse sendTokenRequestAndGetResponse(EventRepresentation loginEvent) {
+ Field eventsField = Reflections.findDeclaredField(this.getClass(), "events");
+ AssertEvents events = null;
+ if(eventsField != null) {
+ events = Reflections.getFieldValue(eventsField, this, AssertEvents.class);
+ }
+
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+ if(eventsField != null) {
+ events.clear();
+ }
String code = new OAuthClient.AuthorizationEndpointResponse(oauth).getCode();
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(200, response.getStatusCode());
-
- Field eventsField = Reflections.findDeclaredField(this.getClass(), "events");
if (eventsField != null) {
- AssertEvents events = Reflections.getFieldValue(eventsField, this, AssertEvents.class);
- events.expectCodeToToken(codeId, sessionId).assertEvent();
+ events.expectCodeToToken(codeId, sessionId).user(loginEvent.getUserId()).session(sessionId).assertEvent();
}
return response;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/SessionRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/SessionRestServiceTest.java
index 9e9512ce24..8ad068fba5 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/SessionRestServiceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/SessionRestServiceTest.java
@@ -188,7 +188,7 @@ public class SessionRestServiceTest extends AbstractRestServiceTest {
// first browser authenticates from Fedora
oauth.setBrowserHeader("User-Agent", "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1");
- codeGrant("public-client-0");
+ OAuthClient.AccessTokenResponse tokenResponse1 = codeGrant("public-client-0");
List devices = getDevicesOtherThanOther();
assertEquals("Should have a single device", 1, devices.size());
List fedoraDevices = devices.stream()
@@ -204,7 +204,7 @@ public class SessionRestServiceTest extends AbstractRestServiceTest {
oauth.setDriver(secondBrowser);
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Gecko/20100101 Firefox/15.0.1");
- codeGrant("public-client-0");
+ OAuthClient.AccessTokenResponse tokenResponse2 = codeGrant("public-client-0");
devices = getDevicesOtherThanOther();
// should have two devices
assertEquals("Should have two devices", 2, devices.size());
@@ -222,23 +222,25 @@ public class SessionRestServiceTest extends AbstractRestServiceTest {
// first browser authenticates from Windows using Edge
oauth.setDriver(firstBrowser);
+ oauth.idTokenHint(tokenResponse1.getIdToken()).openLogout();
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (Windows Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0");
- codeGrant("public-client-0");
+ tokenResponse1 = codeGrant("public-client-0");
// second browser authenticates from Windows using Firefox
oauth.setDriver(secondBrowser);
+ oauth.idTokenHint(tokenResponse2.getIdToken()).openLogout();
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Gecko/20100101 Firefox/15.0.1");
- codeGrant("public-client-0");
+ tokenResponse2 = codeGrant("public-client-0");
// third browser authenticates from Windows using Safari
oauth.setDriver(thirdBrowser);
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/11.0 Safari/603.1.30");
oauth.setBrowserHeader("X-Forwarded-For", "192.168.10.3");
- OAuthClient.AccessTokenResponse tokenResponse = codeGrant("public-client-0");
- devices = getDevicesOtherThanOther(tokenResponse.getAccessToken());
+ OAuthClient.AccessTokenResponse tokenResponse3 = codeGrant("public-client-0");
+ devices = getDevicesOtherThanOther(tokenResponse3.getAccessToken());
assertEquals(
"Should have a single device because all browsers (and sessions) are from the same platform (OS + OS version)",
1, devices.size());
@@ -261,10 +263,11 @@ public class SessionRestServiceTest extends AbstractRestServiceTest {
// third browser authenticates from Windows using a different Windows version
oauth.setDriver(thirdBrowser);
+ oauth.idTokenHint(tokenResponse3.getIdToken()).openLogout();
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (Windows 7) AppleWebKit/537.36 (KHTML, like Gecko) Version/11.0 Safari/603.1.30");
oauth.setBrowserHeader("X-Forwarded-For", "192.168.10.3");
- codeGrant("public-client-0");
+ tokenResponse3 = codeGrant("public-client-0");
devices = getDevicesOtherThanOther();
windowsDevices = devices.stream()
.filter(device -> "Windows".equals(device.getOs())).collect(Collectors.toList());
@@ -272,13 +275,16 @@ public class SessionRestServiceTest extends AbstractRestServiceTest {
assertEquals(2, windowsDevices.size());
oauth.setDriver(firstBrowser);
+ oauth.idTokenHint(tokenResponse1.getIdToken()).openLogout();
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3");
- codeGrant("public-client-0");
+ tokenResponse1 = codeGrant("public-client-0");
+
oauth.setDriver(secondBrowser);
+ oauth.idTokenHint(tokenResponse2.getIdToken()).openLogout();
oauth.setBrowserHeader("User-Agent",
"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1");
- codeGrant("public-client-0");
+ tokenResponse2 = codeGrant("public-client-0");
devices = getDevicesOtherThanOther();
assertEquals("Should have 3 devices", 3, devices.size());
windowsDevices = devices.stream()
@@ -433,7 +439,6 @@ public class SessionRestServiceTest extends AbstractRestServiceTest {
private OAuthClient.AccessTokenResponse codeGrant(String clientId) {
oauth.clientId(clientId);
oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
- oauth.openLogout();
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
return oauth.doAccessTokenRequest(code, "password");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java
index 9ec0fcfd5f..aacd07bfe5 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java
@@ -22,6 +22,7 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
@@ -30,10 +31,12 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.SecondBrowser;
+import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import java.util.List;
@@ -92,7 +95,8 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
EventRepresentation loginEvent = events.expectLogin().assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).assertEvent();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java
index 32e08efa60..ceb277942f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java
@@ -42,6 +42,7 @@ import org.keycloak.testsuite.pages.AccountTotpPage;
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.By;
@@ -357,7 +358,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(authSessionId).assertEvent();
@@ -396,7 +398,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
// Logout
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
// Try to login after logout
@@ -424,8 +427,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
events.expectAccount(EventType.REMOVE_TOTP).user(userId).assertEvent();
// Logout
- oauth.openLogout();
- events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
+ accountTotpPage.logout();
+ events.expectLogout(loginEvent.getSessionId()).user(userId).detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/totp").assertEvent();
// Try to login
loginPage.open();
@@ -464,7 +467,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).assertEvent();
@@ -516,7 +520,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
assertKcActionStatus(SUCCESS);
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).assertEvent();
@@ -527,9 +532,10 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
assertKcActionStatus(null);
- events.expectLogin().assertEvent();
+ loginEvent = events.expectLogin().assertEvent();
- oauth.openLogout();
+ tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(null).session(AssertEvents.isUUID()).assertEvent();
// test lookAheadWindow
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index 522e74e432..47f7193925 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -41,6 +41,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
+import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.cluster.AuthenticationSessionFailoverClusterTest;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
@@ -358,7 +359,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.navigate().to(verificationUrl1.trim());
appPage.assertCurrent();
- appPage.logout();
+ accountPage.setAuthRealm(AuthRealm.TEST);
+ accountPage.navigateTo();
+ accountPage.logOut();
MimeMessage message2 = greenMail.getReceivedMessages()[1];
@@ -768,7 +771,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
accountPage.assertCurrent();
- driver.navigate().to(oauth.getLogoutUrl().redirectUri(accountPage.buildUri().toString()).build());
+ accountPage.logOut();
loginPage.assertCurrent();
verifyEmailDuringAuthFlow();
@@ -809,7 +812,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
assertThat(driver2.getCurrentUrl(), Matchers.startsWith(accountPage.buildUri().toString()));
// Browser 1: Logout
- driver.navigate().to(oauth.getLogoutUrl().redirectUri(accountPage.buildUri().toString()).build());
+ accountPage.logOut();
// Browser 1: Go to account page
accountPage.navigateTo();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
index 50f11477f4..97fc3db523 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
@@ -96,7 +96,8 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe
EventRepresentation loginEvent = events.expectLogin().assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).assertEvent();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index 09f1bf57ef..0a6f63275e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -47,12 +47,14 @@ import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.By;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -341,7 +343,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(authSessionId).assertEvent();
@@ -402,7 +405,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
// Logout
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
// Try to login after logout
@@ -430,8 +434,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
events.expectAccount(EventType.REMOVE_TOTP).user(userId).assertEvent();
// Logout
- oauth.openLogout();
- events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
+ accountTotpPage.logout();
+ events.expectLogout(loginEvent.getSessionId()).user(userId).detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/totp").assertEvent();
// Try to login
loginPage.open();
@@ -480,7 +484,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).assertEvent();
@@ -532,7 +537,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).assertEvent();
@@ -544,9 +550,10 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().assertEvent();
+ loginEvent = events.expectLogin().assertEvent();
- oauth.openLogout();
+ tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(null).session(AssertEvents.isUUID()).assertEvent();
// test lookAheadWindow
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java
index 18b90110dc..5dc0946836 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java
@@ -229,7 +229,7 @@ public abstract class AbstractBasePhotozExampleAdapterTest extends AbstractPhoto
log.debugf("--logging in as '%s' with password: '%s'; scopes: %s", user.getUsername(), user.getCredentials().get(0).getValue(), Arrays.toString(scopes));
if (testExecutor.isLoggedIn()) {
- testExecutor.logout(this::assertOnTestAppUrl);
+ testExecutor.logout(this::assertOnTestAppUrl, logoutConfirmPage);
jsDriver.manage().deleteAllCookies();
jsDriver.navigate().to(testRealmLoginPage.toString());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java
index ebf63f3ff3..5308899592 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.adapter.example.authorization;
import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -31,6 +32,8 @@ import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.UIUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@@ -63,6 +66,12 @@ public abstract class AbstractBaseServletAuthzAdapterTest extends AbstractExampl
@ArquillianResource
private Deployer deployer;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@BeforeClass
public static void enabled() {
ProfileAssume.assumeFeatureEnabled(AUTHORIZATION);
@@ -121,6 +130,10 @@ public abstract class AbstractBaseServletAuthzAdapterTest extends AbstractExampl
private void logOut() {
navigateTo();
UIUtils.clickLink(driver.findElement(By.xpath("//a[text() = 'Sign Out']")));
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ infoPage.assertCurrent();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozJavascriptExecutorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozJavascriptExecutorTest.java
index a22fe5be48..d9f48f5ec7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozJavascriptExecutorTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozJavascriptExecutorTest.java
@@ -10,6 +10,7 @@ import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
import org.keycloak.testsuite.auth.page.login.OAuthGrant;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.JavascriptBrowser;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.javascript.JSObjectBuilder;
@@ -40,6 +41,10 @@ public abstract class AbstractPhotozJavascriptExecutorTest extends AbstractExamp
@JavascriptBrowser
protected OIDCLogin jsDriverTestRealmLoginPage;
+ @Page
+ @JavascriptBrowser
+ protected LogoutConfirmPage logoutConfirmPage;
+
@Page
@JavascriptBrowser
private OAuthGrant oAuthGrantPage;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java
index e0f7bc8719..a0ff6ad986 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java
@@ -29,6 +29,7 @@ import java.net.URL;
import java.util.List;
import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -42,6 +43,8 @@ import org.keycloak.representations.idm.authorization.ResourcePermissionRepresen
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.util.UIUtils;
import org.openqa.selenium.By;
@@ -57,6 +60,12 @@ public class AbstractServletPolicyEnforcerTest extends AbstractExampleAdapterTes
@ArquillianResource
private Deployer deployer;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@BeforeClass
public static void enabled() {
ProfileAssume.assumeFeatureEnabled(AUTHORIZATION);
@@ -548,6 +557,10 @@ public class AbstractServletPolicyEnforcerTest extends AbstractExampleAdapterTes
private void logOut() {
navigateTo();
UIUtils.clickLink(driver.findElement(By.xpath("//a[text() = 'Sign Out']")));
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ infoPage.assertCurrent();
}
private void login(String username, String password) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java
index 4d12c2070e..69ad09612d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java
@@ -87,6 +87,7 @@ import java.util.List;
import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
/**
+ *
* @author Bill Burke
* @version $Revision: 1 $
*/
@@ -422,6 +423,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
Assert.assertNotEquals(externalToken, tokenResponse.getToken());
+ resetTimeOffset();
logoutAll();
@@ -475,7 +477,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
loginPage.login("child", "password");
- Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ Assert.assertTrue("Unexpected page. Current Page URL: " + driver.getCurrentUrl(),loginPage.isCurrent(PARENT_IDP));
loginPage.login(PARENT_USERNAME, "password");
System.out.println("After linking: " + driver.getCurrentUrl());
System.out.println(driver.getPageSource());
@@ -764,10 +766,8 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
}
public void logoutAll() {
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString();
- navigateTo(logoutUri);
- logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString();
- navigateTo(logoutUri);
+ adminClient.realm(CHILD_IDP).logoutAll();
+ adminClient.realm(PARENT_IDP).logoutAll();
}
private void navigateTo(String uri) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java
index 9218858d7c..02eb9c67a9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java
@@ -446,10 +446,8 @@ public class ClientInitiatedAccountLinkTest extends AbstractServletsAdapterTest
}
public void logoutAll() {
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString();
- navigateTo(logoutUri);
- logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString();
- navigateTo(logoutUri);
+ adminClient.realm(CHILD_IDP).logoutAll();
+ adminClient.realm(PARENT_IDP).logoutAll();
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java
index 2c6b893f5e..9a3d94ced7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java
@@ -77,6 +77,9 @@ import org.keycloak.testsuite.adapter.page.TokenRefreshPage;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import org.keycloak.testsuite.auth.page.account.Applications;
@@ -161,6 +164,12 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
@JavascriptBrowser
protected OIDCLogin jsDriverTestRealmLoginPage;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@Page
protected CustomerPortal customerPortal;
@Page
@@ -461,10 +470,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
waitForPageToLoad();
assertPageContains("parameter=hello");
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, customerPortal.toString())
- .build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -517,9 +524,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertEquals(1, Integer.parseInt(productPortalStats.get("active")));
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, customerPortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -711,9 +717,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertCurrentUrlEquals(securePortal);
assertLogged();
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
securePortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -731,9 +736,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertLogged();
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, securePortalWithCustomSessionConfig.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
securePortalWithCustomSessionConfig.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -909,9 +913,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertCurrentUrlEquals(portalUri);
assertLogged();
// logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
securePortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -959,9 +962,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertLogged();
// logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, customerPortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
}
@@ -1099,16 +1101,24 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
.assertEvent();
- driver.navigate().to(testRealmPage.getOIDCLogoutUrl() + "?redirect_uri=" + customerPortal);
+
+ String logoutUrl = oauth.realm("demo")
+ .getLogoutUrl()
+ .build();
+ driver.navigate().to(logoutUrl);
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ infoPage.assertCurrent();
+ driver.navigate().to(customerPortal.toString());
+
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
assertEvents.expectLogout(null)
.realm(realm.getId())
.user(userId)
.session(AssertEvents.isUUID())
- .detail(Details.REDIRECT_URI,
- org.hamcrest.Matchers.anyOf(org.hamcrest.Matchers.equalTo(customerPortal.getInjectedUrl().toString()),
- org.hamcrest.Matchers.equalTo(customerPortal.getInjectedUrl().toString() + "/")))
+ .removeDetail(Details.REDIRECT_URI)
.assertEvent();
assertEvents.assertEmpty();
@@ -1160,10 +1170,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertPageContains("uriEncodeTest=false");
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, customerPortal.toString())
- .build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -1320,9 +1328,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId, clientSecretJwtSecurePortal);
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, clientSecretJwtSecurePortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
}
@Test
@@ -1362,9 +1369,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId, clientSecretJwtSecurePortalValidAlg);
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, clientSecretJwtSecurePortalValidAlg.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java
index 38c1ac79fe..cd4f0458eb 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OIDCPublicKeyRotationAdapterTest.java
@@ -125,11 +125,7 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes
loginToTokenMinTtlApp();
// Logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, tokenMinTTLPage.toString())
- .build("demo").toString();
- driver.navigate().to(logoutUri);
- assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+ ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout();
// Generate new realm key
generateNewRealmKey();
@@ -142,14 +138,13 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes
URLAssert.assertCurrentUrlStartsWith(tokenMinTTLPage.getInjectedUrl().toString());
Assert.assertNull(tokenMinTTLPage.getAccessToken());
- driver.navigate().to(logoutUri);
- assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+ ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout();
setAdapterAndServerTimeOffset(300, tokenMinTTLPage.toString() + "/unsecured/foo");
// Try to login. Should work now due to realm key change
loginToTokenMinTtlApp();
- driver.navigate().to(logoutUri);
+ ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout();
// Revert public keys change
resetKeycloakDeploymentForAdapter(tokenMinTTLPage.toString() + "/unsecured/foo");
@@ -188,9 +183,7 @@ public class OIDCPublicKeyRotationAdapterTest extends AbstractServletsAdapterTes
assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
// Logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ ApiUtil.findUserByUsernameId(adminClient.realm("demo"), "bburke@redhat.com").logout();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java
index 32a8bb34b7..0051d5171f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/OfflineServletsAdapterTest.java
@@ -240,7 +240,7 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
assertThat(offlineClient.getAdditionalGrants(), Matchers.hasItem("Offline Token"));
//This was necessary to be introduced, otherwise other testcases will fail
- offlineTokenPage.logout();
+ accountAppPage.logout();
assertCurrentUrlDoesntStartWith(offlineTokenPage);
loginPage.assertCurrent();
} finally {
@@ -274,7 +274,8 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
if (loginPage.isCurrent()) {
loginPage.login(username, password);
waitForPageToLoad();
- offlineTokenPage.logout();
+ accountAppPage.open();
+ accountAppPage.logout();
}
setTimeOffset(0);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SessionServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SessionServletAdapterTest.java
index d6d28192bc..bb3350632e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SessionServletAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SessionServletAdapterTest.java
@@ -30,12 +30,15 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.page.SessionPortal;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.auth.page.account.Sessions;
import org.keycloak.testsuite.auth.page.login.Login;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import org.keycloak.testsuite.util.SecondBrowser;
import org.openqa.selenium.By;
@@ -46,6 +49,7 @@ import static org.junit.Assert.*;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
+import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
/**
*
@@ -67,6 +71,12 @@ public class SessionServletAdapterTest extends AbstractServletsAdapterTest {
@Page
private Sessions testRealmSessions;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@Override
public void setDefaultPageUriParameters() {
super.setDefaultPageUriParameters();
@@ -110,9 +120,13 @@ public class SessionServletAdapterTest extends AbstractServletsAdapterTest {
// Logout in browser1
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, sessionPortalPage.toString()).build("demo").toString();
+ .build("demo").toString();
driver.navigate().to(logoutUri);
- assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ waitForPageToLoad();
+ infoPage.assertCurrent();
// Assert that I am logged out in browser1
sessionPortalPage.navigateTo();
@@ -124,9 +138,10 @@ public class SessionServletAdapterTest extends AbstractServletsAdapterTest {
pageSource = driver2.getPageSource();
assertThat(pageSource, containsString("Counter=3"));
+ // Logout in driver2
driver2.navigate().to(logoutUri);
- assertCurrentUrlStartsWithLoginUrlOf(testRealmPage, driver2);
-
+ driver2.findElement(By.cssSelector("input[type=\"submit\"]")).click();
+ Assert.assertEquals("You are logged out", driver2.findElement(By.className("instruction")).getText());
}
//KEYCLOAK-741
@@ -150,8 +165,12 @@ public class SessionServletAdapterTest extends AbstractServletsAdapterTest {
// Logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, sessionPortalPage.toString()).build("demo").toString();
+ .build("demo").toString();
driver.navigate().to(logoutUri);
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ waitForPageToLoad();
+ infoPage.assertCurrent();
// Assert that http session was invalidated
sessionPortalPage.navigateTo();
@@ -182,8 +201,12 @@ public class SessionServletAdapterTest extends AbstractServletsAdapterTest {
String pageSource = driver.getPageSource();
assertTrue(pageSource.contains("Counter=3"));
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, sessionPortalPage.toString()).build("demo").toString();
+ .build("demo").toString();
driver.navigate().to(logoutUri);
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ waitForPageToLoad();
+ infoPage.assertCurrent();
}
//KEYCLOAK-1216
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/UserStorageConsentTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/UserStorageConsentTest.java
index d843634ed3..66f36443a4 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/UserStorageConsentTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/UserStorageConsentTest.java
@@ -42,6 +42,8 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.federation.UserMapStorageFactory;
import org.keycloak.testsuite.pages.ConsentPage;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import javax.ws.rs.core.Response;
@@ -76,6 +78,12 @@ public class UserStorageConsentTest extends AbstractServletsAdapterTest {
@Page
protected ConsentPage consentPage;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@Deployment(name = ProductPortal.DEPLOYMENT_NAME)
protected static WebArchive productPortal() {
return servletDeployment(ProductPortal.DEPLOYMENT_NAME, ProductServlet.class);
@@ -172,12 +180,19 @@ public class UserStorageConsentTest extends AbstractServletsAdapterTest {
consentPage.confirm();
assertCurrentUrlEquals(productPortal.toString());
Assert.assertTrue(driver.getPageSource().contains("iPhone"));
+
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, productPortal.toString())
.build("demo").toString();
driver.navigate().to(logoutUri);
waitForPageToLoad();
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ waitForPageToLoad();
+ infoPage.assertCurrent();
+
+ driver.navigate().to(productPortal.toString());
+ waitForPageToLoad();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -186,6 +201,9 @@ public class UserStorageConsentTest extends AbstractServletsAdapterTest {
Assert.assertTrue(driver.getPageSource().contains("iPhone"));
driver.navigate().to(logoutUri);
+ waitForPageToLoad();
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
adminClient.realm("demo").users().delete(uid).close();
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java
index bb0b398be3..44a82b9af9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/OIDCAdapterClusterTest.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.adapter.servlet.cluster;
import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
@@ -42,10 +43,13 @@ import org.keycloak.common.util.Retry;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.adapter.AbstractAdapterClusteredTest;
import org.keycloak.testsuite.adapter.page.SessionPortalDistributable;
import org.keycloak.testsuite.adapter.servlet.SessionServlet;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
@@ -78,6 +82,12 @@ public class OIDCAdapterClusterTest extends AbstractAdapterClusteredTest {
@Page
protected OIDCLogin loginPage;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@Page
protected SessionPortalDistributable sessionPortalPage;
@@ -140,8 +150,13 @@ public class OIDCAdapterClusterTest extends AbstractAdapterClusteredTest {
assertSessionCounter(NODE_2_NAME, NODE_2_URI, NODE_1_URI, proxiedUrl, 4);
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, proxiedUrl).build(AuthRealm.DEMO).toString();
+ .build(AuthRealm.DEMO).toString();
driver.navigate().to(logoutUri);
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ infoPage.assertCurrent();
+
Retry.execute(() -> {
driver.navigate().to(proxiedUrl);
assertCurrentUrlStartsWith(loginPage);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowRelaviteUriAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowRelaviteUriAdapterTest.java
index fe8bf1de32..8f4de6eff2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowRelaviteUriAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowRelaviteUriAdapterTest.java
@@ -116,9 +116,8 @@ public class UndertowRelaviteUriAdapterTest extends AbstractServletsAdapterTest
Assert.assertEquals(1, Integer.parseInt(productPortalStats.get("active")));
// test logout
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
- .queryParam(OAuth2Constants.REDIRECT_URI, customerPortal.toString()).build("demo").toString();
- driver.navigate().to(logoutUri);
+ testRealmAccountPage.navigateTo();
+ testRealmAccountPage.logOut();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
index 0d68752a21..3af96f76d5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
@@ -222,6 +222,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
OAuthClient.AuthorizationEndpointResponse resp = oauth1.doLogin("test-user@localhost", "password");
String code = resp.getCode();
+ String idTokenHint = oauth1.doAccessTokenRequest(code, "password").getIdToken();
Assert.assertNotNull(code);
String codeURL = driver.getCurrentUrl();
@@ -247,11 +248,11 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
run(DEFAULT_THREADS, DEFAULT_THREADS, codeToTokenTask);
- oauth1.openLogout();
+ oauth1.idTokenHint(idTokenHint).openLogout();
// Code should be successfully exchanged for the token at max once. In some cases (EG. Cross-DC) it may not be even successfully exchanged
- Assert.assertThat(codeToTokenSuccessCount.get(), Matchers.lessThanOrEqualTo(1));
- Assert.assertThat(codeToTokenErrorsCount.get(), Matchers.greaterThanOrEqualTo(DEFAULT_THREADS - 1));
+ Assert.assertThat(codeToTokenSuccessCount.get(), Matchers.lessThanOrEqualTo(0));
+ Assert.assertThat(codeToTokenErrorsCount.get(), Matchers.greaterThanOrEqualTo(DEFAULT_THREADS));
log.infof("Iteration %d passed successfully", i);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java
index 1a27e03fea..6f5c8af9ce 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java
@@ -49,11 +49,13 @@ import org.keycloak.testsuite.pages.LoginExpiredPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginTotpPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.pages.ProceedPage;
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.MailServer;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.TimeoutException;
@@ -104,6 +106,9 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
@Page
protected ProceedPage proceedPage;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
@Page
protected InfoPage infoPage;
@@ -304,16 +309,32 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
logoutFromRealm(contextRoot, realm, null);
}
- protected void logoutFromRealm(String contextRoot, String realm, String initiatingIdp) { logoutFromRealm(contextRoot, realm, initiatingIdp, null); }
+ protected void logoutFromRealm(String contextRoot, String realm, String initiatingIdp) {
+ logoutFromRealm(contextRoot, realm, initiatingIdp, null);
+ }
+
+ protected void logoutFromRealm(String contextRoot, String realm, String initiatingIdp, String idTokenHint) {
+ OAuthClient.LogoutUrlBuilder builder = oauth.realm(realm)
+ .getLogoutUrl()
+ .initiatingIdp(initiatingIdp);
+
+ if (idTokenHint != null) {
+ builder
+ .postLogoutRedirectUri(encodeUrl(getAccountUrl(contextRoot, realm)))
+ .idTokenHint(idTokenHint);
+ }
+
+ String logoutUrl = builder.build();
+ driver.navigate().to(logoutUrl);
+
+ // Needs to confirm logout if id_token_hint was not provided
+ if (idTokenHint == null) {
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ infoPage.assertCurrent();
+ driver.navigate().to(getAccountUrl(contextRoot, realm));
+ }
- protected void logoutFromRealm(String contextRoot, String realm, String initiatingIdp, String tokenHint) {
- driver.navigate().to(contextRoot
- + "/auth/realms/" + realm
- + "/protocol/" + "openid-connect"
- + "/logout?redirect_uri=" + encodeUrl(getAccountUrl(contextRoot, realm))
- + (!StringUtils.isBlank(initiatingIdp) ? "&initiating_idp=" + initiatingIdp : "")
- + (!StringUtils.isBlank(tokenHint) ? "&id_token_hint=" + tokenHint : "")
- );
try {
Retry.execute(() -> {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java
index 1a48c604d8..8b5fdf6478 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java
@@ -301,10 +301,7 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
waitForPage(driver, "account already exists", false);
idpConfirmLinkPage.assertCurrent();
idpConfirmLinkPage.clickLinkAccount();
- logoutFromRealm(getProviderRoot(), bc.providerRealmName());
-
- driver.navigate().back();
- logInWithBroker(samlBrokerConfig);
+ loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
totpPage.assertCurrent();
String totpSecret = totpPage.getTotpSecret();
@@ -347,10 +344,7 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
waitForPage(driver, "account already exists", false);
idpConfirmLinkPage.assertCurrent();
idpConfirmLinkPage.clickLinkAccount();
- logoutFromRealm(getProviderRoot(), bc.providerRealmName());
-
- driver.navigate().back();
- logInWithBroker(samlBrokerConfig);
+ loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
loginTotpPage.assertCurrent();
loginTotpPage.login(totp.generateTOTP(totpSecret));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java
index b2c00ef91d..add0ee020e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java
@@ -2720,9 +2720,9 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
).toString();
updateProfiles(json);
- successfulLogin(clientId, clientSecret);
+ OAuthClient.AccessTokenResponse response = successfulLogin(clientId, clientSecret);
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
assertTrue(driver.getPageSource().contains("Front-channel logout is not allowed for this client"));
}
@@ -2939,6 +2939,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
+ String idTokenHint = accessTokenResponse.getIdToken();
assertEquals(200, accessTokenResponse.getStatusCode());
// Check token refresh.
@@ -2995,7 +2996,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
assertEquals(OAuthErrorException.INVALID_GRANT, accessTokenResponse.getError());
// Check frontchannel logout and login.
- oauth.openLogout();
+ oauth.idTokenHint(idTokenHint).openLogout();
loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
Assert.assertNull(loginResponse.getError());
@@ -3183,7 +3184,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
assertEquals("PKCE code verifier not specified", res.getErrorDescription());
events.expect(EventType.CODE_TO_TOKEN_ERROR).client(clientId).session(sessionId).clearDetails().error(Errors.CODE_VERIFIER_MISSING).assertEvent();
- oauth.openLogout();
+ oauth.idTokenHint(res.getIdToken()).openLogout();
events.expectLogout(sessionId).clearDetails().assertEvent();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java
index 817bcf560b..be426458b3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java
@@ -110,9 +110,14 @@ public class ClientRedirectTest extends AbstractTestRealmKeycloakTest {
oauth.doLogin("test-user@localhost", "password");
events.expectLogin().assertEvent();
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code,"password").getIdToken();
+ events.poll();
+
URI logout = KeycloakUriBuilder.fromUri(suiteContext.getAuthServerInfo().getBrowserContextRoot().toURI())
.path("auth" + ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
- .queryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM, "http://example.org/redirected")
+ .queryParam(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM, "http://example.org/redirected")
+ .queryParam(OIDCLoginProtocol.ID_TOKEN_HINT, idTokenHint)
.build("test");
log.debug("log out using: " + logout.toURL());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java
index 0688723872..7da32d1a98 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java
@@ -23,17 +23,22 @@ import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.keycloak.OAuth2Constants;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.Cookie;
+import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -59,6 +64,12 @@ public abstract class AbstractFailoverClusterTest extends AbstractClusterTest {
@Page
protected AppPage appPage;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@BeforeClass
public static void modifyAppRoot() {
// the test app needs to run in the test realm to be able to fetch cookies later
@@ -129,7 +140,15 @@ public abstract class AbstractFailoverClusterTest extends AbstractClusterTest {
}
protected void logout() {
- appPage.logout();
+ String logoutUrl = oauth.getLogoutUrl().build();
+ driver.navigate().to(logoutUrl);
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+
+ // Info page present
+ infoPage.assertCurrent();
+ Assert.assertEquals("You are logged out", infoPage.getInfo());
}
protected Cookie verifyLoggedIn(Cookie sessionCookieForVerification) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java
index b9a88ba0d8..2b13f3263e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java
@@ -205,6 +205,7 @@ public abstract class AbstractKerberosSingleRealmTest extends AbstractKerberosTe
// Logout
oauth.openLogout();
+ events.poll();
// Remove protocolMapper
clientResource.getProtocolMappers().delete(protocolMapperId);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java
index 5ca4d9fe4a..30258a8df8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java
@@ -205,6 +205,8 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
Assert.assertEquals(userId, token.getSubject());
Assert.assertEquals(expectedUsername, token.getPreferredUsername());
+ oauth.idTokenHint(tokenResponse.getIdToken());
+
return token;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/AbstractLDAPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/AbstractLDAPTest.java
index 6ab2954726..ee08a83689 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/AbstractLDAPTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/AbstractLDAPTest.java
@@ -21,10 +21,12 @@ import java.util.List;
import java.util.Map;
import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Rule;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.pages.AccountPasswordPage;
@@ -47,6 +49,9 @@ public abstract class AbstractLDAPTest extends AbstractTestRealmKeycloakTest {
protected static String ldapModelId;
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
@Page
protected AppPage appPage;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADFullNameTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADFullNameTest.java
index 62036d2bb8..d9ab7db300 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADFullNameTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMSADFullNameTest.java
@@ -22,6 +22,7 @@ import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
+import org.keycloak.OAuth2Constants;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
@@ -246,7 +247,10 @@ public class LDAPMSADFullNameTest extends AbstractLDAPTest {
Assert.assertEquals("Username already exists.", registerPage.getInputAccountErrors().getUsernameError());
registerPage.register("John", "Existing", "johnyanth@check.cz", "existingkc2", "Password1", "Password1");
- appPage.logout();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "Password1").getIdToken();
+ appPage.logout(idTokenHint);
loginPage.open();
loginPage.clickRegister();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java
index a96fb3dd46..3591c3cb58 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java
@@ -27,6 +27,7 @@ import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel;
+import org.keycloak.events.EventType;
import org.keycloak.models.GroupModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
@@ -41,6 +42,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
@@ -206,11 +208,14 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
}
private void loginSuccessAndLogout(String username, String password) {
+ events.clear();
loginPage.open();
loginPage.login(username, password);
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
- Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
- oauth.openLogout();
+
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(events.poll());
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
+ events.poll();
}
@Test
@@ -371,10 +376,16 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
- appPage.logout();
+ UserResource user = ApiUtil.findUserByUsernameId(testRealm(), username);
+ String userId = user.toRepresentation().getId();
+
+ events.expectRegister(username, email).assertEvent();
+ EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ appPage.logout(tokenResponse.getIdToken());
+ events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
// Test admin endpoint. Assert federated endpoint returns password in LDAP "supportedCredentials", but there is no stored password
- UserResource user = ApiUtil.findUserByUsernameId(testRealm(), username);
assertPasswordConfiguredThroughLDAPOnly(user);
// Update password through admin REST endpoint. Assert user can authenticate with the new password
@@ -401,7 +412,11 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
requiredActionChangePasswordPage.changePassword("Password1-updated2", "Password1-updated2");
appPage.assertCurrent();
- appPage.logout();
+ events.expect(EventType.UPDATE_PASSWORD).user(userId).assertEvent();
+ loginEvent = events.expectLogin().user(userId).assertEvent();
+ tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ appPage.logout(tokenResponse.getIdToken());
+ events.expectLogout(loginEvent.getSessionId()).user(userId);
// Assert user can authenticate with the new password
loginSuccessAndLogout(username, "Password1-updated2");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSamlIdPInitiatedVaryingLetterCaseTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSamlIdPInitiatedVaryingLetterCaseTest.java
index 434914e6de..7fbafba51c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSamlIdPInitiatedVaryingLetterCaseTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSamlIdPInitiatedVaryingLetterCaseTest.java
@@ -214,7 +214,9 @@ public class LDAPSamlIdPInitiatedVaryingLetterCaseTest extends AbstractLDAPTest
appPage.assertCurrent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
- appPage.logout();
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, USER_PASSWORD).getIdToken();
+ appPage.logout(idTokenHint);
}
protected URI getAuthServerBrokerSamlEndpoint(String realm, String identityProviderAlias, String samlClientId) throws IllegalArgumentException, UriBuilderException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java
index 0b7eabacde..f0cb818987 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java
@@ -30,6 +30,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ModelException;
+import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
@@ -51,6 +52,7 @@ import java.util.List;
import java.util.Objects;
import org.junit.Assume;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
+import org.keycloak.testsuite.util.OAuthClient;
/**
* Test user logins utilizing various LDAP authentication methods and different LDAP connection encryption mechanisms.
@@ -149,12 +151,16 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
// Helper methods
private void verifyLoginSucceededAndLogout(String username, String password) {
+ String userId = findUser(username).getId();
loginPage.open();
loginPage.login(username, password);
appPage.assertCurrent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
- appPage.logout();
+ EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ appPage.logout(tokenResponse.getIdToken());
+ events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
}
private void verifyLoginFailed(String username, String password) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java
index e1fb62cb8d..c2d0ac6a5c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java
@@ -26,20 +26,24 @@ import java.util.List;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.federation.DummyUserFederationProvider;
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
@@ -70,6 +74,9 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
@Page
protected AppPage appPage;
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
protected TimeBasedOTP totp = new TimeBasedOTP();
@@ -164,7 +171,11 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
appPage.assertCurrent();
// Logout
- appPage.logout();
+ events.expect(EventType.UPDATE_TOTP).user(userRep.getId()).assertEvent(); //remove the UPDATE_TOTP event
+ EventRepresentation loginEvent = events.expectLogin().user(userRep.getId()).assertEvent();
+ String idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken();
+ appPage.logout(idTokenHint);
+ events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
// Authenticate as the user again with the dummy OTP should still work
loginPage.open();
@@ -173,7 +184,10 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP);
appPage.assertCurrent();
- appPage.logout();
+ loginEvent = events.expectLogin().user(userRep.getId()).assertEvent();
+ idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken();
+ appPage.logout(idTokenHint);
+ events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
// Authenticate with the new OTP code should work as well
loginPage.open();
@@ -182,7 +196,10 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
loginTotpPage.login(totp.generateTOTP(totpSecret));
appPage.assertCurrent();
- appPage.logout();
+ loginEvent = events.expectLogin().user(userRep.getId()).assertEvent();
+ idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken();
+ appPage.logout(idTokenHint);
+ events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
index 510501b9b9..faf17641e8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
@@ -23,6 +23,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
@@ -602,7 +603,9 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- appPage.logout();
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ appPage.logout(idTokenHint);
events.clear();
}
@@ -671,7 +674,9 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().assertEvent();
- appPage.logout();
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ appPage.logout(idTokenHint);
events.clear();
@@ -698,7 +703,9 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
- appPage.logout();
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ appPage.logout(idTokenHint);
events.clear();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
deleted file mode 100644
index 618c05452c..0000000000
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
+++ /dev/null
@@ -1,417 +0,0 @@
-/*
- * Copyright 2016 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.forms;
-
-import org.jboss.arquillian.graphene.page.Page;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.keycloak.OAuth2Constants;
-import org.keycloak.admin.client.resource.ClientResource;
-import org.keycloak.admin.client.resource.ClientsResource;
-import org.keycloak.common.Profile;
-import org.keycloak.events.Details;
-import org.keycloak.events.Errors;
-import org.keycloak.jose.jws.JWSInput;
-import org.keycloak.models.Constants;
-import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
-import org.keycloak.protocol.oidc.OIDCConfigAttributes;
-import org.keycloak.representations.IDToken;
-import org.keycloak.representations.LogoutToken;
-import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
-import org.keycloak.testsuite.Assert;
-import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
-import org.keycloak.common.util.Retry;
-import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
-import org.keycloak.testsuite.pages.AppPage;
-import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.pages.LoginPage;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.Collections;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
-import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
-import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
-
-import org.keycloak.testsuite.auth.page.account.AccountManagement;
-import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
-import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
-import org.keycloak.testsuite.util.ClientManager;
-import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
-import org.keycloak.testsuite.util.OAuthClient;
-import org.keycloak.testsuite.util.ServerURLs;
-import org.keycloak.testsuite.util.WaitUtils;
-
-/**
- * @author Stian Thorgersen
- * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
- */
-public class LogoutTest extends AbstractTestRealmKeycloakTest {
-
- @Rule
- public AssertEvents events = new AssertEvents(this);
-
- @Rule
- public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
-
- @Page
- protected AppPage appPage;
-
- @Page
- protected LoginPage loginPage;
-
- @Page
- protected AccountManagement accountManagementPage;
-
- @Page
- private ErrorPage errorPage;
-
- @Override
- public void configureTestRealm(RealmRepresentation testRealm) {
- }
-
- @Before
- public void clientConfiguration() {
- ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
- }
-
- @Test
- public void logoutRedirect() {
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- String redirectUri = oauth.APP_AUTH_ROOT + "?logout";
-
- String logoutUrl = oauth.getLogoutUrl().redirectUri(redirectUri).build();
- driver.navigate().to(logoutUrl);
-
- events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
-
- assertCurrentUrlEquals(redirectUri);
-
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId2 = events.expectLogin().assertEvent().getSessionId();
- assertNotEquals(sessionId, sessionId2);
-
- driver.navigate().to(logoutUrl);
- events.expectLogout(sessionId2).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
- }
-
-
- // KEYCLOAK-16517 Make sure that just real clients with standardFlow or implicitFlow enabled are considered for redirectUri
- @Test
- public void logoutRedirectWithStarRedirectUriForDirectGrantClient() {
- // Set "*" as redirectUri for some directGrant client
- ClientResource clientRes = ApiUtil.findClientByClientId(testRealm(), "direct-grant");
- ClientRepresentation clientRepOrig = clientRes.toRepresentation();
- ClientRepresentation clientRep = clientRes.toRepresentation();
- clientRep.setStandardFlowEnabled(false);
- clientRep.setImplicitFlowEnabled(false);
- clientRep.setRedirectUris(Collections.singletonList("*"));
- clientRes.update(clientRep);
-
- try {
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- events.expectLogin().assertEvent();
-
- String invalidRedirectUri = ServerURLs.getAuthServerContextRoot() + "/bar";
-
- String logoutUrl = oauth.getLogoutUrl().redirectUri(invalidRedirectUri).build();
- driver.navigate().to(logoutUrl);
-
- events.expectLogoutError(Errors.INVALID_REDIRECT_URI).assertEvent();
-
- assertCurrentUrlDoesntStartWith(invalidRedirectUri);
- errorPage.assertCurrent();
- Assert.assertEquals("Invalid redirect uri", errorPage.getError());
- } finally {
- // Revert
- clientRes.update(clientRepOrig);
- }
- }
-
- @Test
- public void logoutSession() {
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- String logoutUrl = oauth.getLogoutUrl().sessionState(sessionId).build();
- driver.navigate().to(logoutUrl);
-
- events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
-
- assertCurrentUrlEquals(logoutUrl);
-
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId2 = events.expectLogin().assertEvent().getSessionId();
- assertNotEquals(sessionId, sessionId2);
- }
-
- @Test
- public void logoutWithExpiredSession() throws Exception {
- try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm("test"))
- .updateWith(r -> r.setSsoSessionMaxLifespan(2))
- .update()) {
-
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
-
- oauth.clientSessionState("client-session");
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idTokenString = tokenResponse.getIdToken();
-
- // expire online user session
- setTimeOffset(9999);
-
- String logoutUrl = oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(idTokenString).build();
- driver.navigate().to(logoutUrl);
-
- // should not throw an internal server error
- appPage.assertCurrent();
-
- // check if the back channel logout succeeded
- driver.navigate().to(oauth.getLoginFormUrl());
- WaitUtils.waitForPageToLoad();
- loginPage.assertCurrent();
- }
- }
-
- @Test
- public void logoutMultipleSessions() throws IOException {
- // Login session 1
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- // Check session 1 logged-in
- oauth.openLoginForm();
- events.expectLogin().session(sessionId).removeDetail(Details.USERNAME).assertEvent();
-
- // Logout session 1 by redirect
- driver.navigate().to(oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).build());
- events.expectLogout(sessionId).detail(Details.REDIRECT_URI, oauth.APP_AUTH_ROOT).assertEvent();
-
- // Check session 1 not logged-in
- oauth.openLoginForm();
- loginPage.assertCurrent();
-
- // Login session 3
- oauth.doLogin("test-user@localhost", "password");
- String sessionId3 = events.expectLogin().assertEvent().getSessionId();
- assertNotEquals(sessionId, sessionId3);
-
- // Check session 3 logged-in
- oauth.openLoginForm();
- events.expectLogin().session(sessionId3).removeDetail(Details.USERNAME).assertEvent();
-
- // Logout session 3 by redirect
- driver.navigate().to(oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).build());
- events.expectLogout(sessionId3).detail(Details.REDIRECT_URI, oauth.APP_AUTH_ROOT).assertEvent();
- }
-
- //KEYCLOAK-2741
- @Test
- @DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
- public void logoutWithRememberMe() {
- setRememberMe(true);
-
- try {
- loginPage.open();
- assertFalse(loginPage.isRememberMeChecked());
- loginPage.setRememberMe(true);
- assertTrue(loginPage.isRememberMeChecked());
- loginPage.login("test-user@localhost", "password");
-
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- // Expire session
- testingClient.testing().removeUserSession("test", sessionId);
-
- // Assert rememberMe checked and username/email prefilled
- loginPage.open();
- assertTrue(loginPage.isRememberMeChecked());
- assertEquals("test-user@localhost", loginPage.getUsername());
-
- loginPage.login("test-user@localhost", "password");
-
- //log out
- appPage.openAccount();
- accountManagementPage.signOut();
- // Assert rememberMe not checked nor username/email prefilled
- assertTrue(loginPage.isCurrent());
- assertFalse(loginPage.isRememberMeChecked());
- assertNotEquals("test-user@localhost", loginPage.getUsername());
- } finally {
- setRememberMe(false);
- }
- }
-
- private void setRememberMe(boolean enabled) {
- RealmRepresentation rep = adminClient.realm("test").toRepresentation();
- rep.setRememberMe(enabled);
- adminClient.realm("test").update(rep);
- }
-
- @Test
- public void logoutSessionWhenLoggedOutByAdmin() {
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- adminClient.realm("test").logoutAll();
-
- String logoutUrl = oauth.getLogoutUrl().sessionState(sessionId).build();
- driver.navigate().to(logoutUrl);
-
- assertCurrentUrlEquals(logoutUrl);
-
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
-
- String sessionId2 = events.expectLogin().assertEvent().getSessionId();
- assertNotEquals(sessionId, sessionId2);
-
- driver.navigate().to(logoutUrl);
- events.expectLogout(sessionId2).removeDetail(Details.REDIRECT_URI).assertEvent();
- }
-
- @Test
- public void logoutUserByAdmin() {
- loginPage.open();
- loginPage.login("test-user@localhost", "password");
- assertTrue(appPage.isCurrent());
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- UserRepresentation user = ApiUtil.findUserByUsername(adminClient.realm("test"), "test-user@localhost");
- Assert.assertEquals((Object) 0, user.getNotBefore());
-
- adminClient.realm("test").users().get(user.getId()).logout();
-
- Retry.execute(() -> {
- UserRepresentation u = adminClient.realm("test").users().get(user.getId()).toRepresentation();
- Assert.assertTrue(u.getNotBefore() > 0);
-
- loginPage.open();
- loginPage.assertCurrent();
- }, 10, 200);
- }
-
-
- // KEYCLOAK-5982
- @Test
- public void testLogoutWhenAccountClientRenamed() throws IOException {
- // Temporarily rename client "account" . Revert it back after the test
- try (Closeable accountClientUpdater = ClientAttributeUpdater.forClient(adminClient, "test", Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
- .setClientId("account-changed")
- .update()) {
-
- // Assert logout works
- logoutRedirect();
- }
- }
-
- @Test
- public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception {
- ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
- ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
- rep.setFrontchannelLogout(true);
- rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
- clients.get(rep.getId()).update(rep);
- try {
- oauth.clientSessionState("client-session");
- oauth.doLogin("test-user@localhost", "password");
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idTokenString = tokenResponse.getIdToken();
- String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString)
- .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build();
- driver.navigate().to(logoutUrl);
- LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken();
- Assert.assertNotNull(logoutToken);
-
- IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class);
-
- Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer());
- Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId());
- } finally {
- rep.setFrontchannelLogout(false);
- rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
- clients.get(rep.getId()).update(rep);
- }
- }
-
- @Test
- public void testFrontChannelLogout() throws Exception {
- ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
- ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
- rep.setName("My Testing App");
- rep.setFrontchannelLogout(true);
- rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
- clients.get(rep.getId()).update(rep);
- try {
- oauth.clientSessionState("client-session");
- oauth.doLogin("test-user@localhost", "password");
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idTokenString = tokenResponse.getIdToken();
- String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build();
- driver.navigate().to(logoutUrl);
- LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken();
- Assert.assertNotNull(logoutToken);
- IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class);
- Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer());
- Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId());
- assertTrue(driver.getTitle().equals("Logging out"));
- assertTrue(driver.getPageSource().contains("You are logging out from following apps"));
- assertTrue(driver.getPageSource().contains("My Testing App"));
- } finally {
- rep.setFrontchannelLogout(false);
- rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
- clients.get(rep.getId()).update(rep);
- }
- }
-}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
index ac2997880d..fd539cee67 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
@@ -30,6 +30,7 @@ import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
@@ -46,6 +47,7 @@ import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
import javax.mail.internet.MimeMessage;
@@ -675,11 +677,12 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
.assertEvent()
.getUserId();
- events.expectLogin()
+ EventRepresentation loginEvent = events.expectLogin()
.detail("username", EMAIL_OR_USERNAME.toLowerCase())
.user(userId)
.assertEvent();
-
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken());
assertUserBasicRegisterAttributes(userId, emailAsUsername ? null : USERNAME, EMAIL, "firstName", "lastName");
return userId;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetCredentialsAlternativeFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetCredentialsAlternativeFlowsTest.java
index e0e359e1cf..2f43e463e9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetCredentialsAlternativeFlowsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetCredentialsAlternativeFlowsTest.java
@@ -53,6 +53,7 @@ import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.By;
@@ -357,13 +358,16 @@ public class ResetCredentialsAlternativeFlowsTest extends AbstractTestRealmKeycl
// Login & set up the initial OTP code for the user
loginPage.open();
loginPage.login("login@test.com", "password");
+ String code = new OAuthClient.AuthorizationEndpointResponse(oauth).getCode();
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
accountTotpPage.open();
Assert.assertTrue(accountTotpPage.isCurrent());
String customOtpLabel = "my-original-otp-label";
accountTotpPage.configure(totp.generateTOTP(accountTotpPage.getTotpSecret()), customOtpLabel);
// Logout
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
// Go to login page & click "Forgot password" link to perform the custom 'Reset Credential' flow
loginPage.open();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index dbb42b8c0b..d573547da8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.forms;
import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken;
import org.jboss.arquillian.graphene.page.Page;
@@ -39,7 +40,9 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
+import org.keycloak.testsuite.auth.page.account.AccountManagement;
import org.keycloak.testsuite.federation.kerberos.AbstractKerberosTest;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
@@ -47,6 +50,7 @@ import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.BrowserTabUtil;
@@ -79,6 +83,7 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@@ -145,6 +150,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Page
protected LoginPasswordUpdatePage updatePasswordPage;
+ @Page
+ protected AccountUpdateProfilePage account1ProfilePage;
+
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
@Rule
public AssertEvents events = new AssertEvents(this);
@@ -190,14 +201,16 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.client("account")
.user(userId).detail(Details.USERNAME, username).assertEvent();
- String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, username)
+ EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, username)
.detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/")
.client("account")
- .assertEvent().getSessionId();
+ .assertEvent();
+ String sessionId = loginEvent.getSessionId();
- oauth.openLogout();
+ account1ProfilePage.assertCurrent();
+ account1ProfilePage.logout();
- events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
+ events.expectLogout(sessionId).user(userId).removeDetail(Details.REDIRECT_URI).assertEvent();
loginPage.open();
@@ -311,9 +324,11 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
+ EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).assertEvent();
+ String sessionId = loginEvent.getSessionId();
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
@@ -321,11 +336,13 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.login("login-test", password);
- sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
+ loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+ sessionId = loginEvent.getSessionId();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- oauth.openLogout();
+ tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
@@ -937,9 +954,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
- oauth.openLogout();
+ EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+ String sessionId = loginEvent.getSessionId();
+
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
@@ -1130,7 +1150,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPasswordTwiceInNewTab(defaultUser, CLIENT_ID, false, REDIRECT_URI, REQUIRED_URI);
assertThat(driver.getTitle(), Matchers.equalTo(ACCOUNT_MANAGEMENT_TITLE));
- oauth.openLogout();
+ account1ProfilePage.assertCurrent();
+ account1ProfilePage.logout();
driver.navigate().to(REQUIRED_URI);
resetPasswordTwiceInNewTab(defaultUser, CLIENT_ID, true, REDIRECT_URI, REQUIRED_URI);
@@ -1153,7 +1174,11 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.open();
resetPasswordTwiceInNewTab(defaultUser, CLIENT_ID, false, REDIRECT_URI);
assertThat(driver.getCurrentUrl(), Matchers.containsString(REDIRECT_URI));
- oauth.openLogout();
+
+ String logoutUrl = oauth.getLogoutUrl().build();
+ driver.navigate().to(logoutUrl);
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
loginPage.open();
resetPasswordTwiceInNewTab(defaultUser, CLIENT_ID, true, REDIRECT_URI);
@@ -1250,8 +1275,13 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.detail(Details.REDIRECT_URI, redirectUri)
.client(clientId)
.assertEvent().getSessionId();
- oauth.openLogout();
- events.expectLogout(sessionId).user(user.getId()).session(sessionId).assertEvent();
+
+ String logoutUrl = oauth.getLogoutUrl().build();
+ driver.navigate().to(logoutUrl);
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+
+ events.expectLogout(sessionId).user(user.getId()).removeDetail(Details.REDIRECT_URI).assertEvent();
}
BrowserTabUtil util = BrowserTabUtil.getInstanceAndSetEnv(driver);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
index 0d0c759ddd..35a274f106 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
@@ -147,7 +147,8 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
assertNotEquals(login1.getSessionId(), login2.getSessionId());
- oauth.openLogout();
+ OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(login1);
+ oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(login1.getSessionId()).assertEvent();
oauth.openLoginForm();
@@ -160,7 +161,10 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, RequestType.valueOf(driver2.getTitle()));
Assert.assertNotNull(oauth2.getCurrentQuery().get(OAuth2Constants.CODE));
- oauth2.openLogout();
+ String code = new OAuthClient.AuthorizationEndpointResponse(oauth2).getCode();
+ OAuthClient.AccessTokenResponse response = oauth2.doAccessTokenRequest(code, "password");
+ events.poll();
+ oauth2.idTokenHint(response.getIdToken()).openLogout();
events.expectLogout(login2.getSessionId()).assertEvent();
oauth2.openLoginForm();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index d96e8d5f4b..78587f7dd1 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -226,7 +226,9 @@ public class LoginPageTest extends AbstractI18NTest {
UserRepresentation userRep = user.toRepresentation();
Assert.assertEquals("de", userRep.getAttributes().get("locale").get(0));
- appPage.logout();
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ appPage.logout(idTokenHint);
loginPage.open();
@@ -242,7 +244,9 @@ public class LoginPageTest extends AbstractI18NTest {
userRep = user.toRepresentation();
Assert.assertNull(userRep.getAttributes());
- appPage.logout();
+ code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ appPage.logout(idTokenHint);
loginPage.open();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 20bcf65488..a820253f1d 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -1339,7 +1339,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
String encodedSignature = token.split("\\.",3)[2];
byte[] signature = Base64Url.decode(encodedSignature);
Assert.assertEquals(expectedLength, signature.length);
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
}
private void conductAccessTokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 40e44fa8c7..39cd2a4663 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -383,7 +383,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
assertEquals(200, response.getStatusCode());
oauth.verifyToken(response.getAccessToken());
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
return clientSignedToken;
} finally {
// Revert jwks_url settings
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LegacyLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LegacyLogoutTest.java
new file mode 100644
index 0000000000..49aadbbcd0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LegacyLogoutTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2022 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.oauth;
+
+import java.util.Collections;
+
+import javax.ws.rs.NotFoundException;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.account.AccountManagement;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.ServerURLs;
+
+import static org.hamcrest.Matchers.is;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+
+/**
+ * Test logout endpoint with deprecated "redirect_uri" parameter
+ *
+ * @author Marek Posolda
+ */
+public class LegacyLogoutTest extends AbstractTestRealmKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Rule
+ public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected OAuthGrantPage grantPage;
+
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
+ @Page
+ protected AccountManagement accountManagementPage;
+
+ @Page
+ private ErrorPage errorPage;
+
+ private String APP_REDIRECT_URI;
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Before
+ public void configLegacyRedirectUriEnabled() {
+ getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, "true");
+ getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc.");
+
+ APP_REDIRECT_URI = oauth.APP_AUTH_ROOT;
+ }
+
+ @After
+ public void revertConfiguration() {
+ getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, "false");
+ getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc.");
+ }
+
+
+ // Test logout with deprecated "redirect_uri" and with "id_token_hint" . Should od automatic redirect
+ @Test
+ public void logoutWithLegacyRedirectUriAndIdTokenHint() throws Exception {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+ String sessionId = tokenResponse.getSessionState();
+
+ String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).idTokenHint(idTokenString).build();
+ driver.navigate().to(logoutUrl);
+
+ events.expectLogout(sessionId).detail(Details.REDIRECT_URI, APP_REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(sessionId)));
+ assertCurrentUrlEquals(APP_REDIRECT_URI);
+ }
+
+ // Test logout with deprecated "redirect_uri" and without "id_token_hint" . User should confirm logout
+ @Test
+ public void logoutWithLegacyRedirectUriAndWithoutIdTokenHint() throws Exception {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String sessionId = tokenResponse.getSessionState();
+
+ String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build();
+ driver.navigate().to(logoutUrl);
+
+ // Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
+ logoutConfirmPage.assertCurrent();
+ Assert.assertThat(true, is(isSessionActive(sessionId)));
+ events.assertEmpty();
+ logoutConfirmPage.confirmLogout();
+
+ // Redirected back to the application with expected state
+ events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(sessionId)));
+ assertCurrentUrlEquals(APP_REDIRECT_URI);
+ }
+
+
+ // KEYCLOAK-16517 Make sure that just real clients with standardFlow or implicitFlow enabled are considered for redirectUri
+ @Test
+ public void logoutRedirectWithStarRedirectUriForDirectGrantClient() {
+ // Set "*" as redirectUri for some directGrant client
+ ClientResource clientRes = ApiUtil.findClientByClientId(testRealm(), "direct-grant");
+ ClientRepresentation clientRepOrig = clientRes.toRepresentation();
+ ClientRepresentation clientRep = clientRes.toRepresentation();
+ clientRep.setStandardFlowEnabled(false);
+ clientRep.setImplicitFlowEnabled(false);
+ clientRep.setRedirectUris(Collections.singletonList("*"));
+ clientRes.update(clientRep);
+
+ try {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+
+ String invalidRedirectUri = ServerURLs.getAuthServerContextRoot() + "/bar";
+
+ String idTokenString = tokenResponse.getIdToken();
+
+ String logoutUrl = oauth.getLogoutUrl().redirectUri(invalidRedirectUri).build();
+ driver.navigate().to(logoutUrl);
+
+ events.expectLogoutError(Errors.INVALID_REDIRECT_URI).assertEvent();
+
+ assertCurrentUrlDoesntStartWith(invalidRedirectUri);
+ errorPage.assertCurrent();
+ Assert.assertEquals("Invalid redirect uri", errorPage.getError());
+
+ // Session still active
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ } finally {
+ // Revert
+ clientRes.update(clientRepOrig);
+ }
+ }
+
+
+
+ private OAuthClient.AccessTokenResponse loginUser() {
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ oauth.clientSessionState("client-session");
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ events.clear();
+ return tokenResponse;
+ }
+
+ private boolean isSessionActive(String sessionId) {
+ try {
+ testingClient.testing().getClientSessionsCountInUserSession("test", sessionId);
+ return true;
+ } catch (NotFoundException nfe) {
+ return false;
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
index 0065669169..00c2c6a100 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
@@ -23,9 +23,9 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
-import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.common.util.Retry;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.jose.jws.JWSHeader;
@@ -34,12 +34,13 @@ import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.util.*;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
@@ -50,13 +51,22 @@ import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
+import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
+import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.testsuite.util.Matchers;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.TokenSignatureUtil;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.NoSuchElementException;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
-import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
/**
+ * Tests mostly for backchannel logout scenarios with refresh token (Legacy Logout endpoint not compliant with OIDC specification) and admin logout scenarios
+ *
* @author Stian Thorgersen
*/
public class LogoutTest extends AbstractKeycloakTest {
@@ -75,6 +85,7 @@ public class LogoutTest extends AbstractKeycloakTest {
@Before
public void clientConfiguration() {
ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
+ new RealmAttributeUpdater(adminClient.realm("test")).setNotBefore(0).update();
}
@Override
@@ -122,39 +133,6 @@ public class LogoutTest extends AbstractKeycloakTest {
}
}
- @Test
- public void logoutIDTokenHint() {
- oauth.doLogin("test-user@localhost", "password");
-
- String sessionId = events.expectLogin().assertEvent().getSessionId();
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idToken = tokenResponse.getIdToken();
-
- events.clear();
-
- driver.navigate().to(oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(idToken).build());
- events.expectLogout(sessionId).detail(Details.REDIRECT_URI, oauth.APP_AUTH_ROOT).assertEvent();
-
- assertCurrentUrlEquals(oauth.APP_AUTH_ROOT);
- }
-
- @Test
- public void browserLogoutWithAccessToken() {
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String accessToken = tokenResponse.getAccessToken();
-
- events.clear();
-
- driver.navigate().to(oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(accessToken).build());
-
- events.expectLogoutError(OAuthErrorException.INVALID_TOKEN).assertEvent();
- }
-
@Test
public void postLogoutWithRefreshTokenAfterUserSessionLogoutAndLoginAgain() throws Exception {
// Login
@@ -219,75 +197,23 @@ public class LogoutTest extends AbstractKeycloakTest {
}
@Test
- public void postLogoutWithValidIdToken() throws Exception {
- oauth.doLogin("test-user@localhost", "password");
+ public void logoutUserByAdmin() {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ UserRepresentation user = ApiUtil.findUserByUsername(adminClient.realm("test"), "test-user@localhost");
+ Assert.assertEquals((Object) 0, user.getNotBefore());
- oauth.clientSessionState("client-session");
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idTokenString = tokenResponse.getIdToken();
+ adminClient.realm("test").users().get(user.getId()).logout();
- String logoutUrl = oauth.getLogoutUrl()
- .idTokenHint(idTokenString)
- .postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
- .build();
-
- try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
- CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
- assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
- assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(oauth.APP_AUTH_ROOT));
- }
- }
+ Retry.execute(() -> {
+ UserRepresentation u = adminClient.realm("test").users().get(user.getId()).toRepresentation();
+ Assert.assertTrue(u.getNotBefore() > 0);
- @Test
- public void postLogoutWithExpiredIdToken() throws Exception {
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
-
- oauth.clientSessionState("client-session");
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idTokenString = tokenResponse.getIdToken();
-
- // Logout should succeed with expired ID token, see KEYCLOAK-3399
- setTimeOffset(60 * 60 * 24);
-
- String logoutUrl = oauth.getLogoutUrl()
- .idTokenHint(idTokenString)
- .postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
- .build();
-
- try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
- CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
- assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
- assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(oauth.APP_AUTH_ROOT));
- }
- }
-
- @Test
- public void postLogoutWithValidIdTokenWhenLoggedOutByAdmin() throws Exception {
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
-
- oauth.clientSessionState("client-session");
- OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
- String idTokenString = tokenResponse.getIdToken();
-
- adminClient.realm("test").logoutAll();
-
- // Logout should succeed with user already logged out, see KEYCLOAK-3399
- String logoutUrl = oauth.getLogoutUrl()
- .idTokenHint(idTokenString)
- .postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
- .build();
-
- try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
- CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
- assertThat(response, Matchers.statusCodeIsHC(Status.FOUND));
- assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(oauth.APP_AUTH_ROOT));
- }
+ loginPage.open();
+ loginPage.assertCurrent();
+ }, 10, 200);
}
private void backchannelLogoutRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
@@ -348,12 +274,14 @@ public class LogoutTest extends AbstractKeycloakTest {
clients.get(rep.getId()).update(rep);
oauth.doLogin("test-user@localhost", "password");
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.clientSessionState("client-session");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ events.poll();
String idTokenString = tokenResponse.getIdToken();
String logoutUrl = oauth.getLogoutUrl()
.idTokenHint(idTokenString)
@@ -366,6 +294,9 @@ public class LogoutTest extends AbstractKeycloakTest {
assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(oauth.APP_AUTH_ROOT));
}
+ // Assert logout event triggered for backchannel logout
+ events.expectLogout(sessionId).detail(Details.REDIRECT_URI, oauth.APP_AUTH_ROOT).assertEvent();
+
assertNotNull(testingClient.testApp().getAdminLogoutAction());
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index f36a308052..8dbff282c6 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -46,6 +46,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ProtocolMapperUtil;
@@ -76,6 +77,10 @@ public class OAuthGrantTest extends AbstractKeycloakTest {
protected OAuthGrantPage grantPage;
@Page
protected AccountApplicationsPage accountAppsPage;
+
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
@Page
protected AppPage appPage;
@@ -360,9 +365,12 @@ public class OAuthGrantTest extends AbstractKeycloakTest {
.client(THIRD_PARTY_APP)
.assertEvent();
- oauth.openLogout();
+ String logoutUrl = oauth.getLogoutUrl().idTokenHint(res.getIdToken()).build();
+ driver.navigate().to(logoutUrl);
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
- events.expectLogout(loginEvent.getSessionId()).assertEvent();
+ events.expectLogout(loginEvent.getSessionId()).removeDetail(Details.REDIRECT_URI).assertEvent();
// login again to check whether the Dynamic scope and only the dynamic scope is requested again
oauth.scope("foo-dynamic-scope:withparam");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
index 6c077f6ea2..21b2328fee 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
@@ -295,7 +295,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertThat(jsonClaim.get("c"), instanceOf(Collection.class));
assertThat(jsonClaim.get("d"), instanceOf(Map.class));
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
}
// undo mappers
@@ -334,7 +334,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertNull(idToken.getOtherClaims().get("nested"));
assertNull(idToken.getOtherClaims().get("department"));
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
}
@@ -432,7 +432,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertNull(nulll);
oauth.verifyToken(response.getAccessToken());
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
}
// undo mappers
@@ -457,7 +457,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertNull(idToken.getOtherClaims().get("empty"));
assertNull(idToken.getOtherClaims().get("null"));
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
}
events.clear();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java
new file mode 100644
index 0000000000..b48b1690ec
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2022 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.oauth;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.common.Profile;
+import org.keycloak.common.util.UriUtils;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.models.Constants;
+import org.keycloak.protocol.oidc.OIDCConfigAttributes;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.LogoutToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LoginPage;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+
+import org.keycloak.testsuite.auth.page.account.AccountManagement;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.pages.PageUtils;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
+import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
+import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
+import org.keycloak.testsuite.util.Matchers;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.NoSuchElementException;
+
+/**
+ * Test for OIDC RP-Initiated Logout - https://openid.net/specs/openid-connect-rpinitiated-1_0.html
+ *
+ * This is handled on server-side by the LogoutEndpoint.logout method
+ *
+ * @author Stian Thorgersen
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Rule
+ public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected OAuthGrantPage grantPage;
+
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
+ @Page
+ protected AccountManagement accountManagementPage;
+
+ @Page
+ private ErrorPage errorPage;
+
+ private String APP_REDIRECT_URI;
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Before
+ public void clientConfiguration() {
+ APP_REDIRECT_URI = oauth.APP_AUTH_ROOT;
+ }
+
+ @Test
+ public void logoutRedirect() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String sessionId = tokenResponse.getSessionState();
+
+ String redirectUri = APP_REDIRECT_URI + "?logout";
+
+ String idTokenString = tokenResponse.getIdToken();
+
+ String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(redirectUri).idTokenHint(idTokenString).build();
+ driver.navigate().to(logoutUrl);
+
+ events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(sessionId)));
+
+ assertCurrentUrlEquals(redirectUri);
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId2);
+
+ // Test also "state" parameter is included in the URL after logout
+ logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(redirectUri).idTokenHint(idTokenString).state("something").build();
+ driver.navigate().to(logoutUrl);
+ events.expectLogout(sessionId2).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(sessionId2)));
+ assertCurrentUrlEquals(redirectUri + "&state=something");
+ }
+
+
+ @Test
+ public void logoutWithExpiredSession() throws Exception {
+ try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm("test"))
+ .updateWith(r -> r.setSsoSessionMaxLifespan(20))
+ .update()) {
+
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+
+ // expire online user session
+ setTimeOffset(9999);
+
+ String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).idTokenHint(idTokenString).build();
+ driver.navigate().to(logoutUrl);
+
+ // should not throw an internal server error. But no logout event is sent as nothing was logged-out
+ appPage.assertCurrent();
+ events.assertEmpty();
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+
+ // check if the back channel logout succeeded
+ driver.navigate().to(oauth.getLoginFormUrl());
+ WaitUtils.waitForPageToLoad();
+ loginPage.assertCurrent();
+ }
+ }
+
+
+ //KEYCLOAK-2741
+ @Test
+ @DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
+ public void logoutWithRememberMe() throws IOException {
+ try (RealmAttributeUpdater update = new RealmAttributeUpdater(testRealm()).setRememberMe(true).update()) {
+ loginPage.open();
+ assertFalse(loginPage.isRememberMeChecked());
+ loginPage.setRememberMe(true);
+ assertTrue(loginPage.isRememberMeChecked());
+ loginPage.login("test-user@localhost", "password");
+
+ String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+ // Expire session
+ testingClient.testing().removeUserSession("test", sessionId);
+
+ // Assert rememberMe checked and username/email prefilled
+ loginPage.open();
+ assertTrue(loginPage.isRememberMeChecked());
+ assertEquals("test-user@localhost", loginPage.getUsername());
+
+ loginPage.login("test-user@localhost", "password");
+
+ //log out
+ appPage.openAccount();
+ accountManagementPage.signOut();
+ // Assert rememberMe not checked nor username/email prefilled
+ assertTrue(loginPage.isCurrent());
+ assertFalse(loginPage.isRememberMeChecked());
+ assertNotEquals("test-user@localhost", loginPage.getUsername());
+ }
+ }
+
+
+ @Test
+ public void logoutSessionWhenLoggedOutByAdmin() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String sessionId = tokenResponse.getSessionState();
+ String idTokenString = tokenResponse.getIdToken();
+
+ adminClient.realm("test").logoutAll();
+ Assert.assertThat(false, is(isSessionActive(sessionId)));
+
+ // Try logout even if user already logged-out by admin. Should redirect back to the application, but no logout-event should be triggered
+ String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).idTokenHint(idTokenString).build();
+ driver.navigate().to(logoutUrl);
+ events.assertEmpty();
+ assertCurrentUrlEquals(APP_REDIRECT_URI);
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ assertTrue(appPage.isCurrent());
+
+ String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+ assertNotEquals(sessionId, sessionId2);
+
+ driver.navigate().to(logoutUrl);
+ events.expectLogout(sessionId2).detail(Details.REDIRECT_URI, APP_REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(sessionId2)));
+ }
+
+
+ // KEYCLOAK-5982
+ @Test
+ public void testLogoutWhenAccountClientRenamed() throws IOException {
+ // Temporarily rename client "account" . Revert it back after the test
+ try (Closeable accountClientUpdater = ClientAttributeUpdater.forClient(adminClient, "test", Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
+ .setClientId("account-changed")
+ .update()) {
+
+ // Assert logout works
+ logoutRedirect();
+ }
+ }
+
+ @Test
+ public void browserLogoutWithAccessToken() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String accessToken = tokenResponse.getAccessToken();
+
+ driver.navigate().to(oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).idTokenHint(accessToken).build());
+
+ events.expectLogoutError(OAuthErrorException.INVALID_TOKEN).assertEvent();
+
+ // Session still authenticated
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+ @Test
+ public void logoutWithExpiredIdToken() throws Exception {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+
+ // Logout should succeed with expired ID token, see KEYCLOAK-3399
+ setTimeOffset(60 * 60 * 24);
+
+ String logoutUrl = oauth.getLogoutUrl()
+ .idTokenHint(idTokenString)
+ .postLogoutRedirectUri(APP_REDIRECT_URI)
+ .build();
+
+ try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
+ CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
+ assertThat(response, Matchers.statusCodeIsHC(Response.Status.FOUND));
+ assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(APP_REDIRECT_URI));
+ }
+ events.assertEmpty();
+
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+ @Test
+ public void logoutWithValidIdTokenWhenLoggedOutByAdmin() throws Exception {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+
+ adminClient.realm("test").logoutAll();
+
+ // Logout with HTTP client. Logout should succeed with user already logged out, see KEYCLOAK-3399. But no logout event should be present
+ String logoutUrl = oauth.getLogoutUrl()
+ .idTokenHint(idTokenString)
+ .postLogoutRedirectUri(APP_REDIRECT_URI)
+ .build();
+
+ try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
+ CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
+ assertThat(response, Matchers.statusCodeIsHC(Response.Status.FOUND));
+ assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(APP_REDIRECT_URI));
+ }
+ events.assertEmpty();
+
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+
+ // Parameter "redirect_uri" is not valid in logoutRequest (See LegacyLogoutTest for the scenario with "redirect_uri" allowed by backwards compatibility switch)
+ @Test
+ public void logoutWithRedirectUriParameterShouldFail() throws Exception {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+
+ // Logout with "redirect_uri" parameter alone should fail
+ String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build();
+ driver.navigate().to(logoutUrl);
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent();
+
+ // Logout with "redirect_uri" parameter and with "id_token_hint" should fail
+ oauth.getLogoutUrl().idTokenHint(idTokenString).redirectUri(APP_REDIRECT_URI).build();
+ driver.navigate().to(logoutUrl);
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent();
+
+ // Assert user still authenticated
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+
+ // Test with "post_logout_redirect_uri" without "id_token_hint" should fail
+ @Test
+ public void logoutWithPostLogoutUriWithoutIdTokenHintShouldFail() throws Exception {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+
+ // Logout with "redirect_uri" parameter alone should fail
+ String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).build();
+ driver.navigate().to(logoutUrl);
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent();
+
+ // Assert user still authenticated
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+
+ @Test
+ public void logoutWithInvalidPostLogoutRedirectUri() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+
+ // Completely invalid redirect uri
+ driver.navigate().to(oauth.getLogoutUrl().postLogoutRedirectUri("https://invalid").idTokenHint(idTokenString).build());
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_REDIRECT_URI).detail(Details.REDIRECT_URI, "https://invalid").assertEvent();
+
+ // Redirect uri of different client in the realm should fail as well
+ String rootUrlClientRedirectUri = UriUtils.getOrigin(APP_REDIRECT_URI) + "/foo/bar";
+ driver.navigate().to(oauth.getLogoutUrl().postLogoutRedirectUri(rootUrlClientRedirectUri).idTokenHint(idTokenString).build());
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_REDIRECT_URI).detail(Details.REDIRECT_URI, rootUrlClientRedirectUri).assertEvent();
+
+ // Session still authenticated
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+
+ @Test
+ public void logoutWithInvalidIdTokenHint() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+ String idTokenString = tokenResponse.getIdToken();
+
+ // Removed signature from id_token_hint
+ String idTokenHint = idTokenString.substring(0, idTokenString.lastIndexOf("."));
+ driver.navigate().to(oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).idTokenHint(idTokenHint).build());
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_TOKEN).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+ // Invalid signature
+ idTokenHint = idTokenHint + ".something";
+ driver.navigate().to(oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).idTokenHint(idTokenHint).build());
+ errorPage.assertCurrent();
+ events.expectLogoutError(OAuthErrorException.INVALID_TOKEN).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+ // Session still authenticated
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+
+ // Test without "id_token_hint" and without "post_logout_redirect_uri" . User should confirm logout
+ @Test
+ public void logoutWithoutIdTokenHintWithoutPostLogoutRedirectUri() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+
+ driver.navigate().to(oauth.getLogoutUrl().build());
+
+ // Assert logout confirmation page. Session still exists
+ logoutConfirmPage.assertCurrent();
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ events.assertEmpty();
+ logoutConfirmPage.confirmLogout();
+
+ // Info page present. No link "back to the application"
+ infoPage.assertCurrent();
+ Assert.assertEquals("You are logged out", infoPage.getInfo());
+ try {
+ logoutConfirmPage.clickBackToApplicationLink();
+ fail();
+ }
+ catch (NoSuchElementException ex) {
+ // expected
+ }
+
+ events.expectLogout(tokenResponse.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+ }
+
+
+ // Test with "id_token_hint" and without "post_logout_redirect_uri" . User should see "You were logged-out" at the end of logout
+ @Test
+ public void logoutWithIdTokenHintWithoutPostLogoutRedirectUri() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+
+ driver.navigate().to(oauth.getLogoutUrl().idTokenHint(tokenResponse.getIdToken()).build());
+
+ // Info page present. Link "back to the application" present
+ infoPage.assertCurrent();
+ Assert.assertEquals("You are logged out", infoPage.getInfo());
+
+ events.expectLogout(tokenResponse.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+
+ infoPage.clickBackToApplicationLink();
+ WaitUtils.waitForPageToLoad();
+ Assert.assertThat(driver.getCurrentUrl(), endsWith("/app/auth"));
+ }
+
+
+ // Test for the scenario when "action" inside authentication session is expired
+ @Test
+ public void logoutExpiredConfirmationAction() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+
+ driver.navigate().to(oauth.getLogoutUrl().build());
+
+ // Assert logout confirmation page. Session still exists
+ logoutConfirmPage.assertCurrent();
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ events.assertEmpty();
+
+ // Set time offset to expire "action" inside logoutSession
+ setTimeOffset(310);
+ logoutConfirmPage.confirmLogout();
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Logout failed", errorPage.getError());
+
+ events.expectLogoutError(Errors.EXPIRED_CODE).assertEvent();
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+
+ // Link not present
+ try {
+ errorPage.clickBackToApplication();
+ fail();
+ }
+ catch (NoSuchElementException ex) {
+ // expected
+ }
+ }
+
+ // Test for the scenario when "authenticationSession" itself is expired
+ @Test
+ public void logoutExpiredConfirmationAuthSession() {
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser();
+
+ driver.navigate().to(oauth.getLogoutUrl().build());
+
+ // Assert logout confirmation page. Session still exists
+ logoutConfirmPage.assertCurrent();
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ events.assertEmpty();
+
+ // Set time offset to expire "action" inside logoutSession
+ setTimeOffset(1810);
+ logoutConfirmPage.confirmLogout();
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Logout failed", errorPage.getError());
+
+ events.expectLogoutError(Errors.SESSION_EXPIRED).assertEvent();
+ }
+
+ // Test logout with "consentRequired" . All of "post_logout_redirect_uri", "id_token_hint" and "state" parameters are present in the logout request
+ @Test
+ public void logoutConsentRequired() {
+ oauth.clientId("third-party");
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser(true);
+ String idTokenString = tokenResponse.getIdToken();
+
+ String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).idTokenHint(idTokenString).state("somethingg").build();
+ driver.navigate().to(logoutUrl);
+
+ // Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
+ logoutConfirmPage.assertCurrent();
+ Assert.assertEquals("English", logoutConfirmPage.getLanguageDropdownText());
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ events.assertEmpty();
+ logoutConfirmPage.confirmLogout();
+
+ // Redirected back to the application with expected "state"
+ events.expectLogout(tokenResponse.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+ assertCurrentUrlEquals(APP_REDIRECT_URI + "?state=somethingg");
+
+ UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
+ user.revokeConsent("third-party");
+ }
+
+
+ // Test logout request without "post logout redirect uri" . Also test "ui_locales" parameter works as expected
+ @Test
+ public void logoutConsentRequiredWithoutPostLogoutRedirectUri() throws IOException {
+ try (RealmAttributeUpdater updater = new RealmAttributeUpdater(testRealm()).addSupportedLocale("cs").update()) {
+ oauth.clientId("third-party");
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser(true);
+ String idTokenString = tokenResponse.getIdToken();
+
+ String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).uiLocales("cs").build();
+ driver.navigate().to(logoutUrl);
+
+ // Assert logout confirmation page. Session still exists. Assert czech language on logout page
+ Assert.assertEquals("Odhlašování", PageUtils.getPageTitle(driver)); // Logging out
+ Assert.assertEquals("Čeština", logoutConfirmPage.getLanguageDropdownText());
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ events.assertEmpty();
+ logoutConfirmPage.confirmLogout();
+
+ // Info page present with the link "Back to application"
+ events.expectLogout(tokenResponse.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
+ Assert.assertThat(false, is(isSessionActive(tokenResponse.getSessionState())));
+
+ infoPage.assertCurrent();
+ Assert.assertEquals("Odhlášení bylo úspěšné", infoPage.getInfo()); // Logout success message
+ infoPage.clickBackToApplicationLinkCs();
+ WaitUtils.waitForPageToLoad();
+ Assert.assertThat(driver.getCurrentUrl(), endsWith("/app/auth"));
+
+ UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
+ user.revokeConsent("third-party");
+ }
+ }
+
+ @Test
+ public void logoutConsentRequiredWithExpiredCode() throws IOException {
+ oauth.clientId("third-party");
+ OAuthClient.AccessTokenResponse tokenResponse = loginUser(true);
+ String idTokenString = tokenResponse.getIdToken();
+
+ driver.navigate().to(oauth.getLogoutUrl().idTokenHint(idTokenString).build());
+
+ // Assert logout confirmation page. Session still exists
+ logoutConfirmPage.assertCurrent();
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+ events.assertEmpty();
+
+ // Set time offset to expire "action" inside logoutSession
+ setTimeOffset(310);
+ logoutConfirmPage.confirmLogout();
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Logout failed", errorPage.getError());
+
+ events.expectLogoutError(Errors.EXPIRED_CODE).assertEvent();
+ Assert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
+
+ // Link "Back to application" present
+ errorPage.clickBackToApplication();
+ Assert.assertThat(driver.getCurrentUrl(), endsWith("/app/auth"));
+ }
+
+
+ @Test
+ public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception {
+ ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
+ ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
+ rep.setFrontchannelLogout(true);
+ rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
+ clients.get(rep.getId()).update(rep);
+ try {
+ oauth.clientSessionState("client-session");
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String idTokenString = tokenResponse.getIdToken();
+ String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString)
+ .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build();
+ driver.navigate().to(logoutUrl);
+ LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken();
+ Assert.assertNotNull(logoutToken);
+
+ IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class);
+
+ Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer());
+ Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId());
+ } finally {
+ rep.setFrontchannelLogout(false);
+ rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
+ clients.get(rep.getId()).update(rep);
+ }
+ }
+
+ @Test
+ public void testFrontChannelLogout() throws Exception {
+ ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
+ ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
+ rep.setName("My Testing App");
+ rep.setFrontchannelLogout(true);
+ rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
+ clients.get(rep.getId()).update(rep);
+ try {
+ oauth.clientSessionState("client-session");
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String idTokenString = tokenResponse.getIdToken();
+ String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build();
+ driver.navigate().to(logoutUrl);
+ LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken();
+ Assert.assertNotNull(logoutToken);
+ IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class);
+ Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer());
+ Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId());
+ assertTrue(driver.getTitle().equals("Logging out"));
+ assertTrue(driver.getPageSource().contains("You are logging out from following apps"));
+ assertTrue(driver.getPageSource().contains("My Testing App"));
+ } finally {
+ rep.setFrontchannelLogout(false);
+ rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
+ clients.get(rep.getId()).update(rep);
+ }
+ }
+
+
+ private OAuthClient.AccessTokenResponse loginUser() {
+ return loginUser(false);
+ }
+
+ private OAuthClient.AccessTokenResponse loginUser(boolean consentRequired) {
+ oauth.doLogin("test-user@localhost", "password");
+
+ if (consentRequired) {
+ grantPage.assertCurrent();
+ grantPage.accept();
+ }
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ oauth.clientSessionState("client-session");
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ events.clear();
+ return tokenResponse;
+ }
+
+ private boolean isSessionActive(String sessionId) {
+ try {
+ testingClient.testing().getClientSessionsCountInUserSession("test", sessionId);
+ return true;
+ } catch (NotFoundException nfe) {
+ return false;
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenEndpointCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenEndpointCorsTest.java
index b0d5c22c30..75ee514bde 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenEndpointCorsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenEndpointCorsTest.java
@@ -62,6 +62,7 @@ public class TokenEndpointCorsTest extends AbstractKeycloakTest {
oauth.realm("test");
oauth.clientId("test-app2");
oauth.redirectUri(VALID_CORS_URL + "/realms/master/app");
+ oauth.postLogoutRedirectUri(VALID_CORS_URL + "/realms/master/app");
oauth.doLogin("test-user@localhost", "password");
@@ -87,7 +88,7 @@ public class TokenEndpointCorsTest extends AbstractKeycloakTest {
oauth.origin(VALID_CORS_URL);
// No session
- oauth.openLogout();
+ oauth.idTokenHint(response.getIdToken()).openLogout();
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null);
assertEquals(400, response.getStatusCode());
assertCors(response);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
index e667a73631..be348a240f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
@@ -1298,6 +1298,9 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
clientResource.update(clientRep);
}
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ oauth.idTokenHint(idTokenHint);
oauth.openLogout();
oauth = oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
oauth.doLogin("test-user@localhost", "password");
@@ -1308,7 +1311,6 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
public void testWrongContentEncryptionAlgorithm() throws Exception {
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId());
ClientRepresentation clientRep = clientResource.toRepresentation();
-
try {
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionAlg(RSA_OAEP_256);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectEncryptionEnc(JWEConstants.A192GCM);
@@ -1336,6 +1338,9 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
clientResource.update(clientRep);
}
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ String idTokenHint = oauth.doAccessTokenRequest(code, "password").getIdToken();
+ oauth.idTokenHint(idTokenHint);
oauth.openLogout();
oauth = oauth.request(createEncryptedRequestObject(RSA_OAEP_256));
oauth.doLogin("test-user@localhost", "password");
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java
index 982214d9b6..cab6f94c03 100644
--- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java
@@ -1,5 +1,6 @@
package org.keycloak.testsuite.springboot;
+import static org.hamcrest.Matchers.is;
import static org.keycloak.testsuite.admin.ApiUtil.assignRealmRoles;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
@@ -16,7 +17,6 @@ import javax.ws.rs.core.UriBuilder;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.logging.Logger;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
@@ -27,13 +27,18 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
+import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.TokenUtil;
import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
public abstract class AbstractSpringBootTest extends AbstractKeycloakTest {
@@ -73,6 +78,12 @@ public abstract class AbstractSpringBootTest extends AbstractKeycloakTest {
@Page
protected OIDCLogin testRealmLoginPage;
+ @Page
+ protected LogoutConfirmPage logoutConfirmPage;
+
+ @Page
+ protected InfoPage infoPage;
+
@Page
SpringApplicationPage applicationPage;
@@ -147,12 +158,23 @@ public abstract class AbstractSpringBootTest extends AbstractKeycloakTest {
return result;
}
-
- String logoutPage(String redirectUrl) {
- return getAuthRoot(suiteContext)
+
+ String getLogoutUrl() {
+ return getAuthRoot(suiteContext)
+ "/auth/realms/" + REALM_NAME
+ "/protocol/" + "openid-connect"
- + "/logout?redirect_uri=" + encodeUrl(redirectUrl);
+ + "/logout";
+ }
+
+ void logout(String redirectUrl) {
+ String logoutUrl = getLogoutUrl();
+ driver.navigate().to(logoutUrl);
+
+ logoutConfirmPage.assertCurrent();
+ logoutConfirmPage.confirmLogout();
+ infoPage.assertCurrent();
+
+ driver.navigate().to(redirectUrl);
}
void setAdapterAndServerTimeOffset(int timeOffset, String url) {
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java
index 06f12f2f07..a2e22f069e 100644
--- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java
@@ -548,10 +548,8 @@ public class AccountLinkSpringBootTest extends AbstractSpringBootTest {
}
public void logoutAll() {
- String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(REALM_NAME).toString();
- navigateTo(logoutUri);
- logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_REALM).toString();
- navigateTo(logoutUri);
+ adminClient.realm(REALM_NAME).logoutAll();
+ adminClient.realm(PARENT_REALM).logoutAll();
}
private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception {
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java
index 8ccfc7a746..887c422a1f 100644
--- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java
@@ -67,7 +67,7 @@ public class BasicSpringBootTest extends AbstractSpringBootTest {
adminPage.assertIsCurrent();
assertThat(driver.getPageSource(), containsString("You are now admin"));
- driver.navigate().to(logoutPage(BASE_URL));
+ logout(BASE_URL);
waitForPageToLoad();
assertCurrentUrlStartsWith(testRealmLoginPage);
@@ -87,7 +87,7 @@ public class BasicSpringBootTest extends AbstractSpringBootTest {
assertThat(driver.getPageSource(), containsString("Forbidden"));
- driver.navigate().to(logoutPage(BASE_URL));
+ logout(BASE_URL);
waitForPageToLoad();
}
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java
index debb0e895c..f9f332ec89 100644
--- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java
@@ -85,7 +85,7 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest {
setAdapterAndServerTimeOffset(0, SERVLET_URL);
- driver.navigate().to(logoutPage(SERVLET_URL));
+ logout(SERVLET_URL);
waitForPageToLoad();
assertCurrentUrlStartsWith(testRealmLoginPage);
}
@@ -146,7 +146,7 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest {
tokenPage.assertIsCurrent();
setAdapterAndServerTimeOffset(0, SERVLET_URL);
- driver.navigate().to(logoutPage(SERVLET_URL));
+ logout(SERVLET_URL);
}
@Test
@@ -183,7 +183,7 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest {
assertThat(offlineClient.getAdditionalGrants(), hasItem("Offline Token"));
//This was necessary to be introduced, otherwise other testcases will fail
- driver.navigate().to(logoutPage(SERVLET_URL));
+ logout(SERVLET_URL);
assertCurrentUrlStartsWith(testRealmLoginPage);
events.clear();
diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java
index 74dd0560a1..14db45cd62 100644
--- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java
+++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java
@@ -11,10 +11,13 @@ import org.keycloak.common.Profile;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.auth.page.account.Sessions;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.SecondBrowser;
import org.keycloak.testsuite.util.WaitUtils;
@@ -54,6 +57,14 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
@SecondBrowser
private OIDCLogin secondTestRealmLoginPage;
+ @Page
+ @SecondBrowser
+ protected LogoutConfirmPage secondBrowserLogoutConfirmPage;
+
+ @Page
+ @SecondBrowser
+ protected InfoPage secondBrowserInfoPage;
+
@Page
private Sessions realmSessions;
@@ -120,7 +131,7 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
DroneUtils.removeWebDriver(); // From now driver will be used instead of driver2
// Logout in browser1
- driver.navigate().to(logoutPage(SERVLET_URL));
+ logout(SERVLET_URL);
waitForPageToLoad();
// Assert that I am logged out in browser1
@@ -137,7 +148,17 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
secondBrowserSessionPage.assertIsCurrent();
assertThat(secondBrowserSessionPage.getCounter(), is(equalTo(2)));
- driver2.navigate().to(logoutPage(SERVLET_URL));
+ String logoutUrl = getLogoutUrl();
+ driver2.navigate().to(logoutUrl);
+
+ waitForPageToLoad();
+ Assert.assertThat(true, is(secondBrowserLogoutConfirmPage.isCurrent(driver2)));
+ secondBrowserLogoutConfirmPage.confirmLogout(driver2);
+ waitForPageToLoad();
+ secondBrowserInfoPage.assertCurrent();
+ waitForPageToLoad();
+ driver2.navigate().to(SERVLET_URL);
+
waitForPageToLoad();
assertCurrentUrlStartsWith(secondTestRealmLoginPage, driver2);
@@ -166,8 +187,7 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
loginAndCheckSession();
// Logout
- String logoutUri = logoutPage(SERVLET_URL);
- driver.navigate().to(logoutUri);
+ logout(SERVLET_URL);
waitForPageToLoad();
// Assert that http session was invalidated
@@ -184,7 +204,7 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
realmRep.setAccessCodeLifespan(origTokenLifespan);
realmResource.update(realmRep);
- driver.navigate().to(logoutUri);
+ logout(SERVLET_URL);
waitForPageToLoad();
}
@@ -204,7 +224,7 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
sessionPage.assertIsCurrent();
assertThat(sessionPage.getCounter(), is(equalTo(2)));
- driver.navigate().to(logoutPage(SERVLET_URL));
+ logout(SERVLET_URL);
waitForPageToLoad();
}
@@ -218,7 +238,7 @@ public class SessionSpringBootTest extends AbstractSpringBootTest {
// Assert I need to login again (logout was propagated to the app)
loginAndCheckSession();
- driver.navigate().to(logoutPage(SERVLET_URL));
+ logout(SERVLET_URL);
waitForPageToLoad();
}
}
diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
index 4f4968db1b..e22c841aad 100755
--- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
+++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
@@ -228,6 +228,9 @@
},
"login-protocol": {
+ "openid-connect": {
+ "legacy-logout-redirect-uri": "${keycloak.oidc.legacyLogoutRedirectUri:false}"
+ },
"saml": {
"knownProtocols": [
"http=${auth.server.http.port}",
diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties
index c4bfcc1bc0..11a3c7eef6 100644
--- a/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties
+++ b/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties
@@ -283,6 +283,7 @@ failedToProcessResponseMessage=Nepodařilo se zpracovat odpověď
httpsRequiredMessage=Požadováno HTTPS
realmNotEnabledMessage=Realm není povolen
invalidRequestMessage=Neplatná žádost
+successLogout=Odhlášení bylo úspěšné
failedLogout=Odhlášení se nezdařilo
unknownLoginRequesterMessage=Neznámý žadatel o přihlášení
loginRequesterNotEnabledMessage=Žadatel o přihlášení není povolen
@@ -417,3 +418,6 @@ access-denied=Přístup odepřen
frontchannel-logout.title=Odhlášení
frontchannel-logout.message=Odhlašujete se z následujících aplikací
+logoutConfirmTitle=Odhlašování
+logoutConfirmHeader=Chcete se odhlásit?
+doLogout=Odhlásit
diff --git a/themes/src/main/resources/theme/base/account/template.ftl b/themes/src/main/resources/theme/base/account/template.ftl
index 6f08eefc51..ec44204ee8 100644
--- a/themes/src/main/resources/theme/base/account/template.ftl
+++ b/themes/src/main/resources/theme/base/account/template.ftl
@@ -49,7 +49,7 @@
#if>
<#if referrer?has_content && referrer.url?has_content>${msg("backTo",referrer.name)}#if>
- ${msg("doSignOut")}
+ ${msg("doSignOut")}
diff --git a/themes/src/main/resources/theme/base/login/error.ftl b/themes/src/main/resources/theme/base/login/error.ftl
index a909e0dac7..2efa0feeb2 100755
--- a/themes/src/main/resources/theme/base/login/error.ftl
+++ b/themes/src/main/resources/theme/base/login/error.ftl
@@ -5,8 +5,11 @@
<#elseif section = "form">
#if>
diff --git a/themes/src/main/resources/theme/base/login/logout-confirm.ftl b/themes/src/main/resources/theme/base/login/logout-confirm.ftl
new file mode 100644
index 0000000000..6c0b4e97b6
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/logout-confirm.ftl
@@ -0,0 +1,38 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("logoutConfirmTitle")}
+ <#elseif section = "form">
+
+
${msg("logoutConfirmHeader")}
+
+
+
+
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index ffdd0041e5..d928144031 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -297,6 +297,7 @@ failedToProcessResponseMessage=Failed to process response
httpsRequiredMessage=HTTPS required
realmNotEnabledMessage=Realm not enabled
invalidRequestMessage=Invalid Request
+successLogout=You are logged out
failedLogout=Logout failed
unknownLoginRequesterMessage=Unknown login requester
loginRequesterNotEnabledMessage=Login requester not enabled
@@ -480,3 +481,7 @@ access-denied=Access denied
frontchannel-logout.title=Logging out
frontchannel-logout.message=You are logging out from following apps
+logoutConfirmTitle=Logging out
+logoutConfirmHeader=Do you want to logout?
+doLogout=Logout
+