OIDC RP-Initiated logout endpoint (#10887)

* OIDC RP-Initiated logout endpoint
Closes #10885

Co-Authored-By: Marek Posolda <mposolda@gmail.com>

* Review feedback

Co-authored-by: Douglas Palmer <dpalmer@redhat.com>
This commit is contained in:
Marek Posolda 2022-03-30 11:55:26 +02:00 committed by GitHub
parent da5db5a813
commit 22a16ee899
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 2254 additions and 840 deletions

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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";

View file

@ -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();
%>
<b>Details about user from LDAP</b> | <a href="<%=logoutUri%>">Logout</a><br />
<hr />

View file

@ -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();

View file

@ -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);

View file

@ -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;
}

View file

@ -102,6 +102,8 @@ public interface LoginFormsProvider extends Provider {
Response createFrontChannelLogoutPage();
Response createLogoutConfirmPage();
LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession);
LoginFormsProvider setClientSessionCode(String accessCode);

View file

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

View file

@ -79,6 +79,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
protected String[] referrer;
protected List<Event> events;
protected String stateChecker;
protected String idTokenHint;
protected List<UserSessionModel> 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;

View file

@ -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() {

View file

@ -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<>();

View file

@ -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();
}

View file

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

View file

@ -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();
}

View file

@ -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");
}

View file

@ -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);
}

View file

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

View file

@ -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;
}

View file

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

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -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<AuthenticationSessionModel> 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<String, String> 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;
}
}

View file

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

View file

@ -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<String> validRedirects = getValidateRedirectUris(session);
return verifyRedirectUri(session, null, redirectUri, validRedirects, true);
@ -71,6 +77,7 @@ public class RedirectUtils {
return resolveValidRedirects;
}
@Deprecated
private static Set<String> getValidateRedirectUris(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
return session.clientStorageManager().getAllRedirectUrisOfEnabledClients(realm).entrySet().stream()

View file

@ -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) {

View file

@ -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()) {

View file

@ -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) {

View file

@ -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<AuthenticationSessionModel> found = rootLogoutSession.getAuthenticationSessions().values().stream().filter((AuthenticationSessionModel authSession) -> {
return client.equals(authSession.getClient()) && Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), authSession.getAction());
Optional<AuthenticationSessionModel> 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;
}
}

View file

@ -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";

View file

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

View file

@ -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;
}
}

View file

@ -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));

View file

@ -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();
}

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.rest;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.util.HtmlUtils;
import org.keycloak.common.util.Time;
@ -49,6 +50,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthDetailsRepresentation;
@ -966,6 +968,16 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
}
@GET
@Path("/reinitialize-provider-factory-with-system-properties-scope")
@Consumes(MediaType.TEXT_HTML_UTF_8)
public void reinitializeProviderFactoryWithSystemPropertiesScope(@QueryParam("provider-type") String providerType, @QueryParam("provider-id") String providerId,
@QueryParam("system-properties-prefix") String systemPropertiesPrefix) throws Exception {
Class<? extends Provider> providerClass = (Class<? extends Provider>) Class.forName(providerType);
ProviderFactory factory = session.getKeycloakSessionFactory().getProviderFactory(providerClass, providerId);
factory.init(new Config.SystemPropertiesScope(systemPropertiesPrefix));
}
/**
* This will send POST request to specified URL with specified form parameters. It's not easily possible to "trick" web driver to send POST
* request with custom parameters, which are not directly available in the form.

View file

@ -15,4 +15,4 @@
String authUri = authScheme + "://" + authHost + ":" + authPort + "/auth";
%>
<h2>Click here <a href="<%= KeycloakUriBuilder.fromUri(authUri).path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
.queryParam("redirect_uri", redirectUri).build("servlet-authz").toString()%>">Sign Out</a></h2>
.build("servlet-authz").toString()%>">Sign Out</a></h2>

View file

@ -15,4 +15,4 @@
String authUri = authScheme + "://" + authHost + ":" + authPort + "/auth";
%>
<h2>Click here <a href="<%= KeycloakUriBuilder.fromUri(authUri).path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
.queryParam("redirect_uri", redirectUri).build("servlet-policy-enforcer-authz").toString()%>">Sign Out</a></h2>
.build("servlet-policy-enforcer-authz").toString()%>">Sign Out</a></h2>

View file

@ -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.

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

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

View file

@ -98,6 +98,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
return this;
}
public RealmAttributeUpdater setNotBefore(Integer notBefore) {
rep.setNotBefore(notBefore);
return this;
}
public RealmAttributeUpdater setDefaultLocale(String defaultLocale) {
rep.setDefaultLocale(defaultLocale);
return this;

View file

@ -79,6 +79,7 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
@ -152,6 +153,12 @@ public class OAuthClient {
private String redirectUri;
private String postLogoutRedirectUri;
private String idTokenHint;
private String initiatingIDP;
private String kcAction;
private StateParamProvider state;
@ -201,18 +208,19 @@ public class OAuthClient {
public LogoutUrlBuilder idTokenHint(String idTokenHint) {
if (idTokenHint != null) {
b.queryParam("id_token_hint", idTokenHint);
b.queryParam(OIDCLoginProtocol.ID_TOKEN_HINT, idTokenHint);
}
return this;
}
public LogoutUrlBuilder postLogoutRedirectUri(String redirectUri) {
if (redirectUri != null) {
b.queryParam("post_logout_redirect_uri", redirectUri);
b.queryParam(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM, redirectUri);
}
return this;
}
@Deprecated // Use only in backwards compatibility tests
public LogoutUrlBuilder redirectUri(String redirectUri) {
if (redirectUri != null) {
b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
@ -220,9 +228,23 @@ public class OAuthClient {
return this;
}
public LogoutUrlBuilder sessionState(String sessionState) {
if (sessionState != null) {
b.queryParam("session_state", sessionState);
public LogoutUrlBuilder state(String state) {
if (state != null) {
b.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
}
return this;
}
public LogoutUrlBuilder uiLocales(String uiLocales) {
if (uiLocales != null) {
b.queryParam(OIDCLoginProtocol.UI_LOCALES_PARAM, uiLocales);
}
return this;
}
public LogoutUrlBuilder initiatingIdp(String initiatingIdp) {
if (initiatingIdp != null) {
b.queryParam(AuthenticationManager.INITIATING_IDP_PARAM, initiatingIdp);
}
return this;
}
@ -249,6 +271,7 @@ public class OAuthClient {
realm = "test";
clientId = "test-app";
redirectUri = APP_ROOT + "/auth";
postLogoutRedirectUri = APP_ROOT + "/auth";
state = () -> {
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());
}
@ -1582,6 +1611,21 @@ public class OAuthClient {
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;
return this;

View file

@ -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);
}

View file

@ -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;

View file

@ -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<DeviceRepresentation> devices = getDevicesOtherThanOther();
assertEquals("Should have a single device", 1, devices.size());
List<DeviceRepresentation> 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");

View file

@ -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();

View file

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

View file

@ -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();

View file

@ -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();

View file

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

View file

@ -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());

View file

@ -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();
}

View file

@ -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;

View file

@ -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) {

View file

@ -87,6 +87,7 @@ import java.util.List;
import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
/**
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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) {

View file

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

View file

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

View file

@ -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();
}

View file

@ -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);
}

View file

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

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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);
}

View file

@ -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(() -> {

View file

@ -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));

View file

@ -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();
}

View file

@ -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());

View file

@ -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) {

View file

@ -205,6 +205,7 @@ public abstract class AbstractKerberosSingleRealmTest extends AbstractKerberosTe
// Logout
oauth.openLogout();
events.poll();
// Remove protocolMapper
clientResource.getProtocolMappers().delete(protocolMapperId);

View file

@ -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;
}

View file

@ -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;

View file

@ -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();

View file

@ -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");

View file

@ -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 {

View file

@ -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) {

View file

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

View file

@ -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();
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @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);
}
}
}

View file

@ -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;

View file

@ -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();

View file

@ -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);

View file

@ -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();

View file

@ -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();

View file

@ -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 {

View file

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

View file

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

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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();
Retry.execute(() -> {
UserRepresentation u = adminClient.realm("test").users().get(user.getId()).toRepresentation();
Assert.assertTrue(u.getNotBefore() > 0);
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 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());
}

View file

@ -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");

View file

@ -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();
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @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;
}
}
}

View file

@ -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);

View file

@ -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");

View file

@ -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;
@ -148,11 +159,22 @@ 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) {

View file

@ -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 {

View file

@ -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();
}

View file

@ -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();

View file

@ -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();
}
}

View file

@ -228,6 +228,9 @@
},
"login-protocol": {
"openid-connect": {
"legacy-logout-redirect-uri": "${keycloak.oidc.legacyLogoutRedirectUri:false}"
},
"saml": {
"knownProtocols": [
"http=${auth.server.http.port}",

View file

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

Some files were not shown because too many files have changed in this diff Show more