KEYCLOAK-4016 Provide a Link to go Back to The Application on a Timeout

This commit is contained in:
mposolda 2017-05-19 15:28:04 +02:00
parent ca8577fb4a
commit 8adde64e2c
38 changed files with 541 additions and 191 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,7 +45,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
@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<T extends JsonWebToken> {
}
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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -155,7 +155,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
return UriBuilder.fromUri(getConfig().getAuthorizationUrl())
.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
.queryParam(OAUTH2_PARAMETER_STATE, request.getState())
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncodedState())
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());

View file

@ -100,7 +100,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
.protocolBinding(protocolBinding)
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
.relayState(request.getState());
.relayState(request.getState().getEncodedState());
boolean postBinding = getConfig().isPostBindingAuthnRequest();
if (getConfig().isWantAuthnRequestsSigned()) {

View file

@ -17,7 +17,6 @@
package org.keycloak.forms.login.freemarker;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
@ -40,7 +39,6 @@ import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.FreeMarkerUtil;
@ -75,7 +73,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private List<RoleModel> realmRolesRequested;
private MultivaluedMap<String, RoleModel> resourceRolesRequested;
private List<ProtocolMapperModel> protocolMappersRequested;
private MultivaluedMap<String, String> queryParams;
private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
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<String, String> queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl<String, String>();
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<IdentityProviderModel> 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<String, String> queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl<String, String>();
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");

View file

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

View file

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

View file

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

View file

@ -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 <T extends DefaultActionToken> Response handleActionToken(String tokenString, String execution) {
protected <T extends DefaultActionToken> Response handleActionToken(String tokenString, String execution, String clientId) {
T token;
ActionTokenHandler<T> handler;
ActionTokenContext<T> 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<AuthenticationSessionModel> 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<String, String> 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();
}

View file

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

View file

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

View file

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

View file

@ -74,7 +74,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
URI uri = new URI(request.getRedirectUri() + "?state=" + request.getState());
URI uri = new URI(request.getRedirectUri() + "?state=" + request.getState().getEncodedState());
RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString());
AuthenticationSessionModel authSession = request.getAuthenticationSession();

View file

@ -0,0 +1,89 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.core.UriBuilder;
/**
* Helper for parse action-uri from the HTML login page and do something with it (eg. open in new browser, parse code parameter and use it somewhere else etc)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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("&amp;", "&");
} else {
return null;
}
}
public static Map<String, String> 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<String, String> result = new HashMap<>(); // Don't take multivalued into account for now
for (int i=0 ; i<params.length ; i+=2) {
String paramName = params[i];
String paramValue = params[i+1];
result.put(paramName, paramValue);
}
return result;
} else {
return Collections.emptyMap();
}
}
public static String removeQueryParamFromURI(String actionURI, String paramName) {
return UriBuilder.fromUri(actionURI)
.replaceQueryParam(paramName, null)
.build().toString();
}
/*
private static final String TEST = "<form id=\"kc-form-login\" class=\"form-horizontal\" action=\"http://localhost:8180/auth/realms/child/login-actions/authenticate?code=1WnqOmapgo0cj3mpRQ-vbleIKUJdwFzonzy1fjvnWQQ&amp;execution=3ac92a20-9c31-49de-a3c8-f2a4fff80986&amp;client_id=client-linking\" method=\"post\">";
public static void main(String[] args) {
String actionURI = getActionURIFromPageSource(TEST);
System.out.println("action uri: " + actionURI);
Map<String, String> params = parseQueryParamsFromActionURI(actionURI);
System.out.println("params: " + params);
String actionURI2 = removeQueryParamFromURI(actionURI, "execution");
System.out.println("action uri 2: " + actionURI2);
}*/
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("&amp;", "&");
}
@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("&amp;", "&");
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("&amp;", "&");
actionUrl = actionUrl.replaceFirst("&execution=.*", "");
String actionUrl = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource());
actionUrl = ActionURIUtils.removeQueryParamFromURI(actionUrl, Constants.EXECUTION);
driver.navigate().to(actionUrl);

View file

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

View file

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

View file

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

View file

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

View file

@ -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/*"
],

View file

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