package org.apereo.cas.support.saml.web.idp.profile.sso;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.authentication.AuthenticationSystemSupport;
import org.apereo.cas.authentication.principal.ServiceFactory;
import org.apereo.cas.authentication.principal.WebApplicationService;
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.SamlProtocolConstants;
import org.apereo.cas.support.saml.services.idp.metadata.cache.SamlRegisteredServiceCachingMetadataResolver;
import org.apereo.cas.support.saml.web.idp.profile.AbstractSamlProfileHandlerController;
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.jasig.cas.client.ssl.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.common.binding.SAMLBindingSupport;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import javax.net.ssl.HostnameVerifier;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Set;
/**
* This is {@link SSOPostProfileCallbackHandlerController}, which handles
* the profile callback request to build the final saml response.
*
* @author Misagh Moayyed
* @since 5.0.0
*/
public class SSOPostProfileCallbackHandlerController extends AbstractSamlProfileHandlerController {
private static final Logger LOGGER = LoggerFactory.getLogger(SSOPostProfileCallbackHandlerController.class);
private final HostnameVerifier hostnameVerifier;
/**
* Instantiates a new idp-sso post 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 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 SSOPostProfileCallbackHandlerController(final BaseSamlObjectSigner samlObjectSigner,
final ParserPool parserPool,
final AuthenticationSystemSupport authenticationSystemSupport,
final ServicesManager servicesManager,
final ServiceFactory<WebApplicationService> webApplicationServiceFactory,
final SamlRegisteredServiceCachingMetadataResolver samlRegisteredServiceCachingMetadataResolver,
final OpenSamlConfigBean configBean,
final SamlProfileObjectBuilder<Response> responseBuilder,
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,
final HostnameVerifier hostnameVerifier) {
super(samlObjectSigner,
parserPool,
authenticationSystemSupport,
servicesManager,
webApplicationServiceFactory,
samlRegisteredServiceCachingMetadataResolver,
configBean,
responseBuilder,
authenticationContextClassMappings,
serverPrefix,
serverName,
authenticationContextRequestParameter,
loginUrl,
logoutUrl,
forceSignedLogoutRequests,
singleLogoutCallbacksDisabled,
samlObjectSignatureValidator);
this.hostnameVerifier = hostnameVerifier;
}
/**
* Handle callback profile request.
*
* @param response the response
* @param request the request
* @throws Exception the exception
*/
@GetMapping(path = SamlIdPConstants.ENDPOINT_SAML2_SSO_PROFILE_POST_CALLBACK)
protected void handleCallbackProfileRequest(final HttpServletResponse response, final HttpServletRequest request) throws Exception {
LOGGER.info("Received SAML callback profile request [{}]", request.getRequestURI());
final AuthnRequest authnRequest = retrieveSamlAuthenticationRequestFromHttpRequest(request);
if (authnRequest == null) {
LOGGER.error("Can not validate the request because the original Authn request can not be found.");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
final String ticket = CommonUtils.safeGetParameter(request, CasProtocolConstants.PARAMETER_TICKET);
if (StringUtils.isBlank(ticket)) {
LOGGER.error("Can not validate the request because no [{}] is provided via the request", CasProtocolConstants.PARAMETER_TICKET);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
final Pair<AuthnRequest, MessageContext> authenticationContext = buildAuthenticationContextPair(request, authnRequest);
final Assertion assertion = validateRequestAndBuildCasAssertion(response, request, authenticationContext);
buildSamlResponse(response, request, authenticationContext, assertion, SAMLConstants.SAML2_POST_BINDING_URI);
}
/**
* Build authentication context pair pair.
*
* @param request the request
* @param authnRequest the authn request
* @return the pair
*/
protected static Pair<AuthnRequest, MessageContext> buildAuthenticationContextPair(final HttpServletRequest request,
final AuthnRequest authnRequest) {
final MessageContext<SAMLObject> messageContext = bindRelayStateParameter(request);
return Pair.of(authnRequest, messageContext);
}
private static MessageContext<SAMLObject> bindRelayStateParameter(final HttpServletRequest request) {
final MessageContext<SAMLObject> messageContext = new MessageContext<>();
final String relayState = request.getParameter(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE);
LOGGER.debug("RelayState is [{}]", relayState);
SAMLBindingSupport.setRelayState(messageContext, relayState);
return messageContext;
}
private Assertion validateRequestAndBuildCasAssertion(final HttpServletResponse response,
final HttpServletRequest request,
final Pair<AuthnRequest, MessageContext> pair) throws Exception {
final AuthnRequest authnRequest = pair.getKey();
final String ticket = CommonUtils.safeGetParameter(request, CasProtocolConstants.PARAMETER_TICKET);
final Cas30ServiceTicketValidator validator = new Cas30ServiceTicketValidator(this.serverPrefix);
final HttpsURLConnectionFactory factory = new HttpsURLConnectionFactory();
factory.setHostnameVerifier(this.hostnameVerifier);
validator.setURLConnectionFactory(factory);
validator.setRenew(authnRequest.isForceAuthn());
final String serviceUrl = constructServiceUrl(request, response, pair);
LOGGER.debug("Created service url for validation: [{}]", serviceUrl);
final Assertion assertion = validator.validate(ticket, serviceUrl);
logCasValidationAssertion(assertion);
return assertion;
}
}