NetScaler introduced native OTP (One Time Password) some time ago, which you can incorporate in the login process as long as you have AD servers ready to use as a database.
The examples that are shown on citrix.com only covers the scenario where you have one domain connecting through NetScaler(and the abuse of policy “true” to make sure you catch the right requests), and the logical way to adding more domains, would be to add more vServers if you were to follow the same way of configuration. However with the right set of policies, its possible to have one set of vServers with multiple policies to support n* number of domains.
I’ll proceed the same way in this article as in my previous, which means it will give you an understanding on how the policies work together, rather than give you a copy and paste solution.
The solution is divided into 2 parts, a login part, and a management part.
User flow
The user look and feel is described in: https://support.citrix.com/article/CTX229941 with one exception.
I’ve added a link in the regular login flow to point to the management page. In the article it expects that the user types in the URL manually.
Prerequisites
Natie OTP uses AD as a database to store information about a user’s authenticated devices. Therefor special LDAP actions are required on NetScaler to write and search into a special field for a UserObject in AD. The citrix articles use the field called “userParameters”, but this could be changed if required.
Used for authenticating the user
1 |
add authentication ldapAction lbvip-LDAP-DomainA -serverIP 1.1.1.1 -ldapBase "dc=DomainA,dc=local" -ldapBindDn svcaccount@DomainA.local -ldapLoginName userPrincipalName -passwdChange ENABLED -Attribute2 userParameters |
Used for writing into AD
1 |
add authentication ldapAction lbvip-LDAP-DomainA_otp_mgmt_noauth -serverIP 1.1.1.1 -ldapBase "dc=DomainA,dc=local" -ldapBindDn svcaccount@DomainA.local -ldapLoginName userPrincipalName -authentication DISABLED -OTPSecret UserParameters |
Used for fetching the value of userParamteres
1 |
add authentication ldapAction lbvip-LDAP-DomainA_otp_verify_noauth -serverIP 1.1.1.1 -ldapBase "dc=DomainA,dc=local" -ldapBindDn svcaccount@DomainA.local -ldapLoginName userPrincipalName -searchFilter "userParameters>=#@" -groupAttrName memberOf -subAttributeName cn -authentication DISABLED -OTPSecret UserParameters |
Policies that use these 3 LDAP actions:
1 2 3 |
add authentication Policy AUTHPOL_DomainA_ldap -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainA.com\")" -action lbvip-LDAP-DomainA add authentication Policy AUTHPOL_DomainA_Manage_OTP -rule " AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainA.com\") && HTTP.REQ.COOKIE.VALUE(\"NSC_TASS\").TO_LOWER.CONTAINS(\"manageotp\")" -action lbvip-LDAP-DomainA_otp_mgmt_noauth add authentication Policy AUTHPOL_DomainA_Confirm_OTP-pol -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainA.com\") && AAA.USER.GROUPS.TO_LOWER.CONTAINS(\"netscaler_auth_2fa_native_otp\")" -action lbvip-LDAP-DomainA_otp_verify_noauth |
LDAP actions are required for each domain that you want to support.
1 2 3 4 5 6 |
add authentication ldapAction lbvip-LDAP-DomainB -serverIP 1.1.1.10 -ldapBase "dc=DomainB,dc=local" -ldapBindDn svcaccount@DomainB.local -ldapLoginName userPrincipalName -groupAttrName memberOf -followReferrals ON -maxLDAPReferrals 10 -Attribute2 userParameters add authentication ldapAction lbvip-LDAP-DomainB_otp_mgmt_noauth -serverIP 1.1.1.10 -ldapBase "dc=DomainB,dc=local" -ldapBindDn svcaccount@DomainB.local -ldapLoginName userPrincipalName -groupAttrName memberOf -authentication DISABLED -OTPSecret UserParameters add authentication ldapAction lbvip-LDAP-DomainB_otp_verify_noauth -serverIP 1.1.1.10 -ldapBase "dc=DomainB,dc=local" -ldapBindDn svcaccount@DomainB.local -ldapLoginName userPrincipalName -searchFilter "userParameters>=#@" -groupAttrName memberOf -subAttributeName cn -authentication DISABLED -OTPSecret UserParameters add authentication Policy AUTHPOL_DomainB_Confirm_OTP-pol -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainB.com\") && AAA.USER.GROUPS.TO_LOWER.CONTAINS(\"netscaler_auth_2fa_native_otp\")" -action lbvip-LDAP-DomainB_otp_verify_noauth add authentication Policy AUTHPOL_DomainB_Manage_OTP -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainB.com\") && HTTP.REQ.COOKIE.VALUE(\"NSC_TASS\").TO_LOWER.CONTAINS(\"manageotp\")" -action lbvip-LDAP-DomainB_otp_mgmt_noauth add authentication Policy AUTHPOL_DomainB_ldap -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainB.com\")" -action lbvip-LDAP-DomainB |
Management flow (Add / remove devices)
Before your able to login you need to add a device(mobile/tablet) that NetScaler will accept tokens from. This is done on a special page – a authentication schema – that presents the user with the option to add or remove devices. This loginschema ships will NetScaler.
The page is invoked with a special url, in this case “/manageotp” (could be something else)
First add the authentication schema to a loginschema and tie it to a policylabel.
1 2 |
add authentication loginSchema ALS_Manage_OTP -authenticationSchema "/nsconfig/loginschema/LoginSchema/SingleAuthManageOTP.xml" add authentication policylabel AUTHPOLLABEL_2fa_native_otp -loginSchema ALS_Manage_OTP |
Add an catch all policy to get the userinput into the flow.
1 |
bind authentication policylabel AUTHPOLLABEL_2fa_native_otp -policyName AUTHPOL_NOAUTHN_true -priority 100 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_otp_ldap |
Present the policylabel to the user. Note the special way of matching on URL in the authentication framework. It is the cookie called NSC_TASS that contains the value of the URL.
It is important that this policy is hit fairly early in the process, so the user is not caught in a different/default flow, thats why it has priority 90 (in my setup, this is the first policy that’s processed)
1 2 |
add authentication Policy AUTHPOL_entry_manageotp -rule " HTTP.REQ.COOKIE.VALUE(\"NSC_TASS\").TO_LOWER.CONTAINS(\"manageotp\")" -action NO_AUTHN bind authentication vserver AAAVS_portal_entry_v1 -policy AUTHPOL_entry_manageotp -priority 90 -nextFactor AUTHPOLLABEL_2fa_native_otp -gotoPriorityExpression END |
Depending on security requirements, you might want to add ip restrictions on this mangement page
Authenticate the users before sending them to the management page.
1 2 3 4 5 6 7 8 9 10 11 |
add authentication Policy AUTHPOL_NOAUTHN_true -rule true -action NO_AUTHN add authentication policylabel AUTHPOLLABEL_int_otp_ldap -loginSchema LSCHEMA_INT add authentication Policy AUTHPOL_DomainB_ldap -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainB.com\")" -action lbvip-LDAP-DomainB add authentication Policy AUTHPOL_DomainA_ldap -rule "AAA.USER.NAME.TO_LOWER.CONTAINS(\"@DomainA.com\")" -action lbvip-LDAP-DomainA bind authentication policylabel AUTHPOLLABEL_int_otp_ldap -policyName AUTHPOL_DomainB_ldap -priority 100 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_verify_otp bind authentication policylabel AUTHPOLLABEL_int_otp_ldap -policyName AUTHPOL_DomainA_ldap -priority 110 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_verify_otp add authentication policylabel AUTHPOLLABEL_int_verify_otp -loginSchema LSCHEMA_INT bind authentication policylabel AUTHPOLLABEL_int_verify_otp -policyName AUTHPOL_DomainA_Manage_OTP -priority 100 -gotoPriorityExpression NEXT bind authentication policylabel AUTHPOLLABEL_int_verify_otp -policyName AUTHPOL_DomainA_Confirm_OTP-pol -priority 110 -gotoPriorityExpression NEXT bind authentication policylabel AUTHPOLLABEL_int_verify_otp -policyName AUTHPOL_DomainB_Confirm_OTP-pol -priority 120 -gotoPriorityExpression NEXT bind authentication policylabel AUTHPOLLABEL_int_verify_otp -policyName AUTHPOL_DomainB_Manage_OTP -priority 130 -gotoPriorityExpression NEXT |
Now the users are able to see the page(loginschema) where they can add and remove devices. There is something fishy going on this page, it does not update the list of added devices after adding new device. Try and restart the flow if you cant see the added device.
1 2 3 4 5 6 7 8 |
add authentication policylabel AUTHPOLLABEL_int_verify_ldap -loginSchema LSCHEMA_INT bind authentication policylabel AUTHPOLLABEL_int_verify_ldap -policyName AUTHPOL_DomainA_ldap -priority 100 -gotoPriorityExpression END bind authentication policylabel AUTHPOLLABEL_int_verify_ldap -policyName AUTHPOL_DomainB_ldap -priority 110 -gotoPriorityExpression END add authentication policylabel AUTHPOLLABEL_1fa -loginSchema ALS_only_password bind authentication policylabel AUTHPOLLABEL_1fa -policyName AUTHPOL_NOAUTHN_true -priority 100 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_verify_ldap add authentication Policy AUTHPOL_2fa_native_otp -rule "AAA.USER.GROUPS.TO_LOWER.CONTAINS(\"netscaler_auth_2fa_native_otp\")" -action NO_AUTHN bind authentication policylabel AUTHPOLLABEL_int_verify_grp -policyName AUTHPOL_1fa -priority 100 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_1fa bind authentication policylabel AUTHPOLLABEL_int_verify_grp -policyName AUTHPOL_2fa_native_otp -priority 80 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_2fa_with_otp_link |
Login flow
Alright, the user has now added a device via the previous flow and needs to login.
The first policy on the AAAVS was catching the url “/manageotp” – the next is the default login flow.
1 2 3 |
add authentication loginSchema ALS_2fa_nousername_with_otp_link -authenticationSchema "/nsconfig/loginschema/2fa_nousername_with_otp_link.xml" add authentication policylabel AUTHPOLLABEL_2fa_with_otp_link -loginSchema ALS_2fa_nousername_with_otp_link bind authentication vserver AAAVS_portal_entry_v1 -policy AUTHPOL_NOAUTHN_true -priority 100 -nextFactor AUTHPOLLABEL_2fa_with_otp_link -gotoPriorityExpression END |
Now the user sees a prompt with a username, password + token dialogbox.
The first policy in the policylabel fetches the input and sends the user onwards to an internal schema to verify the token.
1 2 |
add authentication policylabel AUTHPOLLABEL_int_verify_radius -loginSchema LSCHEMA_INT bind authentication policylabel AUTHPOLLABEL_2fa_with_otp_link -policyName AUTHPOL_NOAUTHN_true -priority 100 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_verify_radius |
The next policylabel checks the Token first, and if that succeeds, verify if the password is also correct.
1 2 |
bind authentication policylabel AUTHPOLLABEL_int_verify_radius -policyName AUTHPOL_DomainB_Confirm_OTP-pol -priority 50 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_verify_ldap_as_2ndFA bind authentication policylabel AUTHPOLLABEL_int_verify_radius -policyName AUTHPOL_DomainA_Confirm_OTP-pol -priority 120 -gotoPriorityExpression END -nextFactor AUTHPOLLABEL_int_verify_ldap_as_2ndFA |
Checking the password is also an internal schema, but we cannot use the default one, since we need to specify where to extract the password from.
1 2 3 4 |
add authentication loginSchema ALS_verify_Password1 -authenticationSchema noschema -passwdExpression AAA.LOGIN.PASSWORD add authentication policylabel AUTHPOLLABEL_int_verify_ldap_as_2ndFA -loginSchema ALS_verify_Password1 bind authentication policylabel AUTHPOLLABEL_int_verify_ldap_as_2ndFA -policyName AUTHPOL_DomainA_ldap -priority 100 -gotoPriorityExpression END bind authentication policylabel AUTHPOLLABEL_int_verify_ldap_as_2ndFA -policyName AUTHPOL_DomainB_ldap -priority 110 -gotoPriorityExpression END |
The configsnips are a part of a bigger configuration with more functionality(test if user exists, extract grp to decide login flow), so if it doesn’t quite make sense, it’s because i haven’t pasted ALL the config into the article. It’s difficult enough to keep it short as it is 😉 Hopefully there is enough for you to decode what’s going on and how to take the small pieces and implement in your setup.
Do not get to excited
But before you run off and replace SMSpasscode, RSA, token1-2-3 software, you need to know that its a very simple solution that you’ll get with the native otp, as it is right now(i don’t know if this is going to change going forward).
For example, there is no general managementpage on your users devices, each piece of metadata for every phone, is store in a userobject in AD. NetScaler does not come with an management page for all your users/devices, so you have to search and view/edit in AD, or go by the /manageotp page in NetScaler as the specific user(!).
Besides from that, it’s a very simple solution and seems to work very well. I’ve only tested with Google Authenticator app, but im guessing any authenticator app that follows the standard (if there is one) would work.
Hi all,
Excelent post. At some point it is stated that the managment page /manageotp could be other thing, I’ve been trying to change it to something else by editing the rules when checking the NSC_TASS cookie with no luck so far…
Do we know if it is possible?
Hey Noel,
We know its possible 🙂 the expression is some what like the lines of HTTP.REQ.COOKIE.VALUE(“NSC_TASS”).CONTANS(“/some/url”) but i am not 100% sure, it might be a bit different.
Hi Morten,
Thank you for your response 🙂
That’s what I’ve been trying with no luck so far, i’ll try again and investigate a little more (I’m by no means a netscaler expert)..
I find surprising that so far I haven’t been able to find any documentation or blog or post of anyone who has done it before..
I’ll write again if I find something.