What is Single Sign-On?
Single Sign-On is a process that allows network users to access all authorized network resources without having to separately log in to each resource. Single Sign-On also enables your organization to integrate with an external identity management system or perform web-based single sign-on to Force.com. Single Sign-On enables multiple types of authentication integration, but the most common are:
- Use an existing directory of user names and passwords, such as Active Directory or LDAP, for Salesforce users.
- Allow seamless sign-on to Force.com applications, eliminating the need for explicit user log on actions
Advantage of Single Sign-On
Single Sign-On produces benefits in three main areas – reduction in administrative costs, increased ease of use and better implementation of security schemes.
- Reduced Administrative Costs: With Single Sign-On, all user authentication information resides in a central directory, which reduces the need to maintain, monitor and potentially synchronized multiple stores, as well as reducing user support requests around passwords.
- Increased ease of use: Each user only has a single username and password which grants them access to corporate resources and Salesforce. Reduced complexity means an easier to use environment that provides seamless access to all resources. Single Sign-On also saves users time, since each individual sign-on process can take 5 to 20 seconds to complete. And removing the extra step of logging into Salesforce can increase user adoption of your Salesforce applications by lowering the barrier to use.
- Increased Security: Any password policies that you have established for your corporate network will also be in effect for Salesforce. In addition, sending an authentication credential that is only valid for a single use can increase security for users who have access to sensitive data.
How Single Sign-On Works?
The high-level process for authenticating users via Single Sign-On is as follows:
- When a user tries to log in—either online or using the API—Salesforce validates the username and checks the user’s profile settings.
- If the user’s profile has the “Uses Single Sign-on” user permission, then Salesforce does not authenticate the username with the password. Instead, a Web Services call is made to the user’s single sign-on service, asking it to validate the username and password.
- The Web Services call passes the username, password, and sourceIp to a Web Service defined for your organization. (sourceIp is the IP address that originated the login request). You must create and deploy an implementation of the Web Service that can be accessed by Salesforce.com servers.
- Your implementation of the Web Service validates the passed information and returns either “true” or “false.”
- If the response is “true,” then the login process continues, a new session is generated, and the user proceeds to the application. If “false” is returned, then the user is informed that his or her username and password combination was invalid.
Single Sign-On Configuration
Step 1:
- From Setup, click “Security Controls | Single Sign-On Settings”, then click Federated Single Sign-On Using SAML Edit.
- Select the SAML Enabled check box.
Step 2:
- Click ‘New’ to fill out Single Sign On settings.
- Use the following settings:
- Name: Put the name of SSO
- SAML Version: 2.0
- API Name: Will auto populate as per the Name field
- Issuer: (The issuer can be found in SSO form provided by External System team) Issuer is nothing but domain URL of Identity provider Org.
- Entity Id: It would be the URL of the target org.
- Identity Provider Certificate: Upload the Identity provider certificate from the Auth. Single Sign On server.
- Request Signing Certificate: Default Certificate
- Request Signature Method: RSA-SHA1
- SAML Identity Type: Select – Assertion contains the Federation ID from the User object.
- SAML Identity Location: Identity is in the NameIdentifier element of the Subject statement.
- Service Provider Initiated Request Binding: HTTP Redirect
- Identity Provider Login URL: (External system login page) Parameter of your application)
- Identity Provider Logout URL: (External system site logout page) Parameter of your application.
The input fields in the image should be entered as described in above.
Step 3:
- From Setup, click Domain Management | My Domain – Enter a new subdomain name, and click Check Availability. If the name is available, click the Terms and Conditions check box, then click Register Domain.
Step 4:
- Test App sign on option should be visible on the login page of the target org. So, we need to enable it in Authentication configuration. From Setup, click Domain Management | My Domain | Edit – Authentication Configuration | Enable Test App. After enable it we can see the Test App link in Salesforce Login page. Click it and once Test App portal shows up, login using Test App Credentials.
Step 5:
- To tie External System to SFDC User account, the system admin needs to know the unique id associated with External System ID that gets passed by SAML assertion. As of now, only way to get this is via reading SAML assertion as noted above and this can cause a bad user experience. The best way to approach this issue is via creating new SFDC user account whenever a use wants to setup sync between his or her External System ID and SFDC account. On the context of on-boarding, this means that the user would need to get External System ID first prior to getting SFDC account. Once the user creates External System ID, the user can use login using External System ID feature to create a new SFDC account. If the user go through this route, the federation ID field of newly created user will be auto-populated with the entity from SAML message that is specified in SAML single sign on setting. SFDC provides such option under SAML Single Sign-On Settings. It can be found at the bottom of the page.
- To automate sync the External System user to Salesforce we need to write an apex class. And that class must be executed by a user, which user has the permission to create/edit of users.
Sample JIT Handler Class
//This class provides logic for inbound just-in-time provisioning of single sign-on users in your Salesforce organization.
global class SSOUserHandler implements Auth.SamlJitHandler {
private class JitException extends Exception{}
private void handleUser(boolean create, User u, Map<String, String> attributes,
String federationIdentifier, boolean isStandard) {
if(create && attributes.containsKey('User.Username')) {
u.Username = attributes.get('User.Username');
}
if(create) {
if(attributes.containsKey('User.FederationIdentifier')) {
u.FederationIdentifier = attributes.get('User.FederationIdentifier');
} else {
u.FederationIdentifier = federationIdentifier;
}
}
if(attributes.containsKey('User.ProfileId')) {
String profileId = attributes.get('User.ProfileId');
Profile p = [SELECT Id FROM Profile WHERE Id=:profileId];
u.ProfileId = p.Id;
}
if(attributes.containsKey('User.UserRoleId')) {
String userRole = attributes.get('User.UserRoleId');
UserRole r = [SELECT Id FROM UserRole WHERE Id=:userRole];
u.UserRoleId = r.Id;
}
if(attributes.containsKey('User.Phone')) {
u.Phone = attributes.get('User.Phone');
}
if(attributes.containsKey('User.Email')) {
u.Email = attributes.get('User.Email');
}
if(attributes.containsKey('User.FirstName')) {
u.FirstName = attributes.get('User.FirstName');
}
if(attributes.containsKey('FirstName')) {
u.FirstName = attributes.get('FirstName');
}
if(attributes.containsKey('User.LastName')) {
u.LastName = attributes.get('User.LastName');
}
if(attributes.containsKey('User.Title')) {
u.Title = attributes.get('User.Title');
}
if(attributes.containsKey('User.CompanyName')) {
u.CompanyName = attributes.get('User.CompanyName');
}
if(attributes.containsKey('User.AboutMe')) {
u.AboutMe = attributes.get('User.AboutMe');
}
if(attributes.containsKey('User.Street')) {
u.Street = attributes.get('User.Street');
}
if(attributes.containsKey('User.State')) {
u.State = attributes.get('User.State');
}
if(attributes.containsKey('User.City')) {
u.City = attributes.get('User.City');
}
if(attributes.containsKey('User.Zip')) {
u.PostalCode = attributes.get('User.Zip');
}
if(attributes.containsKey('User.Country')) {
u.Country = attributes.get('User.Country');
}
if(attributes.containsKey('User.CallCenter')) {
u.CallCenterId = attributes.get('User.CallCenter');
}
if(attributes.containsKey('User.Manager')) {
u.ManagerId = attributes.get('User.Manager');
}
if(attributes.containsKey('User.MobilePhone')) {
u.MobilePhone = attributes.get('User.MobilePhone');
}
if(attributes.containsKey('User.DelegatedApproverId')) {
u.DelegatedApproverId = attributes.get('User.DelegatedApproverId');
}
if(attributes.containsKey('User.Department')) {
u.Department = attributes.get('User.Department');
}
if(attributes.containsKey('User.Division')) {
u.Division = attributes.get('User.Division');
}
if(attributes.containsKey('User.EmployeeNumber')) {
u.EmployeeNumber = attributes.get('User.EmployeeNumber');
}
if(attributes.containsKey('User.Extension')) {
u.Extension = attributes.get('User.Extension');
}
if(attributes.containsKey('User.Fax')) {
u.Fax = attributes.get('User.Fax');
}
if(attributes.containsKey('User.CommunityNickname')) {
u.CommunityNickname = attributes.get('User.CommunityNickname');
}
if(attributes.containsKey('User.IsActive')) {
String IsActiveVal = attributes.get('User.IsActive');
u.IsActive = '1'.equals(IsActiveVal) || Boolean.valueOf(IsActiveVal);
}
if(attributes.containsKey('User.ReceivesAdminInfoEmails')) {
String ReceivesAdminInfoEmailsVal = attributes.get('User.ReceivesAdminInfoEmails');
u.ReceivesAdminInfoEmails = '1'.equals(ReceivesAdminInfoEmailsVal) || Boolean.valueOf(ReceivesAdminInfoEmailsVal);
}
if(attributes.containsKey('User.ReceivesInfoEmails')) {
String ReceivesInfoEmailsVal = attributes.get('User.ReceivesInfoEmails');
u.ReceivesInfoEmails = '1'.equals(ReceivesInfoEmailsVal) || Boolean.valueOf(ReceivesInfoEmailsVal);
}
if(attributes.containsKey('User.ForecastEnabled')) {
String ForecastEnabledVal = attributes.get('User.ForecastEnabled');
u.ForecastEnabled = '1'.equals(ForecastEnabledVal) || Boolean.valueOf(ForecastEnabledVal);
}
String uid = UserInfo.getUserId();
User currentUser =
[SELECT LocaleSidKey, LanguageLocaleKey, TimeZoneSidKey, EmailEncodingKey FROM User WHERE Id=:uid];
if(attributes.containsKey('User.LocaleSidKey')) {
u.LocaleSidKey = attributes.get('User.LocaleSidKey');
} else if(create) {
u.LocaleSidKey = currentUser.LocaleSidKey;
}
if(attributes.containsKey('User.LanguageLocaleKey')) {
u.LanguageLocaleKey = attributes.get('User.LanguageLocaleKey');
} else if(create) {
u.LanguageLocaleKey = currentUser.LanguageLocaleKey;
}
if(attributes.containsKey('User.Alias')) {
u.Alias = attributes.get('User.Alias');
} else if(create) {
String alias = '';
if(u.FirstName == null) {
alias = u.LastName;
} else {
alias = u.FirstName.charAt(0) + u.LastName;
}
if(alias.length() > 5) {
alias = alias.substring(0, 5);
}
u.Alias = alias;
}
if(attributes.containsKey('User.TimeZoneSidKey')) {
u.TimeZoneSidKey = attributes.get('User.TimeZoneSidKey');
} else if(create) {
u.TimeZoneSidKey = currentUser.TimeZoneSidKey;
}
if(attributes.containsKey('User.EmailEncodingKey')) {
u.EmailEncodingKey = attributes.get('User.EmailEncodingKey');
} else if(create) {
u.EmailEncodingKey = currentUser.EmailEncodingKey;
}
if(!create) {
update(u);
}
else{
Insert u;
}
}
private void handleJit(boolean create, User u, Id samlSsoProviderId, Id communityId, Id portalId, String federationIdentifier, Map<String, String> attributes, String assertion) {
handleUser(create, u, attributes, federationIdentifier);
}
global User createUser(Id samlSsoProviderId, Id communityId, Id portalId,
String federationIdentifier, Map<String, String> attributes, String assertion) {
User u = new User();
handleJit(true, u, samlSsoProviderId, communityId, portalId, federationIdentifier, attributes, assertion);
return u;
}
global void updateUser(Id userId, Id samlSsoProviderId, Id communityId, Id portalId,
String federationIdentifier, Map<String, String> attributes, String assertion) {
User u = [SELECT Id, FirstName, ContactId FROM User WHERE Id=:userId];
handleJit(false, u, samlSsoProviderId, communityId, portalId, federationIdentifier, attributes, assertion);
}
}
Sample SOAP Message
As part of the Single Sign-On process, a Salesforce.com server makes a SOAP 1.1 request to authenticate the user who is passing in the credentials. Below is an example of this type of request. Your Single Sign-On service needs to accept this request, process it, and return a “true” or “false” response.
Sample Request
<?xml version="1.0" encoding="UTF-8" ?>
<soapenv:Envelope
xmlns:soapenv="<a rel="nofollow" class="external free" href="http://schemas.xmlsoap.org/soap/envelope/">http://schemas.xmlsoap.org/soap/envelope/</a>">
<soapenv:Body>
<Authenticate xmlns="urn:authentication.soap.sforce.com">
<username>testuser@testorg.com</username>
<password>test12345</password>
<sourceIp>1.2.3.4</sourceIp>
</Authenticate>
</soapenv:Body>
</soapenv:Envelope>
Sample Response Message
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="<a rel="nofollow" class="external free" href="http://schemas.xmlsoap.org/soap/envelope/">http://schemas.xmlsoap.org/soap/envelope/</a>">
<soapenv:Body>
<AuthenticateResponse
xmlns="urn:authentication.soap.sforce.com">
<Authenticated>true</Authenticated>
</AuthenticateResponse>
</soapenv:Body>
</soapenv:Envelope>
Sample SAML Assertion for Just-In-Time Provisioning
<saml:AttributeStatement>
<saml:Attribute Name="User.Username"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">testuser@testorg.com
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.Phone"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">123-456-7890
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.FirstName"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">Biswajeet
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.LanguageLocaleKey"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">en_US
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.CompanyName"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">ABC
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.Alias"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">Kunal
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.CommunityNickname"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">Kunal
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.UserRoleId"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">000000000000000
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.Title"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">Mr.
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.LocaleSidKey"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">en_CA
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.Email"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">testuser@testorg.com
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name=" User.FederationIdentifier"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">123542
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.TimeZoneSidKey"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">America/Los_Angeles
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.LastName"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">Samal
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.ProfileId"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">00ex0000001pDNP
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.IsActive"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">1
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="User.EmailEncodingKey"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xsi:type="xs:anyType">UTF-8
</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>