Single Sign-On with Force.com and Microsoft Active Directory Federation Services

Abstract

As resources move to the cloud, users experience a proliferation of credentials - the usernames, passwords and, sometimes, devices they use to log in (or authenticate) to cloud-based services. Single sign-on technologies come to the rescue, allowing users to authenticate at a single location and access a range of services without re-authenticating.

Since its release in 2005, the Security Assertion Markup Language (better known as SAML) version 2.0 has established itself as the dominant standard for cross-domain web single sign-on in the enterprise space, with salesforce.com introducing support in the Winter '09 release (October 2008) and Microsoft in Active Directory Federation Services (AD FS) version 2.0 in May 2010.

Now it is possible to configure a seamless single sign-on experience from a Microsoft environment to Force.com (and hence other salesforce.com products) without a third-party federation product.

This article is based on an original blog post by Rhys Goodwin. Our thanks to Rhys for permission to adapt his post as an article here on Developer Force.

UPDATE: When configuring ADFSv2 for use with Salesforce, it's recommended that you select the new "HTTP Redirect" option available in the Salesforce Single Sign-On settings under "Service Provider Initiated Request Binding". This will improve interoperability with iOS based devices.

Prerequisites

You need a Microsoft Windows Server 2008 R2 Enterprise or Datacenter edition. If you are just configuring this environment for an evaluation, you can download a trial version here. It's good for 180 days, which should be long enough to get SSO working and decide if you want to put it into production.

You also need Microsoft Active Directory Federation Services 2.0. Note that Windows Server 2008 R2 includes AD FS 1.0, which does not support SAML 2.0 - you need to download the AD FS 2.0 'release to web' (RTW) package.

Finally, you'll want a Force.com environment (commonly known as an 'org'). If you're just trying this out, a free Developer Edition environment is a great choice.

Overview

SAML 2.0 defines several roles for parties involved in single sign-on:

ADFSSAML2 Roles.png

The user authenticates (logs in) to the identity provider (or IdP) - in our case, this will be AD FS 2.0. The user is then able to access a resource at one or more service providers (abbreviated as SP, and also known as relying parties) without needing to log in at each service provider.

Let’s first take a look at an overview of the process, then we’ll dive into the configuration. The diagram below shows the process for an IdP-initiated login into Force.com – later we’ll look at SP-initiated login.

ADFSOverviewDiagram.jpg

  1. The user authenticates to the AD FS server using Integrated Windows Authentication (Kerberos tokens over HTTP) and requests login to Force.com
  2. AD FS returns a SAML assertion to the user’s browser
  3. The browser automatically submits the assertion to Force.com, which logs the user in

The rest of this article shows how to make this a reality:

Install

  • Start by installing Windows Server 2008 R2. Note that the AD FS server must be a member of an Active Directory domain. If you're building a lab setup for evaluation, the AD FS server can itself be a domain controller, but this is not a recommended production configuration!
  • Create a friendly DNS name for AD FS and point it to your adfs server. In this article, we'll use adfs.testzone.local.
  • Download and install AD FS 2.0. This will automatically install other pre-requisite Windows components such as IIS.
  • In the IIS manager create an SSL certificate for your friendly DNS name. If you have the IIS 6.0 resource kit, you can use SelfSSL to create a self-signed certificate.
  • Run through the AD FS Server configuration wizard
    • Create a new Federation Service
    • Select 'Stand-alone Server'
    • Select the certificate that you created for your friendly DNS name
  • You may encounter an error when the installer registers a service principal name (SPN). In this case, manually create a Kerberos SPN for the DNS name so that Integrated Windows Authentication between the browser and the AD FS IIS instance works correctly:
setspn -a HOST/adfs.testzone.local testzone\ADFSSVR01
setspn -a HOST/adfs testzone\ADFSSVR01

For more info on Kerberos SPNs see Active Directory and Kerberos SPNs Made Easy.

Configuration

To build a federation between two parties we need to establish a trust relationship by exchanging metadata. The metadata for the AD FS 2.0 instance is entered manually into the Force.com configuration. Force.com metadata is downloaded as an XML file which AD FS 2.0 can consume.

Force.com Configuration

There are two things to configure - the domain, and the SAML 2.0 setup.

My Domain

The Force.com 'My Domain' feature allows you to select a custom domain name for your application. A 'My Domain' URL looks like https://customer.my.salesforce.com/ (for a production org) or https://customer-developer-edition.my.salesforce.com/ (for a Developer Edition).

A benefit of configuring 'My Domain' is that it enables support for SP-initiated single sign-on, improving the user experience, and allowing users to access 'deep links' into their environment via SSO.

Configure 'My Domain' in Setup | Company Profile | My Domain. You will need to complete the process of configuring, testing and deploying 'My Domain' for SP-initiated SSO to work correctly.

SAML 2.0

In the AD FS 2.0 MMC snap-in select the certificates node and double click the token-signing certificate to view it.

ADFSExportCertificate.jpg

Click the 'Details' tab then 'Copy to File'. Save the certificate in DER format.

On the AD FS server browse to your federation metadata URL which can be found in the AD FS MMC at Service|Endpoints|Metadata|Type:Federation Metadata. In the example it is https://adfs.testzone.local/FederationMetadata/2007-06/FederationMetadata.xml.

ADFSGetEntityID.jpg

Copy the value of the entityID attribute. In the example, it is http://adfs.testzone.local.

In Force.com, navigate to Setup | Security Controls | Single Sign-On Settings

ADFSSalesforceConfig.jpg

Configure the settings as follows:

  • SAML Enabled: Checked
  • SAML Version: 2.0
  • Identity Provider Certificate: Browse and select the token-signing certificate you exported earlier
  • Issuer: Paste your entityID in here
  • Identity Provider Login URL: This is the URL of your AD FS SAML endpoint, to which Force.com will send SAML requests for SP-initiated login. This can be found in the AD FS MMC at Endpoints|Token Issuance|Type:SAML 2.0/WS-Federation (In the example, it is https://adfs.testzone.local/adfs/ls/ - note, you must include the slash at the end of the URL!)
  • Identity Provider Logout URL: You can configure a URL to which the user will be sent after they log out - for example, http://intranet.mycompany.com/.
  • SAML User ID Type: To log a user in we can either match against either their Force.com username or their federation ID, which would need to be populated in the profile of every user. For testing select federation ID. If your users use their email address as their Force.com username then when you come to roll out SSO into production you can switch to matching against username.
  • SAML User ID Location: To log the user in we can use either the NameID in the SAML assertion or another attribute. We can use NameID, since AD FS will populate NameID in the SAML Assertion.
  • Service Provider Initiated Request Binding: It's recommended you choose "HTTP Redirect"
  • Entity ID: This is how our AD FS IdP will identify the Salesforce SP. Although you can choose either https://saml.salesforce.com or an endpoint based on your 'My Domain' URL, it's worth configuring 'My Domain' as mentioned above and selecting that entity ID, as this will enable SP-initiated SSO.

Save the settings and download the metadata xml file.

AD FS 2.0 Configuration

Now that we have the metadata for Force.com we can create the AD FS side of the trust relationship.

Open the AD FS 2.0 MMC snapin and add a new “Relying Party Trust”:

  • Select Data Source: Import data about a relying party from a file. Browse to the XML you downloaded from Force.com
  • Display Name: Give the trust a display name e.g. ‘Salesforce Test’
  • Choose Issuance Authorization Rules: Permit all users to access this relying party
  • Open Edit Claim Rules Dialog: Checked

In the claim rules editor select the “Issuance Transform Rules” tab

Add a new rule:

ADFSClaimRule.jpg

  • Claim Rule Template: Send LDAP Attributes as Claims
  • Claim Rule Name: For testing we’ll send the User Principal Name (UPN) as NameID so call the rule: “Send UPN as NameID”. In production you might send the user’s email address or employee ID. It's important to use an attribute with a value that is unlikely to change over time, as any change will break SSO for that user.
  • LDAP Attribute: User Principal Name
  • Outgoing Claim Type: Name ID

SP-Initiated Login

With IdP-initiated login you typically set up a link on the company intranet that users would click to get access to Force.com. SP-initiated login happens when a user clicks a direct link to Force.com.

If you configured a 'My Domain' entity ID in the Force.com SAML settings, for example, https://testinfo-developer-edition.my.salesforce.com, users can go to URLs in that domain and be automatically redirected to AD FS for authentication.

For SP-initiated login to work, we need to set AD FS' Secure Hash Algorithm parameter to SHA-1, since Force.com uses the SHA-1 algorithm when signing SAML requests, and AD FS defaults to SHA-256.

This is set in AD FS' trust properties for the Force.com relying party under 'Advanced'.

ADFSSecureHashSetting.jpg

If you don’t set this you’ll get the following (slightly misleading) message in to the AD FS event log:

Event ID: 378
SAML request is not signed with expected signature algorithm. SAML request is signed with signature algorithm http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 . Expected signature algorithm is http://www.w3.org/2000/09/xmldsig#rsa-sha1

Testing

We can now set the Federation ID of a Force.com user to the UPN of our own AD user account and see if it all works.

ADFSUserProfileFedID.jpg

For SP-initiated login, assuming you configured a 'My Domain' entity ID, you can just go straight to it, e.g. https://testinfo-developer-edition.my.salesforce.com.

For IdP-initiated login, you will need to use the AD FS login URL and specify the loginToRp parameter as the Force.com SAML entity ID, e.g. https://adfs.testzone.local/adfs/ls/idpinitiatedsignon.aspx?loginToRp=https://saml.salesforce.com

In either case, the browser should follow a chain of redirects, ultimately logging you in to your application on Force.com. If you get a Force.com login error use the SAML assertion validator tool on the Force.com single sign-on configuration page. It will display the results of the last failed SAML login.

ADFSSAMLValidator.jpg

If you get an error from AD FS then check the AD FS logs in Server Manager\Diagnostics\Applications and Services Logs\AD FS 2.0\Admin. There is also a very good MSDN blog post on AD FS 2.0 diagnostics.

If you configured a 'My Domain' entity ID, then SP-initiated login will work for 'deep-links'. Bookmark a link from deep inside Force.com then log out. Reload your browser and select the bookmark. You should be seamlessly redirected to your IdP, authenticated and then redirected back to the bookmarked link.

Analysis

In case you’re wondering how the browser collects and passes these SAML requests and responses, let's take a closer look at the entire process.

We’ll go over the SP-Initiated login because it has the most steps and really demonstrates SAML and federation at its best. The HTTP protocol messages show you exactly what’s happening at each step. You can use a tool such as ieHTTPheaders or Fiddler2 to capture these messages for yourself, but note that Fiddler2 interferes with Integrated Windows Authentication to IIS so you’ll need to turn off extended protection on the /adfs/ls/ virtual directory if you want to try this, otherwise your browser won’t authenticate with AD FS and you’ll see event 4625 with error 0xc000035b in the Windows security log on the AD FS server.

In the interests of clarity, some extraneous HTTP headers are omitted and long strings of base 64 encoded data and sensitive identifiers are replaced by ellipses.

Step 1

The user clicks a deep link to a Force.com page; in our example, it's https://customer-developer-edition.my.salesforce.com/home/home.jsp. The browser requests the page and Force.com renders a page containing JavaScript to redirect the browser to the Force.com SAML request generator.

GET https://customer-developer-edition.my.salesforce.com/home/home.jsp HTTP/1.1
Host: customer-developer-edition.my.salesforce.com
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1432

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
<script>
var escapedHash = '';
var url = '/saml/authn-request.jsp?RelayState=%2Fhome%2Fhome.jsp&saml_request_id=_29...nXQ&saml_acs=https%3A%2F%2Flogin.salesforce.com%2F%3Fsaml%3DEK0...a0M%3D&saml_binding_type=HttpPost&Issuer=https%3A%2F%2Fcustomer-developer-edition.my.salesforce.com&ssostartpage=https%3A%2F%2Fadfs.testzone.local%2Fadfs%2Fls%2F';
if (window.location.hash) {
   escapedHash = '%23' + window.location.hash.slice(1);
}
if (window.location.replace){ 
window.location.replace(url + escapedHash);
} else {;
window.location.href = url + escapedHash;
} 
</script>
</head>
</html>

The SAML request generator creates a SAML request for the IdP by sending an HTML form with hidden fields back to the browser.

	GET https://customer-developer-edition.my.salesforce.com/saml/authn-request.jsp?RelayState=%2Fhome%2Fhome.jsp&saml_request_id=_29...nXQ&saml_acs=https%3A%2F%2Flogin.salesforce.com%2F%3Fsaml%3DEK0...a0M%3D&saml_binding_type=HttpPost&Issuer=https%3A%2F%2Fcustomer-developer-edition.my.salesforce.com&ssostartpage=https%3A%2F%2Fadfs.testzone.local%2Fadfs%2Fls%2F HTTP/1.1
	Host: customer-developer-edition.my.salesforce.com

It then uses JavaScript to automatically submit the form to the IdP SAML endpoint. Note the text in the <noscript> element instructing the user to click the 'Continue' button to proceed.

HTTP/1.1 200 OK
Content-Length: 6310

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <body onload="document.forms[0].submit()">
        <noscript>
            <p>
                <strong>Note:</strong> Since your browser does not support JavaScript,
                you must press the Continue button once to proceed.
            </p>
        </noscript>
        <form action="https://adfs.testzone.local/adfs/ls/" method="post">
            <div>
                <input type="hidden" name="RelayState" value="/home/home.jsp"/>                
                <input type="hidden" name="SAMLRequest" value="PD9...0Pg=="/>                
            </div>
            <noscript>
                <div>
                    <input type="submit" value="Continue"/>
                </div>
            </noscript>
        </form>

    </body>
</html>

We can decode the SAMLRequest using a tool such as the SAML 2.0 Debugger:

<?xml version="1.0" encoding="UTF-8"?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="https://login.salesforce.com/?saml=EK0...a0M=" Destination="https://adfs.testzone.local/adfs/ls/" ID="_29...nXQ" IssueInstant="2011-05-12T00:38:14.498Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0">
	<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://customer-developer-edition.my.salesforce.com</saml:Issuer>
	<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
		<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
			<ds:CanonicalizationMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
			<ds:SignatureMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
			<ds:Reference xmlns:ds="http://www.w3.org/2000/09/xmldsig#" URI="#_293...nXQ">
				<ds:Transforms xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
					<ds:Transform xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
					<ds:Transform xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
						<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="ds saml samlp"/>
					</ds:Transform>
				</ds:Transforms>
				<ds:DigestMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
				<ds:DigestValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#">tM59k5o2VJ0FzCCv/i9mEqmZJWE=</ds:DigestValue>
			</ds:Reference>
		</ds:SignedInfo>
		<ds:SignatureValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
SqR...RVg=
</ds:SignatureValue>
		<ds:KeyInfo>
			<ds:X509Data>
				<ds:X509Certificate>MII...r4=</ds:X509Certificate>
			</ds:X509Data>
		</ds:KeyInfo>
	</ds:Signature>
</samlp:AuthnRequest>

Step 2

The browser submits the HTML form containing the SAML request to the AD FS SAML endpoint:

POST https://adfs.testzone.local/adfs/ls/ HTTP/1.1
Host: adfs.testzone.local
Content-Length: 5504

RelayState=%2Fhome%2Fhome.jsp&SAMLRequest=PD9...0Pg%3D%3D

Since we are using Integrated Windows Authentication, AD FS redirects the browser to the /auth/integrated/ directory:

HTTP/1.1 302 Found
Content-Type: text/html; charset=utf-8
Location: /adfs/ls/auth/integrated/
Content-Length: 152

<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href="%2fadfs%2fls%2fauth%2fintegrated%2f">here</a>.</h2>
</body></html>

Finally, the user is authenticated using Integrated Windows Authentication, comprising several HTTP request/response exchanges, and AD FS serves up a SAML response.

GET https://adfs.testzone.local/adfs/ls/auth/integrated/ HTTP/1.1
Host: adfs.testzone.local
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
Content-Length: 6320

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
	<!--
	    ...Content to render in browsers that do not support Integrated Windows Authentication... 
	--> 
</html> 
GET https://adfs.testzone.local/adfs/ls/auth/integrated/ HTTP/1.1
Authorization: Negotiate YHM...01Z
Host: adfs.testzone.local
HTTP/1.1 401 Unauthorized
Content-Type: text/html; charset=us-ascii
WWW-Authenticate: Negotiate oYH...AAA=
Content-Length: 341

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML>
	<!--
	    ...Content to render in browsers that do not support Integrated Windows Authentication... 
	-->
</HTML>
GET https://adfs.testzone.local/adfs/ls/auth/integrated/ HTTP/1.1
Host: adfs.testzone.local
Authorization: Negotiate oXc...AAA==

Again, the SAML message is returned to the browser in an HTML form which is then submitted to the Force.com SAML endpoint using JavaScript.

HTTP/1.1 200 OK
WWW-Authenticate: Negotiate oRs...AAA=
Content-Length: 6113

<?xml version="1.0"?>
<html>
	<head>
		<title>Working...</title>
	</head>
	<body>
		<form method="POST" name="hiddenform" action="https://login.salesforce.com/?saml=EK0...a0M=">
			<input type="hidden" name="SAMLResponse" value="PHN...2U+"/>
			<input type="hidden" name="RelayState" value="/home/home.jsp"/>
			<noscript>
				<p>Script is disabled. Click Submit to continue.</p>
				<input type="submit" value="Submit"/>
			</noscript>
		</form>
		<script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script>
	</body>
</html>

Decoding the SAML response (note the UPN in the NameID element):

<?xml version="1.0"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_cf6d08f1-96cc-4797-a12e-3963ba7aa312" Version="2.0" IssueInstant="2011-05-12T00:38:15.483Z" Destination="https://login.salesforce.com/?saml=EK0...a0M=" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_293...QnXQ">
	<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://adfs.testzone.local/adfs/services/trust</Issuer>
	<samlp:Status>
		<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
	</samlp:Status>
	<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_a2e919ee-d87f-4559-adf0-2d2f102422a6" IssueInstant="2011-05-12T00:38:15.483Z" Version="2.0">
		<Issuer>http://adfs.testzone.local/adfs/services/trust</Issuer>
		<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
			<ds:SignedInfo>
				<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
				<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
				<ds:Reference URI="#_a2e919ee-d87f-4559-adf0-2d2f102422a6">
					<ds:Transforms>
						<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
						<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
					</ds:Transforms>
					<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
					<ds:DigestValue>WljRNawSZlEKsdBtVxI4Un+hNU4=</ds:DigestValue>
				</ds:Reference>
			</ds:SignedInfo>
			<ds:SignatureValue>LRx...5Xg==</ds:SignatureValue>
			<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
				<ds:X509Data>
					<ds:X509Certificate>MII...G8Q==</ds:X509Certificate>
				</ds:X509Data>
			</KeyInfo>
		</ds:Signature>
		<Subject>
			<NameID Format="http://schemas.xmlsoap.org/claims/UPN">[email protected]</NameID>
			<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
				<SubjectConfirmationData InResponseTo="_293...83Z" Recipient="https://login.salesforce.com/?saml=EK0...a0M="/>
			</SubjectConfirmation>
		</Subject>
		<Conditions NotBefore="2011-05-12T00:38:15.479Z" NotOnOrAfter="2011-05-12T01:38:15.479Z">
			<AudienceRestriction>
				<Audience>https://customer-developer-edition.my.salesforce.com</Audience>
			</AudienceRestriction>
		</Conditions>
		<AuthnStatement AuthnInstant="2011-05-12T00:38:15.390Z">
			<AuthnContext>
				<AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef>
			</AuthnContext>
		</AuthnStatement>
	</Assertion>
</samlp:Response>

Step 3

The browser submits the HTML form which contains the SAML response to the Force.com SAML endpoint which verifies the SAML assertion, logs the user in and redirects the browser to the original requested URL.

POST https://login.salesforce.com/?saml=EK03Almz90BDRtlxL1Gg9zaFYSzNQ6PtNIa0M= HTTP/1.1
Host: login.salesforce.com
Content-Length: 5687

SAMLResponse=PHN...2U%2B&RelayState=%2Fhome%2Fhome.jsp
HTTP/1.1 302 Found
Server: 
Location: https://customer-developer-edition.my.salesforce.com/secur/frontdoor.jsp?sid=00D...ZWf&retURL=%2Fhome%2Fhome.jsp&loginURL=https%3A%2F%2Fsaml.salesforce.com%3Fssostartpage%3Dhttps%253A%252F%252Fadfs.testzone.local%252Fadfs%252Fls%252F%26saml_request_id%3D_2S...0hg&cshc=000...Z3Z
Set-Cookie: login=[email protected]; domain=.salesforce.com; path=/; expires=Mon, 11-Jul-2011 00:38:17 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 652

The URL has moved <a href="https://customer-developer-edition.my.salesforce.com/secur/frontdoor.jsp?sid=00D...ZWf&retURL=%2Fhome%2Fhome.jsp&loginURL=https%3A%2F%2Fsaml.salesforce.com%3Fssostartpage%3Dhttps%253A%252F%252Fadfs.testzone.local%252Fadfs%252Fls%252F%26saml_request_id%3D_2S...0hg&cshc=000...Z3Z">here</a>
GET https://customer-developer-edition.my.salesforce.com/secur/frontdoor.jsp?sid=00D...ZWf&retURL=%2Fhome%2Fhome.jsp&loginURL=https%3A%2F%2Fsaml.salesforce.com%3Fssostartpage%3Dhttps%253A%252F%252Fadfs.testzone.local%252Fadfs%252Fls%252F%26saml_request_id%3D_2S...0hg&cshc=000...Z3Z HTTP/1.1
Host: customer-developer-edition.my.salesforce.com
Cookie: login=[email protected]
HTTP/1.1 302 Found
Location: https://customer-developer-edition--c.na3.content.force.com/secur/contentDoor?startURL=https%3A%2F%2Fcustomer-developer-edition.my.salesforce.com%2Fhome%2Fhome.jsp&sid=00D...CBL&skipRedirect=1&lm=Vv_...JUQ%3D%3D
Set-Cookie: sid=00D...QLn; path=/; secure
Set-Cookie: oid=00D...Z3Z; path=/; secure; expires=Sat, 11-May-2013 00:38:17 GMT
Content-Type: text/html
Content-Length: 367

The URL has moved <a href="https://customer-developer-edition--c.na3.content.force.com/secur/contentDoor?startURL=https%3A%2F%2Fcustomer-developer-edition.my.salesforce.com%2Fhome%2Fhome.jsp&sid=00D...CBL&skipRedirect=1&lm=Vv_...JUQ%3D%3D">here</a>
GET https://customer-developer-edition--c.na3.content.force.com/secur/contentDoor?startURL=https%3A%2F%2Fcustomer-developer-edition.my.salesforce.com%2Fhome%2Fhome.jsp&sid=00D...CBL&skipRedirect=1&lm=Vv_...JUQ%3D%3D HTTP/1.1
Host: customer-developer-edition--c.na3.content.force.com
HTTP/1.1 200 OK
Set-Cookie: sid=00D...CBL; path=/; secure
Content-Type: text/html; charset=UTF-8

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
<script  src="/static/102010/js/ClientHash.js" type="text/javascript"></script><script  type="text/javascript">ClientHash.prototype.needsClientHash('sid_Client', '000...Z3Z', '24.5.122.10', '/servlet/servlet.ClientHashValidator?ResponseRequestedURL=%2Fsecur%2FcontentDoor%3FstartURL%3Dhttps%253A%252F%252Fsuperpat-developer-edition.my.salesforce.com%252Fhome%252Fhome.jsp%26sid%3D00D...CBL%26skipRedirect%3D1%26lm%3DVv_...JUQ%253D%253D');
</script><script type='text/javascript'>
var escapedHash = '';
var url = 'https://superpat-developer-edition.my.salesforce.com/home/home.jsp';
if (window.location.hash) {
   escapedHash = '%23' + window.location.hash.slice(1);
}
if (window.location.replace){ 
window.location.replace(url + escapedHash);} else {;
window.location.href = url + escapedHash;} 
</script>
</head>
<body>
<noscript>
You do not have Javascript enabled. Javascript is required to use Force.com. Please enable Javascript, then click <a href="https://superpat-developer-edition.my.salesforce.com/home/home.jsp">here</a> to continue.
</noscript>
</body>
</html>
GET https://customer-developer-edition.my.salesforce.com/home/home.jsp HTTP/1.1
Host: customer-developer-edition.my.salesforce.com
Cookie: login=[email protected]; sid=00D...QLn; oid=00D...Z3Z
HTTP/1.1 200 OK
Server: 
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Content-Length: 65192
Date: Thu, 12 May 2011 00:38:19 GMT

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html class="ext-strict">
	<!-- ...Page content... -->
</html>

Common Issues & Troubleshooting

Here are some of the issues you might com across. Thanks to everyone who commented on the original blog post. If you encounter a new issue, you can leave a comment there and we'll update this section.

Federation ID is case sensitive

One thing to watch out for is that the Federation ID is case-sensitive. If this is your organizational email address, be sure to enter it exactly as AD FS sends it, or Force.com won’t be able to find the matching user.

Unfortunately it’s not possible to write a custom claim rule to normalize the case of the LDAP attribute before sending it since the claims language doesn’t seem to have any string manipulation except a basic regular expression replace.

Assertion Expired

Assertions with a timestamp more than five minutes old will be rejected.

Note: Force.com does make an allowance of three minutes for clock skew. This means, in practice, that an assertion can be as much as eight minutes past the timestamp time, or three minutes before it. This amount of time may be less if the assertion’s validity period is less than five minutes.

Ensure your AD FS server's system clock is synchronized to a good internet time source using Network Time Protocol (NTP).

Login at Force.com

If a configuration error prevents you from from logging in to Force.com via SSO, you can still log in via username and password by appending ?login to the login URL - for example https://login.salesforce.com/?login or https://testinfo-developer-edition.my.salesforce.com/?login. After logging in you can disable SSO if necessary while you troubleshoot the issue.

Summary

Applications running on the Force.com platform have access to SAML 2.0 cross-domain Web single sign-on, as does Microsoft Active Directory Federation Services 2.0. This article demonstrates how to configure the two systems to enable seamless SSO from the Windows desktop to Force.com without the need for any additional third-party products.

References

About the Authors

Rhys Goodwin is a senior network engineer working for St John in New Zealand where he actively manages systems and designs and implements solutions. Rhys considers himself a general practitioner in the IT world, getting his hands dirty on a daily basis with just about any technology you can think of - virtualization, storage, networking and authentication, to name a few of his favorites.

Pat Patterson is a relatively recent addition to the Developer Evangelism team at salesforce.com. Describing himself as an 'articulate techie', Pat hacks all manner of code from Ruby web apps down to Linux kernel drivers, writing it all up on the Force.com blog, his own blog Superpatterns, tweeting along the way.