The CreatorCon Call for Content is officially open! Get started here.

SSO SAML 2.0: On-Demand Login/User Provisioning

enojardi
ServiceNow Employee
ServiceNow Employee

Overview

Following somewhat similar principles as the LDAP On-Demand Login functionality, this extension to the SAML 2.0 plugin allows the configuration of auto user provisioning. Out of the box, the SAML2.0 plugin requires that the user record exists in ServiceNow for authentication to be successful.

Detailed Explanation


This solution leverages the AttributeStatement element found in the SAML Assertion Response (XML), once authentication takes place. Most IdPs embed user attributes in the AttributeStatement element. This solution parses all the attribute-value pairs and then creating an import set. A Transform Map can then be defined to create/update the user in the sys_user (target) table. All of this happens before the Installation Exit performs the query on the user table to see if a user is found.

Technical Components


  • (Script Include) SAML2_UserProvisioning: This is the main class used for parsing the XML Response.
  • (Script Include) ImportSetUtilExtended: Extension of the out of the box ImportSetUtil Script Include.
  • (Installation Exit) SAML2SingleSignon_update1: Extension of the existing SAML2.0 Installation Exit (Note: only 4 lines of code were added from the original)
  • (Import Set Table) u_imp_saml_user: This is where all the staging data parsed from the XML will be saved
  • (Transform Map) Import SAML User: Transform map used for transforming the user data
  • ... And a few little things not worth mentioning...

If you want to watch a demo video on how it works, I have uploaded a short 2 min video here.

Steps to Implement


Attached is the update set which contains all the required configuration. Very simple configuration is required after installed. It goes without saying that the SAML2.0_update1 plugin must be installed prior to loading the update set.

Important: In Eureka patch 5, there's been an upgrade to the SAML2_update1 Script Include. This requires some small modifications to the extended Installation Exit included in the update set. Contact me if you get stuck. I will upload a new version once Fuji goes GA.

Some important things to note/configure after installation:

  1. The update set will create an import set table called Import SAML User (u_imp_saml_user)
  2. The import set table will not contain any fields initially (apart from the fields already inherited from sys_import_set_row)
  3. Familiarise yourself with the Properties page and ensure no changes are needed for the two properties
  4. For the first ever login, ensure the user already exists in ServiceNow. This will allow for the new fields to be added* to the import set table dynamically (that is, it will depend on how many Attributes are included in the AttributeStatement)
  5. The first login may take a couple of extra seconds as the import set table will create the new fields
  6. After first login, you're ready to map fields in the "Import SAML User" Transform Map, since the data should have been loaded already.
  7. Ensure you use the same field for coalescing as the field you're using to match the user in the SAML Properties page
  8. I recommend mapping at least the user_name and email in the Transform Map, otherwise it will be blank for new users.
  9. New users created will have a random password set - this is to avoid local login if external authentication is not enforced (or if accessing side_door.do)


* If fields are not added, it could be because the AttributeStatement does not follow standard SAML format. You may need to tweak the Script Include "SAML_UserProvisioning". There are some comments/instructions in the _process method.


Bonus: Override NameId Policy

I've found that a few IdPs do not provide a persistent or unique key in the NameId Policy element. Instead, they provide a variable string (transient), which can sometimes be a sessionID. As you may have guessed, this value could never be used to match a user in ServiceNow, as the value will always be different. If for some X reason, you cannot change this, I have included some functionality to allow you to override the NameID policy. This works by providing the "Name" of the Attribute (from the AttributeStatement) you want to use, instead. To configure this is very simple and can be done through this new System Property: glide.authenticate.sso.saml2.nameid_policy_override.


Things to Remember


This is a custom solution. That being said, I have tested it successfully with 3 different IdPs: SSOCircle, Microsoft ADFS and OpenIdP - the last one even needing the NameId override. You could also apply the same logic from this solution for Unencrypted HTTP Header and Digest Token SSO methods as described in this other post here.

Message was edited by: Jonatan Jardi

1 ACCEPTED SOLUTION

Lawrence D
Giga Contributor

For us, this breaks SAML deep linking.



'action' looks to be overwritten by the user import.   I updated the installation exit script, SAML2SingleSignon_update1 with:



/*


* {action} is changed by the auto provisioning process and breaks SAML deep linking, store it.


* Changes from the object com.glide.processors.LoginAction to a string, e.g. 'update'.


*/


this._action = action;




/*Auto Provisioning changes */


sup.loadUserDetails();


/*Auto Provisioning changes */




if (action && (JSUtil.type_of(action) === 'string')) {


  action = this._action;


}


View solution in original post

45 REPLIES 45

Thanks Jonatan,


We tried out your code and that works just fine, we also came up with something that works as well.   Can you please let me know which one would be better to use?   Both of these are working with deep linking as far as I can tell.   Thanks for all of your help on this issue.   Do you know why the code broke with the Fuji release?


Brian



Our code that works:


/* Begin Auto Provisioning changes */


this._action = action;


   


sup.loadImportSet();



if ( JSUtil.notNil( action ) && JSUtil.type_of( action ) === 'string' ) {  


  action = this._action;


/* End Auto Provisioning Changes */



Jonatan's code that was verified as well:


/*Auto Provisioning changes */


      this._action = action;


      sup.loadImportSet();


      action = this._action;  


/*Auto Provisioning changes */


We've only updated one non-production instance to Fuji, but it appears the action variable is no longer global.   Previously, the action variable was overwritten by the following line:



sup.loadUserDetails();



If it's not global and not overwritten, then it no longer needs to be stored temporarily and reassigned.   It should be simple enough to see if it's being overwritten:



gs.log(typeof action, 'SAML debugging');



sup.loadUserDetails();



gs.log(typeof action, 'SAML debugging');



Or something like:


this._action = action; // copy the value of action before user import



sup.loadUserDetails();



gs.log(action === this._action, 'SAML debugging');


enojardi
ServiceNow Employee
ServiceNow Employee

Thanks, guys! I think leaving the code out is the way to go


Hello ldugan,



Forgive my ignorance, but could you elaborate on this?   We are using the Auto Provisioning verson of SAML2SingleSignon_update1 in our instance and after upgrading to fuji, this line has caused it to break:



gs.log("line 102");


if (action && (JSUtil.type_of(action) === 'string')) {   gs.log("line 103");
action = this._action;

      }



It seems to specifically break on this if statement (my debug doesn't even run the "line 103", just the "line 102").   Is this what you ran into as well when upgrading to Fuji?   Did you have to do anything different with this check to make it work, or did you remove it entirely?



Best regards,


Brian



EDIT:   Nevermind, I missed some comments above and see that resolution is just to change that whole section to:



/*Auto Provisioning changes */


      this._action = action;


      sup.loadImportSet();


      action = this._action;  


/*Auto Provisioning changes */


enojardi
ServiceNow Employee
ServiceNow Employee

No worries. I think it may have to do with scoping on the JSUtil script include - see if global.JSUtil.type_of(action) does the trick and let us know 😉