SalesForceのシングルサインオン実装

SalesForceのSSOを開始しようとする3日間の苦労と実りのない試みの後、将来の世代が頭を壁にぶつけて貴重な時間を突破しないように、問題を解決する正しい方法をコミュニティと共有することを急いでいます。 興味があれば、猫をお願いします。



私は高音節の達人ではないので、 SSO(シングルサインオン)とは何かをwikiに伝える方良いです。

一般に、プロジェクトではSalesForceなどのサービスのSSOサポートを編成する必要がありました。 SAMLはサーバー間の通信に使用されるため、既成の実装の検索でGoogleを苦しめることにしました。 永続的な検索の後、OpenSAMLライブラリがSAMLを生成することがわかりました。これにより、別の自転車の誕生から宇宙が救われました。



まず、証明書を生成します。 JDKのキーツールを使用しましたが、OpenSSLを使用することもできます。

keytool -genkey -keyalg RSA -alias SSO -keystore keystore keytool -export -alias SSO -keystore keystore -file certificate.crt
      
      







キーが生成された後、SSOを使用したログインを許可するようにSalesForceを構成する必要があります。 最良の指示は、WikiのForce.comでのSAMLによるシングルサインオンです。 この記事は優れていますが、必要な項目は「Force.comをSSO用に設定する」だけです。 はい、それは小さな変更です:私の実装はNameIdentifier要素でユーザー名を渡すので、スイッチをデフォルト状態のままにします:「Assertion contains User's salesforce.com username」と「User ID is the NameIdentifier element in the Subject statement」



OpenSAMLライブラリを操作するためのいくつかの例が見つかったため、テストのニーズに適した簡単なジェネレーターがすぐに作成されました。 1日の作業でコードをなめると、有効なSAMLを生成するジェネレーターが受信されました(SalesForceバリデーターによる)。 以下は、なめられたコードです。



SalesForce以外の他のサービスのサポートを強化する予定であるため、ジェネレーターはいくつかのクラスに分けられます:共通部分(SAMLResponseGenerator)、SalesForceの実装(SalesforceSAMLResponseGenerator)、およびこの混乱をすべて実行するプログラム:



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







コードが有効であるというバリデーターのすべての保証にもかかわらず、SSOを使用してSalesForceを許可することは簡単な作業ではありませんでした。 約12のサンプルがwikiでテストされ、どれも「無効なアサーション」と言ってもうまく動作しませんでした...それで3日が経過しました...そして、SAMLが必要とするものについて神聖な知識が得られたOASIS仕様を読みまし 「SAMLResponse」パラメータでPOSTリクエストを送信します...成功を望んでいないため、この知識が実践され、奇跡が起こりました-Salesforceはログイン用のトークンを含むリンクを発行しました。 以下は、SalesForceにSSOを実装するための正しいアプローチを示す完全なサンプルプログラムです。



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







ソースを含むアーカイブはここで取得できます

そして最後に、道徳:誰も信用せず、仕様だけが真実を伝える!



有用なリソースのリスト:

  1. Force.comでのSAMLを使用したシングルサインオン
  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



All Articles