diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java index d3d8317255..49b83857b0 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java @@ -326,6 +326,10 @@ public class LDAPOperationManager { filter = "(&(objectClass=*)(" + getUuidAttributeName() + LDAPConstants.EQUAL + id + "))"; } + if (logger.isTraceEnabled()) { + logger.tracef("Using filter for lookup user by LDAP ID: %s", filter); + } + return filter; } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java index ba8276f497..f8a810e855 100644 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java @@ -17,6 +17,7 @@ package org.keycloak.broker.provider; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.broker.provider.util.IdentityBrokerState; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.sessions.AuthenticationSessionModel; @@ -30,13 +31,13 @@ public class AuthenticationRequest { private final KeycloakSession session; private final UriInfo uriInfo; - private final String state; + private final IdentityBrokerState state; private final HttpRequest httpRequest; private final RealmModel realm; private final String redirectUri; private final AuthenticationSessionModel authSession; - public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) { + public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, IdentityBrokerState state, String redirectUri) { this.session = session; this.realm = realm; this.httpRequest = httpRequest; @@ -54,7 +55,7 @@ public class AuthenticationRequest { return this.uriInfo; } - public String getState() { + public IdentityBrokerState getState() { return this.state; } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java new file mode 100644 index 0000000000..c44b4c49fc --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/util/IdentityBrokerState.java @@ -0,0 +1,90 @@ +/* + * 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.broker.provider.util; + +import java.util.regex.Pattern; + +/** + * Encapsulates parsing logic related to state passed to identity provider in "state" (or RelayState) parameter + * + * Not Thread-safe + * + * @author Marek Posolda + */ +public class IdentityBrokerState { + + private String decodedState; + private String clientId; + private String encodedState; + + private IdentityBrokerState() { + } + + public static IdentityBrokerState decoded(String decodedState, String clientId) { + IdentityBrokerState state = new IdentityBrokerState(); + state.decodedState = decodedState; + state.clientId = clientId; + return state; + } + + public static IdentityBrokerState encoded(String encodedState) { + IdentityBrokerState state = new IdentityBrokerState(); + state.encodedState = encodedState; + return state; + } + + + public String getDecodedState() { + if (decodedState == null) { + decode(); + } + return decodedState; + } + + public String getClientId() { + if (decodedState == null) { + decode(); + } + return clientId; + } + + public String getEncodedState() { + if (encodedState == null) { + encode(); + } + return encodedState; + } + + + private void decode() { + String[] decoded = DOT.split(encodedState, 0); + decodedState = decoded[0]; + if (decoded.length > 0) { + clientId = decoded[1]; + } + } + + + private void encode() { + encodedState = decodedState + "." + clientId; + } + + private static final Pattern DOT = Pattern.compile("\\."); + + +} diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java index 32195ef4db..ac435e3b9c 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java @@ -22,7 +22,6 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.Provider; -import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index 260ac1dbf7..40f9081b77 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -52,8 +52,12 @@ public interface Constants { int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000; String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY"; + String EXECUTION = "execution"; + String CLIENT_ID = "client_id"; String KEY = "key"; + String SKIP_LINK = "skipLink"; + // Prefix for user attributes used in various "context"data maps String USER_ATTRIBUTES_PREFIX = "user.attributes."; diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 242709195f..23d06e3d0f 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -33,6 +33,7 @@ import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; 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.UserModel; @@ -485,15 +486,17 @@ public class AuthenticationProcessor { return LoginActionsService.loginActionsBaseUrl(getUriInfo()) .path(AuthenticationProcessor.this.flowPath) .queryParam(OAuth2Constants.CODE, code) - .queryParam("execution", getExecution().getId()) + .queryParam(Constants.EXECUTION, getExecution().getId()) + .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .build(getRealm().getName()); } @Override public URI getActionTokenUrl(String tokenString) { return LoginActionsService.actionTokenProcessor(getUriInfo()) - .queryParam("key", tokenString) - .queryParam("execution", getExecution().getId()) + .queryParam(Constants.KEY, tokenString) + .queryParam(Constants.EXECUTION, getExecution().getId()) + .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .build(getRealm().getName()); } @@ -501,7 +504,8 @@ public class AuthenticationProcessor { public URI getRefreshExecutionUrl() { return LoginActionsService.loginActionsBaseUrl(getUriInfo()) .path(AuthenticationProcessor.this.flowPath) - .queryParam("execution", getExecution().getId()) + .queryParam(Constants.EXECUTION, getExecution().getId()) + .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .build(getRealm().getName()); } diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java index 955879f8ac..82c12ec500 100755 --- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java @@ -24,6 +24,8 @@ import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -247,9 +249,11 @@ public class FormAuthenticationFlow implements AuthenticationFlow { } public URI getActionUrl(String executionId, String code) { + ClientModel client = processor.getAuthenticationSession().getClient(); return LoginActionsService.registrationFormProcessor(processor.getUriInfo()) .queryParam(OAuth2Constants.CODE, code) - .queryParam("execution", executionId) + .queryParam(Constants.EXECUTION, executionId) + .queryParam(Constants.CLIENT_ID, client.getClientId()) .build(processor.getRealm().getName()); } diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index 87b3403b85..1d9475a80d 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -23,6 +23,8 @@ import org.keycloak.common.ClientConnection; import org.keycloak.common.util.Time; import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -132,9 +134,11 @@ public class RequiredActionContextResult implements RequiredActionContext { @Override public URI getActionUrl(String code) { + ClientModel client = authenticationSession.getClient(); return LoginActionsService.requiredActionProcessor(getUriInfo()) .queryParam(OAuth2Constants.CODE, code) - .queryParam("execution", factory.getId()) + .queryParam(Constants.EXECUTION, factory.getId()) + .queryParam(Constants.CLIENT_ID, client.getClientId()) .build(getRealm().getName()); } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java index a55c5870f7..ca00b0dbc5 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java @@ -45,7 +45,7 @@ public class ActionTokenContext { @FunctionalInterface public interface ProcessBrokerFlow { - Response brokerLoginFlow(String code, String execution, String flowPath); + Response brokerLoginFlow(String code, String execution, String clientId, String flowPath); }; private final KeycloakSession session; @@ -158,6 +158,7 @@ public class ActionTokenContext { } public Response brokerFlow(String code, String flowPath) { - return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), flowPath); + ClientModel client = authenticationSession.getClient(); + return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), client.getClientId(), flowPath); } } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java index 389441ed34..bd56eea4ab 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java @@ -23,6 +23,7 @@ import org.keycloak.authentication.actiontoken.*; import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator; import org.keycloak.events.*; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.messages.Messages; @@ -86,7 +87,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH return tokenContext.getSession().getProvider(LoginFormsProvider.class) .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername()) - .setAttribute("skipLink", true) + .setAttribute(Constants.SKIP_LINK, true) .createInfoPage(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java index a2a9b3a320..7189b955f6 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java @@ -132,7 +132,10 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias() ); UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); - String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString(); + String link = builder + .queryParam(Constants.EXECUTION, context.getExecution().getId()) + .queryParam(Constants.CLIENT_ID, context.getExecution().getId()) + .build(realm.getName()).toString(); long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs); try { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java index cb31e8d234..b255acebfe 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java @@ -64,8 +64,9 @@ public class IdentityProviderAuthenticator implements Authenticator { for (IdentityProviderModel identityProvider : identityProviders) { if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) { String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getCode(); + String clientId = context.getAuthenticationSession().getClient().getClientId(); Response response = Response.seeOther( - Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode)) + Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId)) .build(); LOG.debugf("Redirecting to %s", providerId); diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java index cb608614be..72b602f69c 100755 --- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java +++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPage.java @@ -36,7 +36,6 @@ import java.util.List; */ public class RegistrationPage implements FormAuthenticator, FormAuthenticatorFactory { - public static final String EXECUTION = "execution"; public static final String FIELD_PASSWORD_CONFIRM = "password-confirm"; public static final String FIELD_PASSWORD = "password"; public static final String FIELD_EMAIL = "email"; diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 339747a560..f4a877e17e 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -155,7 +155,7 @@ public abstract class AbstractOAuth2IdentityProvider realmRolesRequested; private MultivaluedMap resourceRolesRequested; private List protocolMappersRequested; - private MultivaluedMap queryParams; private Map httpResponseHeaders = new HashMap(); private String accessRequestMessage; private URI actionUri; @@ -146,8 +143,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { ClientModel client = session.getContext().getClient(); UriInfo uriInfo = session.getContext().getUri(); - MultivaluedMap queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl(); - String requestURI = uriInfo.getBaseUri().getPath(); UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); if (page == LoginFormsPages.OAUTH_GRANT) { @@ -155,19 +150,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { uriBuilder.replaceQuery(null); } + if (client != null) { + uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()); + } + URI baseUri = uriBuilder.build(); if (accessCode != null) { uriBuilder.queryParam(OAuth2Constants.CODE, accessCode); } - URI baseUriWithCode = uriBuilder.build(); - for (String k : queryParameterMap.keySet()) { - - Object[] objects = queryParameterMap.get(k).toArray(); - if (objects.length == 1 && objects[0] == null) continue; // - uriBuilder.replaceQueryParam(k, objects); - } + URI baseUriWithCodeAndClientId = uriBuilder.build(); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme; @@ -220,7 +213,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { List identityProviders = realm.getIdentityProviders(); identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData); - attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode)); + attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId)); attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); @@ -301,16 +294,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { ClientModel client = session.getContext().getClient(); UriInfo uriInfo = session.getContext().getUri(); - MultivaluedMap queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl(); - String requestURI = uriInfo.getBaseUri().getPath(); UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); - for (String k : queryParameterMap.keySet()) { - - Object[] objects = queryParameterMap.get(k).toArray(); - if (objects.length == 1 && objects[0] == null) continue; // - uriBuilder.replaceQueryParam(k, objects); + if (client != null) { + uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()); } URI baseUri = uriBuilder.build(); @@ -318,6 +306,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { if (accessCode != null) { uriBuilder.queryParam(OAuth2Constants.CODE, accessCode); } + URI baseUriWithCode = uriBuilder.build(); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index e92aa05a74..51f505e71e 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -18,6 +18,7 @@ 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.services.resources.AccountService; @@ -73,13 +74,16 @@ public class Urls { .build(realmName, providerId); } - public static URI identityProviderAuthnRequest(URI baseUri, String providerId, String realmName, String accessCode) { + public static URI identityProviderAuthnRequest(URI baseUri, String providerId, String realmName, String accessCode, String clientId) { UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService") .path(IdentityBrokerService.class, "performLogin"); if (accessCode != null) { uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); } + if (clientId != null) { + uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId); + } return uriBuilder.build(realmName, providerId); } @@ -99,20 +103,22 @@ public class Urls { } public static URI identityProviderAuthnRequest(URI baseURI, String providerId, String realmName) { - return identityProviderAuthnRequest(baseURI, providerId, realmName, null); + return identityProviderAuthnRequest(baseURI, providerId, realmName, null, null); } - public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode) { + public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId) { return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") .path(IdentityBrokerService.class, "afterFirstBrokerLogin") .replaceQueryParam(OAuth2Constants.CODE, accessCode) + .replaceQueryParam(Constants.CLIENT_ID, clientId) .build(realmName); } - public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode) { + public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId) { return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") .path(IdentityBrokerService.class, "afterPostBrokerLoginFlow") .replaceQueryParam(OAuth2Constants.CODE, accessCode) + .replaceQueryParam(Constants.CLIENT_ID, clientId) .build(realmName); } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 31217f1eae..805867bf6c 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -78,9 +78,6 @@ public class AuthenticationManager { public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS"; public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN"; - // Last authenticated client in userSession. - public static final String LAST_AUTHENTICATED_CLIENT = "LAST_AUTHENTICATED_CLIENT"; - // userSession note with authTime (time when authentication flow including requiredActions was finished) public static final String AUTH_TIME = "AUTH_TIME"; // clientSession note with flag that clientSession was authenticated through SSO cookie @@ -95,7 +92,6 @@ public class AuthenticationManager { public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION"; public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME"; public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL"; - public static final String CURRENT_REQUIRED_ACTION = "CURRENT_REQUIRED_ACTION"; public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) { if (userSession == null) { @@ -463,8 +459,6 @@ public class AuthenticationManager { userSession.setNote(AUTH_TIME, String.valueOf(authTime)); } - userSession.setNote(LAST_AUTHENTICATED_CLIENT, clientSession.getClient().getId()); - return protocol.authenticated(userSession, clientSession); } @@ -496,9 +490,11 @@ public class AuthenticationManager { .path(LoginActionsService.REQUIRED_ACTION); if (requiredAction != null) { - uriBuilder.queryParam("execution", requiredAction); + uriBuilder.queryParam(Constants.EXECUTION, requiredAction); } + uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId()); + URI redirect = uriBuilder.build(realm.getName()); return Response.status(302).location(redirect).build(); @@ -526,7 +522,7 @@ public class AuthenticationManager { } } else { - infoPage.setAttribute("skipLink", true); + infoPage.setAttribute(Constants.SKIP_LINK, true); } Response response = infoPage .createInfoPage(); diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 333a1cf029..530fce2a50 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -30,6 +30,7 @@ import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; +import org.keycloak.broker.provider.util.IdentityBrokerState; import org.keycloak.broker.saml.SAMLEndpoint; import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.common.ClientConnection; @@ -338,14 +339,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @POST @Path("/{provider_id}/login") - public Response performPostLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code) { - return performLogin(providerId, code); + public Response performPostLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code, @QueryParam("client_id") String clientId) { + return performLogin(providerId, code, clientId); } @GET @NoCache @Path("/{provider_id}/login") - public Response performLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code) { + public Response performLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code, @QueryParam("client_id") String clientId) { this.event.detail(Details.IDENTITY_PROVIDER, providerId); if (isDebugEnabled()) { @@ -353,7 +354,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } try { - ParsedCodeContext parsedCode = parseClientSessionCode(code); + ParsedCodeContext parsedCode = parseSessionCode(code, clientId); if (parsedCode.response != null) { return parsedCode.response; } @@ -479,7 +480,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (context.getContextData().get(SAMLEndpoint.SAML_IDP_INITIATED_CLIENT_ID) != null) { parsedCode = samlIdpInitiatedSSO((String) context.getContextData().get(SAMLEndpoint.SAML_IDP_INITIATED_CLIENT_ID)); } else { - parsedCode = parseClientSessionCode(context.getCode()); + parsedCode = parseEncodedSessionCode(context.getCode()); } if (parsedCode.response != null) { return parsedCode.response; @@ -549,6 +550,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal ctx.saveToAuthenticationSession(authenticationSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo) + .queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId()) .build(realmModel.getName()); return Response.status(302).location(redirect).build(); @@ -584,8 +586,8 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @GET @NoCache @Path("/after-first-broker-login") - public Response afterFirstBrokerLogin(@QueryParam("code") String code) { - ParsedCodeContext parsedCode = parseClientSessionCode(code); + public Response afterFirstBrokerLogin(@QueryParam("code") String code, @QueryParam("client_id") String clientId) { + ParsedCodeContext parsedCode = parseSessionCode(code, clientId); if (parsedCode.response != null) { return parsedCode.response; } @@ -701,6 +703,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal authSession.setAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin)); URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo) + .queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId()) .build(realmModel.getName()); return Response.status(302).location(redirect).build(); } @@ -711,8 +714,8 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @GET @NoCache @Path("/after-post-broker-login") - public Response afterPostBrokerLoginFlow(@QueryParam("code") String code) { - ParsedCodeContext parsedCode = parseClientSessionCode(code); + public Response afterPostBrokerLoginFlow(@QueryParam("code") String code, @QueryParam("client_id") String clientId) { + ParsedCodeContext parsedCode = parseSessionCode(code, clientId); if (parsedCode.response != null) { return parsedCode.response; } @@ -804,7 +807,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @Override public Response cancelled(String code) { - ParsedCodeContext parsedCode = parseClientSessionCode(code); + ParsedCodeContext parsedCode = parseEncodedSessionCode(code); if (parsedCode.response != null) { return parsedCode.response; } @@ -820,7 +823,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @Override public Response error(String code, String message) { - ParsedCodeContext parsedCode = parseClientSessionCode(code); + ParsedCodeContext parsedCode = parseEncodedSessionCode(code); if (parsedCode.response != null) { return parsedCode.response; } @@ -960,14 +963,21 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } } - private ParsedCodeContext parseClientSessionCode(String code) { - if (code == null) { - logger.debugf("Invalid request. Authorization code was null"); + private ParsedCodeContext parseEncodedSessionCode(String encodedCode) { + IdentityBrokerState state = IdentityBrokerState.encoded(encodedCode); + String code = state.getDecodedState(); + String clientId = state.getClientId(); + return parseSessionCode(code, clientId); + } + + private ParsedCodeContext parseSessionCode(String code, String clientId) { + if (code == null || clientId == null) { + logger.debugf("Invalid request. Authorization code or clientId was null. Code=" + code + ", clientId=" + clientId); Response staleCodeError = redirectToErrorPage(Messages.INVALID_REQUEST); return ParsedCodeContext.response(staleCodeError); } - SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, clientConnection, session, event, code, null, LoginActionsService.AUTHENTICATE_PATH); + SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, clientConnection, session, event, code, null, clientId, LoginActionsService.AUTHENTICATE_PATH); checks.initialVerify(); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { @@ -1041,14 +1051,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode clientSessionCode) { AuthenticationSessionModel authSession = null; - String relayState = null; + IdentityBrokerState encodedState = null; if (clientSessionCode != null) { authSession = clientSessionCode.getClientSession(); - relayState = clientSessionCode.getCode(); + String relayState = clientSessionCode.getCode(); + encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getClientId()); } - return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.uriInfo, relayState, getRedirectUri(providerId)); + return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.uriInfo, encodedState, getRedirectUri(providerId)); } private String getRedirectUri(String providerId) { diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 8f0d39ecce..e4364125ec 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -179,16 +179,16 @@ public class LoginActionsService { } } - private SessionCodeChecks checksForCode(String code, String execution, String flowPath) { - SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, flowPath); + private SessionCodeChecks checksForCode(String code, String execution, String clientId, String flowPath) { + SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, clientId, flowPath); res.initialVerify(); return res; } - protected URI getLastExecutionUrl(String flowPath, String executionId) { + protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId) { return new AuthenticationFlowURLHelper(session, realm, uriInfo) - .getLastExecutionUrl(flowPath, executionId); + .getLastExecutionUrl(flowPath, executionId, clientId); } @@ -199,9 +199,9 @@ public class LoginActionsService { */ @Path(RESTART_PATH) @GET - public Response restartSession() { + public Response restartSession(@QueryParam("client_id") String clientId) { event.event(EventType.RESTART_AUTHENTICATION); - SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, null); + SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, clientId, null); AuthenticationSessionModel authSession = checks.initialVerifyAuthSession(); if (authSession == null) { @@ -215,7 +215,7 @@ public class LoginActionsService { AuthenticationProcessor.resetFlow(authSession, flowPath); - URI redirectUri = getLastExecutionUrl(flowPath, null); + URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId()); logger.debugf("Flow restart requested. Redirecting to %s", redirectUri); return Response.status(Response.Status.FOUND).location(redirectUri).build(); } @@ -230,10 +230,11 @@ public class LoginActionsService { @Path(AUTHENTICATE_PATH) @GET public Response authenticate(@QueryParam("code") String code, - @QueryParam("execution") String execution) { + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { event.event(EventType.LOGIN); - SessionCodeChecks checks = checksForCode(code, execution, AUTHENTICATE_PATH); + SessionCodeChecks checks = checksForCode(code, execution, clientId, AUTHENTICATE_PATH); if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { return checks.getResponse(); } @@ -298,22 +299,24 @@ public class LoginActionsService { @Path(AUTHENTICATE_PATH) @POST public Response authenticateForm(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return authenticate(code, execution); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return authenticate(code, execution, clientId); } @Path(RESET_CREDENTIALS_PATH) @POST public Response resetCredentialsPOST(@QueryParam("code") String code, @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId, @QueryParam(Constants.KEY) String key) { if (key != null) { - return handleActionToken(key, execution); + return handleActionToken(key, execution, clientId); } event.event(EventType.RESET_PASSWORD); - return resetCredentials(code, execution); + return resetCredentials(code, execution, clientId); } /** @@ -327,7 +330,8 @@ public class LoginActionsService { @Path(RESET_CREDENTIALS_PATH) @GET public Response resetCredentialsGET(@QueryParam("code") String code, - @QueryParam("execution") String execution) { + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); // we allow applications to link to reset credentials without going through OAuth or SAML handshakes @@ -343,7 +347,7 @@ public class LoginActionsService { } event.event(EventType.RESET_PASSWORD); - return resetCredentials(code, execution); + return resetCredentials(code, execution, clientId); } AuthenticationSessionModel createAuthenticationSessionForClient() @@ -370,8 +374,8 @@ public class LoginActionsService { * @param execution * @return */ - protected Response resetCredentials(String code, String execution) { - SessionCodeChecks checks = checksForCode(code, execution, RESET_CREDENTIALS_PATH); + protected Response resetCredentials(String code, String execution, String clientId) { + SessionCodeChecks checks = checksForCode(code, execution, clientId, RESET_CREDENTIALS_PATH); if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) { return checks.getResponse(); } @@ -397,11 +401,12 @@ public class LoginActionsService { @Path("action-token") @GET public Response executeActionToken(@QueryParam("key") String key, - @QueryParam("execution") String execution) { - return handleActionToken(key, execution); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return handleActionToken(key, execution, clientId); } - protected Response handleActionToken(String tokenString, String execution) { + protected Response handleActionToken(String tokenString, String execution, String clientId) { T token; ActionTokenHandler handler; ActionTokenContext tokenContext; @@ -411,6 +416,15 @@ public class LoginActionsService { event.event(EventType.EXECUTE_ACTION_TOKEN); + // Setup client, so error page will contain "back to application" link + ClientModel client = null; + if (clientId != null) { + client = realm.getClientByClientId(clientId); + } + if (client != null) { + session.getContext().setClient(client); + } + // First resolve action token handler try { if (tokenString == null) { @@ -570,8 +584,9 @@ public class LoginActionsService { @Path(REGISTRATION_PATH) @GET public Response registerPage(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return registerRequest(code, execution, false); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return registerRequest(code, execution, clientId, false); } @@ -584,19 +599,20 @@ public class LoginActionsService { @Path(REGISTRATION_PATH) @POST public Response processRegister(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return registerRequest(code, execution, true); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return registerRequest(code, execution, clientId, true); } - private Response registerRequest(String code, String execution, boolean isPostRequest) { + private Response registerRequest(String code, String execution, String clientId, boolean isPostRequest) { event.event(EventType.REGISTER); if (!realm.isRegistrationAllowed()) { event.error(Errors.REGISTRATION_DISABLED); return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED); } - SessionCodeChecks checks = checksForCode(code, execution, REGISTRATION_PATH); + SessionCodeChecks checks = checksForCode(code, execution, clientId, REGISTRATION_PATH); if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { return checks.getResponse(); } @@ -612,39 +628,43 @@ public class LoginActionsService { @Path(FIRST_BROKER_LOGIN_PATH) @GET public Response firstBrokerLoginGet(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return brokerLoginFlow(code, execution, clientId, FIRST_BROKER_LOGIN_PATH); } @Path(FIRST_BROKER_LOGIN_PATH) @POST public Response firstBrokerLoginPost(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return brokerLoginFlow(code, execution, clientId, FIRST_BROKER_LOGIN_PATH); } @Path(POST_BROKER_LOGIN_PATH) @GET public Response postBrokerLoginGet(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return brokerLoginFlow(code, execution, clientId, POST_BROKER_LOGIN_PATH); } @Path(POST_BROKER_LOGIN_PATH) @POST public Response postBrokerLoginPost(@QueryParam("code") String code, - @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH); + @QueryParam("execution") String execution, + @QueryParam("client_id") String clientId) { + return brokerLoginFlow(code, execution, clientId, POST_BROKER_LOGIN_PATH); } - protected Response brokerLoginFlow(String code, String execution, String flowPath) { + protected Response brokerLoginFlow(String code, String execution, String clientId, String flowPath) { boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH); EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN; event.event(eventType); - SessionCodeChecks checks = checksForCode(code, execution, flowPath); + SessionCodeChecks checks = checksForCode(code, execution, clientId, flowPath); if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { return checks.getResponse(); } @@ -702,8 +722,9 @@ public class LoginActionsService { ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession); authSession.setTimestamp(Time.currentTime()); - URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) : - Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ; + String clientId = authSession.getClient().getClientId(); + URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode(), clientId) : + Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode(), clientId) ; logger.debugf("Redirecting to '%s' ", redirect); return Response.status(302).location(redirect).build(); @@ -722,7 +743,8 @@ public class LoginActionsService { public Response processConsent(final MultivaluedMap formData) { event.event(EventType.LOGIN); String code = formData.getFirst("code"); - SessionCodeChecks checks = checksForCode(code, null, REQUIRED_ACTION); + String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID); + SessionCodeChecks checks = checksForCode(code, null, clientId, REQUIRED_ACTION); if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) { return checks.getResponse(); } @@ -811,21 +833,23 @@ public class LoginActionsService { @Path(REQUIRED_ACTION) @POST public Response requiredActionPOST(@QueryParam("code") final String code, - @QueryParam("execution") String action) { - return processRequireAction(code, action); + @QueryParam("execution") String action, + @QueryParam("client_id") String clientId) { + return processRequireAction(code, action, clientId); } @Path(REQUIRED_ACTION) @GET public Response requiredActionGET(@QueryParam("code") final String code, - @QueryParam("execution") String action) { - return processRequireAction(code, action); + @QueryParam("execution") String action, + @QueryParam("client_id") String clientId) { + return processRequireAction(code, action, clientId); } - private Response processRequireAction(final String code, String action) { + private Response processRequireAction(final String code, String action, String clientId) { event.event(EventType.CUSTOM_REQUIRED_ACTION); - SessionCodeChecks checks = checksForCode(code, action, REQUIRED_ACTION); + SessionCodeChecks checks = checksForCode(code, action, clientId, REQUIRED_ACTION); if (!checks.verifyRequiredAction(action)) { return checks.getResponse(); } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java index 87eaf20505..3c29ede4d7 100644 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java @@ -120,16 +120,8 @@ public class LoginActionsServiceChecks { LoginFormsProvider loginForm = context.getSession().getProvider(LoginFormsProvider.class) .setSuccess(Messages.ALREADY_LOGGED_IN); - ClientModel client = null; - String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT); - if (lastClientUuid != null) { - client = context.getRealm().getClientById(lastClientUuid); - } - - if (client != null) { - context.getSession().getContext().setClient(client); - } else { - loginForm.setAttribute("skipLink", true); + if (context.getSession().getContext().getClient() == null) { + loginForm.setAttribute(Constants.SKIP_LINK, true); } throw new LoginActionsServiceException(loginForm.createInfoPage()); diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java index 978ad6f26e..941fa5c4cd 100644 --- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -33,6 +33,7 @@ import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; @@ -66,10 +67,11 @@ public class SessionCodeChecks { private final String code; private final String execution; + private final String clientId; private final String flowPath; - public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String flowPath) { + public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String clientId, String flowPath) { this.realm = realm; this.uriInfo = uriInfo; this.clientConnection = clientConnection; @@ -78,6 +80,7 @@ public class SessionCodeChecks { this.code = code; this.execution = execution; + this.clientId = clientId; this.flowPath = flowPath; } @@ -134,6 +137,16 @@ public class SessionCodeChecks { return authSession; } + // Setup client to be shown on error/info page based on "client_id" parameter + logger.debugf("Will use client '%s' in back-to-application link", clientId); + ClientModel client = null; + if (clientId != null) { + client = realm.getClientByClientId(clientId); + } + if (client != null) { + session.getContext().setClient(client); + } + // See if we are already authenticated and userSession with same ID exists. String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm); if (sessionId != null) { @@ -143,16 +156,8 @@ public class SessionCodeChecks { LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class) .setSuccess(Messages.ALREADY_LOGGED_IN); - ClientModel client = null; - String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT); - if (lastClientUuid != null) { - client = realm.getClientById(lastClientUuid); - } - - if (client != null) { - session.getContext().setClient(client); - } else { - loginForm.setAttribute("skipLink", true); + if (client == null) { + loginForm.setAttribute(Constants.SKIP_LINK, true); } response = loginForm.createInfoPage(); @@ -234,7 +239,7 @@ 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); + URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, client.getClientId()); logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri); authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION); @@ -289,7 +294,7 @@ public class SessionCodeChecks { authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT); - URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null); + URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, authSession.getClient().getClientId()); logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri); response = Response.status(Response.Status.FOUND).location(redirectUri).build(); return false; @@ -351,7 +356,7 @@ public class SessionCodeChecks { flowPath = LoginActionsService.AUTHENTICATE_PATH; } - URI redirectUri = getLastExecutionUrl(flowPath, null); + URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId()); logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri); return Response.status(Response.Status.FOUND).location(redirectUri).build(); } else { @@ -367,16 +372,20 @@ public class SessionCodeChecks { .path(LoginActionsService.REQUIRED_ACTION); if (action != null) { - uriBuilder.queryParam("execution", action); + uriBuilder.queryParam(Constants.EXECUTION, action); } + + ClientModel client = authSession.getClient(); + uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()); + URI redirect = uriBuilder.build(realm.getName()); return Response.status(302).location(redirect).build(); } - private URI getLastExecutionUrl(String flowPath, String executionId) { + private URI getLastExecutionUrl(String flowPath, String executionId, String clientId) { return new AuthenticationFlowURLHelper(session, realm, uriInfo) - .getLastExecutionUrl(flowPath, executionId); + .getLastExecutionUrl(flowPath, executionId, clientId); } diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java index b97963e009..3726b99f29 100644 --- a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java +++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java @@ -26,6 +26,7 @@ import javax.ws.rs.core.UriInfo; import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.AuthorizationEndpointBase; @@ -61,13 +62,15 @@ public class AuthenticationFlowURLHelper { } - public URI getLastExecutionUrl(String flowPath, String executionId) { + public URI getLastExecutionUrl(String flowPath, String executionId, String clientId) { UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo) .path(flowPath); if (executionId != null) { - uriBuilder.queryParam("execution", executionId); + uriBuilder.queryParam(Constants.EXECUTION, executionId); } + uriBuilder.queryParam(Constants.CLIENT_ID, clientId); + return uriBuilder.build(realm.getName()); } @@ -84,7 +87,7 @@ public class AuthenticationFlowURLHelper { latestFlowPath = LoginActionsService.AUTHENTICATE_PATH; } - return getLastExecutionUrl(latestFlowPath, executionId); + return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId()); } } diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index 00be27c8b4..77009e7f86 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -74,7 +74,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProviderMarek Posolda + */ +public class ActionURIUtils { + + private static final Pattern ACTION_URI_PATTERN = Pattern.compile("action=\"([^\"]+)\""); + + private static final Pattern QUERY_STRING_PATTERN = Pattern.compile("[^\\?]+\\?([^#]+).*"); + + private static final Pattern PARAMS_PATTERN = Pattern.compile("[=\\&]"); + + public static String getActionURIFromPageSource(String htmlPageSource) { + Matcher m = ACTION_URI_PATTERN.matcher(htmlPageSource); + if (m.find()) { + return m.group(1).replaceAll("&", "&"); + } else { + return null; + } + } + + public static Map parseQueryParamsFromActionURI(String actionURI) { + Matcher m = QUERY_STRING_PATTERN.matcher(actionURI); + if (m.find()) { + String queryString = m.group(1); + + String[] params = PARAMS_PATTERN.split(queryString, 0); + Map result = new HashMap<>(); // Don't take multivalued into account for now + + for (int i=0 ; i"; + + public static void main(String[] args) { + String actionURI = getActionURIFromPageSource(TEST); + System.out.println("action uri: " + actionURI); + + Map params = parseQueryParamsFromActionURI(actionURI); + System.out.println("params: " + params); + + String actionURI2 = removeQueryParamFromURI(actionURI, "execution"); + System.out.println("action uri 2: " + actionURI2); + }*/ +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ErrorPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ErrorPage.java index 5b4a1163da..fc1c078efc 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ErrorPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ErrorPage.java @@ -43,6 +43,14 @@ public class ErrorPage extends AbstractPage { backToApplicationLink.click(); } + public String getBackToApplicationLink() { + if (backToApplicationLink == null) { + return null; + } else { + return backToApplicationLink.getAttribute("href"); + } + } + public boolean isCurrent() { return driver.getTitle() != null && driver.getTitle().equals("We're sorry..."); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index 9c18070672..d829d5bc70 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; @@ -32,6 +33,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.util.UserBuilder; @@ -53,6 +55,9 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe @Page protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage; + @Page + protected ErrorPage errorPage; + @Override public void configureTestRealm(RealmRepresentation testRealm) { ActionUtil.addRequiredActionForUser(testRealm, "test-user@localhost", UserModel.RequiredAction.UPDATE_PROFILE.name()); @@ -294,4 +299,23 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe events.assertEmpty(); } + @Test + public void updateProfileExpiredCookies() { + loginPage.open(); + loginPage.login("john-doh@localhost", "password"); + + updateProfilePage.assertCurrent(); + + // Expire cookies and assert the page with "back to application" link present + driver.manage().deleteAllCookies(); + + updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); + errorPage.assertCurrent(); + + String backToAppLink = errorPage.getBackToApplicationLink(); + + ClientRepresentation client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app").toRepresentation(); + Assert.assertEquals(backToAppLink, client.getBaseUrl()); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java index 750df031f7..ea9937eda3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java @@ -24,6 +24,7 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.util.Base64Url; @@ -37,6 +38,7 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; @@ -57,6 +59,7 @@ import javax.ws.rs.core.UriBuilder; import java.net.URL; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -496,23 +499,11 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer // ok, now scrape the code from page String pageSource = driver.getPageSource(); - Pattern p = Pattern.compile("action=\"(.+)\""); - Matcher m = p.matcher(pageSource); - String action = null; - if (m.find()) { - action = m.group(1); + String action = ActionURIUtils.getActionURIFromPageSource(pageSource); + System.out.println("action uri: " + action); - } - System.out.println("action: " + action); - - p = Pattern.compile("code=(.+)&"); - m = p.matcher(action); - String code = null; - if (m.find()) { - code = m.group(1); - - } - System.out.println("code: " + code); + Map queryParams = ActionURIUtils.parseQueryParamsFromActionURI(action); + System.out.println("query params: " + queryParams); // now try and use the code to login to remote link-only idp @@ -520,7 +511,8 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot()) .path(uri) - .queryParam("code", code) + .queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE)) + .queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID)) .build().toString(); System.out.println("hack uri: " + uri); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java index 81292439ca..6500ad0ce1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java @@ -387,4 +387,22 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { logoutFromRealm(bc.providerRealmName()); logoutFromRealm(bc.consumerRealmName()); } + + + // KEYCLOAK-4016 + @Test + public void testExpiredCode() { + driver.navigate().to(getAccountUrl(bc.consumerRealmName())); + + log.debug("Expire all browser cookies"); + driver.manage().deleteAllCookies(); + + log.debug("Clicking social " + bc.getIDPAlias()); + accountLoginPage.clickSocial(bc.getIDPAlias()); + + waitForPage(driver, "sorry"); + errorPage.assertCurrent(); + String link = errorPage.getBackToApplicationLink(); + Assert.assertTrue(link.endsWith("/auth/realms/consumer/account")); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java index 2d6d3afb5f..148d7e143c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java @@ -43,6 +43,7 @@ import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.util.KerberosRule; /** @@ -156,10 +157,7 @@ public class KerberosStandaloneTest extends AbstractKerberosTest { Assert.assertTrue(context.contains("Log in to test")); - Pattern pattern = Pattern.compile("action=\"([^\"]+)\""); - Matcher m = pattern.matcher(context); - Assert.assertTrue(m.find()); - String url = m.group(1); + String url = ActionURIUtils.getActionURIFromPageSource(context); // Follow login with HttpClient. Improve if needed diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 07006771b3..907d4abd3b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -27,6 +27,7 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.BrowserSecurityHeaders; import org.keycloak.models.Constants; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -54,6 +55,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; /** * @author Stian Thorgersen @@ -600,9 +602,13 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { driver.manage().deleteAllCookies(); - // Cookies are expired including KC_RESTART. No way to continue login. Error page must be shown + // Cookies are expired including KC_RESTART. No way to continue login. Error page must be shown with the "back to application" link loginPage.login("login@test.com", "password"); errorPage.assertCurrent(); + String link = errorPage.getBackToApplicationLink(); + + ClientRepresentation thirdParty = findClientByClientId(adminClient.realm("test"), "third-party").toRepresentation(); + Assert.assertNotNull(link, thirdParty.getBaseUrl()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java index 24a70fd6f8..9d914bdeb2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java @@ -27,6 +27,7 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; @@ -120,22 +121,16 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { public void openMultipleTabs() { oauth.openLoginForm(); loginPage.assertCurrent(); - String actionUrl1 = getActionUrl(driver.getPageSource()); + String actionUrl1 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); oauth.openLoginForm(); loginPage.assertCurrent(); - String actionUrl2 = getActionUrl(driver.getPageSource()); + String actionUrl2 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); Assert.assertEquals(actionUrl1, actionUrl2); } - - private String getActionUrl(String pageSource) { - return pageSource.split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); - } - - @Test public void multipleTabsParallelLoginTest() { oauth.openLoginForm(); @@ -173,7 +168,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Simulate to open login form in 2 tabs oauth.openLoginForm(); loginPage.assertCurrent(); - String actionUrl1 = getActionUrl(driver.getPageSource()); + String actionUrl1 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); // Click "register" in tab2 loginPage.clickRegister(); @@ -204,7 +199,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Simulate to open login form in 2 tabs oauth.openLoginForm(); loginPage.assertCurrent(); - String actionUrl1 = getActionUrl(driver.getPageSource()); + String actionUrl1 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); loginPage.login("invalid", "invalid"); loginPage.assertCurrent(); @@ -228,7 +223,7 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Open tab1 oauth.openLoginForm(); loginPage.assertCurrent(); - String actionUrl1 = getActionUrl(driver.getPageSource()); + String actionUrl1 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); // Authenticate in tab2 loginPage.login("login-test", "password"); @@ -253,8 +248,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { oauth.openLoginForm(); // Manually remove execution from the URL and try to simulate the request just with "code" parameter - String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); - actionUrl = actionUrl.replaceFirst("&execution=.*", ""); + String actionUrl = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); + actionUrl = ActionURIUtils.removeQueryParamFromURI(actionUrl, Constants.EXECUTION); driver.navigate().to(actionUrl); @@ -272,8 +267,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); // Manually remove execution from the URL and try to simulate the request just with "code" parameter - String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); - actionUrl = actionUrl.replaceFirst("&execution=.*", ""); + String actionUrl = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); + actionUrl = ActionURIUtils.removeQueryParamFromURI(actionUrl, Constants.EXECUTION); driver.navigate().to(actionUrl); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 04ee91117f..16d85ef370 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -418,6 +418,51 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { } } + // KEYCLOAK-4016 + @Test + public void resetPasswordExpiredCodeAndAuthSession() throws IOException, MessagingException, InterruptedException { + final AtomicInteger originalValue = new AtomicInteger(); + + RealmRepresentation realmRep = testRealm().toRepresentation(); + originalValue.set(realmRep.getActionTokenGeneratedByUserLifespan()); + realmRep.setActionTokenGeneratedByUserLifespan(60); + testRealm().update(realmRep); + + try { + initiateResetPasswordFromResetPasswordPage("login-test"); + + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) + .session((String)null) + .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); + + assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String changePasswordUrl = getPasswordResetEmailLink(message); + + setTimeOffset(70); + + log.debug("Going to reset password URI."); + driver.navigate().to(oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials"); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path + log.debug("Removing cookies."); + driver.manage().deleteAllCookies(); + driver.navigate().to(changePasswordUrl.trim()); + + errorPage.assertCurrent(); + Assert.assertEquals("Reset Credential not allowed", errorPage.getError()); + String backToAppLink = errorPage.getBackToApplicationLink(); + Assert.assertTrue(backToAppLink.endsWith("/app/auth")); + + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent(); + } finally { + setTimeOffset(0); + + realmRep.setActionTokenGeneratedByUserLifespan(originalValue.get()); + testRealm().update(realmRep); + } + } + @Test public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException { UserRepresentation user = findUser("login-test"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index ecf540cc30..5209ef5be6 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -56,6 +56,7 @@ 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.ActionURIUtils; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.util.ClientBuilder; @@ -210,7 +211,8 @@ public class AccessTokenTest extends AbstractKeycloakTest { oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console/nosuch.html"); oauth.openLoginForm(); - String loginPageCode = driver.getPageSource().split("code=")[1].split("&")[0].split("\"")[0]; + String actionURI = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); + String loginPageCode = ActionURIUtils.parseQueryParamsFromActionURI(actionURI).get("code"); oauth.fillLoginForm("test-user@localhost", "password"); @@ -441,7 +443,8 @@ public class AccessTokenTest extends AbstractKeycloakTest { oauth.doLogin("test-user@localhost", "password"); - String code = driver.getPageSource().split("code=")[1].split("&")[0].split("\"")[0]; + String actionURI = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); + String code = ActionURIUtils.parseQueryParamsFromActionURI(actionURI).get("code"); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); Assert.assertEquals(400, response.getStatusCode()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java index 7a01e4e291..84144f6702 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java @@ -36,6 +36,7 @@ import org.keycloak.models.Constants; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.ActionURIUtils; import java.io.IOException; import java.net.URLEncoder; @@ -71,10 +72,7 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { String s = IOUtils.toString(response.getEntity().getContent()); response.close(); - Matcher matcher = Pattern.compile("action=\"([^\"]*)\"").matcher(s); - matcher.find(); - - String action = matcher.group(1); + String action = ActionURIUtils.getActionURIFromPageSource(s); HttpPost post = new HttpPost(action); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java index c5304ff61c..926ff69c6f 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java @@ -42,6 +42,7 @@ import org.keycloak.testsuite.account.AccountTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AccountApplicationsPage; import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; @@ -77,6 +78,9 @@ public class OAuthGrantTest extends AbstractKeycloakTest { @Page protected AppPage appPage; + @Page + protected ErrorPage errorPage; + @Override public void addTestRealms(List testRealms) { @@ -405,4 +409,23 @@ public class OAuthGrantTest extends AbstractKeycloakTest { } + @Test + public void oauthGrantExpiredAuthSession() throws Exception { + oauth.clientId(THIRD_PARTY_APP); + oauth.doLoginGrant("test-user@localhost", "password"); + + grantPage.assertCurrent(); + + // Expire cookies + driver.manage().deleteAllCookies(); + + grantPage.accept(); + + // Assert link "back to application" present + errorPage.assertCurrent(); + String backToAppLink = errorPage.getBackToApplicationLink(); + ClientRepresentation thirdParty = findClientByClientId(adminClient.realm(REALM_NAME), THIRD_PARTY_APP).toRepresentation(); + Assert.assertEquals(backToAppLink, thirdParty.getBaseUrl()); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json index edb0f61585..f4b118e54f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json @@ -162,6 +162,7 @@ "enabled": true, "consentRequired": true, + "baseUrl": "http://localhost:8180/auth/realms/master/app/auth", "redirectUris": [ "http://localhost:8180/auth/realms/master/app/*" ], diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index 2c6e8849ca..6439950f9f 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -82,8 +82,13 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error #log4j.logger.org.apache.http.impl.conn=debug # Enable to view details from identity provider authenticator -log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace -log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace -log4j.logger.org.keycloak.broker=trace +#log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace +#log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace +#log4j.logger.org.keycloak.broker=trace -# log4j.logger.io.undertow=trace +#log4j.logger.io.undertow=trace + +#log4j.logger.org.keycloak.protocol=debug +#log4j.logger.org.keycloak.services.resources.LoginActionsService=debug +#log4j.logger.org.keycloak.services.managers=debug +#log4j.logger.org.keycloak.services.resources.SessionCodeChecks=debug \ No newline at end of file