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