Friday, January 22, 2010

Idp-Initiated SSO to SalesForce.com and AD FS 2.0 in 5 minutes

This post is to demonstrate how you can configure cloud-based applications for federated authentication using AD FS 2.0 RC. Basically, any service provider supporting SAML authentication via IdP-initiated SSO or SP-initiated SSO profiles. In Beta 2, Joe and I had to extend this functionality through code but AD FS 2.0 RC eliminates the need for this making it a strictly out-of-box solution.

To get this working, the only thing you’ll need is a workable ADFS 2.0 installation and rights within SalesForce.com to configure SSO. My 5 minute timeframe only factors in the actual configuration work.

SalesForce.com enables SSO at the user level by Profiles. You can create an SSO-enabled profile and test user for your test case. AD requires a valid user which will contain valid assertions (claims) which will map to profile information in SalesForce.com.

To enable SAML SSO in Salesforce.com, do the following steps:

clip_image002

  1. Enable Federated single sign-on using SAML.
  2. Specify the issuer name. For example, https://idp.identityjunkie.com/adfs/services/trust
  3. Upload token signing certificate.
  4. Specify the Username ID Type = Username and Location which will be within the NameIdentifier element of the SAML token.

Salesforce.com provides a SAML Assertion Validation tool you can run against their configurations. This can be extracted from ADFS using Fiddler.

On the ADFS side, create a Relying Party Trust.

  1. There is no exchange of federated metadata, so you’d select Enter data about the relying party manually.
  2. Assign a Display Name.
  3. Select AD FS 2.0 profile
  4. Specify an optional encryption certificate. This is used to encrypt the claims being sent to the RP. For this case, none was used.
  5. You only need to enable support for the SAML 2.0 Web SSO protocol. WS-Federation is not used here. Enter the URL provided by Salesforce.com as the SAML 2.0 SSO Service URL.
  6. Add the relying party trust identifier. This is https://saml.salesforce.com.
  7. Define your Issuance Authorization Rules.
  8. Configure the Claim Rules. Here you would map your issuance transform rules to Send LDAP Attributes as Claims. For example, E-Mail-Addresses > Name ID.

That’s it. As the user, the experience is to initiate the sign-on process at the IdP which is your ADFS server:

https://sts.identityjunkie.com/adfs/ls/IdpInitiatedSignOn.aspx

clip_image004

You can bypass the entire site selection process by using the loginToRp=federation.urn. For example: https://sts.identityjunkie.com/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=https://saml.salesforce.com

This will provide the user with an “auto-login” experience.

Tuesday, January 12, 2010

Append a result to a csv import

Say you are using a CSV file to import changes to AD and get the import file from a non-technical source, say HR. Most likely you don't have the complete DN of the user. What you can do is create a function to return the DN of the user based on some search criteria, then append it to the result which you can pipe to another command to execute.

   1: function format-source {Param($file)
   2: $a = Import-Csv $file
   3: $result = New-Object PSObject
   4: foreach ($i in $a) 
   5: {    
   6:     $dn = find-user $i.Username  Select-Object DN        
   7:     $i  Add-Member -Name "DN" -Value $dn -MemberType NoteProperty
   8:     $result = $i    
   9: }
  10:  
  11: $result 
  12:  
  13: }

Powershell Fun...

I know the Quest cmdlets are out there and in Windows 2008 R2, you have the AD cmdlets; however, in the case you still have to do things manually, it’s good to know how to do things through PS – the long way. Here are some snippets I wrote for my current gig. This one gets the member of a user or group object. Handy if you want to quickly see what a user is a member of.


   1: function get-memberof {Param($name)
   2: $filter = "(samaccountname=$name)"
   3:  
   4: # Use global catalog to query active directory
   5: $dom = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
   6: $objDomain = [ADSI]"GC://$($dom.Name)"
   7:  
   8: $objSearcher = New-Object System.DirectoryServices.DirectorySearcher($objDomain)
   9: $objSearcher.PageSize = 1000
  10: $objSearcher.Filter = $filter
  11: $results = $objSearcher.FindOne()
  12:  
  13: if($results -ne $null)
  14: {
  15:     foreach($i in $results)
  16:     {
  17:         $entry = $i.GetDirectoryEntry()        
  18:         $groups = $entry.memberof
  19:         
  20:         foreach($group in $groups)
  21:         {
  22:             Write-Host $group                                    
  23:         }            
  24:     }        
  25: }
  26: else
  27: {
  28:     $object = "object not found."
  29: }
  30:     return $object
  31: }

Another useful snippet is the ability to update or clear user attributes. Here is use ADSI directly which I can then set which flag I want to use to depending on the operation. Below are the flags.



   1: [int] $ADS_PROPERTY_CLEAR = 1
   2: [int] $ADS_PROPERTY_UPDATE = 2
   3: [int] $ADS_PROPERTY_APPEND = 3
   4: [int] $ADS_PROPERTY_DELETE = 2



   1: function update-user {Param($adspath,$title,$description)
   2:  
   3: $user = [ADSI]"$adspath"
   4: $user.Put("title",$title)
   5: $user.Put("description",$description)
   6: $user.SetInfo()    
   7: Write-Host "Updating object successfully."
   8:  
   9: }

I’ve never been one to be dependent on third-party plug-ins….yes, I know Quest has cool cmdlets for this. But doing it yourself is still way cooler. Search users using System.DirectoryServices.


   1: function find-user{Param($user)
   2:  
   3: $filter = "(&(objectclass=user)(samaccountname=$user))"
   4:  
   5: # Specify seach domain or directly query a global catalog
   6: #$dn = 'LDAP://dc=dogfood,dc=identityjunkie,dc=com'
   7: #$objDomain = New-Object system.DirectoryServices.DirectoryEntry($dn)
   8:  
   9: # Use global catalog to query active directory
  10: $dom = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
  11: $objDomain = [ADSI]"GC://$($dom.Name)"
  12:  
  13: $objSearcher = New-Object System.DirectoryServices.DirectorySearcher($objDomain)
  14: $objSearcher.PageSize = 1000
  15: $objSearcher.Filter = $filter
  16: $results = $objSearcher.FindOne()
  17:  
  18: if($results -ne $null)
  19: {
  20:     foreach($i in $results)
  21:     {
  22:         $entry = $i.GetDirectoryEntry()        
  23:         
  24:         $hash = @{                
  25:             ObjectCategory = $entry.objectcategory
  26:             ObjectClass = $entry.objectclass            
  27:             DN = $entry.distinguishedname.ToString()        
  28:             FirstName = $entry.givenname.ToString()
  29:             LastName = $entry.sn.ToString()
  30:             Initials = $entry.initials.ToString()
  31:             Username = $entry.samaccountname.ToString()
  32:             DisplayName = $entry.displayname.ToString()
  33:             Upn = $entry.userprincipalname.ToString()
  34:             Email = $entry.mail.ToString()
  35:             Title = $entry.title.ToString()
  36:             Department = $entry.department.ToString()
  37:             Description = $entry.description.ToString()
  38:             EmployeeID = $entry.employeeid.ToString()
  39:             UserAccountControl = $entry.useraccountcontrol.ToString()
  40:         }        
  41:     }        
  42: }
  43: else
  44: {
  45:     $hash = @{        
  46:          ErrLog = " $user does not exist in directory.`n"     
  47:         }        
  48: }
  49:     $user = New-Object PSObject -Property $hash    
  50:     return $user
  51: }

Note, on line 24 I’m using a hash table to build out my psObject which makes life easier in powershell 2.0.