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:
parent
da5db5a813
commit
22a16ee899
104 changed files with 2254 additions and 840 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -102,6 +102,8 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
Response createFrontChannelLogoutPage();
|
||||
|
||||
Response createLogoutConfirmPage();
|
||||
|
||||
LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession);
|
||||
|
||||
LoginFormsProvider setClientSessionCode(String accessCode);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -205,6 +205,7 @@ public abstract class AbstractKerberosSingleRealmTest extends AbstractKerberosTe
|
|||
|
||||
// Logout
|
||||
oauth.openLogout();
|
||||
events.poll();
|
||||
|
||||
// Remove protocolMapper
|
||||
clientResource.getProtocolMappers().delete(protocolMapperId);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,6 +228,9 @@
|
|||
},
|
||||
|
||||
"login-protocol": {
|
||||
"openid-connect": {
|
||||
"legacy-logout-redirect-uri": "${keycloak.oidc.legacyLogoutRedirectUri:false}"
|
||||
},
|
||||
"saml": {
|
||||
"knownProtocols": [
|
||||
"http=${auth.server.http.port}",
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue