KEYCLOAK-14483 Broker state param fix

This commit is contained in:
mposolda 2021-02-23 18:07:04 +01:00 committed by Pedro Igor
parent 52a939f61a
commit 41dc94fead
8 changed files with 324 additions and 211 deletions

View file

@ -42,7 +42,6 @@ public class BrokeredIdentityContext {
private String lastName;
private String brokerSessionId;
private String brokerUserId;
private String code;
private String token;
private IdentityProviderModel idpConfig;
private IdentityProvider idp;
@ -136,14 +135,6 @@ public class BrokeredIdentityContext {
this.token = token;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public IdentityProviderModel getIdpConfig() {
return idpConfig;
}

View file

@ -38,18 +38,39 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
interface AuthenticationCallback {
/**
* Common method to return current authenticationSession and verify if it is not expired
*
* @param encodedCode
* @return see description
*/
AuthenticationSessionModel getAndVerifyAuthenticationSession(String encodedCode);
/**
* This method should be called by provider after the JAXRS callback endpoint has finished authentication
* with the remote IDP
* with the remote IDP. There is an assumption that authenticationSession is set in the context when this method is called
*
* @param context
* @return
* @return see description
*/
Response authenticated(BrokeredIdentityContext context);
Response cancelled(String code);
/**
* Called when user cancelled authentication on the IDP side - for example user didn't approve consent page on the IDP side.
* Assumption is that authenticationSession is set in the {@link org.keycloak.models.KeycloakContext} when this method is called
*
* @return see description
*/
Response cancelled();
Response error(String code, String message);
/**
* Called when error happened on the IDP side.
* Assumption is that authenticationSession is set in the {@link org.keycloak.models.KeycloakContext} when this method is called
*
* @return see description
*/
Response error(String message);
}

View file

@ -479,18 +479,20 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return errorIdentityProviderLogin(Messages.IDENTITY_PROVIDER_MISSING_STATE_ERROR);
}
if (error != null) {
logger.error(error + " for broker login " + getConfig().getProviderId());
if (error.equals(ACCESS_DENIED)) {
return callback.cancelled(state);
} else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) {
return callback.error(state, error);
} else {
return callback.error(state, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
}
try {
AuthenticationSessionModel authSession = this.callback.getAndVerifyAuthenticationSession(state);
session.getContext().setAuthenticationSession(authSession);
if (error != null) {
logger.error(error + " for broker login " + getConfig().getProviderId());
if (error.equals(ACCESS_DENIED)) {
return callback.cancelled();
} else if (error.equals(OAuthErrorException.LOGIN_REQUIRED) || error.equals(OAuthErrorException.INTERACTION_REQUIRED)) {
return callback.error(error);
} else {
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
}
if (authorizationCode != null) {
String response = generateTokenRequest(authorizationCode).asString();
@ -505,7 +507,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
federatedIdentity.setIdpConfig(getConfig());
federatedIdentity.setIdp(AbstractOAuth2IdentityProvider.this);
federatedIdentity.setCode(state);
federatedIdentity.setAuthenticationSession(authSession);
return callback.authenticated(federatedIdentity);
}
@ -518,7 +520,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
}
private Response errorIdentityProviderLogin(String message) {
event.event(EventType.LOGIN);
event.event(EventType.IDENTITY_PROVIDER_LOGIN);
event.error(Errors.IDENTITY_PROVIDER_LOGIN_FAILURE);
return ErrorPage.error(session, null, Response.Status.BAD_GATEWAY, message);
}

View file

@ -20,6 +20,7 @@ package org.keycloak.broker.saml;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.IdentityProvider;
@ -39,13 +40,17 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.protocol.saml.SamlService;
import org.keycloak.protocol.saml.SamlSessionUtils;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
@ -63,6 +68,7 @@ import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.Consumes;
@ -88,6 +94,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
@ -99,6 +106,8 @@ import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.validators.ConditionsValidator;
import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@ -122,7 +131,6 @@ public class SAMLEndpoint {
public static final String SAML_FEDERATED_SUBJECT_NAMEID = "SAML_FEDERATED_SUBJECT_NAME_ID";
public static final String SAML_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
public static final String SAML_ASSERTION = "SAML_ASSERTION";
public static final String SAML_IDP_INITIATED_CLIENT_ID = "SAML_IDP_INITIATED_CLIENT_ID";
public static final String SAML_AUTHN_STATEMENT = "SAML_AUTHN_STATEMENT";
protected RealmModel realm;
protected EventBuilder event;
@ -391,13 +399,21 @@ public class SAMLEndpoint {
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState, String clientId) {
try {
AuthenticationSessionModel authSession;
if (clientId != null && ! clientId.trim().isEmpty()) {
authSession = samlIdpInitiatedSSO(clientId);
} else {
authSession = callback.getAndVerifyAuthenticationSession(relayState);
}
session.getContext().setAuthenticationSession(authSession);
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
if (! isSuccessfulSamlResponse(responseType)) {
String statusMessage = responseType.getStatus() == null ? Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR : responseType.getStatus().getStatusMessage();
return callback.error(relayState, statusMessage);
return callback.error(statusMessage);
}
if (responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
return callback.error(relayState, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
return callback.error(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
boolean assertionIsEncrypted = AssertionUtil.isAssertionEncrypted(responseType);
@ -406,7 +422,7 @@ public class SAMLEndpoint {
logger.error("The assertion is not encrypted, which is required.");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
}
Element assertionElement;
@ -429,7 +445,7 @@ public class SAMLEndpoint {
logger.error("validation failed");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SIGNATURE);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
}
AssertionType assertion = responseType.getAssertions().get(0).getAssertion();
@ -440,16 +456,14 @@ public class SAMLEndpoint {
logger.errorf("no principal in assertion; expected: %s", expectedPrincipalType());
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
}
//Map<String, String> notes = new HashMap<>();
BrokeredIdentityContext identity = new BrokeredIdentityContext(principal);
identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType);
identity.getContextData().put(SAML_ASSERTION, assertion);
if (clientId != null && ! clientId.trim().isEmpty()) {
identity.getContextData().put(SAML_IDP_INITIATED_CLIENT_ID, clientId);
}
identity.setAuthenticationSession(authSession);
identity.setUsername(principal);
@ -478,7 +492,7 @@ public class SAMLEndpoint {
logger.error("Assertion expired.");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
}
AuthnStatementType authn = null;
@ -502,7 +516,6 @@ public class SAMLEndpoint {
if (authn != null && authn.getSessionIndex() != null) {
identity.setBrokerSessionId(identity.getBrokerUserId() + "." + authn.getSessionIndex());
}
identity.setCode(relayState);
return callback.authenticated(identity);
@ -514,6 +527,42 @@ public class SAMLEndpoint {
}
/**
* If there is a client whose SAML IDP-initiated SSO URL name is set to the
* given {@code clientUrlName}, creates a fresh authentication session for that
* client and returns a {@link AuthenticationSessionModel} object with that session.
* Otherwise returns "client not found" response.
*
* @param clientUrlName
* @return see description
*/
private AuthenticationSessionModel samlIdpInitiatedSSO(final String clientUrlName) {
event.event(EventType.LOGIN);
CacheControlUtil.noBackButtonCacheControlHeader();
Optional<ClientModel> oClient = SAMLEndpoint.this.realm.getClientsStream()
.filter(c -> Objects.equals(c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME), clientUrlName))
.findFirst();
if (! oClient.isPresent()) {
event.error(Errors.CLIENT_NOT_FOUND);
Response response = ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
throw new WebApplicationException(response);
}
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, SamlProtocol.LOGIN_PROTOCOL);
SamlService samlService = (SamlService) factory.createProtocolEndpoint(SAMLEndpoint.this.realm, event);
ResteasyProviderFactory.getInstance().injectProperties(samlService);
AuthenticationSessionModel authSession = samlService.getOrCreateLoginSessionForIdpInitiatedSso(session, SAMLEndpoint.this.realm, oClient.get(), null);
if (authSession == null) {
event.error(Errors.INVALID_REDIRECT_URI);
Response response = ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
throw new WebApplicationException(response);
}
return authSession;
}
private boolean isSuccessfulSamlResponse(ResponseType responseType) {
return responseType != null
&& responseType.getStatus() != null

View file

@ -372,12 +372,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
try {
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
if (parsedCode.response != null) {
return parsedCode.response;
}
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId);
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = parsedCode.clientSessionCode;
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
IdentityProviderModel identityProviderModel = realmModel.getIdentityProviderByAlias(providerId);
if (identityProviderModel == null) {
throw new IdentityBrokerException("Identity Provider [" + providerId + "] not found.");
@ -506,17 +504,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
public Response authenticated(BrokeredIdentityContext context) {
IdentityProviderModel identityProviderConfig = context.getIdpConfig();
final ParsedCodeContext parsedCode;
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 = parseEncodedSessionCode(context.getCode());
}
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode<AuthenticationSessionModel> clientCode = parsedCode.clientSessionCode;
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
String providerId = identityProviderConfig.getAlias();
if (!identityProviderConfig.isStoreToken()) {
@ -525,9 +513,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
context.setToken(null);
}
AuthenticationSessionModel authenticationSession = clientCode.getClientSession();
context.setAuthenticationSession(authenticationSession);
StatusResponseType loginResponse = (StatusResponseType) context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
if (loginResponse != null) {
@ -629,7 +614,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
authenticationSession.setAuthenticatedUser(federatedUser);
return finishOrRedirectToPostBrokerLogin(authenticationSession, context, false, parsedCode.clientSessionCode);
return finishOrRedirectToPostBrokerLogin(authenticationSession, context, false);
}
}
@ -655,15 +640,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
if (parsedCode.response != null) {
return parsedCode.response;
}
return afterFirstBrokerLogin(parsedCode.clientSessionCode);
AuthenticationSessionModel authSession = parseSessionCode(code, clientId, tabId);
return afterFirstBrokerLogin(authSession);
}
private Response afterFirstBrokerLogin(ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
AuthenticationSessionModel authSession = clientSessionCode.getClientSession();
private Response afterFirstBrokerLogin(AuthenticationSessionModel authSession) {
try {
this.event.detail(Details.CODE_ID, authSession.getParentSession().getId())
.removeDetail("auth_method");
@ -742,7 +723,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
updateFederatedIdentity(context, federatedUser);
}
return finishOrRedirectToPostBrokerLogin(authSession, context, true, clientSessionCode);
return finishOrRedirectToPostBrokerLogin(authSession, context, true);
} catch (Exception e) {
return redirectToErrorPage(authSession, Response.Status.INTERNAL_SERVER_ERROR, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
@ -750,12 +731,12 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
private Response finishOrRedirectToPostBrokerLogin(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
private Response finishOrRedirectToPostBrokerLogin(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin) {
String postBrokerLoginFlowId = context.getIdpConfig().getPostBrokerLoginFlowId();
if (postBrokerLoginFlowId == null) {
logger.debugf("Skip redirect to postBrokerLogin flow. PostBrokerLogin flow not set for identityProvider '%s'.", context.getIdpConfig().getAlias());
return afterPostBrokerLoginFlowSuccess(authSession, context, wasFirstBrokerLogin, clientSessionCode);
return afterPostBrokerLoginFlowSuccess(authSession, context, wasFirstBrokerLogin);
} else {
logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias());
@ -783,11 +764,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
if (parsedCode.response != null) {
return parsedCode.response;
}
AuthenticationSessionModel authenticationSession = parsedCode.clientSessionCode.getClientSession();
AuthenticationSessionModel authenticationSession = parseSessionCode(code, clientId, tabId);
try {
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
@ -810,13 +787,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
authenticationSession.removeAuthNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
authenticationSession.removeAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
return afterPostBrokerLoginFlowSuccess(authenticationSession, context, wasFirstBrokerLogin, parsedCode.clientSessionCode);
return afterPostBrokerLoginFlowSuccess(authenticationSession, context, wasFirstBrokerLogin);
} catch (IdentityBrokerException e) {
return redirectToErrorPage(authenticationSession, Response.Status.INTERNAL_SERVER_ERROR, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
}
}
private Response afterPostBrokerLoginFlowSuccess(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
private Response afterPostBrokerLoginFlowSuccess(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin) {
String providerId = context.getIdpConfig().getAlias();
UserModel federatedUser = authSession.getAuthenticatedUser();
@ -831,7 +808,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, serializedCtx.getIdentityProviderId());
return afterFirstBrokerLogin(clientSessionCode);
return afterFirstBrokerLogin(authSession);
} else {
return finishBrokerAuthentication(context, federatedUser, authSession, providerId);
}
@ -873,40 +850,32 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Override
public Response cancelled(String code) {
ParsedCodeContext parsedCode = parseEncodedSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode<AuthenticationSessionModel> clientCode = parsedCode.clientSessionCode;
public Response cancelled() {
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.CONSENT_DENIED);
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(authSession, Messages.CONSENT_DENIED);
if (accountManagementFailedLinking != null) {
return accountManagementFailedLinking;
}
return browserAuthentication(clientCode.getClientSession(), null);
return browserAuthentication(authSession, null);
}
@Override
public Response error(String code, String message) {
ParsedCodeContext parsedCode = parseEncodedSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode<AuthenticationSessionModel> clientCode = parsedCode.clientSessionCode;
public Response error(String message) {
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), message);
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(authSession, message);
if (accountManagementFailedLinking != null) {
return accountManagementFailedLinking;
}
Response passiveLoginErrorReturned = checkPassiveLoginError(clientCode.getClientSession(), message);
Response passiveLoginErrorReturned = checkPassiveLoginError(authSession, message);
if (passiveLoginErrorReturned != null) {
return passiveLoginErrorReturned;
}
return browserAuthentication(clientCode.getClientSession(), message);
return browserAuthentication(authSession, message);
}
@ -1059,7 +1028,8 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
}
private ParsedCodeContext parseEncodedSessionCode(String encodedCode) {
@Override
public AuthenticationSessionModel getAndVerifyAuthenticationSession(String encodedCode) {
IdentityBrokerState state = IdentityBrokerState.encoded(encodedCode);
String code = state.getDecodedState();
String clientId = state.getClientId();
@ -1067,11 +1037,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return parseSessionCode(code, clientId, tabId);
}
private ParsedCodeContext parseSessionCode(String code, String clientId, String tabId) {
/**
* This method will throw JAX-RS exception in case it is not able to retrieve AuthenticationSessionModel. It never returns null
*/
private AuthenticationSessionModel parseSessionCode(String code, String clientId, String tabId) {
if (code == null || clientId == null || tabId == null) {
logger.debugf("Invalid request. Authorization code, clientId or tabId was null. Code=%s, clientId=%s, tabID=%s", code, clientId, tabId);
Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
return ParsedCodeContext.response(staleCodeError);
throw new WebApplicationException(staleCodeError);
}
SessionCodeChecks checks = new SessionCodeChecks(realmModel, session.getContext().getUri(), request, clientConnection, session, event, null, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
@ -1083,59 +1056,26 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
// Check if error happened during login or during linking from account management
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(authSession, Messages.STALE_CODE_ACCOUNT);
if (accountManagementFailedLinking != null) {
return ParsedCodeContext.response(accountManagementFailedLinking);
throw new WebApplicationException(accountManagementFailedLinking);
} else {
Response errorResponse = checks.getResponse();
// Remove "code" from browser history
errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true, request);
return ParsedCodeContext.response(errorResponse);
throw new WebApplicationException(errorResponse);
}
} else {
return ParsedCodeContext.response(checks.getResponse());
throw new WebApplicationException(checks.getResponse());
}
} else {
if (isDebugEnabled()) {
logger.debugf("Authorization code is valid.");
}
return ParsedCodeContext.clientSessionCode(checks.getClientCode());
return checks.getClientCode().getClientSession();
}
}
/**
* If there is a client whose SAML IDP-initiated SSO URL name is set to the
* given {@code clientUrlName}, creates a fresh client session for that
* client and returns a {@link ParsedCodeContext} object with that session.
* Otherwise returns "client not found" response.
*
* @param clientUrlName
* @return see description
*/
private ParsedCodeContext samlIdpInitiatedSSO(final String clientUrlName) {
event.event(EventType.LOGIN);
CacheControlUtil.noBackButtonCacheControlHeader();
Optional<ClientModel> oClient = this.realmModel.getClientsStream()
.filter(c -> Objects.equals(c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME), clientUrlName))
.findFirst();
if (! oClient.isPresent()) {
event.error(Errors.CLIENT_NOT_FOUND);
return ParsedCodeContext.response(redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND));
}
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, SamlProtocol.LOGIN_PROTOCOL);
SamlService samlService = (SamlService) factory.createProtocolEndpoint(realmModel, event);
ResteasyProviderFactory.getInstance().injectProperties(samlService);
AuthenticationSessionModel authSession = samlService.getOrCreateLoginSessionForIdpInitiatedSso(session, realmModel, oClient.get(), null);
if (authSession == null) {
event.error(Errors.INVALID_REDIRECT_URI);
return ParsedCodeContext.response(redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI));
}
return ParsedCodeContext.clientSessionCode(new ClientSessionCode<>(session, this.realmModel, authSession));
}
private Response checkAccountManagementFailedLinking(AuthenticationSessionModel authSession, String error, Object... parameters) {
UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authSession);
if (userSession != null && authSession.getClient() != null && authSession.getClient().getClientId().equals(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)) {
@ -1348,21 +1288,4 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
}
private static class ParsedCodeContext {
private ClientSessionCode<AuthenticationSessionModel> clientSessionCode;
private Response response;
public static ParsedCodeContext clientSessionCode(ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
ParsedCodeContext ctx = new ParsedCodeContext();
ctx.clientSessionCode = clientSessionCode;
return ctx;
}
public static ParsedCodeContext response(Response response) {
ParsedCodeContext ctx = new ParsedCodeContext();
ctx.response = response;
return ctx;
}
}
}

View file

@ -184,28 +184,27 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
public Response authResponse(@QueryParam("state") String state,
@QueryParam("denied") String denied,
@QueryParam("oauth_verifier") String verifier) {
if (denied != null) {
return callback.cancelled(state);
IdentityBrokerState idpState = IdentityBrokerState.encoded(state);
String clientId = idpState.getClientId();
String tabId = idpState.getTabId();
if (clientId == null || tabId == null) {
logger.errorf("Invalid state parameter: %s", state);
sendErrorEvent();
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}
ClientModel client = realm.getClientByClientId(clientId);
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(state, tabId, session, realm, client, event, AuthenticationSessionModel.class);
if (denied != null) {
return callback.cancelled();
}
AuthenticationSessionModel authSession = null;
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret()));
IdentityBrokerState idpState = IdentityBrokerState.encoded(state);
String clientId = idpState.getClientId();
String tabId = idpState.getTabId();
if (clientId == null || tabId == null) {
logger.errorf("Invalid state parameter: %s", state);
sendErrorEvent();
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}
ClientModel client = realm.getClientByClientId(clientId);
authSession = ClientSessionCode.getClientSession(state, tabId, session, realm, client, event, AuthenticationSessionModel.class);
String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
@ -236,7 +235,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
identity.getContextData().put(IdentityProvider.FEDERATED_ACCESS_TOKEN, token);
identity.setIdpConfig(getConfig());
identity.setCode(state);
identity.setAuthenticationSession(authSession);
return callback.authenticated(identity);
} catch (WebApplicationException e) {

View file

@ -0,0 +1,174 @@
/*
* Copyright 2020 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.broker;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginExpiredPage;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
/**
* Tests related to OIDC "state" parameter used in the OIDC AuthenticationResponse sent by the IDP to the SP endpoint
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KcOidcBrokerStateParameterTest extends AbstractInitializedBaseBrokerTest {
@Page
protected AppPage appPage;
@Page
protected LoginExpiredPage loginExpiredPage;
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcOidcBrokerConfiguration.INSTANCE;
}
@Test
public void testMissingStateParameter() {
final String consumerEndpointUrl = getURLOfOIDCIdpEndpointOnConsumerSide() + "?code=foo123";
events.clear();
// Manually open the consumer endpoint URL
driver.navigate().to(consumerEndpointUrl);
waitForPage(driver, "sign in to consumer", true);
errorPage.assertCurrent();
assertThat(errorPage.getError(), Matchers.is("Missing state parameter in response from identity provider."));
// Test that only loginEvent happened on consumer side. There should *not* be request sent to provider realm codeToToken endpoint (Assert that event is not there)
String consumerRealmId = realmsResouce().realm(bc.consumerRealmName()).toRepresentation().getId();
events.expect(EventType.IDENTITY_PROVIDER_LOGIN_ERROR)
.clearDetails()
.session((String) null)
.realm(consumerRealmId)
.user((String) null)
.client((String) null)
.error("identity_provider_login_failure")
.assertEvent();
events.assertEmpty();
}
@Test
public void testIncorrectStateParameter() throws Exception {
final String consumerEndpointUrl = KeycloakUriBuilder.fromUri(getURLOfOIDCIdpEndpointOnConsumerSide())
.queryParam(OAuth2Constants.CODE, "foo456")
.queryParam(OAuth2Constants.STATE, "someIncorrectState")
.build().toString();
events.clear();
// Manually open the consumer endpoint URL
String consumerRealmId = realmsResouce().realm(bc.consumerRealmName()).toRepresentation().getId();
driver.navigate().to(consumerEndpointUrl);
// Test that only loginEvent happened on consumer side. There should *not* be request sent to provider realm codeToToken endpoint (Assert that event is not there)
events.expect(EventType.IDENTITY_PROVIDER_LOGIN_ERROR)
.clearDetails()
.session((String) null)
.realm(consumerRealmId)
.user((String) null)
.client((String) null)
.error("invalidRequestMessage")
.assertEvent();
events.assertEmpty();
}
@Test
public void testCorrectStateParameterButIncorrectCode() throws Exception {
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
waitForPage(driver, "sign in to", true);
loginPage.clickSocial(bc.getIDPAlias());
waitForPage(driver, "sign in to", true);
// Get the "state", which was generated by "consumer" before sending OIDC AuthenticationRequest to "provider"
String url = driver.getCurrentUrl();
String stateParamValue = UriUtils.decodeQueryString(url).getFirst(OAuth2Constants.STATE);
final String consumerEndpointUrl = KeycloakUriBuilder.fromUri(getURLOfOIDCIdpEndpointOnConsumerSide())
.queryParam(OAuth2Constants.CODE, "foo123")
.queryParam(OAuth2Constants.STATE, stateParamValue)
.build().toString();
events.clear();
// Manually open the consumer endpoint URL
String providerRealmId = realmsResouce().realm(bc.providerRealmName()).toRepresentation().getId();
String consumerRealmId = realmsResouce().realm(bc.consumerRealmName()).toRepresentation().getId();
driver.navigate().to(consumerEndpointUrl);
// Check that loginError on consumer side was triggered. Also CodeToToken request was sent to the "provider", but failed due the incorrect code
events.expect(EventType.CODE_TO_TOKEN_ERROR)
.clearDetails()
.session((String) null)
.realm(providerRealmId)
.user((String) null)
.client("brokerapp")
.error("invalid_code")
.assertEvent();
events.expect(EventType.IDENTITY_PROVIDER_LOGIN_ERROR)
.clearDetails()
.session((String) null)
.realm(consumerRealmId)
.user((String) null)
.client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
.error("identity_provider_login_failure")
.assertEvent();
// Re-send the request to same URL. There should *not* be additional
// request sent to provider realm codeToToken endpoint due the "state" already used on consumer side (Assert that CodeToToken event is not there)
// The consumer should display "Page has expired" error
driver.navigate().to(consumerEndpointUrl);
loginExpiredPage.assertCurrent();
events.assertEmpty();
}
// Return the endpoint on consumer side where the IDentity Provider redirects the browser after successful authentication on IDP side.
private String getURLOfOIDCIdpEndpointOnConsumerSide() {
BrokerConfiguration brokerConfig = getBrokerConfiguration();
return oauth.AUTH_SERVER_ROOT + "/realms/" + brokerConfig.consumerRealmName() + "/broker/" + brokerConfig.getIDPAlias() + "/endpoint";
}
}

View file

@ -413,52 +413,6 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
errorPage.assertCurrent();
}
@Test
public void testMissingStateParameter() {
final String IDP_NAME = "github";
RealmResource realmResource = Optional.ofNullable(realmsResouce().realm(bc.providerRealmName())).orElse(null);
assertThat(realmResource, Matchers.notNullValue());
final int COUNT_PROVIDERS = Optional.of(realmResource.identityProviders().findAll().size()).orElse(0);
try {
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
idp.setAlias(IDP_NAME);
idp.setDisplayName(IDP_NAME);
idp.setProviderId(IDP_NAME);
idp.setEnabled(true);
Response response = realmResource.identityProviders().create(idp);
assertThat(response, Matchers.notNullValue());
assertThat(response.getStatus(), Matchers.is(Response.Status.CREATED.getStatusCode()));
assertThat(realmResource.identityProviders().findAll().size(), Matchers.is(COUNT_PROVIDERS + 1));
assertThat(ApiUtil.getCreatedId(response), Matchers.notNullValue());
IdentityProviderRepresentation provider = Optional.ofNullable(realmResource.identityProviders().get(IDP_NAME).toRepresentation()).orElse(null);
assertThat(provider, Matchers.notNullValue());
assertThat(provider.getProviderId(), Matchers.is(IDP_NAME));
assertThat(provider.getAlias(), Matchers.is(IDP_NAME));
assertThat(provider.getDisplayName(), Matchers.is(IDP_NAME));
final String REALM_NAME = Optional.ofNullable(realmResource.toRepresentation().getRealm()).orElse(null);
assertThat(REALM_NAME, Matchers.notNullValue());
final String LINK = oauth.AUTH_SERVER_ROOT + "/realms/" + REALM_NAME + "/broker/" + IDP_NAME + "/endpoint?code=foo123";
driver.navigate().to(LINK);
waitForPage(driver, "sign in to provider", true);
errorPage.assertCurrent();
assertThat(errorPage.getError(), Matchers.is("Missing state parameter in response from identity provider."));
} finally {
IdentityProviderResource resource = Optional.ofNullable(realmResource.identityProviders().get(IDP_NAME)).orElse(null);
assertThat(resource, Matchers.notNullValue());
resource.remove();
assertThat(Optional.of(realmResource.identityProviders().findAll().size()).orElse(0), Matchers.is(COUNT_PROVIDERS));
}
}
@Test
public void testIdPNotFound() {
final String notExistingIdP = "not-exists";