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.

Monday, December 14, 2009

Troubleshooting Certificate Issues

For the past few days, we’ve been working on SAML 2.0 interoperability with OIOSAML and had to dig pretty deep to troubleshoot some issues we were running into. OIOSAML is an implementation of a SAML 2.0 compliant service provider for Java and J2EE applications which runs on Apache Tomcat. The issue wasn’t rocket science; however if we could not resolve it, federated authentication couldn’t be enabled for this application. The only error on the SP was:

Stack Trace:

Caused by: dk.itst.oiosaml.sp.model.validation.ValidationException: The response is not signed correctly

at com.sf.sfv4.authentication.saml2.extend.SFSAML2Response.validateResponse(SFSAML2Response.java:97)

at com.sf.sfv4.authentication.saml2.SFSAML2AssertionConsumerHandler.handleSAMLResponse(SFSAML2AssertionConsumerHandler.java:392)

... 45 more

A Google search on the error provided some possible leads to what the problem could be. However, one would immediately assume the issue was within the signing certificate due to the error. Yes, but you’d have to prove it.

When the SP consumes the SAMLResponse from an issuing IDP, the SP checks the IDP metadata file for a valid issuer; or Entity Id. This value should is stored in the local .xml of an STS within the EntityDescriptor element under the Entity ID attribute. For example, when publishing federation metadata in ADFSv2, these values are within the first element:

+ <EntityDescriptor wsu:Id="8e0d3ee9-0865-49c7-9c05-c8c64399757f" entityID="https://xxx.xxxx.com/Trust" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">

Understanding what happens under the hood helps extremely in troubleshooting problems. When an SP (RP-STS) consumes an incoming SAMLResponse, it checks its policy store for a valid entity id, which then tells the token issuance service which certificate to validate the authenticity of the message by digital signature within the response or assertion sections of the SAML token.

However, in my scenario we never exchanged federation metadata other than manually providing parameters because the delivery method was IdP-initiated POST binding; therefore we had to prove the signature was bad. To do so, we basically wrote an application and published it as an RP to our IP-STS. The core functionality of signing an XML document can be referenced in Rebecca Croft’s blog, Apollo Jack using the System.Security.Cryptography namespace and System.Security.Cryptography.XML.

The SAMLResponse you consume can be displayed in a simple web form which you can write code to validate the SAML formatting and digital signature. The message will be encoded in Base64, therefore you’d need to decode it then check the signature.

To decode the message, you can use this method:

   1: public static String decodeMessage(string samlResponse)
   2:         {          
   3:             byte[] encodedDataAsBytes = System.Convert.FromBase64String(samlResponse);
   4:             string decodedSAMLResponse = System.Text.Encoding.UTF8.GetString(encodedDataAsBytes);
   5:             return decodedSAMLResponse;
   6:         }



To check a signature, you’d use the public key portion of the signing certificate and the SignedXml.CheckSignature method. From there, you can be insured your signing certificates should be validated on the RP side.