From 0bdb05e152ff661dce8b0d50c21cc3ee3f73534f Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 16 Nov 2015 17:20:22 +0100 Subject: [PATCH 1/3] KEYCLOAK-2075 - support for IsPassive mode in SAML IdP endpoint --- .../keycloak/protocol/saml/SamlProtocol.java | 259 +++++++++--------- .../keycloak/protocol/saml/SamlService.java | 196 ++++--------- .../AuthenticationProcessor.java | 3 +- .../protocol/AuthorizationEndpointBase.java | 137 +++++++++ .../org/keycloak/protocol/LoginProtocol.java | 27 +- .../protocol/oidc/OIDCLoginProtocol.java | 49 ++-- .../oidc/endpoints/AuthorizationEndpoint.java | 129 ++------- .../managers/AuthenticationManager.java | 3 +- .../resources/LoginActionsService.java | 5 +- 9 files changed, 383 insertions(+), 425 deletions(-) create mode 100644 services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 8d6fa15823..6729c9097b 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -1,5 +1,22 @@ package org.keycloak.protocol.saml; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; @@ -36,30 +53,14 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.admin.ClientAttributeCertificateResource; -import org.keycloak.services.ErrorPage; import org.w3c.dom.Document; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.security.PublicKey; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - /** * @author Bill Burke * @version $Revision: 1 $ @@ -67,7 +68,6 @@ import java.util.UUID; public class SamlProtocol implements LoginProtocol { protected static final Logger logger = Logger.getLogger(SamlProtocol.class); - public static final String ATTRIBUTE_TRUE_VALUE = "true"; public static final String ATTRIBUTE_FALSE_VALUE = "false"; public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE; @@ -115,7 +115,6 @@ public class SamlProtocol implements LoginProtocol { protected EventBuilder event; - @Override public SamlProtocol setSession(KeycloakSession session) { this.session = session; @@ -135,7 +134,7 @@ public class SamlProtocol implements LoginProtocol { } @Override - public SamlProtocol setHttpHeaders(HttpHeaders headers){ + public SamlProtocol setHttpHeaders(HttpHeaders headers) { this.headers = headers; return this; } @@ -146,22 +145,62 @@ public class SamlProtocol implements LoginProtocol { return this; } - @Override - public Response cancelLogin(ClientSessionModel clientSession) { + public Response sendError(ClientSessionModel clientSession, Error error) { RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); + session.sessions().removeClientSession(realm, clientSession); if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) { - UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO"); - Map params = new HashMap<>(); - params.put("realm", realm.getName()); - params.put("protocol", LOGIN_PROTOCOL); - params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME)); - session.sessions().removeClientSession(realm, clientSession); - URI redirect = builder.buildFromMap(params); - return Response.status(302).location(redirect).build(); + if (error == Error.CANCELLED_BY_USER) { + UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO"); + Map params = new HashMap<>(); + params.put("realm", realm.getName()); + params.put("protocol", LOGIN_PROTOCOL); + params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME)); + URI redirect = builder.buildFromMap(params); + return Response.status(302).location(redirect).build(); + } else { + return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error)); + } } else { - session.sessions().removeClientSession(realm, clientSession); - return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()); + SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get()); + try { + JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); + Document document = builder.buildDocument(); + if (isPostBinding(clientSession)) { + return binding.postBinding(document).response(clientSession.getRedirectUri()); + } else { + return binding.redirectBinding(document).response(clientSession.getRedirectUri()); + } + } catch (Exception e) { + return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); + } + } + } + + private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) { + switch (error) { + case CANCELLED_BY_USER: + case CONSENT_DENIED: + return JBossSAMLURIConstants.STATUS_REQUEST_DENIED; + case PASSIVE_INTERACTION_REQUIRED: + case PASSIVE_LOGIN_REQUIRED: + return JBossSAMLURIConstants.STATUS_NO_PASSIVE; + default: + logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error"); + return JBossSAMLURIConstants.STATUS_REQUEST_DENIED; + } + } + + private String translateErrorToIdpInitiatedErrorMessage(Error error) { + switch (error) { + case CONSENT_DENIED: + return Messages.CONSENT_DENIED; + case PASSIVE_INTERACTION_REQUIRED: + case PASSIVE_LOGIN_REQUIRED: + return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST; + default: + logger.warn("Untranslated protocol Error: " + error.name() + " so we return default error message"); + return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST; } } @@ -169,25 +208,6 @@ public class SamlProtocol implements LoginProtocol { return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(); } - protected Response getErrorResponse(ClientSessionModel clientSession, String status) { - SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder() - .destination(clientSession.getRedirectUri()) - .issuer(getResponseIssuer(realm)) - .status(status); - try { - JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder() - .relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); - Document document = builder.buildDocument(); - if (isPostBinding(clientSession)) { - return binding.postBinding(document).response(clientSession.getRedirectUri()); - } else { - return binding.redirectBinding(document).response(clientSession.getRedirectUri()); - } - } catch (Exception e) { - return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); - } - } - protected boolean isPostBinding(ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client); @@ -198,8 +218,6 @@ public class SamlProtocol implements LoginProtocol { return SamlProtocol.SAML_POST_BINDING.equals(note); } - - protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE); @@ -207,7 +225,8 @@ public class SamlProtocol implements LoginProtocol { if (logoutPostUrl == null) { // if we don't have a redirect uri either, return true and default to the admin url + POST binding - if (logoutRedirectUrl == null) return true; + if (logoutRedirectUrl == null) + return true; return false; } @@ -218,11 +237,13 @@ public class SamlProtocol implements LoginProtocol { String bindingType = clientSession.getNote(SAML_BINDING); // if the login binding was POST, return true - if (SAML_POST_BINDING.equals(bindingType)) return true; + if (SAML_POST_BINDING.equals(bindingType)) + return true; - if (logoutRedirectUrl == null) return true; // we don't have a redirect binding url, so use post binding + if (logoutRedirectUrl == null) + return true; // we don't have a redirect binding url, so use post binding - return false; // redirect binding + return false; // redirect binding } @@ -248,7 +269,8 @@ public class SamlProtocol implements LoginProtocol { nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get(); } } - if(nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT; + if (nameIdFormat == null) + return SAML_DEFAULT_NAMEID_FORMAT; return nameIdFormat; } @@ -259,20 +281,21 @@ public class SamlProtocol implements LoginProtocol { protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) { if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) { return userSession.getUser().getEmail(); - } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) { + } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) { // "G-" stands for "generated" Add this for the slight possibility of collisions. return "G-" + UUID.randomUUID().toString(); - } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) { + } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) { // generate a persistent user id specifically for each client. UserModel user = userSession.getUser(); String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId(); String samlPersistentId = user.getFirstAttribute(name); - if (samlPersistentId != null) return samlPersistentId; + if (samlPersistentId != null) + return samlPersistentId; // "G-" stands for "generated" samlPersistentId = "G-" + UUID.randomUUID().toString(); user.setSingleAttribute(name, samlPersistentId); return samlPersistentId; - } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){ + } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) { // TODO: Support for persistent NameID (pseudo-random identifier persisted in user object) return userSession.getUser().getUsername(); } else { @@ -297,15 +320,8 @@ public class SamlProtocol implements LoginProtocol { clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat); SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder(); - builder.requestID(requestID) - .destination(redirectUri) - .issuer(responseIssuer) - .assertionExpiration(realm.getAccessCodeLifespan()) - .subjectExpiration(realm.getAccessTokenLifespan()) - .sessionIndex(clientSession.getId()) - .requestIssuer(clientSession.getClient().getClientId()) - .nameIdentifier(nameIdFormat, nameId) - .authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get()); + builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()).sessionIndex(clientSession.getId()) + .requestIssuer(clientSession.getClient().getClientId()).nameIdentifier(nameIdFormat, nameId).authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get()); if (!includeAuthnStatement(client)) { builder.disableAuthnStatement(true); } @@ -317,20 +333,20 @@ public class SamlProtocol implements LoginProtocol { Set mappings = accessCode.getRequestedProtocolMappers(); for (ProtocolMapperModel mapping : mappings) { - ProtocolMapper mapper = (ProtocolMapper)session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); - if (mapper == null) continue; + ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); + if (mapper == null) + continue; if (mapper instanceof SAMLAttributeStatementMapper) { - attributeStatementMappers.add(new ProtocolMapperProcessor((SAMLAttributeStatementMapper)mapper, mapping)); + attributeStatementMappers.add(new ProtocolMapperProcessor((SAMLAttributeStatementMapper) mapper, mapping)); } if (mapper instanceof SAMLLoginResponseMapper) { - loginResponseMappers.add(new ProtocolMapperProcessor((SAMLLoginResponseMapper)mapper, mapping)); + loginResponseMappers.add(new ProtocolMapperProcessor((SAMLLoginResponseMapper) mapper, mapping)); } if (mapper instanceof SAMLRoleListMapper) { - roleListMapper = new ProtocolMapperProcessor((SAMLRoleListMapper)mapper, mapping); + roleListMapper = new ProtocolMapperProcessor((SAMLRoleListMapper) mapper, mapping); } } - Document samlDocument = null; try { ResponseType samlModel = builder.buildModel(); @@ -351,18 +367,14 @@ public class SamlProtocol implements LoginProtocol { if (canonicalization != null) { bindingBuilder.canonicalizationMethod(canonicalization); } - bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)) - .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) - .signDocument(); + bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } if (requiresAssertionSignature(client)) { String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE); if (canonicalization != null) { bindingBuilder.canonicalizationMethod(canonicalization); } - bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)) - .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) - .signAssertions(); + bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signAssertions(); } if (requiresEncryption(client)) { PublicKey publicKey = null; @@ -402,7 +414,8 @@ public class SamlProtocol implements LoginProtocol { String alg = client.getAttribute(SAML_SIGNATURE_ALGORITHM); if (alg != null) { SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); - if (algorithm != null) return algorithm; + if (algorithm != null) + return algorithm; } return SignatureAlgorithm.RSA_SHA256; } @@ -421,10 +434,8 @@ public class SamlProtocol implements LoginProtocol { } } - public void transformAttributeStatement(List> attributeStatementMappers, - ResponseType response, - KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + public void transformAttributeStatement(List> attributeStatementMappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, + ClientSessionModel clientSession) { AssertionType assertion = response.getAssertions().get(0).getAssertion(); AttributeStatementType attributeStatement = new AttributeStatementType(); @@ -432,50 +443,32 @@ public class SamlProtocol implements LoginProtocol { processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession); } - //SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute - if(attributeStatement.getAttributes().size() > 0) { + // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute + if (attributeStatement.getAttributes().size() > 0) { assertion.addStatement(attributeStatement); } } - public ResponseType transformLoginResponse(List> mappers, - ResponseType response, - KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + public ResponseType transformLoginResponse(List> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { for (ProtocolMapperProcessor processor : mappers) { response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession); } return response; } - public void populateRoles(ProtocolMapperProcessor roleListMapper, - ResponseType response, - KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { - if (roleListMapper == null) return; + public void populateRoles(ProtocolMapperProcessor roleListMapper, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + if (roleListMapper == null) + return; AssertionType assertion = response.getAssertions().get(0).getAssertion(); AttributeStatementType attributeStatement = new AttributeStatementType(); roleListMapper.mapper.mapRoles(attributeStatement, roleListMapper.model, session, userSession, clientSession); - //SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute - if(attributeStatement.getAttributes().size() > 0) { + // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute + if (attributeStatement.getAttributes().size() > 0) { assertion.addStatement(attributeStatement); } } - - @Override - public Response consentDenied(ClientSessionModel clientSession) { - RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); - if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) { - session.sessions().removeClientSession(realm, clientSession); - return ErrorPage.error(session, Messages.CONSENT_DENIED); - } else { - session.sessions().removeClientSession(realm, clientSession); - return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()); - } - } - public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) { String logoutServiceUrl = null; if (SAML_POST_BINDING.equals(bindingType)) { @@ -483,8 +476,10 @@ public class SamlProtocol implements LoginProtocol { } else { logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE); } - if (logoutServiceUrl == null && client instanceof ClientModel) logoutServiceUrl = ((ClientModel)client).getManagementUrl(); - if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null; + if (logoutServiceUrl == null && client instanceof ClientModel) + logoutServiceUrl = ((ClientModel) client).getManagementUrl(); + if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) + return null; return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl); } @@ -492,7 +487,8 @@ public class SamlProtocol implements LoginProtocol { @Override public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); - if (!(client instanceof ClientModel)) return null; + if (!(client instanceof ClientModel)) + return null; try { if (isLogoutPostBindingForClient(clientSession)) { String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING); @@ -541,9 +537,7 @@ public class SamlProtocol implements LoginProtocol { if (canonicalization != null) { binding.canonicalizationMethod(canonicalization); } - binding.signatureAlgorithm(algorithm) - .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) - .signDocument(); + binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } try { @@ -561,8 +555,6 @@ public class SamlProtocol implements LoginProtocol { } } - - @Override public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); @@ -573,7 +565,6 @@ public class SamlProtocol implements LoginProtocol { } SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client); - String logoutRequestString = null; try { JaxrsSAML2BindingBuilder binding = createBindingBuilder(client); @@ -583,20 +574,21 @@ public class SamlProtocol implements LoginProtocol { return; } - HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient(); for (int i = 0; i < 2; i++) { // follow redirects once try { List formparams = new ArrayList(); formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString)); - formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink todo remove this + formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink + // todo remove + // this UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); HttpPost post = new HttpPost(logoutUrl); post.setEntity(form); HttpResponse response = httpClient.execute(post); try { int status = response.getStatusLine().getStatusCode(); - if (status == 302 && !logoutUrl.endsWith("/")) { + if (status == 302 && !logoutUrl.endsWith("/")) { String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); String withSlash = logoutUrl + "/"; if (withSlash.equals(redirect)) { @@ -608,7 +600,8 @@ public class SamlProtocol implements LoginProtocol { HttpEntity entity = response.getEntity(); if (entity != null) { InputStream is = entity.getContent(); - if (is != null) is.close(); + if (is != null) + is.close(); } } @@ -622,21 +615,15 @@ public class SamlProtocol implements LoginProtocol { protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, ClientSessionModel clientSession, ClientModel client) { // build userPrincipal with subject used at login - SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() - .assertionExpiration(realm.getAccessCodeLifespan()) - .issuer(getResponseIssuer(realm)) - .sessionIndex(clientSession.getId()) - .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)) - .destination(logoutUrl); + SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)).sessionIndex(clientSession.getId()) + .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl); return logoutBuilder; } private JaxrsSAML2BindingBuilder createBindingBuilder(ClientModel client) { JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(); if (requiresRealmSignature(client)) { - binding.signatureAlgorithm(getSignatureAlgorithm(client)) - .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) - .signDocument(); + binding.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } return binding; } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index c370d673ad..ff1275a986 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -1,11 +1,23 @@ package org.keycloak.protocol.saml; +import java.io.InputStream; +import java.net.URI; +import java.security.PublicKey; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + import org.jboss.logging.Logger; -import org.jboss.resteasy.spi.HttpRequest; -import org.jboss.resteasy.spi.HttpResponse; -import org.keycloak.common.ClientConnection; import org.keycloak.common.VerificationException; -import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.common.util.StreamUtil; import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; @@ -16,15 +28,12 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.protocol.RestartLoginCookie; +import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAMLRequestParser; @@ -33,33 +42,10 @@ import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.services.ErrorPage; -import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; -import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.RealmsResource; -import org.keycloak.common.util.StreamUtil; - -import javax.ws.rs.Consumes; -import javax.ws.rs.FormParam; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.core.UriInfo; -import javax.ws.rs.ext.Providers; -import java.io.InputStream; -import java.net.URI; -import java.security.PublicKey; -import java.util.List; /** * Resource class for the oauth/openid connect token service @@ -67,40 +53,12 @@ import java.util.List; * @author Bill Burke * @version $Revision: 1 $ */ -public class SamlService { +public class SamlService extends AuthorizationEndpointBase { protected static final Logger logger = Logger.getLogger(SamlService.class); - protected RealmModel realm; - private EventBuilder event; - protected AuthenticationManager authManager; - - @Context - protected Providers providers; - @Context - protected SecurityContext securityContext; - @Context - protected UriInfo uriInfo; - @Context - protected HttpHeaders headers; - @Context - protected HttpRequest request; - @Context - protected HttpResponse response; - @Context - protected KeycloakSession session; - @Context - protected ClientConnection clientConnection; - - /* - @Context - protected ResourceContext resourceContext; - */ - public SamlService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { - this.realm = realm; - this.event = event; - this.authManager = authManager; + super(realm, event, authManager); } public abstract class BindingProtocol { @@ -243,7 +201,7 @@ public class SamlService { bindingType = SamlProtocol.SAML_POST_BINDING; String redirect = null; URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); - if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes + if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); } else { if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) { @@ -262,7 +220,6 @@ public class SamlService { return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI); } - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirect); @@ -286,13 +243,9 @@ public class SamlService { } } - return newBrowserAuthentication(clientSession); + return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive()); } - - - - private String getBindingType(AuthnRequestType requestAbstractType) { URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); @@ -308,10 +261,8 @@ public class SamlService { } private boolean isSupportedNameIdFormat(String nameIdFormat) { - if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || - nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || - nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()) || - nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) { + if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()) + || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) { return true; } return false; @@ -340,7 +291,8 @@ public class SamlService { userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString()); } - if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState); + if (relayState != null) + userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState); userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID()); userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding); userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE)); @@ -356,7 +308,8 @@ public class SamlService { } else if (logoutRequest.getSessionIndex() != null) { for (String sessionIndex : logoutRequest.getSessionIndex()) { ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex); - if (clientSession == null) continue; + if (clientSession == null) + continue; UserSessionModel userSession = clientSession.getUserSession(); if (clientSession.getClient().getClientId().equals(client.getClientId())) { // remove requesting client from logout @@ -391,13 +344,10 @@ public class SamlService { builder.logoutRequestID(logoutRequest.getID()); builder.destination(logoutBindingUri); builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString()); - JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder() - .relayState(logoutRelayState); + JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState); if (SamlProtocol.requiresRealmSignature(client)) { SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client); - binding.signatureAlgorithm(algorithm) - .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) - .signDocument(); + binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } try { @@ -420,7 +370,6 @@ public class SamlService { } } - protected class PostBindingProtocol extends BindingProtocol { @Override @@ -443,12 +392,14 @@ public class SamlService { return SamlProtocol.SAML_POST_BINDING; } - public Response execute(String samlRequest, String samlResponse, String relayState) { Response response = basicChecks(samlRequest, samlResponse); - if (response != null) return response; - if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); - else return handleSamlResponse(samlResponse, relayState); + if (response != null) + return response; + if (samlRequest != null) + return handleSamlRequest(samlRequest, relayState); + else + return handleSamlResponse(samlResponse, relayState); } } @@ -464,7 +415,6 @@ public class SamlService { SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY); } - @Override protected SAMLDocumentHolder extractRequestDocument(String samlRequest) { return SAMLRequestParser.parseRequestRedirectBinding(samlRequest); @@ -480,74 +430,35 @@ public class SamlService { return SamlProtocol.SAML_REDIRECT_BINDING; } - public Response execute(String samlRequest, String samlResponse, String relayState) { Response response = basicChecks(samlRequest, samlResponse); - if (response != null) return response; - if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); - else return handleSamlResponse(samlResponse, relayState); + if (response != null) + return response; + if (samlRequest != null) + return handleSamlRequest(samlRequest, relayState); + else + return handleSamlResponse(samlResponse, relayState); } } - - private Response buildRedirectToIdentityProvider(String providerId, String accessCode) { - logger.debug("Automatically redirect to identity provider: " + providerId); - return Response.temporaryRedirect( - Urls.identityProviderAuthnRequest(uriInfo.getBaseUri(), providerId, realm.getName(), accessCode)) - .build(); + protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) { + return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive); } - protected Response newBrowserAuthentication(ClientSessionModel clientSession) { - List identityProviders = realm.getIdentityProviders(); - for (IdentityProviderModel identityProvider : identityProviders) { - if (identityProvider.isAuthenticateByDefault()) { - return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() ); - } - } - AuthenticationFlowModel flow = realm.getBrowserFlow(); - String flowId = flow.getId(); - AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clientSession) - .setFlowPath(LoginActionsService.AUTHENTICATE_PATH) - .setFlowId(flowId) - .setBrowserFlow(true) - .setConnection(clientConnection) - .setEventBuilder(event) - .setProtector(authManager.getProtector()) - .setRealm(realm) - .setSession(session) - .setUriInfo(uriInfo) - .setRequest(request); - - try { - RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); - return processor.authenticate(); - } catch (Exception e) { - return processor.handleBrowserException(e); - } - } - - - /** */ @GET - public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, - @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, - @QueryParam(GeneralConstants.RELAY_STATE) String relayState) { + public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) { logger.debug("SAML GET"); return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState); } - /** */ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, - @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, - @FormParam(GeneralConstants.RELAY_STATE) String relayState) { + public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState) { logger.debug("SAML POST"); return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState); } @@ -570,13 +481,13 @@ public class SamlService { @GET @Path("clients/{client}") @Produces(MediaType.TEXT_HTML) - public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, - @QueryParam("RelayState") String relayState) { + public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, @QueryParam("RelayState") String relayState) { event.event(EventType.LOGIN); ClientModel client = null; for (ClientModel c : realm.getClients()) { String urlName = c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME); - if (urlName == null) continue; + if (urlName == null) + continue; if (urlName.equals(clientUrlName)) { client = c; break; @@ -586,18 +497,14 @@ public class SamlService { event.error(Errors.CLIENT_NOT_FOUND); return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND); } - if (client.getManagementUrl() == null - && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null - && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) { + if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) { logger.error("SAML assertion consumer url not set up"); event.error(Errors.INVALID_REDIRECT_URI); return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI); } String bindingType = SamlProtocol.SAML_POST_BINDING; - if (client.getManagementUrl() == null - && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null - && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) { + if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) { bindingType = SamlProtocol.SAML_REDIRECT_BINDING; } @@ -626,8 +533,7 @@ public class SamlService { clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); } - - return newBrowserAuthentication(clientSession); + return newBrowserAuthentication(clientSession, false); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 951bb94bf1..dafaf1a0cf 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -21,6 +21,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.AuthenticationManager; @@ -470,7 +471,7 @@ public class AuthenticationProcessor { protocol.setRealm(getRealm()) .setHttpHeaders(getHttpRequest().getHttpHeaders()) .setUriInfo(getUriInfo()); - Response response = protocol.cancelLogin(getClientSession()); + Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER); forceChallenge(response); } diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java new file mode 100644 index 0000000000..a1fc4a7708 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -0,0 +1,137 @@ +package org.keycloak.protocol; + +import java.util.List; + +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.common.ClientConnection; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.LoginProtocol.Error; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.resources.LoginActionsService; + +/** + * Common base class for Authorization REST endpoints implementation, which have to be implemented by each protocol. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public abstract class AuthorizationEndpointBase { + + private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class); + + protected RealmModel realm; + protected EventBuilder event; + protected AuthenticationManager authManager; + + @Context + protected UriInfo uriInfo; + @Context + protected HttpHeaders headers; + @Context + protected HttpRequest request; + @Context + protected KeycloakSession session; + @Context + protected ClientConnection clientConnection; + + public AuthorizationEndpointBase(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { + this.realm = realm; + this.event = event; + this.authManager = authManager; + } + + protected AuthenticationProcessor createProcessor(ClientSessionModel clientSession, String flowId, String flowPath) { + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowPath(flowPath) + .setFlowId(flowId) + .setBrowserFlow(true) + .setConnection(clientConnection) + .setEventBuilder(event) + .setProtector(authManager.getProtector()) + .setRealm(realm) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); + return processor; + } + + /** + * Common method to handle browser authentication request in protocols unified way. + * + * @param clientSession for current request + * @param protocol handler for protocol used to initiate login + * @param isPassive set to true if login should be passive (without login screen shown) + * @return response to be returned to the browser + */ + protected Response handleBrowserAuthenticationRequest(ClientSessionModel clientSession, LoginProtocol protocol, boolean isPassive) { + + List identityProviders = realm.getIdentityProviders(); + for (IdentityProviderModel identityProvider : identityProviders) { + if (identityProvider.isAuthenticateByDefault()) { + // TODO if we are isPassive we should propagate this flag to default identity provider also if possible + return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode()); + } + } + + AuthenticationFlowModel flow = realm.getBrowserFlow(); + String flowId = flow.getId(); + AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH); + + if (isPassive) { + // OIDC prompt == NONE or SAML 2 IsPassive flag + // This means that client is just checking if the user is already completely logged in. + // We cancel login if any authentication action or required action is required + Response challenge = null; + Response challenge2 = null; + try { + challenge = processor.authenticateOnly(); + if (challenge == null) { + challenge2 = processor.attachSessionExecutionRequiredActions(); + } + } catch (Exception e) { + return processor.handleBrowserException(e); + } + + if (challenge != null || challenge2 != null) { + if (processor.isUserSessionCreated()) { + session.sessions().removeUserSession(realm, processor.getUserSession()); + } + if (challenge != null) + return protocol.sendError(clientSession, Error.PASSIVE_LOGIN_REQUIRED); + else + return protocol.sendError(clientSession, Error.PASSIVE_INTERACTION_REQUIRED); + } else { + return processor.finishAuthentication(); + } + } else { + try { + RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); + return processor.authenticate(); + } catch (Exception e) { + return processor.handleBrowserException(e); + } + } + } + + protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) { + logger.debug("Automatically redirect to identity provider: " + providerId); + return Response.temporaryRedirect( + Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode)) + .build(); + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java index 4a7836df32..5cc450336e 100755 --- a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java @@ -17,6 +17,28 @@ import javax.ws.rs.core.UriInfo; * @version $Revision: 1 $ */ public interface LoginProtocol extends Provider { + + public static enum Error { + + /** + * Login cancelled by the user + */ + CANCELLED_BY_USER, + /** + * Consent denied by the user + */ + CONSENT_DENIED, + /** + * Passive authentication mode requested but nobody is logged in + */ + PASSIVE_LOGIN_REQUIRED, + /** + * Passive authentication mode requested, user is logged in, but some other user interaction is necessary (eg. some required login actions exist or Consent approval is necessary for logged in + * user) + */ + PASSIVE_INTERACTION_REQUIRED; + } + LoginProtocol setSession(KeycloakSession session); LoginProtocol setRealm(RealmModel realm); @@ -27,11 +49,12 @@ public interface LoginProtocol extends Provider { LoginProtocol setEventBuilder(EventBuilder event); - Response cancelLogin(ClientSessionModel clientSession); Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode); - Response consentDenied(ClientSessionModel clientSession); + + Response sendError(ClientSessionModel clientSession, Error error); void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); Response finishLogout(UserSessionModel userSession); + } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 332a547c3b..b9d55db316 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -82,7 +82,7 @@ public class OIDCLoginProtocol implements LoginProtocol { this.event = event; } - public OIDCLoginProtocol(){ + public OIDCLoginProtocol() { } @@ -105,7 +105,7 @@ public class OIDCLoginProtocol implements LoginProtocol { } @Override - public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers){ + public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers) { this.headers = headers; return this; } @@ -116,19 +116,6 @@ public class OIDCLoginProtocol implements LoginProtocol { return this; } - @Override - public Response cancelLogin(ClientSessionModel clientSession) { - String redirect = clientSession.getRedirectUri(); - String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); - UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied"); - if (state != null) { - redirectUri.queryParam(OAuth2Constants.STATE, state); - } - session.sessions().removeClientSession(realm, clientSession); - RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); - return Response.status(302).location(redirectUri.build()).build(); - } - @Override public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { ClientSessionModel clientSession = accessCode.getClientSession(); @@ -144,10 +131,11 @@ public class OIDCLoginProtocol implements LoginProtocol { return location.build(); } - public Response consentDenied(ClientSessionModel clientSession) { + @Override + public Response sendError(ClientSessionModel clientSession, Error error) { String redirect = clientSession.getRedirectUri(); String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); - UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied"); + UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, translateError(error)); if (state != null) redirectUri.queryParam(OAuth2Constants.STATE, state); session.sessions().removeClientSession(realm, clientSession); @@ -156,20 +144,25 @@ public class OIDCLoginProtocol implements LoginProtocol { return location.build(); } - - public Response invalidSessionError(ClientSessionModel clientSession) { - String redirect = clientSession.getRedirectUri(); - String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); - UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied"); - if (state != null) { - redirectUri.queryParam(OAuth2Constants.STATE, state); + private String translateError(Error error) { + switch (error) { + case CANCELLED_BY_USER: + case CONSENT_DENIED: + return "access_denied"; + case PASSIVE_INTERACTION_REQUIRED: + return "interaction_required"; + case PASSIVE_LOGIN_REQUIRED: + return "login_required"; + default: + log.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error"); + return "access_denied"; } - return Response.status(302).location(redirectUri.build()).build(); } @Override public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { - if (!(clientSession.getClient() instanceof ClientModel)) return; + if (!(clientSession.getClient() instanceof ClientModel)) + return; ClientModel app = clientSession.getClient(); new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession); } @@ -190,10 +183,10 @@ public class OIDCLoginProtocol implements LoginProtocol { } event.user(userSession.getUser()).session(userSession).success(); - if (redirectUri != null) { UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri); - if (state != null) uriBuilder.queryParam(STATE_PARAM, state); + if (state != null) + uriBuilder.queryParam(STATE_PARAM, state); return Response.status(302).location(uriBuilder.build()).build(); } else { return Response.ok().build(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index e013857d27..c8d56563d0 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -1,10 +1,19 @@ package org.keycloak.protocol.oidc.endpoints; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.common.ClientConnection; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.common.ClientConnection; import org.keycloak.constants.AdapterConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -18,6 +27,7 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; @@ -28,45 +38,19 @@ import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; -import javax.ws.rs.GET; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import java.util.List; - /** * @author Stian Thorgersen */ -public class AuthorizationEndpoint { +public class AuthorizationEndpoint extends AuthorizationEndpointBase { private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class); + public static final String CODE_AUTH_TYPE = "code"; private enum Action { REGISTER, CODE, FORGOT_CREDENTIALS } - @Context - private KeycloakSession session; - - @Context - private HttpRequest request; - - @Context - private HttpHeaders headers; - - @Context - private UriInfo uriInfo; - - @Context - private ClientConnection clientConnection; - - private final AuthenticationManager authManager; - private final RealmModel realm; - private final EventBuilder event; - private ClientModel client; private ClientSessionModel clientSession; @@ -86,9 +70,7 @@ public class AuthorizationEndpoint { private String legacyResponseType; public AuthorizationEndpoint(AuthenticationManager authManager, RealmModel realm, EventBuilder event) { - this.authManager = authManager; - this.realm = realm; - this.event = event; + super(realm, event, authManager); event.event(EventType.LOGIN); } @@ -249,7 +231,6 @@ public class AuthorizationEndpoint { } private Response buildAuthorizationCodeAuthorizationResponse() { - String accessCode = new ClientSessionCode(realm, clientSession).getCode(); if (idpHint != null && !"".equals(idpHint)) { IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint); @@ -259,65 +240,13 @@ public class AuthorizationEndpoint { .setError(Messages.IDENTITY_PROVIDER_NOT_FOUND, idpHint) .createErrorPage(); } - return buildRedirectToIdentityProvider(idpHint, accessCode); + return buildRedirectToIdentityProvider(idpHint, new ClientSessionCode(realm, clientSession).getCode()); } - return browserAuthentication(accessCode); - } - - protected Response browserAuthentication(String accessCode) { this.event.event(EventType.LOGIN); - List identityProviders = realm.getIdentityProviders(); - for (IdentityProviderModel identityProvider : identityProviders) { - if (identityProvider.isAuthenticateByDefault()) { - return buildRedirectToIdentityProvider(identityProvider.getAlias(), accessCode); - } - } clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); - - AuthenticationFlowModel flow = realm.getBrowserFlow(); - String flowId = flow.getId(); - AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.AUTHENTICATE_PATH); - - if (prompt != null && prompt.equals("none")) { - // OIDC prompt == NONE - // This means that client is just checking if the user is already completely logged in. - // - // here we cancel login if any authentication action or required action is required - Response challenge = null; - try { - challenge = processor.authenticateOnly(); - if (challenge == null) { - challenge = processor.attachSessionExecutionRequiredActions(); - } - } catch (Exception e) { - return processor.handleBrowserException(e); - } - - if (challenge != null) { - if (processor.isUserSessionCreated()) { - session.sessions().removeUserSession(realm, processor.getUserSession()); - } - OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event); - return oauth.cancelLogin(clientSession); - } - - if (challenge == null) { - return processor.finishAuthentication(); - } else { - RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); - return challenge; - } - } else { - try { - RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); - return processor.authenticate(); - } catch (Exception e) { - return processor.handleBrowserException(e); - } - - } + return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), prompt != null && prompt.equals("none")); } private Response buildRegister() { @@ -326,7 +255,7 @@ public class AuthorizationEndpoint { AuthenticationFlowModel flow = realm.getRegistrationFlow(); String flowId = flow.getId(); - AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.REGISTRATION_PATH); + AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.REGISTRATION_PATH); return processor.authenticate(); } @@ -337,32 +266,12 @@ public class AuthorizationEndpoint { AuthenticationFlowModel flow = realm.getResetCredentialsFlow(); String flowId = flow.getId(); - AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.RESET_CREDENTIALS_PATH); + AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH); return processor.authenticate(); } - private AuthenticationProcessor createProcessor(String flowId, String flowPath) { - AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clientSession) - .setFlowPath(flowPath) - .setFlowId(flowId) - .setBrowserFlow(true) - .setConnection(clientConnection) - .setEventBuilder(event) - .setProtector(authManager.getProtector()) - .setRealm(realm) - .setSession(session) - .setUriInfo(uriInfo) - .setRequest(request); - return processor; - } - private Response buildRedirectToIdentityProvider(String providerId, String accessCode) { - logger.debug("Automatically redirect to identity provider: " + providerId); - return Response.temporaryRedirect( - Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode)) - .build(); - } + } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index ea47ddc781..c820e4d091 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -36,6 +36,7 @@ import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; @@ -523,7 +524,7 @@ public class AuthenticationManager { .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) .setUriInfo(context.getUriInfo()); event.error(Errors.REJECTED_BY_USER); - return protocol.consentDenied(context.getClientSession()); + return protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED); } else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId()); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 7f15d2d025..7927991c41 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -57,6 +57,7 @@ import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.RestartLoginCookie; +import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.ErrorPage; import org.keycloak.services.Urls; @@ -591,7 +592,7 @@ public class LoginActionsService { .setHttpHeaders(headers) .setUriInfo(uriInfo); event.error(Errors.REJECTED_BY_USER); - return protocol.consentDenied(clientSession); + return protocol.sendError(clientSession, Error.CONSENT_DENIED); } UserConsentModel grantedConsent = user.getConsentByClient(client.getId()); @@ -828,7 +829,7 @@ public class LoginActionsService { .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) .setUriInfo(context.getUriInfo()); event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER); - return protocol.consentDenied(context.getClientSession()); + return protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED); } throw new RuntimeException("Unreachable"); From 18fa03bf974585ce12f9630d175a30b451faaad3 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Thu, 19 Nov 2015 10:56:32 +0100 Subject: [PATCH 2/3] KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side --- .../en/en-US/modules/adapter-config.xml | 19 +++++- .../en/en-US/modules/tomcat-adapter.xml | 2 +- .../keycloak/adapters/spi/AuthOutcome.java | 5 +- .../adapters/saml/DefaultSamlDeployment.java | 29 ++++---- .../keycloak/adapters/saml/InitiateLogin.java | 2 +- .../adapters/saml/SamlAuthenticator.java | 35 +++++++++- .../adapters/saml/SamlDeployment.java | 1 + .../org/keycloak/adapters/saml/config/SP.java | 9 +++ .../config/parsers/ConfigXmlConstants.java | 7 +- .../config/parsers/DeploymentBuilder.java | 1 + .../saml/config/parsers/SPXmlParser.java | 42 ++++++------ .../schema/keycloak_saml_adapter_1_6.xsd | 1 + .../test/adapters/saml/XmlParserTest.java | 1 + .../core/src/test/resources/keycloak-saml.xml | 3 +- .../src/test/resources/keycloak-saml2.xml | 3 +- .../adapters/saml/servlet/SamlFilter.java | 67 +++++++++++-------- .../saml/undertow/AbstractSamlAuthMech.java | 35 +++++----- .../dom/saml/v2/protocol/StatusCodeType.java | 6 ++ .../dom/saml/v2/protocol/StatusType.java | 5 ++ .../saml/SAML2AuthnRequestBuilder.java | 15 +++-- .../saml/SAML2ErrorResponseBuilder.java | 10 ++- .../keycloak/protocol/saml/SamlProtocol.java | 51 +++++++------- .../keycloaksaml/SamlAdapterTest.java | 6 ++ .../keycloaksaml/SamlAdapterTestStrategy.java | 31 +++++++++ .../testsuite/saml/SamlBindingTest.java | 43 ++---------- .../testsuite/samlfilter/SamlAdapterTest.java | 10 +-- .../WEB-INF/keycloak-saml.xml | 25 +++++++ .../resources/keycloak-saml/testsaml.json | 18 +++++ 28 files changed, 318 insertions(+), 164 deletions(-) create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml index cce3faff4d..5335362ad9 100755 --- a/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml +++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml @@ -11,7 +11,8 @@ sslPolicy="EXTERNAL" nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" logoutPage="/logout.jsp" - forceAuthentication="false"> + forceAuthentication="false" + isPassive="false"> @@ -63,7 +64,8 @@ + forceAuthentication="true" + isPassive="false"> ... ]]> @@ -106,12 +108,23 @@ SAML clients can request that a user is re-authenticated even if - they are already logged in at the IDP. Set this to true if you + they are already logged in at the IDP. Set this to true if you want this. OPTIONAL.. Set to false by default. + + isPassive + + + SAML clients can request that a user is never asked to authenticate even if + they are not logged in at the IDP. Set this to true if you want this. + Do not use together with forceAuthentication as they are opposite. + OPTIONAL.. Set to false by default. + + + diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml index f6db1b038e..37ffbe194a 100755 --- a/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml +++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml @@ -1,5 +1,5 @@ - Tomcat 6, 7 and 8 SAML dapters + Tomcat 6, 7 and 8 SAML adapters To be able to secure WAR apps deployed on Tomcat 6, 7 and 8 you must install the Keycloak Tomcat 6, 7 or 8 SAML adapter into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java index 60a34b2b20..24798845a8 100755 --- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java +++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java @@ -5,8 +5,5 @@ package org.keycloak.adapters.spi; * @version $Revision: 1 $ */ public enum AuthOutcome { - NOT_ATTEMPTED, - FAILED, - AUTHENTICATED, - LOGGED_OUT + NOT_ATTEMPTED, FAILED, AUTHENTICATED, NOT_AUTHENTICATED, LOGGED_OUT } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java index 26fad92c23..7aab09546b 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java @@ -1,13 +1,13 @@ package org.keycloak.adapters.saml; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.saml.SignatureAlgorithm; - import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Set; +import org.keycloak.common.enums.SslRequired; +import org.keycloak.saml.SignatureAlgorithm; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -31,7 +31,7 @@ public class DefaultSamlDeployment implements SamlDeployment { return validateResponseSignature; } - @Override + @Override public Binding getRequestBinding() { return requestBinding; } @@ -97,7 +97,7 @@ public class DefaultSamlDeployment implements SamlDeployment { return signResponse; } - @Override + @Override public Binding getRequestBinding() { return requestBinding; } @@ -150,12 +150,8 @@ public class DefaultSamlDeployment implements SamlDeployment { } } - - public static class DefaultIDP implements IDP { - - private String entityID; private PublicKey signatureValidationKey; private SingleSignOnService singleSignOnService; @@ -204,6 +200,7 @@ public class DefaultSamlDeployment implements SamlDeployment { private String entityID; private String nameIDPolicyFormat; private boolean forceAuthentication; + private boolean isPassive; private PrivateKey decryptionKey; private KeyPair signingKeyPair; private String assertionConsumerServiceUrl; @@ -214,7 +211,6 @@ public class DefaultSamlDeployment implements SamlDeployment { private SignatureAlgorithm signatureAlgorithm; private String signatureCanonicalizationMethod; - @Override public IDP getIDP() { return idp; @@ -244,6 +240,11 @@ public class DefaultSamlDeployment implements SamlDeployment { public boolean isForceAuthentication() { return forceAuthentication; } + + @Override + public boolean isIsPassive() { + return isPassive; + } @Override public PrivateKey getDecryptionKey() { @@ -265,7 +266,7 @@ public class DefaultSamlDeployment implements SamlDeployment { return roleAttributeNames; } - @Override + @Override public PrincipalNamePolicy getPrincipalNamePolicy() { return principalNamePolicy; } @@ -298,6 +299,10 @@ public class DefaultSamlDeployment implements SamlDeployment { public void setForceAuthentication(boolean forceAuthentication) { this.forceAuthentication = forceAuthentication; } + + public void setIsPassive(boolean isPassive){ + this.isPassive = isPassive; + } public void setDecryptionKey(PrivateKey decryptionKey) { this.decryptionKey = decryptionKey; @@ -332,7 +337,7 @@ public class DefaultSamlDeployment implements SamlDeployment { this.logoutPage = logoutPage; } - @Override + @Override public String getSignatureCanonicalizationMethod() { return signatureCanonicalizationMethod; } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java index 158bae89b9..4c7cbab5e0 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java @@ -48,7 +48,7 @@ public class InitiateLogin implements AuthChallenge { SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder() .destination(destinationUrl) .issuer(issuerURL) - .forceAuthn(deployment.isForceAuthentication()) + .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive()) .nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat)); if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) { String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get(); diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java index 1d289d764e..919ec35b1a 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java @@ -17,7 +17,9 @@ import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.RequestAbstractType; import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusCodeType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusType; import org.keycloak.saml.BaseSAML2BindingBuilder; import org.keycloak.saml.SAML2LogoutRequestBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder; @@ -27,6 +29,7 @@ import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.util.Base64; +import org.keycloak.saml.common.util.StringUtil; import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil; @@ -207,7 +210,26 @@ public abstract class SamlAuthenticator { log.error("Request URI does not match SAML request destination"); return AuthOutcome.FAILED; } + if (statusResponse instanceof ResponseType) { + + //validate status + StatusType status = statusResponse.getStatus(); + if(status == null){ + log.error("Missing Status in SAML response"); + return AuthOutcome.FAILED; + } + if(!checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_SUCCESS.get())){ + if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){ + // KEYCLOAK-2107 - handle user not authenticated due passive mode + log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString()); + return AuthOutcome.NOT_AUTHENTICATED; + } + log.error("Error Status found in SAML response: " + status.toString()); + return AuthOutcome.FAILED; + + } + try { if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) { try { @@ -287,7 +309,16 @@ public abstract class SamlAuthenticator { } } + private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){ + if(statusCode != null && statusCode.getValue()!=null){ + String v = statusCode.getValue().toString(); + return expectedValue.equals(v); + } + return false; + } + protected AuthOutcome handleLoginResponse(ResponseType responseType) { + AssertionType assertion = null; try { assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey()); @@ -295,7 +326,7 @@ public abstract class SamlAuthenticator { return initiateLogin(); } } catch (Exception e) { - log.error("Error extracting SAML assertion, e"); + log.error("Error extracting SAML assertion: " + e.getMessage()); challenge = new AuthChallenge() { @Override public boolean challenge(HttpFacade exchange) { @@ -434,9 +465,9 @@ public abstract class SamlAuthenticator { return SAMLRequestParser.parseRequestRedirectBinding(response); } + protected SAMLDocumentHolder extractPostBindingResponse(String response) { byte[] samlBytes = PostBindingUtil.base64Decode(response); - String xml = new String(samlBytes); return SAMLRequestParser.parseResponseDocument(samlBytes); } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java index 072844738c..8c53236603 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java @@ -56,6 +56,7 @@ public interface SamlDeployment { String getEntityID(); String getNameIDPolicyFormat(); boolean isForceAuthentication(); + boolean isIsPassive(); PrivateKey getDecryptionKey(); KeyPair getSigningKeyPair(); String getSignatureCanonicalizationMethod(); diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java index f37f930fea..5203b13c23 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java @@ -33,6 +33,7 @@ public class SP implements Serializable { private String entityID; private String sslPolicy; private boolean forceAuthentication; + private boolean isPassive; private String logoutPage; private List keys; private String nameIDPolicyFormat; @@ -64,6 +65,14 @@ public class SP implements Serializable { this.forceAuthentication = forceAuthentication; } + public boolean isIsPassive() { + return isPassive; + } + + public void setIsPassive(boolean isPassive) { + this.isPassive = isPassive; + } + public List getKeys() { return keys; } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java index 404fc5822a..91a016c65f 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java @@ -5,17 +5,17 @@ package org.keycloak.adapters.saml.config.parsers; * @version $Revision: 1 $ */ public class ConfigXmlConstants { - public static final String KEYCLOAK_SAML_ADAPTER ="keycloak-saml-adapter"; - public static final String SP_ELEMENT="SP"; + public static final String KEYCLOAK_SAML_ADAPTER = "keycloak-saml-adapter"; + public static final String SP_ELEMENT = "SP"; public static final String ENTITY_ID_ATTR = "entityID"; public static final String SSL_POLICY_ATTR = "sslPolicy"; public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat"; public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication"; + public static final String IS_PASSIVE_ATTR = "isPassive"; public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm"; public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod"; public static final String LOGOUT_PAGE_ATTR = "logoutPage"; - public static final String KEYS_ELEMENT = "Keys"; public static final String KEY_ELEMENT = "Key"; public static final String SIGNING_ATTR = "signing"; @@ -36,7 +36,6 @@ public class ConfigXmlConstants { public static final String POLICY_ATTR = "policy"; public static final String ATTRIBUTE_ATTR = "attribute"; - public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers"; public static final String ATTRIBUTE_ELEMENT = "Attribute"; public static final String NAME_ATTR = "name"; diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java index b200b61cb2..5de2072350 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java @@ -41,6 +41,7 @@ public class DeploymentBuilder { deployment.setConfigured(true); deployment.setEntityID(sp.getEntityID()); deployment.setForceAuthentication(sp.isForceAuthentication()); + deployment.setIsPassive(sp.isIsPassive()); deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat()); deployment.setLogoutPage(sp.getLogoutPage()); deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod()); diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java index 3446f20759..14b04e1110 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java @@ -1,21 +1,22 @@ package org.keycloak.adapters.saml.config.parsers; -import org.keycloak.adapters.saml.config.IDP; -import org.keycloak.adapters.saml.config.Key; -import org.keycloak.adapters.saml.config.SP; -import org.keycloak.saml.common.exceptions.ParsingException; -import org.keycloak.saml.common.parsers.AbstractParser; -import org.keycloak.saml.common.util.StaxParserUtil; -import org.keycloak.common.util.StringPropertyReplacer; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.events.EndElement; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; -import java.util.HashSet; -import java.util.List; -import java.util.Set; + +import org.keycloak.adapters.saml.config.IDP; +import org.keycloak.adapters.saml.config.Key; +import org.keycloak.adapters.saml.config.SP; +import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.parsers.AbstractParser; +import org.keycloak.saml.common.util.StaxParserUtil; /** * @author Bill Burke @@ -25,13 +26,16 @@ public class SPXmlParser extends AbstractParser { public static String getAttributeValue(StartElement startElement, String tag) { String str = StaxParserUtil.getAttributeValue(startElement, tag); - if (str != null) return StringPropertyReplacer.replaceProperties(str); - else return str; + if (str != null) + return StringPropertyReplacer.replaceProperties(str); + else + return str; } public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) { String result = getAttributeValue(startElement, tag); - if (result == null) return defaultValue; + if (result == null) + return defaultValue; return Boolean.valueOf(result); } @@ -41,11 +45,11 @@ public class SPXmlParser extends AbstractParser { public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException { String result = StaxParserUtil.getElementText(xmlEventReader); - if (result != null) result = StringPropertyReplacer.replaceProperties(result); + if (result != null) + result = StringPropertyReplacer.replaceProperties(result); return result; } - @Override public Object parse(XMLEventReader xmlEventReader) throws ParsingException { StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader); @@ -61,6 +65,7 @@ public class SPXmlParser extends AbstractParser { sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR)); sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR)); sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR)); + sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR)); while (xmlEventReader.hasNext()) { XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader); if (xmlEvent == null) @@ -79,7 +84,7 @@ public class SPXmlParser extends AbstractParser { String tag = StaxParserUtil.getStartElementName(startElement); if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) { KeysXmlParser parser = new KeysXmlParser(); - List keys = (List)parser.parse(xmlEventReader); + List keys = (List) parser.parse(xmlEventReader); sp.setKeys(keys); } else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) { StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader); @@ -98,7 +103,7 @@ public class SPXmlParser extends AbstractParser { parseRoleMapping(xmlEventReader, sp); } else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) { IDPXmlParser parser = new IDPXmlParser(); - IDP idp = (IDP)parser.parse(xmlEventReader); + IDP idp = (IDP) parser.parse(xmlEventReader); sp.setIdp(idp); } else { StaxParserUtil.bypassElementBlock(xmlEventReader, tag); @@ -108,7 +113,7 @@ public class SPXmlParser extends AbstractParser { return sp; } - protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException { + protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException { StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader); StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT); Set roleAttributes = new HashSet<>(); @@ -144,7 +149,6 @@ public class SPXmlParser extends AbstractParser { sp.setRoleAttributes(roleAttributes); } - @Override public boolean supports(QName qname) { return false; diff --git a/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd b/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd index 534c9aef56..d3e55f9dc1 100755 --- a/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd +++ b/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd @@ -33,6 +33,7 @@ + diff --git a/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java b/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java index c92fec666c..5a6e037937 100755 --- a/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java +++ b/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java @@ -68,6 +68,7 @@ public class XmlParserTest { Assert.assertEquals("ssl", sp.getSslPolicy()); Assert.assertEquals("format", sp.getNameIDPolicyFormat()); Assert.assertTrue(sp.isForceAuthentication()); + Assert.assertTrue(sp.isIsPassive()); Assert.assertEquals(2, sp.getKeys().size()); Key signing = sp.getKeys().get(0); Assert.assertTrue(signing.isSigning()); diff --git a/saml/client-adapter/core/src/test/resources/keycloak-saml.xml b/saml/client-adapter/core/src/test/resources/keycloak-saml.xml index ef910dceca..ae8c96d1ea 100755 --- a/saml/client-adapter/core/src/test/resources/keycloak-saml.xml +++ b/saml/client-adapter/core/src/test/resources/keycloak-saml.xml @@ -2,7 +2,8 @@ + forceAuthentication="true" + isPassive="true"> diff --git a/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml b/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml index ee6388d885..7662e61aec 100755 --- a/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml +++ b/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml @@ -4,7 +4,8 @@ nameIDPolicyFormat="format" signatureAlgorithm="" signatureCanonicalizationMethod="" - forceAuthentication="true"> + forceAuthentication="true" + isPassive="true"> diff --git a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java index b2a0deda68..ac95784c6b 100755 --- a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java +++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java @@ -1,18 +1,11 @@ package org.keycloak.adapters.saml.servlet; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.InMemorySessionIdMapper; -import org.keycloak.adapters.spi.SessionIdMapper; -import org.keycloak.adapters.saml.DefaultSamlDeployment; -import org.keycloak.adapters.saml.SamlAuthenticator; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.SamlSession; -import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; -import org.keycloak.adapters.saml.config.parsers.ResourceLoader; -import org.keycloak.adapters.servlet.ServletHttpFacade; -import org.keycloak.saml.common.exceptions.ParsingException; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -24,12 +17,20 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.logging.Level; -import java.util.logging.Logger; + +import org.keycloak.adapters.saml.DefaultSamlDeployment; +import org.keycloak.adapters.saml.SamlAuthenticator; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlDeploymentContext; +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; +import org.keycloak.adapters.servlet.ServletHttpFacade; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.InMemorySessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.saml.common.exceptions.ParsingException; /** * @author Bill Burke @@ -38,7 +39,7 @@ import java.util.logging.Logger; public class SamlFilter implements Filter { protected SamlDeploymentContext deploymentContext; protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); - private final static Logger log = Logger.getLogger(""+SamlFilter.class); + private final static Logger log = Logger.getLogger("" + SamlFilter.class); @Override public void init(final FilterConfig filterConfig) throws ServletException { @@ -46,12 +47,14 @@ public class SamlFilter implements Filter { if (configResolverClass != null) { try { throw new RuntimeException("Not implemented yet"); - //KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); - //deploymentContext = new SamlDeploymentContext(configResolver); - //log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); + // KeycloakConfigResolver configResolver = (KeycloakConfigResolver) + // context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance(); + // deploymentContext = new SamlDeploymentContext(configResolver); + // log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", + // configResolverClass); } catch (Exception ex) { - log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); - //deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); + log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() }); + // deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); } } else { String fp = filterConfig.getInitParameter("keycloak.config.file"); @@ -65,7 +68,8 @@ public class SamlFilter implements Filter { } else { String path = "/WEB-INF/keycloak-saml.xml"; String pathParam = filterConfig.getInitParameter("keycloak.config.path"); - if (pathParam != null) path = pathParam; + if (pathParam != null) + path = pathParam; is = filterConfig.getServletContext().getResourceAsStream(path); } final SamlDeployment deployment; @@ -105,7 +109,6 @@ public class SamlFilter implements Filter { } FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper); - SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) { @Override protected void completeAuthentication(SamlSession account) { @@ -139,6 +142,16 @@ public class SamlFilter implements Filter { challenge.challenge(facade); return; } + + if (deployment.isIsPassive() && outcome == AuthOutcome.NOT_AUTHENTICATED) { + log.fine("PASSIVE_NOT_AUTHENTICATED"); + if (facade.isEnded()) { + return; + } + chain.doFilter(req, res); + return; + } + if (!facade.isEnded()) { response.sendError(403); } diff --git a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java index 8f1929af58..3d632dd9ac 100755 --- a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java +++ b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java @@ -16,6 +16,15 @@ */ package org.keycloak.adapters.saml.undertow; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlDeploymentContext; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.undertow.UndertowHttpFacade; +import org.keycloak.adapters.undertow.UndertowUserSessionManagement; + import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.NotificationReceiver; import io.undertow.security.api.SecurityContext; @@ -24,14 +33,6 @@ import io.undertow.server.HttpServerExchange; import io.undertow.util.AttachmentKey; import io.undertow.util.Headers; import io.undertow.util.StatusCodes; -import org.keycloak.adapters.spi.AuthChallenge; -import org.keycloak.adapters.spi.AuthOutcome; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.saml.SamlDeployment; -import org.keycloak.adapters.saml.SamlDeploymentContext; -import org.keycloak.adapters.saml.SamlSessionStore; -import org.keycloak.adapters.undertow.UndertowHttpFacade; -import org.keycloak.adapters.undertow.UndertowUserSessionManagement; /** * Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism. @@ -44,8 +45,7 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism { protected UndertowUserSessionManagement sessionManagement; protected String errorPage; - public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, - String errorPage) { + public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) { this.deploymentContext = deploymentContext; this.sessionManagement = sessionManagement; this.errorPage = errorPage; @@ -69,19 +69,19 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism { } static void sendRedirect(final HttpServerExchange exchange, final String location) { - // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this. + // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better + // handle this. String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; exchange.getResponseHeaders().put(Headers.LOCATION, loc); } - - protected void registerNotifications(final SecurityContext securityContext) { final NotificationReceiver logoutReceiver = new NotificationReceiver() { @Override public void handleNotification(SecurityNotification notification) { - if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return; + if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) + return; HttpServerExchange exchange = notification.getExchange(); UndertowHttpFacade facade = createFacade(exchange); @@ -104,13 +104,16 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism { return AuthenticationMechanismOutcome.NOT_ATTEMPTED; } SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext); - UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, - deploymentContext.resolveDeployment(facade), sessionStore); + UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, deploymentContext.resolveDeployment(facade), sessionStore); AuthOutcome outcome = authenticator.authenticate(); if (outcome == AuthOutcome.AUTHENTICATED) { registerNotifications(securityContext); return AuthenticationMechanismOutcome.AUTHENTICATED; } + if (outcome == AuthOutcome.NOT_AUTHENTICATED) { + // we are in passive mode and user is not authenticated, let app server to try another auth mechanism + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } if (outcome == AuthOutcome.LOGGED_OUT) { securityContext.logout(); if (deployment.getLogoutPage() != null) { diff --git a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java index 482834b873..826928ae78 100755 --- a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java +++ b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java @@ -80,4 +80,10 @@ public class StatusCodeType implements Serializable { public void setValue(URI value) { this.value = value; } + + @Override + public String toString() { + return "StatusCodeType [value=" + value + ", statusCode=" + statusCode + "]"; + } + } \ No newline at end of file diff --git a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java index 2e2eab97ff..ec92dd4522 100755 --- a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java +++ b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java @@ -100,4 +100,9 @@ public class StatusType implements Serializable { this.statusDetail = value; } + @Override + public String toString() { + return "StatusType [statusCode=" + statusCode + ", statusMessage=" + statusMessage + ", statusDetail=" + statusDetail + "]"; + } + } diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java index 40db78c5f3..b043a120c1 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java @@ -17,16 +17,16 @@ */ package org.keycloak.saml; +import java.net.URI; + +import org.keycloak.dom.saml.v2.assertion.NameIDType; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; -import org.keycloak.dom.saml.v2.assertion.NameIDType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.w3c.dom.Document; -import java.net.URI; - /** * @author pedroigor */ @@ -64,6 +64,11 @@ public class SAML2AuthnRequestBuilder { return this; } + public SAML2AuthnRequestBuilder isPassive(boolean isPassive) { + this.authnRequestType.setIsPassive(isPassive); + return this; + } + public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) { this.authnRequestType.setNameIDPolicy(nameIDPolicy.build()); return this; @@ -74,7 +79,7 @@ public class SAML2AuthnRequestBuilder { return this; } - public Document toDocument() { + public Document toDocument() { try { AuthnRequestType authnRequestType = this.authnRequestType; diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java index 237365673b..a3d080052a 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java @@ -1,21 +1,28 @@ package org.keycloak.saml; +<<<<<<< Upstream, based on keycloak/master import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.protocol.StatusCodeType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusType; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +======= +import org.keycloak.dom.saml.v2.protocol.ResponseType; +>>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory; +<<<<<<< Upstream, based on keycloak/master import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; +======= +>>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side import org.w3c.dom.Document; import java.net.URI; @@ -45,7 +52,6 @@ public class SAML2ErrorResponseBuilder { return this; } - public Document buildDocument() throws ProcessingException { try { @@ -65,8 +71,6 @@ public class SAML2ErrorResponseBuilder { } catch (ParsingException e) { throw new ProcessingException(e); } - } - } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 6729c9097b..3a30c2ee6f 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -147,33 +147,36 @@ public class SamlProtocol implements LoginProtocol { @Override public Response sendError(ClientSessionModel clientSession, Error error) { - RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); - session.sessions().removeClientSession(realm, clientSession); - if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) { - if (error == Error.CANCELLED_BY_USER) { - UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO"); - Map params = new HashMap<>(); - params.put("realm", realm.getName()); - params.put("protocol", LOGIN_PROTOCOL); - params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME)); - URI redirect = builder.buildFromMap(params); - return Response.status(302).location(redirect).build(); - } else { - return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error)); - } - } else { - SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get()); - try { - JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); - Document document = builder.buildDocument(); - if (isPostBinding(clientSession)) { - return binding.postBinding(document).response(clientSession.getRedirectUri()); + try { + if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) { + if (error == Error.CANCELLED_BY_USER) { + UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO"); + Map params = new HashMap<>(); + params.put("realm", realm.getName()); + params.put("protocol", LOGIN_PROTOCOL); + params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME)); + URI redirect = builder.buildFromMap(params); + return Response.status(302).location(redirect).build(); } else { - return binding.redirectBinding(document).response(clientSession.getRedirectUri()); + return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error)); + } + } else { + SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get()); + try { + JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); + Document document = builder.buildDocument(); + if (isPostBinding(clientSession)) { + return binding.postBinding(document).response(clientSession.getRedirectUri()); + } else { + return binding.redirectBinding(document).response(clientSession.getRedirectUri()); + } + } catch (Exception e) { + return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); } - } catch (Exception e) { - return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); } + } finally { + RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); + session.sessions().removeClientSession(realm, clientSession); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java index 0d20f7a7d3..9f23b7043a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java @@ -19,6 +19,7 @@ public class SamlAdapterTest { ClassLoader classLoader = SamlAdapterTest.class.getClassLoader(); initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader); + initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader); @@ -96,6 +97,11 @@ public class SamlAdapterTest { testStrategy.testPostSimpleLoginLogout(); } + @Test + public void testPostPassiveLoginLogout() { + testStrategy.testPostPassiveLoginLogout(true); + } + @Test public void testPostSignedLoginLogoutTransientNameID() { testStrategy.testPostSignedLoginLogoutTransientNameID(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java index b63c9609cb..383c3fc7a4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java @@ -139,6 +139,37 @@ public class SamlAdapterTestStrategy extends ExternalResource { checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post/"); } + public void testPostPassiveLoginLogout(boolean forbiddenIfNotauthenticated) { + // first request on passive app - no login page shown, user not logged in as we are in passive mode + driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/"); + assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl()); + System.out.println(driver.getPageSource()); + if (forbiddenIfNotauthenticated) { + Assert.assertTrue(driver.getPageSource().contains("Forbidden")); + } else { + Assert.assertTrue(driver.getPageSource().contains("principal=null")); + } + + // login user by asking login from other app + driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/"); + loginPage.login("bburke", "password"); + + // navigate to the passive app again, we have to be logged in now + driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/"); + assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl()); + System.out.println(driver.getPageSource()); + Assert.assertTrue(driver.getPageSource().contains("bburke")); + + // logout from both app + driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive?GLO=true"); + driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true"); + + // refresh passive app page, not logged in again as we are in passive mode + driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/"); + assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl()); + Assert.assertFalse(driver.getPageSource().contains("bburke")); + } + public void testPostSimpleUnauthorized(CheckAuthError error) { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/"); assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java index 019bfea94e..7878843e2c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java @@ -1,35 +1,24 @@ package org.keycloak.testsuite.saml; import org.apache.commons.io.IOUtils; -import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.keycloak.Config; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper; import org.keycloak.protocol.saml.mappers.HardcodedRole; import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.protocol.saml.mappers.RoleNameMapper; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; @@ -47,19 +36,10 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import static org.junit.Assert.assertEquals; @@ -166,6 +146,7 @@ public class SamlBindingTest { driver.navigate().to("http://localhost:8081/sales-post?GLO=true"); checkLoggedOut("http://localhost:8081/sales-post/"); } + @Test public void testPostSimpleLoginLogoutIdpInitiated() { driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post"); @@ -188,6 +169,7 @@ public class SamlBindingTest { checkLoggedOut("http://localhost:8081/sales-post-sig/"); } + @Test public void testPostSignedLoginLogoutTransientNameID() { driver.navigate().to("http://localhost:8081/sales-post-sig-transient/"); @@ -452,23 +434,10 @@ public class SamlBindingTest { Assert.assertTrue(driver.getPageSource().contains("null")); } - private static String createToken() { - KeycloakSession session = keycloakRule.startSession(); - try { - RealmManager manager = new RealmManager(session); - - RealmModel adminRealm = manager.getRealm(Config.getAdminRealm()); - ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); - TokenManager tm = new TokenManager(); - UserModel admin = session.users().getUserByUsername("admin", adminRealm); - ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole); - clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master"); - UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null); - AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession); - return tm.encodeToken(adminRealm, token); - } finally { - keycloakRule.stopSession(session, true); - } + @Test + public void testPassiveMode() { + // KEYCLOAK-2075 test SAML IsPassive handling - PicketLink SP client library doesn't support this option unfortunately. + // But the test of server side is included in test of SAML Keycloak adapter } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java index 9a47b4414e..d0c5d2150b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java @@ -5,7 +5,6 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy; -import org.keycloak.testsuite.keycloaksaml.SamlSPFacade; import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet; import org.openqa.selenium.WebDriver; @@ -25,6 +24,7 @@ public class SamlAdapterTest { ClassLoader classLoader = SamlAdapterTest.class.getClassLoader(); initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader); + initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader); @@ -37,9 +37,6 @@ public class SamlAdapterTest { initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader); SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth"); - - - } @Override @@ -105,6 +102,11 @@ public class SamlAdapterTest { testStrategy.testPostSimpleLoginLogout(); } + @Test + public void testPostPassiveLoginLogout() { + testStrategy.testPostPassiveLoginLogout(false); + } + @Test public void testPostSignedLoginLogoutTransientNameID() { testStrategy.testPostSignedLoginLogoutTransientNameID(); diff --git a/testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml b/testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..315462788c --- /dev/null +++ b/testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json b/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json index 95b9fb96a4..7a50a91c8a 100755 --- a/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json @@ -85,6 +85,24 @@ "saml_idp_initiated_sso_url_name": "sales-post" } }, + { + "name": "http://localhost:8081/sales-post-passive/", + "enabled": true, + "fullScopeAllowed": true, + "protocol": "saml", + "baseUrl": "http://localhost:8081/sales-post-passive", + "redirectUris": [ + "http://localhost:8081/sales-post-passive/*" + ], + "attributes": { + "saml.authnstatement": "true", + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-passive/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-passive/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-passive/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-passive/", + "saml_idp_initiated_sso_url_name": "sales-post-passive" + } + }, { "name": "http://localhost:8081/sales-post-sig/", "enabled": true, From e3060e5e58dc4f469a564d2703fd8b45be68bcca Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Wed, 25 Nov 2015 13:46:29 +0100 Subject: [PATCH 3/3] rebased to latest master --- .../adapters/OIDCAuthenticationError.java | 7 +++++ .../saml/SamlAuthenticationError.java | 6 ++++ .../adapters/saml/SamlAuthenticator.java | 28 ++++++------------- .../saml/SAML2ErrorResponseBuilder.java | 10 ++----- .../keycloaksaml/SamlAdapterTestStrategy.java | 5 ++-- .../keycloak/testsuite/rule/ErrorServlet.java | 10 +++++-- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java index 5b3f45d67e..089b689993 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java @@ -36,4 +36,11 @@ public class OIDCAuthenticationError implements AuthenticationError { public String getDescription() { return description; } + + @Override + public String toString() { + return "OIDCAuthenticationError [reason=" + reason + ", description=" + description + "]"; + } + + } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java index 8b631031cb..c85fd63ef3 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java @@ -40,4 +40,10 @@ public class SamlAuthenticationError implements AuthenticationError { public StatusResponseType getStatus() { return status; } + + @Override + public String toString() { + return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]"; + } + } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java index 919ec35b1a..13f52de169 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java @@ -211,25 +211,7 @@ public abstract class SamlAuthenticator { return AuthOutcome.FAILED; } - if (statusResponse instanceof ResponseType) { - - //validate status - StatusType status = statusResponse.getStatus(); - if(status == null){ - log.error("Missing Status in SAML response"); - return AuthOutcome.FAILED; - } - if(!checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_SUCCESS.get())){ - if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){ - // KEYCLOAK-2107 - handle user not authenticated due passive mode - log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString()); - return AuthOutcome.NOT_AUTHENTICATED; - } - log.error("Error Status found in SAML response: " + status.toString()); - return AuthOutcome.FAILED; - - } - + if (statusResponse instanceof ResponseType) { try { if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) { try { @@ -276,7 +258,15 @@ public abstract class SamlAuthenticator { } } else if (sessionStore.isLoggingIn()) { + try { + // KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly. + StatusType status = statusResponse.getStatus(); + if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){ + log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString()); + return AuthOutcome.NOT_AUTHENTICATED; + } + challenge = new AuthChallenge() { @Override public boolean challenge(HttpFacade exchange) { diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java index a3d080052a..237365673b 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java @@ -1,28 +1,21 @@ package org.keycloak.saml; -<<<<<<< Upstream, based on keycloak/master import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.protocol.StatusCodeType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusType; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -======= -import org.keycloak.dom.saml.v2.protocol.ResponseType; ->>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory; -<<<<<<< Upstream, based on keycloak/master import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder; import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; -======= ->>>>>>> 9408d08 KEYCLOAK-2107 - support IsPassive mode in SAML SP adapter library KEYCLOAK-2075 - added integration tests for both server and adapter side import org.w3c.dom.Document; import java.net.URI; @@ -52,6 +45,7 @@ public class SAML2ErrorResponseBuilder { return this; } + public Document buildDocument() throws ProcessingException { try { @@ -71,6 +65,8 @@ public class SAML2ErrorResponseBuilder { } catch (ParsingException e) { throw new ProcessingException(e); } + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java index 383c3fc7a4..f2d8b68045 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java @@ -140,12 +140,13 @@ public class SamlAdapterTestStrategy extends ExternalResource { } public void testPostPassiveLoginLogout(boolean forbiddenIfNotauthenticated) { - // first request on passive app - no login page shown, user not logged in as we are in passive mode + // first request on passive app - no login page shown, user not logged in as we are in passive mode. + // Shown page depends on used authentication mechanism, some may return forbidden error, some return requested page with anonymous user (not logged in) driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/"); assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl()); System.out.println(driver.getPageSource()); if (forbiddenIfNotauthenticated) { - Assert.assertTrue(driver.getPageSource().contains("Forbidden")); + Assert.assertTrue(driver.getPageSource().contains("HTTP status code: 403")); } else { Assert.assertTrue(driver.getPageSource().contains("principal=null")); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java index 68410d48bf..0cffa3c249 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java @@ -6,7 +6,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; @@ -20,10 +19,17 @@ public class ErrorServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { authError = (AuthenticationError)req.getAttribute(AuthenticationError.class.getName()); + Integer statusCode = (Integer) req.getAttribute("javax.servlet.error.status_code"); + resp.setContentType("text/html"); PrintWriter pw = resp.getWriter(); pw.printf("%s", "Error Page"); - pw.print("

There was an error

"); + pw.print("

There was an error

"); + if (statusCode != null) + pw.print("
HTTP status code: " + statusCode); + if (authError != null) + pw.print("
Error info: " + authError.toString()); + pw.print(""); pw.flush();