Over the past few months, I have received some requests by customers to restrict the Basic Authentication access in ServiceNow for some specific situations.
What is Basic Authentication?
Basic Authentication is a very common web authentication method that sends authentication credentials over the HTTP/HTTPS Header. (Read More on Wikipedia)
Basic Authentication is mostly used in ServiceNow with non-user-interactive requests such as HTTPS Fetch requests (incident.do?EXCEL) or via SOAP Requests. Users authenticating through the browser use a separate mechanism altogether.
There are policies and properties built into ServiceNow that let you force ALL basic auth requests to be blocked or to authenticate as a user, etc. However, if you want to block Basic Authentication access for only some people or locations but not others, you have to get your hands dirty with a little customization.
The Scenario
The purpose of this blog is to show you how you can modify your Basic Authentication mechanism in the product to meet these challenges. Please note, however, that you should only modify the default Basic Authentication mechanism if no other option will work for your situation.
The scenario presented in this case is as follows:
We have a number of MID Servers in our organization that currently communicate with our ServiceNow instance. They all use a different user name to authenticate into the instance. We only want these credentials to be used with those specific MID Servers. We want to block any Basic Authentication requests from those users if they are not coming from the machine where the MID Server is installed.
In order to solve this simple problem, I have really only made two changes to our instance. First, I created a table in the instance that stores records of an IP Address and a UserName. Second, I modified the BasicAuth Script Include to query that table on all Basic Authentications.
Basic Auth Restrictions Table
I created a table that really has only two important fields: “Allowable IP Address” and “Restricted User”
The Allowable IP Address field will accept IPv4 ip addresses. The Restricted User field will accept a user record in the sys_user table.
These fields can have a many-to-many relationship. So we don’t have any restrictions for what goes in side as long as they meet the data type criteria.
BasicAuth Script Include
The most technical work in this example comes in modifying the out-of-the-box BasicAuth Script Include. This is the script that all Basic Authentication attempts run through in the system. Just to be safe, I suggest deactivating the old script and creating a new one. This way you can always refer to it, or revert back to it should you need to.
The name of the basic authentication Script Include must be labeled “BasicAuth” so that the system can execute it during a standard basic authentication request.
Here is my script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | var BasicAuth = Class.create(); BasicAuth.prototype = { initialize : function(request, response, auth_type, auth_value) { this.request = request; this.response = response; this.auth_type = auth_type; this.auth_value = auth_value; }, getUserObj : function(username){ var gr = new GlideRecord("sys_user"); gr.addQuery("user_name", username); gr.addActiveQuery(); gr.query(); if(!gr.next()){ gs.log("Could not find active user: " + username); return null; } else { gs.log("Returning user object for: " + gr.getDisplayValue() + " ("+gr.sys_id+")"); return gr; } }, allowAuthentication : function(username){ gs.log("Attempting to authorize: " + username); var user = this.getUserObj(username); if(!user.isValid()){ gs.log("No active user by username: " + username); return false; } var forwardIP = this.request.getHeader("x-forwarded-for"); gs.log("Forwarded IP: " + forwardIP); var gr = new GlideRecord("u_basic_auth_restrictions"); gr.addQuery("u_restricted_user", user.sys_id); gr.query(); var userHasRestrictions = false; while(gr.next()){ userHasRestrictions = true; if(gr.u_allowable_ip_address == forwardIP){ gs.log(username +" has restrictions, but this IP Address is allowed; Attempt to Authenticate."); return true; } } if( userHasRestrictions ){ gs.log(username+" has restrictions and has attempted to perform basic auth from a non-authorized ip address"); return false; } else { gs.log(username+ " has no restrictions, try to authenticate"); return true; } }, getAuthorized : function() { var up = Packages.com.glide.util.StringUtil.base64Decode(this.auth_value); var split = up.indexOf(":"); if (split == -1) { gs.log("Basic authentication not well formed"); return null; } // locate user and impersonate var userName = up.substring(0, split); var password = up.substring(split + 1); if(!this.allowAuthentication(userName)){ gs.log("There is a Basic Auth User Restriction that failed for user: " + userName); return null; } if (!Packages.com.glide.sys.User.authenticate(userName, password)) { gs.log("Basic authentication failed for user: " + userName); return null; } this.updateLastLogin(userName); // user is authenticated, so return it... return userName; }, updateLastLogin : function(userName) { if ('true' != gs.getProperty("glide.basicauth.update_last_login_time", "false")) return; var user = new GlideRecord("sys_user"); user.addQuery("user_name", userName); user.query(); if(user.next()){ user.last_login_time = new GlideDateTime(); user.update(); } } } |
This script will first see if the user attempting to authenticate is in our restrictions table. If the user exists in the table, we then verify the source IP address and ensure that there is a record in the restrictions table that contains that same IP Address. If there is, then the authentication takes place. If not, then we fail the authentication and log something to the system log.
Summary
When you need to refine some rules for basic authentication requests, you can do so through the BasicAuth script include. You an easily base decisions on the user credentials or the request header itself.
Fine Print
Please keep in mind that it is not shipping ready. Some things that would still need to be considered are:
- Performance enhancements…right now I am using two glide record queries for every login. I am sure you could tweak things a bit such that we only need to use one if there are performance concerns.
- Need an option for the consequence of failing the IP Restriction. Currently I just fail the authentication and write it out to the system log. I should probably put an event in place, and make the result configurable (allow, but send an email/event vs. disallow authentication)
Hello John,
In Servicenow i am getting everywhere user’s session IP Address not the user’s Machine (IPV 4 IP address) IP Address . Session IP address is available in Transaction Log and Event Parm2 field also . I want to know can we get machine IP of Current logged in user ? And will it work in SSO case ?
Thanks and regards,
Kumar Satyam