package org.apereo.cas.ws.idp.web; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.cxf.ws.security.tokenstore.SecurityToken; import org.apache.http.client.utils.URIBuilder; import org.apereo.cas.CasProtocolConstants; import org.apereo.cas.authentication.AuthenticationServiceSelectionStrategy; 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.configuration.CasConfigurationProperties; import org.apereo.cas.configuration.model.support.wsfed.WsFederationProperties; import org.apereo.cas.services.RegexRegisteredService; import org.apereo.cas.services.RegisteredServiceAccessStrategyUtils; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.services.UnauthorizedServiceException; import org.apereo.cas.support.saml.SamlException; import org.apereo.cas.ticket.SecurityTokenTicket; import org.apereo.cas.ticket.SecurityTokenTicketFactory; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.ticket.registry.TicketRegistrySupport; import org.apereo.cas.util.http.HttpClient; import org.apereo.cas.web.support.CookieRetrievingCookieGenerator; import org.apereo.cas.web.support.WebUtils; import org.apereo.cas.ws.idp.WSFederationConstants; import org.apereo.cas.ws.idp.services.WSFederationRegisteredService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.security.SecureRandom; import java.util.Date; /** * This is {@link BaseWSFederationRequestController}. * * @author Misagh Moayyed * @since 5.1.0 */ @Controller public abstract class BaseWSFederationRequestController { private static final Logger LOGGER = LoggerFactory.getLogger(BaseWSFederationRequestController.class); /** * The Services manager. */ protected final ServicesManager servicesManager; /** * The Web application service factory. */ protected final ServiceFactory<WebApplicationService> webApplicationServiceFactory; /** * The Callback service. */ protected final Service callbackService; /** * The Cas properties. */ protected final CasConfigurationProperties casProperties; /** * The Service selection strategy. */ protected final AuthenticationServiceSelectionStrategy serviceSelectionStrategy; /** * The Http client. */ protected final HttpClient httpClient; /** * The Security token ticket factory. */ protected final SecurityTokenTicketFactory securityTokenTicketFactory; /** * The Ticket registry. */ protected final TicketRegistry ticketRegistry; /** * The Ticket granting ticket cookie generator. */ protected final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator; /** * The Ticket registry support. */ protected final TicketRegistrySupport ticketRegistrySupport; /** * Instantiates a new Base ws federation request controller. * * @param servicesManager the services manager * @param webApplicationServiceFactory the web application service factory * @param casProperties the cas properties * @param serviceSelectionStrategy the service selection strategy * @param httpClient the http client * @param securityTokenTicketFactory the security token ticket factory * @param ticketRegistry the ticket registry * @param ticketGrantingTicketCookieGenerator the ticket granting ticket cookie generator * @param ticketRegistrySupport the ticket registry support */ public BaseWSFederationRequestController( final ServicesManager servicesManager, final ServiceFactory<WebApplicationService> webApplicationServiceFactory, final CasConfigurationProperties casProperties, final AuthenticationServiceSelectionStrategy serviceSelectionStrategy, final HttpClient httpClient, final SecurityTokenTicketFactory securityTokenTicketFactory, final TicketRegistry ticketRegistry, final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator, final TicketRegistrySupport ticketRegistrySupport) { this.servicesManager = servicesManager; this.webApplicationServiceFactory = webApplicationServiceFactory; this.casProperties = casProperties; this.serviceSelectionStrategy = serviceSelectionStrategy; this.httpClient = httpClient; this.securityTokenTicketFactory = securityTokenTicketFactory; this.ticketRegistry = ticketRegistry; this.ticketGrantingTicketCookieGenerator = ticketGrantingTicketCookieGenerator; this.ticketRegistrySupport = ticketRegistrySupport; this.callbackService = registerCallback(WSFederationConstants.ENDPOINT_FEDERATION_REQUEST_CALLBACK); } /** * Register callback service. * * @param callbackUrl the callback url * @return the service */ private Service registerCallback(final String callbackUrl) { final Service callbackService = this.webApplicationServiceFactory.createService(callbackUrl); if (!this.servicesManager.matchesExistingService(callbackService)) { LOGGER.debug("Initializing callback service [{}]", callbackService); final RegexRegisteredService service = new RegexRegisteredService(); service.setId(Math.abs(new SecureRandom().nextLong())); service.setEvaluationOrder(0); service.setName(service.getClass().getSimpleName()); service.setDescription("WS-Federation Authentication Request"); service.setServiceId(callbackService.getId().concat(".+")); LOGGER.debug("Saving callback service [{}] into the registry", service); this.servicesManager.save(service); this.servicesManager.load(); } return callbackService; } /** * Construct service url string. * * @param request the request * @param response the response * @param wsfedRequest the ws federation request * @return the service url */ protected String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response, final WSFederationRequest wsfedRequest) { try { final URIBuilder builder = new URIBuilder(this.callbackService.getId()); builder.addParameter(WSFederationConstants.WA, wsfedRequest.getWa()); builder.addParameter(WSFederationConstants.WREPLY, wsfedRequest.getWreply()); builder.addParameter(WSFederationConstants.WTREALM, wsfedRequest.getWtrealm()); if (StringUtils.isNotBlank(wsfedRequest.getWctx())) { builder.addParameter(WSFederationConstants.WCTX, wsfedRequest.getWctx()); } if (StringUtils.isNotBlank(wsfedRequest.getWfresh())) { builder.addParameter(WSFederationConstants.WREFRESH, wsfedRequest.getWfresh()); } if (StringUtils.isNotBlank(wsfedRequest.getWhr())) { builder.addParameter(WSFederationConstants.WHR, wsfedRequest.getWhr()); } if (StringUtils.isNotBlank(wsfedRequest.getWreq())) { builder.addParameter(WSFederationConstants.WREQ, wsfedRequest.getWreq()); } final URI url = builder.build(); LOGGER.debug("Built service callback url [{}]", url); return org.jasig.cas.client.util.CommonUtils.constructServiceUrl(request, response, url.toString(), casProperties.getServer().getName(), CasProtocolConstants.PARAMETER_SERVICE, CasProtocolConstants.PARAMETER_TICKET, false); } catch (final Exception e) { throw new SamlException(e.getMessage(), e); } } /** * Gets security token from request. * * @param request the request * @return the security token from request */ protected SecurityToken getSecurityTokenFromRequest(final HttpServletRequest request) { final String cookieValue = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request); if (StringUtils.isNotBlank(cookieValue)) { final TicketGrantingTicket tgt = this.ticketRegistry.getTicket(cookieValue, TicketGrantingTicket.class); if (tgt != null) { final String sts = tgt.getDescendantTickets().stream() .filter(t -> t.startsWith(SecurityTokenTicket.PREFIX)) .findFirst() .orElse(null); if (StringUtils.isNotBlank(sts)) { final SecurityTokenTicket stt = ticketRegistry.getTicket(sts, SecurityTokenTicket.class); if (stt == null || stt.isExpired()) { LOGGER.warn("Security token ticket [{}] is not found or has expired", sts); return null; } if (stt.getSecurityToken().isExpired()) { LOGGER.warn("Security token linked to ticket [{}] has expired", sts); return null; } return stt.getSecurityToken(); } } } return null; } /** * Is authentication required? * * @param fedRequest the fed request * @param request the request * @return the boolean */ protected boolean shouldRenewAuthentication(final WSFederationRequest fedRequest, final HttpServletRequest request) { if (StringUtils.isBlank(fedRequest.getWfresh()) || NumberUtils.isCreatable(fedRequest.getWfresh())) { return false; } final long ttl = Long.parseLong(fedRequest.getWfresh().trim()); if (ttl == 0) { return false; } final SecurityToken idpToken = getSecurityTokenFromRequest(request); if (idpToken == null) { return true; } final long ttlMs = ttl * 60L * 1000L; if (ttlMs > 0) { final Date createdDate = idpToken.getCreated(); if (createdDate != null) { final Date expiryDate = new Date(); expiryDate.setTime(createdDate.getTime() + ttlMs); if (expiryDate.before(new Date())) { return true; } } } return false; } /** * Gets ws federation registered service. * * @param response the response * @param request the request * @param fedRequest the fed request * @return the ws federation registered service */ protected WSFederationRegisteredService findAndValidateFederationRequestForRegisteredService(final HttpServletResponse response, final HttpServletRequest request, final WSFederationRequest fedRequest) { final String serviceUrl = constructServiceUrl(request, response, fedRequest); final Service targetService = this.serviceSelectionStrategy.resolveServiceFrom(this.webApplicationServiceFactory.createService(serviceUrl)); final WSFederationRegisteredService svc = getWsFederationRegisteredService(targetService); final WsFederationProperties.IdentityProvider idp = casProperties.getAuthn().getWsfedIdP().getIdp(); if (StringUtils.isBlank(fedRequest.getWtrealm()) || !StringUtils.equals(fedRequest.getWtrealm(), svc.getRealm())) { LOGGER.warn("Realm [{}] is not authorized for matching service [{}]", fedRequest.getWtrealm(), svc); throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, StringUtils.EMPTY); } if (!StringUtils.equals(idp.getRealm(), svc.getRealm())) { LOGGER.warn("Realm [{}] is not authorized for the identity provider realm [{}]", fedRequest.getWtrealm(), idp.getRealm()); throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, StringUtils.EMPTY); } return svc; } /** * Gets ws federation registered service. * * @param targetService the target service * @return the ws federation registered service */ protected WSFederationRegisteredService getWsFederationRegisteredService(final Service targetService) { final WSFederationRegisteredService svc = this.servicesManager.findServiceBy(targetService, WSFederationRegisteredService.class); RegisteredServiceAccessStrategyUtils.ensureServiceAccessIsAllowed(targetService, svc); return svc; } /** * Handle unauthorized service exception. * * @param req the req * @param ex the ex * @return the model and view */ @ExceptionHandler(Exception.class) public ModelAndView handleUnauthorizedServiceException(final HttpServletRequest req, final Exception ex) { return WebUtils.produceUnauthorizedErrorView(); } }