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 providerClass = (Class) 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"; %>

Click here ">Sign Out

\ 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"; %>

Click here ">Sign Out

\ 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 referrer?has_content && referrer.url?has_content>
  • ${msg("backTo",referrer.name)}
  • -
  • ${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">

    ${message.summary?no_esc}

    - <#if client?? && client.baseUrl?has_content> -

    ${kcSanitize(msg("backToApplication"))?no_esc}

    + <#if skipLink??> + <#else> + <#if client?? && client.baseUrl?has_content> +

    ${kcSanitize(msg("backToApplication"))?no_esc}

    +
    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 logoutConfirm.skipLink> + <#else> + <#if (client.baseUrl)?has_content> +

    ${kcSanitize(msg("backToApplication"))?no_esc}

    + + +
    + +
    +
    + + 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 +