package org.apereo.cas.support.saml.authentication.principal; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Throwables; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apereo.cas.authentication.principal.AbstractWebApplicationServiceResponseBuilder; import org.apereo.cas.authentication.principal.Response; import org.apereo.cas.authentication.principal.WebApplicationService; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.services.UnauthorizedServiceException; import org.apereo.cas.support.saml.SamlProtocolConstants; import org.apereo.cas.support.saml.util.GoogleSaml20ObjectBuilder; import org.apereo.cas.util.crypto.PrivateKeyFactoryBean; import org.apereo.cas.util.crypto.PublicKeyFactoryBean; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.AuthnContext; import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.core.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import java.io.StringWriter; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.HashMap; import java.util.Map; /** * Builds the google accounts service response. * * @author Misagh Moayyed * @since 4.2 */ public class GoogleAccountsServiceResponseBuilder extends AbstractWebApplicationServiceResponseBuilder { private static final long serialVersionUID = -4584732364007702423L; private static final Logger LOGGER = LoggerFactory.getLogger(GoogleAccountsServiceResponseBuilder.class); @JsonIgnore private PrivateKey privateKey; @JsonIgnore private PublicKey publicKey; @JsonIgnore private ServicesManager servicesManager; @JsonProperty private final String publicKeyLocation; @JsonProperty private final String privateKeyLocation; @JsonProperty private final String keyAlgorithm; @JsonProperty private GoogleSaml20ObjectBuilder samlObjectBuilder; @JsonProperty private int skewAllowance; @JsonProperty private String casServerPrefix; /** * Instantiates a new Google accounts service response builder. * * @param privateKeyLocation the private key * @param publicKeyLocation the public key * @param keyAlgorithm the key algorithm * @param servicesManager the services manager * @param samlObjectBuilder the saml object builder * @param skewAllowance the skew allowance * @param casServerPrefix the cas server prefix */ public GoogleAccountsServiceResponseBuilder(final String privateKeyLocation, final String publicKeyLocation, final String keyAlgorithm, final ServicesManager servicesManager, final GoogleSaml20ObjectBuilder samlObjectBuilder, final int skewAllowance, final String casServerPrefix) { this(privateKeyLocation, publicKeyLocation, keyAlgorithm, 0); this.samlObjectBuilder = samlObjectBuilder; this.servicesManager = servicesManager; this.skewAllowance = skewAllowance; this.casServerPrefix = casServerPrefix; } /** * Instantiates a new Google accounts service response builder. * * @param privateKeyLocation the private key * @param publicKeyLocation the public key * @param keyAlgorithm the key algorithm * @param skewAllowance the skew allowance */ @JsonCreator public GoogleAccountsServiceResponseBuilder(@JsonProperty("privateKeyLocation") final String privateKeyLocation, @JsonProperty("publicKeyLocation") final String publicKeyLocation, @JsonProperty("keyAlgorithm") final String keyAlgorithm, @JsonProperty("skewAllowance") final int skewAllowance) { Assert.notNull(privateKeyLocation); Assert.notNull(publicKeyLocation); try { this.privateKeyLocation = privateKeyLocation; this.publicKeyLocation = publicKeyLocation; this.keyAlgorithm = keyAlgorithm; this.skewAllowance = skewAllowance; createGoogleAppsPrivateKey(); createGoogleAppsPublicKey(); } catch (final Exception e) { throw Throwables.propagate(e); } } @Override public Response build(final WebApplicationService webApplicationService, final String serviceTicket) { final GoogleAccountsService service = (GoogleAccountsService) webApplicationService; final Map<String, String> parameters = new HashMap<>(); final String samlResponse = constructSamlResponse(service); final String signedResponse = this.samlObjectBuilder.signSamlResponse(samlResponse, this.privateKey, this.publicKey); parameters.put(SamlProtocolConstants.PARAMETER_SAML_RESPONSE, signedResponse); parameters.put(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE, service.getRelayState()); return buildPost(service, parameters); } /** * Construct SAML response. * <a href="http://bit.ly/1uI8Ggu">See this reference for more info.</a> * * @param service the service * @return the SAML response */ protected String constructSamlResponse(final GoogleAccountsService service) { final ZonedDateTime currentDateTime = ZonedDateTime.now(ZoneOffset.UTC); final ZonedDateTime notBeforeIssueInstant = ZonedDateTime.parse("2003-04-17T00:46:02Z"); final RegisteredService registeredService = servicesManager.findServiceBy(service); if (registeredService == null || !registeredService.getAccessStrategy().isServiceAccessAllowed()) { throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE); } final String userId = registeredService.getUsernameAttributeProvider().resolveUsername(service.getPrincipal(), service, registeredService); final org.opensaml.saml.saml2.core.Response response = this.samlObjectBuilder.newResponse( this.samlObjectBuilder.generateSecureRandomId(), currentDateTime, null, service); response.setStatus(this.samlObjectBuilder.newStatus(StatusCode.SUCCESS, null)); final String sessionIndex = '_' + String.valueOf(Math.abs(new SecureRandom().nextLong())); final AuthnStatement authnStatement = this.samlObjectBuilder.newAuthnStatement(AuthnContext.PASSWORD_AUTHN_CTX, currentDateTime, sessionIndex); final Assertion assertion = this.samlObjectBuilder.newAssertion(authnStatement, casServerPrefix, notBeforeIssueInstant, this.samlObjectBuilder.generateSecureRandomId()); final Conditions conditions = this.samlObjectBuilder.newConditions(notBeforeIssueInstant, currentDateTime.plusSeconds(this.skewAllowance), service.getId()); assertion.setConditions(conditions); final Subject subject = this.samlObjectBuilder.newSubject(NameID.EMAIL, userId, service.getId(), currentDateTime.plusSeconds(this.skewAllowance), service.getRequestId()); assertion.setSubject(subject); response.getAssertions().add(assertion); final StringWriter writer = new StringWriter(); this.samlObjectBuilder.marshalSamlXmlObject(response, writer); final String result = writer.toString(); LOGGER.debug("Generated Google SAML response: [{}]", result); return result; } /** * Sets the allowance for time skew in seconds * between CAS and the client server. Default 0s. * This value will be subtracted from the current time when setting the SAML * {@code NotBeforeDate} attribute, thereby allowing for the * CAS server to be ahead of the client by as much as the value defined here. * * @param skewAllowance Number of seconds to allow for variance. */ public void setSkewAllowance(final int skewAllowance) { LOGGER.debug("Using [{}] seconds as skew allowance.", skewAllowance); this.skewAllowance = skewAllowance; } /** * Create the private key. * * @throws Exception if key creation ran into an error */ protected void createGoogleAppsPrivateKey() throws Exception { if (!isValidConfiguration()) { LOGGER.debug("Google Apps private key bean will not be created, because it's not configured"); return; } final PrivateKeyFactoryBean bean = new PrivateKeyFactoryBean(); if (this.privateKeyLocation.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { bean.setLocation(new ClassPathResource(StringUtils.removeStart(this.privateKeyLocation, ResourceUtils.CLASSPATH_URL_PREFIX))); } else if (this.privateKeyLocation.startsWith(ResourceUtils.FILE_URL_PREFIX)) { bean.setLocation(new FileSystemResource(StringUtils.removeStart(this.privateKeyLocation, ResourceUtils.FILE_URL_PREFIX))); } else { bean.setLocation(new FileSystemResource(this.privateKeyLocation)); } bean.setAlgorithm(this.keyAlgorithm); LOGGER.debug("Loading Google Apps private key from [{}] with key algorithm [{}]", bean.getLocation(), bean.getAlgorithm()); bean.afterPropertiesSet(); LOGGER.debug("Creating Google Apps private key instance via [{}]", this.privateKeyLocation); this.privateKey = bean.getObject(); } /** * Create the public key. * * @throws Exception if key creation ran into an error */ protected void createGoogleAppsPublicKey() throws Exception { if (!isValidConfiguration()) { LOGGER.debug("Google Apps public key bean will not be created, because it's not configured"); return; } final PublicKeyFactoryBean bean = new PublicKeyFactoryBean(); if (this.publicKeyLocation.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { bean.setLocation(new ClassPathResource(StringUtils.removeStart(this.publicKeyLocation, ResourceUtils.CLASSPATH_URL_PREFIX))); } else if (this.publicKeyLocation.startsWith(ResourceUtils.FILE_URL_PREFIX)) { bean.setLocation(new FileSystemResource(StringUtils.removeStart(this.publicKeyLocation, ResourceUtils.FILE_URL_PREFIX))); } else { bean.setLocation(new FileSystemResource(this.publicKeyLocation)); } bean.setAlgorithm(this.keyAlgorithm); LOGGER.debug("Loading Google Apps public key from [{}] with key algorithm [{}]", bean.getResource(), bean.getAlgorithm()); bean.afterPropertiesSet(); LOGGER.debug("Creating Google Apps public key instance via [{}]", this.publicKeyLocation); this.publicKey = bean.getObject(); } private boolean isValidConfiguration() { return StringUtils.isNotBlank(this.privateKeyLocation) || StringUtils.isNotBlank(this.publicKeyLocation) || StringUtils.isNotBlank(this.keyAlgorithm); } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (obj.getClass() != getClass()) { return false; } final GoogleAccountsServiceResponseBuilder rhs = (GoogleAccountsServiceResponseBuilder) obj; final EqualsBuilder builder = new EqualsBuilder(); return builder .appendSuper(super.equals(obj)) .append(this.publicKeyLocation, rhs.publicKeyLocation) .append(this.privateKeyLocation, rhs.privateKeyLocation) .append(this.keyAlgorithm, rhs.keyAlgorithm) .append(this.samlObjectBuilder, rhs.samlObjectBuilder) .append(this.skewAllowance, rhs.skewAllowance) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder() .appendSuper(super.hashCode()) .append(publicKeyLocation) .append(privateKeyLocation) .append(keyAlgorithm) .append(skewAllowance) .append(samlObjectBuilder) .toHashCode(); } @Override public boolean supports(final WebApplicationService service) { return service instanceof GoogleAccountsService; } }