Single Sign-On Implementation for SalesForce

After 3 days of torment and fruitless attempts to start an SSO for SalesForce, I hasten to share with the community the right way to solve the problem, so that future generations do not spend a breakthrough of precious time banging their heads against the wall. If interested, then I ask for a cat.

About what is SSO (Single Sign-On) is better to tell the wiki, because I am not a master of high syllable.
In general, the project needed to organize SSO support for a service such as SalesForce . Since SAML is used for communication between servers, it was decided to torment Google with searches for a ready-made implementation. After persistent searches, the OpenSAML library was found to generate SAML, which saved the universe from the birth of another bike.

First of all we will generate certificates. I used keytool from the JDK but you can also use OpenSSL:
keytool -genkey -keyalg RSA -alias SSO -keystore keystore
keytool -export -alias SSO -keystore keystore -file certificate.crt


After the keys are generated, you need to configure SalesForce to allow login using SSO. The best instruction is on their wiki - Single Sign-On with SAML on Force.com . The article is good and great, but we only need one item, “Configuring Force.com for SSO”. Yes, and it is with minor changes: Since my implementation passes the username in the NameIdentifier element, we leave the switches in their default state: "Assertion contains User's salesforce.com username" and "User ID is in the NameIdentifier element of the Subject statement".

Since a couple of examples were found for working with the OpenSAML library, a simple generator was quickly written that was suitable for test needs. After a working day spent on licking the code, a generator generating valid SAML was received (according to the SalesForce validator). Below is the licked code.

Since it is planned to tighten the support of other services besides SalesForce, the generator is divided into several classes: the common part (SAMLResponseGenerator), the implementation for SalesForce (SalesforceSAMLResponseGenerator) and the program for launching all this mess:

SAMLResponseGenerator.java:
public abstract class SAMLResponseGenerator {
	private static XMLObjectBuilderFactory builderFactory = null;
	private String issuerId;
	private X509Certificate certificate;
	private PublicKey publicKey;
	private PrivateKey privateKey;
	protected abstract Assertion buildAssertion();
	public SAMLResponseGenerator(X509Certificate certificate, PublicKey publicKey, PrivateKey privateKey, String issuerId) {
		this.certificate = certificate;
		this.publicKey = publicKey;
		this.privateKey = privateKey;
		this.issuerId = issuerId;
	}
	public String generateSAMLAssertionString() throws UnrecoverableKeyException, InvalidKeyException, NoSuchAlgorithmException, CertificateException, KeyStoreException, NoSuchProviderException, SignatureException, MarshallingException, ConfigurationException, IOException, org.opensaml.xml.signature.SignatureException, UnmarshallingException {
		Response response = buildDefaultResponse(issuerId);
		Assertion assertion = buildAssertion();
		response.getAssertions().add(assertion);
		assertion = signObject(assertion, certificate, publicKey, privateKey);
		response = signObject(response, certificate, publicKey, privateKey);
		Element plaintextElement = marshall(response);
		return XMLHelper.nodeToString(plaintextElement);
	}
	@SuppressWarnings("unchecked")
	protected  XMLObjectBuilder getXMLObjectBuilder(QName qname)
			throws ConfigurationException {
		if (builderFactory == null) {
			// OpenSAML 2.3
			DefaultBootstrap.bootstrap();
			builderFactory = Configuration.getBuilderFactory();
		}
		return (XMLObjectBuilder) builderFactory.getBuilder(qname);
	}
	protected  T buildXMLObject(QName qname)
			throws ConfigurationException {
		XMLObjectBuilder keyInfoBuilder = getXMLObjectBuilder(qname);
		return keyInfoBuilder.buildObject(qname);
	}
	protected Attribute buildStringAttribute(String name, String value)
			throws ConfigurationException {
		Attribute attrFirstName = buildXMLObject(Attribute.DEFAULT_ELEMENT_NAME);
		attrFirstName.setName(name);
		attrFirstName.setNameFormat(Attribute.UNSPECIFIED);
		// Set custom Attributes
		XMLObjectBuilder stringBuilder = getXMLObjectBuilder(XSString.TYPE_NAME);
		XSString attrValueFirstName = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
		attrValueFirstName.setValue(value);
		attrFirstName.getAttributeValues().add(attrValueFirstName);
		return attrFirstName;
	}
	private  Element marshall(T object) throws MarshallingException {
		return Configuration.getMarshallerFactory().getMarshaller(object).marshall(object);
	}
	@SuppressWarnings("unchecked")
	private  T unmarshall(Element element) throws MarshallingException, UnmarshallingException {
		return (T) Configuration.getUnmarshallerFactory().getUnmarshaller(element).unmarshall(element);
	}
	protected  T signObject(T object, X509Certificate certificate, PublicKey publicKey, PrivateKey privateKey) throws MarshallingException, ConfigurationException, IOException, UnrecoverableKeyException, InvalidKeyException, NoSuchAlgorithmException, CertificateException, KeyStoreException, NoSuchProviderException, SignatureException, org.opensaml.xml.signature.SignatureException, UnmarshallingException {
		BasicX509Credential signingCredential = new BasicX509Credential();
        signingCredential.setEntityCertificate(certificate);
        signingCredential.setPrivateKey(privateKey);
        signingCredential.setPublicKey(publicKey);
        KeyInfo keyInfo = buildXMLObject(KeyInfo.DEFAULT_ELEMENT_NAME);
        X509Data x509Data = buildXMLObject(X509Data.DEFAULT_ELEMENT_NAME);
        org.opensaml.xml.signature.X509Certificate x509Certificate = buildXMLObject(org.opensaml.xml.signature.X509Certificate.DEFAULT_ELEMENT_NAME);
        x509Certificate.setValue(Base64.encodeBase64String(certificate.getEncoded()));
        x509Data.getX509Certificates().add(x509Certificate);
        keyInfo.getX509Datas().add(x509Data);
		Signature signature = buildXMLObject(Signature.DEFAULT_ELEMENT_NAME);
		signature.setSigningCredential(signingCredential);
		signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
		signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
		signature.setKeyInfo(keyInfo);
		object.setSignature(signature);
		Element element = marshall(object);
		Signer.signObject(signature);
		return unmarshall(element);
	}
	protected Response buildDefaultResponse(String issuerId) {
		try {
			DateTime now = new DateTime();
			// Create Status
			StatusCode statusCode = buildXMLObject(StatusCode.DEFAULT_ELEMENT_NAME);
			statusCode.setValue(StatusCode.SUCCESS_URI);
			Status status = buildXMLObject(Status.DEFAULT_ELEMENT_NAME);
			status.setStatusCode(statusCode);
			// Create Issuer
			Issuer issuer = buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME);
			issuer.setValue(issuerId);
			issuer.setFormat(Issuer.ENTITY);
			// Create the response
			Response response = buildXMLObject(Response.DEFAULT_ELEMENT_NAME);
			response.setIssuer(issuer);
			response.setStatus(status);
			response.setIssueInstant(now);
			response.setVersion(SAMLVersion.VERSION_20);
			return response;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	public String getIssuerId() {
		return issuerId;
	}
	public void setIssuerId(String issuerId) {
		this.issuerId = issuerId;
	}
}


SalesforceSAMLResponseGenerator.java:
public class SalesforceSAMLResponseGenerator extends SAMLResponseGenerator {
	private static final String SALESFORCE_LOGIN_URL = "https://login.salesforce.com";
	private static final String SALESFORCE_AUDIENCE_URI = "https://saml.salesforce.com";
	private static final Logger logger = Logger.getLogger(SalesforceSAMLResponseGenerator.class);
	private static final int maxSessionTimeoutInMinutes = 10;
	private String nameId;
	public SalesforceSAMLResponseGenerator(X509Certificate certificate, PublicKey publicKey, PrivateKey privateKey,
			String issuerId, String nameId) {
		super(certificate, publicKey, privateKey, issuerId);
		this.nameId = nameId;
	}
	@Override
	protected Assertion buildAssertion() {
		try {
			// Create the NameIdentifier
			NameID nameId = buildXMLObject(NameID.DEFAULT_ELEMENT_NAME);
			nameId.setValue(this.nameId);
			nameId.setFormat(NameID.EMAIL);
			// Create the SubjectConfirmation
			SubjectConfirmationData confirmationMethod = buildXMLObject(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
			DateTime notBefore = new DateTime();
			DateTime notOnOrAfter = notBefore.plusMinutes(maxSessionTimeoutInMinutes);
			confirmationMethod.setNotOnOrAfter(notOnOrAfter);
			confirmationMethod.setRecipient(SALESFORCE_LOGIN_URL);
			SubjectConfirmation subjectConfirmation = buildXMLObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME);
			subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER);
			subjectConfirmation.setSubjectConfirmationData(confirmationMethod);
			// Create the Subject
			Subject subject = buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
			subject.setNameID(nameId);
			subject.getSubjectConfirmations().add(subjectConfirmation);
			// Create Authentication Statement
			AuthnStatement authnStatement = buildXMLObject(AuthnStatement.DEFAULT_ELEMENT_NAME);
			DateTime now2 = new DateTime();
			authnStatement.setAuthnInstant(now2);
			authnStatement.setSessionNotOnOrAfter(now2.plus(maxSessionTimeoutInMinutes));
			AuthnContext authnContext = buildXMLObject(AuthnContext.DEFAULT_ELEMENT_NAME);
			AuthnContextClassRef authnContextClassRef = buildXMLObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
			authnContextClassRef.setAuthnContextClassRef(AuthnContext.UNSPECIFIED_AUTHN_CTX);
			authnContext.setAuthnContextClassRef(authnContextClassRef);
			authnStatement.setAuthnContext(authnContext);
			Audience audience = buildXMLObject(Audience.DEFAULT_ELEMENT_NAME);
			audience.setAudienceURI(SALESFORCE_AUDIENCE_URI);
			AudienceRestriction audienceRestriction = buildXMLObject(AudienceRestriction.DEFAULT_ELEMENT_NAME);
			audienceRestriction.getAudiences().add(audience);
			Conditions conditions = buildXMLObject(Conditions.DEFAULT_ELEMENT_NAME);
			conditions.setNotBefore(notBefore);
			conditions.setNotOnOrAfter(notOnOrAfter);
			conditions.getConditions().add(audienceRestriction);
			// Create Issuer
			Issuer issuer = (Issuer) buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME);
			issuer.setValue(getIssuerId());
			// Create the assertion
			Assertion assertion = buildXMLObject(Assertion.DEFAULT_ELEMENT_NAME);
			assertion.setIssuer(issuer);
			assertion.setID(UUID.randomUUID().toString());
			assertion.setIssueInstant(notBefore);
			assertion.setVersion(SAMLVersion.VERSION_20);
			assertion.getAuthnStatements().add(authnStatement);
			assertion.setConditions(conditions);
			assertion.setSubject(subject);
			return assertion;
		} catch (ConfigurationException e) {
			logger.error(e, e);
		}
		return null;
	}
}


TestSSO.java:
public class TestSSO {
	private PrivateKey privateKey;
	private X509Certificate certificate;
	public void readCertificate(InputStream inputStream, String alias, String password) throws NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, KeyStoreException {
		KeyStore keyStore = KeyStore.getInstance("JKS");
		keyStore.load(inputStream, password.toCharArray());
		Key key = keyStore.getKey(alias, password.toCharArray());
		if (key == null) {
			throw new RuntimeException("Got null key from keystore!");
		}
		privateKey = (PrivateKey) key;
		certificate = (X509Certificate) keyStore.getCertificate(alias);
		if (certificate == null) {
			throw new RuntimeException("Got null cert from keystore!");
		}
	}
	public void run() throws ConfigurationException, UnrecoverableKeyException, InvalidKeyException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, KeyStoreException, NoSuchProviderException, SignatureException, IOException, org.opensaml.xml.signature.SignatureException, URISyntaxException, UnmarshallingException, MarshallingException {
		String strIssuer = "Eugene Burtsev";
		String strNameID = "user@test.com";
		InputStream inputStream = TestSSO.class.getResourceAsStream("/keystore");
		readCertificate(inputStream, "SSO", "12345678");
		SAMLResponseGenerator responseGenerator = new SalesforceSAMLResponseGenerator(certificate, certificate.getPublicKey(), privateKey, strIssuer, strNameID);
		String samlAssertion = responseGenerator.generateSAMLAssertionString();
		System.out.println();
		System.out.println("Assertion String: " + samlAssertion);
	}
	public static void main(String[] args) throws ConfigurationException, UnrecoverableKeyException, InvalidKeyException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, KeyStoreException, NoSuchProviderException, SignatureException, IOException, org.opensaml.xml.signature.SignatureException, URISyntaxException, UnmarshallingException, MarshallingException {
		new TestSSO().run();
	}
}


Despite all the assurances of the validator that the code is valid, making SalesForce authorize using SSO was not a trivial task. About a dozen examples were tested from their wiki and none of them worked at best saying that they say "invalid assertion" ... Three days passed ... And then it occurred to me to read the OASIS specifications , in which the sacred knowledge was learned that SAML was needed send a POST request in the “SAMLResponse” parameter ... No longer hoping for success, this knowledge was put into practice, and a miracle happened - Salesforce issued a link with a token for the login. Below is a complete example of a program that illustrates the correct approach for implementing SSO for SalesForce.

public class TestSSO {
	private static final Logger logger = Logger.getLogger(TestSSO.class);
	public static DefaultHttpClient getThreadSafeClient() {
		DefaultHttpClient client = new DefaultHttpClient();
		ClientConnectionManager mgr = client.getConnectionManager();
		HttpParams params = client.getParams();
		client = new DefaultHttpClient(new ThreadSafeClientConnManager(
				mgr.getSchemeRegistry()), params);
		return client;
	}
	private static HttpClient createHttpClient() {
		HttpClient httpclient = getThreadSafeClient();
		httpclient.getParams().setParameter(
				CoreProtocolPNames.PROTOCOL_VERSION,
				new ProtocolVersion("HTTP", 1, 1));
		return httpclient;
	}
	private static void sendSamlRequest(String samlAssertion) {
		HttpClient httpClient = createHttpClient();
		try {
			System.out.println(samlAssertion);
			HttpPost httpPost = new HttpPost("https://login.salesforce.com/");
			MultipartEntity entity = new MultipartEntity(HttpMultipartMode.STRICT);
			entity.addPart("SAMLResponse", new StringBody(samlAssertion));
			httpPost.setEntity(entity);
			HttpResponse httpResponse = httpClient.execute(httpPost);
			Header location = httpResponse.getFirstHeader("Location");
			if (null != location) {
				System.out.println(location.getValue());
			}
		} catch (Exception e) {
			logger.error(e, e);
		} finally {
			httpClient.getConnectionManager().shutdown();
		}
	}
	private PrivateKey privateKey;
	private X509Certificate certificate;
	public void readCertificate(InputStream inputStream, String alias, String password) throws NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, KeyStoreException {
		KeyStore keyStore = KeyStore.getInstance("JKS");
		keyStore.load(inputStream, password.toCharArray());
		Key key = keyStore.getKey(alias, password.toCharArray());
		if (key == null) {
			throw new RuntimeException("Got null key from keystore!");
		}
		privateKey = (PrivateKey) key;
		certificate = (X509Certificate) keyStore.getCertificate(alias);
		if (certificate == null) {
			throw new RuntimeException("Got null cert from keystore!");
		}
	}
	public void run() throws ConfigurationException, UnrecoverableKeyException, InvalidKeyException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, KeyStoreException, NoSuchProviderException, SignatureException, IOException, org.opensaml.xml.signature.SignatureException, URISyntaxException, UnmarshallingException, MarshallingException {
		String strIssuer = "Eugene Burtsev";
		String strNameID = "user@test.com";
		InputStream inputStream = TestSSO.class.getResourceAsStream("/keystore");
		readCertificate(inputStream, "SSO", "12345678");
		SAMLResponseGenerator responseGenerator = new SalesforceSAMLResponseGenerator(certificate, certificate.getPublicKey(), privateKey, strIssuer, strNameID);
		String samlAssertion = responseGenerator.generateSAMLAssertionString();
		System.out.println();
		System.out.println("Assertion String: " + samlAssertion);
		sendSamlRequest(Base64.encodeBase64String(samlAssertion.getBytes("UTF-8")));
	}
	public static void main(String[] args) throws ConfigurationException, UnrecoverableKeyException, InvalidKeyException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, KeyStoreException, NoSuchProviderException, SignatureException, IOException, org.opensaml.xml.signature.SignatureException, URISyntaxException, UnmarshallingException, MarshallingException {
		new TestSSO().run();
	}
}


An archive with source codes can be taken here.
And finally, moral: Don’t trust anyone, only specs bring the truth!

List of useful resources:
  1. Single Sign-On with SAML on Force.com
  2. docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
  3. www.sslshopper.com/article-most-common-java-keytool-keystore-commands.html

Also popular now: