package org.apereo.cas.support.saml.web.idp.profile;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.apache.commons.lang3.tuple.Pair;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.AuthenticationException;
import org.apereo.cas.authentication.AuthenticationResult;
import org.apereo.cas.authentication.AuthenticationSystemSupport;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.UsernamePasswordCredential;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.authentication.principal.ServiceFactory;
import org.apereo.cas.authentication.principal.WebApplicationService;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.support.saml.OpenSamlConfigBean;
import org.apereo.cas.support.saml.SamlIdPConstants;
import org.apereo.cas.support.saml.SamlIdPUtils;
import org.apereo.cas.support.saml.SamlUtils;
import org.apereo.cas.support.saml.services.SamlRegisteredService;
import org.apereo.cas.support.saml.services.idp.metadata.SamlRegisteredServiceServiceProviderMetadataFacade;
import org.apereo.cas.support.saml.services.idp.metadata.cache.SamlRegisteredServiceCachingMetadataResolver;
import org.apereo.cas.support.saml.web.idp.profile.builders.SamlProfileObjectBuilder;
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.BaseSamlObjectSigner;
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.SamlObjectSignatureValidator;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.cas.web.support.WebUtils;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.AssertionImpl;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.common.binding.BindingDescriptor;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.binding.decoding.impl.HTTPSOAP11Decoder;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.soap.messaging.context.SOAP11Context;
import org.opensaml.soap.soap11.Envelope;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.UsernamePasswordCredentials;
import org.pac4j.core.credentials.extractor.BasicAuthExtractor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* This is {@link ECPProfileHandlerController}.
*
* @author Misagh Moayyed
* @since 5.1.0
*/
public class ECPProfileHandlerController extends AbstractSamlProfileHandlerController {
private static final Logger LOGGER = LoggerFactory.getLogger(ECPProfileHandlerController.class);
private final SamlProfileObjectBuilder<? extends SAMLObject> samlEcpFaultResponseBuilder;
/**
* Instantiates a new ecp saml profile handler controller.
*
* @param samlObjectSigner the saml object signer
* @param parserPool the parser pool
* @param authenticationSystemSupport the authentication system support
* @param servicesManager the services manager
* @param webApplicationServiceFactory the web application service factory
* @param samlRegisteredServiceCachingMetadataResolver the saml registered service caching metadata resolver
* @param configBean the config bean
* @param responseBuilder the response builder
* @param samlEcpFaultResponseBuilder the saml ecp fault response builder
* @param authenticationContextClassMappings the authentication context class mappings
* @param serverPrefix the server prefix
* @param serverName the server name
* @param authenticationContextRequestParameter the authentication context request parameter
* @param loginUrl the login url
* @param logoutUrl the logout url
* @param forceSignedLogoutRequests the force signed logout requests
* @param singleLogoutCallbacksDisabled the single logout callbacks disabled
* @param samlObjectSignatureValidator the saml object signature validator
*/
public ECPProfileHandlerController(final BaseSamlObjectSigner samlObjectSigner,
final ParserPool parserPool,
final AuthenticationSystemSupport authenticationSystemSupport,
final ServicesManager servicesManager,
final ServiceFactory<WebApplicationService> webApplicationServiceFactory,
final SamlRegisteredServiceCachingMetadataResolver samlRegisteredServiceCachingMetadataResolver,
final OpenSamlConfigBean configBean,
final SamlProfileObjectBuilder<org.opensaml.saml.saml2.ecp.Response> responseBuilder,
final SamlProfileObjectBuilder<? extends SAMLObject> samlEcpFaultResponseBuilder,
final Set<String> authenticationContextClassMappings,
final String serverPrefix,
final String serverName,
final String authenticationContextRequestParameter,
final String loginUrl,
final String logoutUrl,
final boolean forceSignedLogoutRequests,
final boolean singleLogoutCallbacksDisabled,
final SamlObjectSignatureValidator samlObjectSignatureValidator) {
super(samlObjectSigner, parserPool, authenticationSystemSupport,
servicesManager, webApplicationServiceFactory,
samlRegisteredServiceCachingMetadataResolver,
configBean, responseBuilder,
authenticationContextClassMappings,
serverPrefix, serverName,
authenticationContextRequestParameter, loginUrl, logoutUrl,
forceSignedLogoutRequests, singleLogoutCallbacksDisabled,
samlObjectSignatureValidator);
this.samlEcpFaultResponseBuilder = samlEcpFaultResponseBuilder;
}
/**
* Handle ecp request.
*
* @param response the response
* @param request the request
* @throws Exception the exception
*/
@PostMapping(path = SamlIdPConstants.ENDPOINT_SAML2_IDP_ECP_PROFILE_SSO,
consumes = {MediaType.TEXT_XML_VALUE, SamlIdPConstants.ECP_SOAP_PAOS_CONTENT_TYPE},
produces = {MediaType.TEXT_XML_VALUE, SamlIdPConstants.ECP_SOAP_PAOS_CONTENT_TYPE})
public void handleEcpRequest(final HttpServletResponse response,
final HttpServletRequest request) throws Exception {
final MessageContext soapContext = decodeSoapRequest(request);
final Credential credential = extractBasicAuthenticationCredential(request, response);
if (credential == null) {
LOGGER.error("Credentials could not be extracted from the SAML ECP request");
return;
}
if (soapContext == null) {
LOGGER.error("SAML ECP request could not be determined from the authentication request");
return;
}
handleEcpRequest(response, request, soapContext, credential, SAMLConstants.SAML2_PAOS_BINDING_URI);
}
/**
* Handle ecp request.
*
* @param response the response
* @param request the request
* @param soapContext the soap context
* @param credential the credential
* @param binding the binding
*/
protected void handleEcpRequest(final HttpServletResponse response, final HttpServletRequest request,
final MessageContext soapContext, final Credential credential,
final String binding) {
LOGGER.debug("Handling ECP request for SOAP context [{}]", soapContext);
final Envelope envelope = soapContext.getSubcontext(SOAP11Context.class).getEnvelope();
SamlUtils.logSamlObject(configBean, envelope);
final AuthnRequest authnRequest = (AuthnRequest) soapContext.getMessage();
final Pair<AuthnRequest, MessageContext> authenticationContext = Pair.of(authnRequest, soapContext);
try {
LOGGER.debug("Verifying ECP authentication request [{}]", authnRequest);
final Pair<SamlRegisteredService, SamlRegisteredServiceServiceProviderMetadataFacade> serviceRequest =
verifySamlAuthenticationRequest(authenticationContext, request);
LOGGER.debug("Attempting to authenticate ECP request for credential id [{}]", credential.getId());
final Authentication authentication = authenticateEcpRequest(credential, authenticationContext);
LOGGER.debug("Authenticated [{}] successfully with authenticated principal [{}]",
credential.getId(), authentication.getPrincipal());
LOGGER.debug("Building ECP SAML response for [{}]", credential.getId());
final String issuer = SamlIdPUtils.getIssuerFromSamlRequest(authnRequest);
final Service service = webApplicationServiceFactory.createService(issuer);
final Assertion casAssertion = buildEcpCasAssertion(authentication, service, serviceRequest.getKey());
LOGGER.debug("CAS assertion to use for building ECP SAML response is [{}]", casAssertion);
buildSamlResponse(response, request, authenticationContext, casAssertion, binding);
} catch (final AuthenticationException e) {
LOGGER.error(e.getMessage(), e);
final String error = e.getHandlerErrors().values()
.stream()
.map(Class::getSimpleName)
.collect(Collectors.joining(","));
buildEcpFaultResponse(response, request, Pair.of(authnRequest, error));
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
buildEcpFaultResponse(response, request, Pair.of(authnRequest, e.getMessage()));
}
}
/**
* Build ecp fault response.
*
* @param response the response
* @param request the request
* @param authenticationContext the authentication context
*/
protected void buildEcpFaultResponse(final HttpServletResponse response,
final HttpServletRequest request,
final Pair<AuthnRequest, String> authenticationContext) {
request.setAttribute(SamlIdPConstants.REQUEST_ATTRIBUTE_ERROR, authenticationContext.getValue());
samlEcpFaultResponseBuilder.build(authenticationContext.getKey(), request, response,
null, null, null, SAMLConstants.SAML2_PAOS_BINDING_URI);
}
/**
* Authenticate ecp request.
*
* @param credential the credential
* @param authnRequest the authn request
* @return the authentication
*/
protected Authentication authenticateEcpRequest(final Credential credential,
final Pair<AuthnRequest, MessageContext> authnRequest) {
final String issuer = SamlIdPUtils.getIssuerFromSamlRequest(authnRequest.getKey());
LOGGER.debug("Located issuer [{}] from request prior to authenticating [{}]", issuer, credential.getId());
final Service service = webApplicationServiceFactory.createService(issuer);
LOGGER.debug("Executing authentication request for service [{}] on behalf of credential id [{}]", service, credential.getId());
final AuthenticationResult authenticationResult = authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, credential);
return authenticationResult.getAuthentication();
}
/**
* Build ecp cas assertion assertion.
*
* @param authentication the authentication
* @param service the service
* @param registeredService the registered service
* @return the assertion
*/
protected Assertion buildEcpCasAssertion(final Authentication authentication,
final Service service,
final RegisteredService registeredService) {
final Map attributes = registeredService.getAttributeReleasePolicy()
.getAttributes(authentication.getPrincipal(), service, registeredService);
final AttributePrincipal principal = new AttributePrincipalImpl(
authentication.getPrincipal().getId(), attributes);
return new AssertionImpl(principal, DateTimeUtils.dateOf(authentication.getAuthenticationDate()),
null, DateTimeUtils.dateOf(authentication.getAuthenticationDate()),
authentication.getAttributes());
}
/**
* Decode soap 11 context.
*
* @param request the request
* @return the soap 11 context
*/
protected MessageContext decodeSoapRequest(final HttpServletRequest request) {
try {
final HTTPSOAP11Decoder decoder = new HTTPSOAP11Decoder();
decoder.setParserPool(parserPool);
decoder.setHttpServletRequest(request);
final BindingDescriptor binding = new BindingDescriptor();
binding.setId(getClass().getName());
binding.setShortName(getClass().getName());
binding.setSignatureCapable(true);
binding.setSynchronous(true);
decoder.setBindingDescriptor(binding);
decoder.initialize();
decoder.decode();
return decoder.getMessageContext();
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
}
return null;
}
private Credential extractBasicAuthenticationCredential(final HttpServletRequest request,
final HttpServletResponse response) {
try {
final BasicAuthExtractor extractor = new BasicAuthExtractor(this.getClass().getSimpleName());
final WebContext webContext = WebUtils.getPac4jJ2EContext(request, response);
final UsernamePasswordCredentials credentials = extractor.extract(webContext);
if (credentials != null) {
LOGGER.debug("Received basic authentication ECP request from credentials [{}]", credentials);
return new UsernamePasswordCredential(credentials.getUsername(), credentials.getPassword());
}
} catch (final Exception e) {
LOGGER.warn(e.getMessage(), e);
}
return null;
}
}