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;
}
}