📜 ⬆️ ⬇️

Single Sign-On for SalesForce

After 3 days of torment and fruitless attempts to start SSO for SalesForce, I hasten to share with the community the right way to solve the problem, so that future generations will not waste my precious time hitting my head against the wall. If interested, then please under the cat.

What is SSO (Single Sign-On) is best described by the wiki, because I am not a master of high syllable.
In general, the project required the organization of SSO support for such a service as SalesForce . Since for communication between servers SAML was used, it was decided to torment Google with searches for a ready implementation. After persistent searches, the OpenSAML library was found to generate SAML, which saved the universe from the birth of another bicycle.

First of all, we will generate certificates. I used the keytool from the JDK but I can use OpenSSL as well:
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 logging in using SSO. The best instruction is on their wiki - Single Sign-On with SAML on Force.com . The article is good and big, but we need only one item “Configuring Force.com for SSO”. Yes, and with minor changes: Since my implementation passes the user name in the NameIdentifier element, we leave the switches in their default state: “Assertion contains User’s salesforce.com username” and “User ID is the nameIdentifier element of the Subject statement”.
')
Since a couple of examples were found to work with the OpenSAML library, a simple generator was quickly written, suitable for test needs. After a working day spent on licking the code, a generator generating a valid SAML (according to the SalesForce validator) was obtained. Below is a licked code.

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

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 <T extends XMLObject> XMLObjectBuilder<T> getXMLObjectBuilder(QName qname) throws ConfigurationException { if (builderFactory == null) { // OpenSAML 2.3 DefaultBootstrap.bootstrap(); builderFactory = Configuration.getBuilderFactory(); } return (XMLObjectBuilder<T>) builderFactory.getBuilder(qname); } protected <T extends XMLObject> T buildXMLObject(QName qname) throws ConfigurationException { XMLObjectBuilder<T> 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<XSString> 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 <T extends XMLObject> Element marshall(T object) throws MarshallingException { return Configuration.getMarshallerFactory().getMarshaller(object).marshall(object); } @SuppressWarnings("unchecked") private <T extends XMLObject> T unmarshall(Element element) throws MarshallingException, UnmarshallingException { return (T) Configuration.getUnmarshallerFactory().getUnmarshaller(element).unmarshall(element); } protected <T extends SignableSAMLObject> 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, forcing SalesForce to authenticate using SSO was not a trivial task. About a dozen examples were tested with their wiki and none earned at best saying that they say "invalid assertion" ... So three days passed ... And then it came to my mind to read the OASIS specifications , in which sacred knowledge was drawn that SAML needed send in the parameter “SAMLResponse” POST request ... Already not hoping for success, this knowledge was applied in practice, and the miracle did happen - salesforce issued a link with a token for the login. Below is a complete example of a program illustrating the right 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(); } } 


The source archive can be found here.
And finally, the moral: Do not believe anyone, only the specs are 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

Source: https://habr.com/ru/post/151006/


All Articles