NetScaler – OAuth to Azure AD with login_hint Subject Field

Reading Time: 6 minutes


What’s the biggest difference when choosing SAML instead of OAuth as the protocol when using Azure AD as IdP for NetScaler when it comes to User Experience (UX)?

You should consider this Question when you’re NOT doing an instant redirect to the IdP, instead doing first a Group Extraction (NoAuth) with eg OnlyUsername loginschema on the NetScaler side for filter mechanisms.

Your Users will enter their UPN / email on the NetScaler loginschema, followed by a second time after the redirect to Azure AD took place.

I stumbled first across this issue in my own Thread on Citrix Discussions and here also.

The restriction is based on Azure AD’s SAML flow. Spec-wise, login_hint is a subject field in SAML authN request. It’s only supporting a maximum of domain_hint, see Home Realm Discovery policy – Microsoft Entra | Microsoft Learn However, Azure AD does not support parsing out user_hint / login_hint from subject claim in the request.

So, as of now, Azure AD can use login_hint only when OIDC/OAuth is used and that’s enabled by default. Here’s what the redirected Browser-URL looks like when I first entered my UPN to the NetScaler loginschema with enabled OAuth:

Let’s trash the SAML Enterprise Application and create an App Registration with OAuth!


Azure AD

Go to Azure AD, App registrations, start with a new registration and enter your public FQDN of Gateway / AAA vServer:

Azure AD new App registration

After creation, click on Endpoints and copy the authorization and token endpoint URL:

OAuth endpoints (v2)

Go on with Certificates & secrets, create a new Client secret and choose your preferred lifetime

Client secret

Client secret values cannot be viewed, except for immediately after creation. Be sure to save the secret value when created before leaving the page.

Copy Secret Value

Now switch to Enterprise applications and you will notice there’s the same name of your previously created App registration. Set your preferred settings like if an Assignment is required and assign Users or Groups. Assign at a minimum one Administrator and sign in later first with that Account, so you can consent on behalf of your organization and no User will get that notification:

OAuth App AAD Permissions


For OAuth, you have to use an AAA vServer, you can’t bind an OAuth Policy to a Gateway vServer directly.

Go to OAuth Action and create a new one:

OAuth NetScaler Action

Insert your previously copied values:

OAuth NetScaler Action Settings

Cert Endpoint isn’t mandatory but you should configure this to get a successful verification of JWT, which includes configured Attributes & Claims in Azure AD. The URL is – see also my Script below.

Logfile when there’s no Cert Endpoint configured: (no parsing of the JWT token, no Claims inserted, so username is Anonymous)

Mar 27 11:16:26 <> 03/27/2023:09:16:26 GMT HTZ-ADC02 0-PPE-0 : default AAATM Message 8508389 0 :  "OAUTH RP: idtoken length 1303, access token length 2134, certendpoint len 0, conf-keys len 0" 
Mar 27 11:16:26 <> 03/27/2023:09:16:26 GMT HTZ-ADC02 0-PPE-0 : default AAATM Message 8508390 0 :  "OAUTH RP: Successfully verified incoming token/code, username: <Anonymous>, client ip 0xd4f0cf4f"

Logfile when there’s Cert Endpoint configured: (successful parsing of the JWT token and all Claims)

Mar 27 11:13:43 <> 03/27/2023:09:13:43 GMT HTZ-ADC02 0-PPE-0 : default AAATM Message 8506628 0 :  "OAUTH RP: idtoken length 1095, access token length 2123, certendpoint len 90, conf-keys len 0" 
Mar 27 11:13:43 <> 03/27/2023:09:13:43 GMT HTZ-ADC02 0-PPE-0 : default AAATM Message 8506629 0 :  "OAUTH: JWT: Successfully verified signature on JWT using the algorithm RS256" 
Mar 27 11:13:43 <> 03/27/2023:09:13:43 GMT HTZ-ADC02 0-PPE-0 : default AAATM Message 8506630 0 :  "OAUTH: successfully verified JWT; Parsing the result {"aud":"80cd3e19-9e08-4aac-a292-a58f974854d7","iss":"","iat":1679907903,"nbf":1679907903,"exp":1679911803,"name":"Julian Jakob","oid":"2c4f5c70-XXXXXXX","preferred_username":"","rh":"0.ATwAq4xCC9IcxkG7bEG3Q_D-DBk-zYAInqxKopKlj5dIVNc8AAw.","sub":"n_6x2KxQWOiCdEylWzRcDcL9BQy5uhKm90w51TwCkXQ","tid":"0b428cab-1cd2-41c6-bb6c-41b743f0fe0c","uti":"eEvEESUmI06FLykrqghBAA","ver":"2.0"}" 
Mar 27 11:13:43 <> 03/27/2023:09:13:43 GMT HTZ-ADC02 0-PPE-0 : default AAATM Message 8506631 0 :  "OAUTH RP: Successfully verified incoming token/code, username: <>, client ip 0xd4f0cf4f" 

After the creation of an OAuth action, make sure the status switched to COMPLETE, which identifies the NetScaler was able to fetch a certificate from the AAD Cert Endpoint:

OAuth Action Status

Attributes & Claims

For additional Claims to be send, you have to enable the issuing setting, otherwise your configured claims will never reach NetScaler:

Enable Claims to be send


By default, the following claims will reach your NetScaler after successful authentication on Azure AD:

  • name (Julian Jakob)
  • oid (Azure AD object identifier attribute)
  • preferred_username (

Which is than transferred to the following NetScaler attributes:

  • ssoDomain (
  • ssoUsername (
  • ssoUsername2 (

at a minimum, you should configure preferred_username as the User Name Field in the OAuth Server, otherwise your Session on NetScaler / StoreFront will be translated to “Anonymous”

NetScaler User Name Field for SSO

Following is a summary of possible claims if you’re needing more than the default is providing:

Optional User Claims Azure AD


The same goes to Group claims:

Group Claims in Azure AD

The group-membership claim is only transferring OnPrem Synced groups. If you need additional membership of Cloud groups, which makes sense when NetScaler is IdP, too (for example OAuth to Citrix DaaS as IdP) you have to set the types to Groups assigned to the application (and make sure all required Cloud + Synced groups to be read from are directly assigned to the corresponding Enterprise Application (NOT the App Registration itself))

Cloud Groups Claim

First testing you will notice the Cloud groups claim on NetScaler looks like this “groups”:[“f89d6013-9cc5-4918-b288-c84e6ede9063” – jep, that’s the Object ID.

To show the Display Name, edit the JSON and add ,”cloud_displayname” to the additionalProperties of the groups claim underneath idToken:

Enable Cloud Groups Display Name in Manifest


Simple Script for creating a OAuth Action connecting to Azure AD with a LDAPS Group Extraction (NoAuth) in front of.

#Creating OAuth Action linked to Azure AD with preferred_username extraction for SSO to NetScaler / StoreFront and some additional useful attribute claims
#Binding to an AAA vServer with first doing LDAPS Group Extraction (NoAuth)
#Replace yourazureadid with your Azure AD Tenant ID
#Replace yourclientid with your Application (client) ID
#Replace yourclientsecret with your Client secret value

add authentication OAuthAction AuthAct_OAuth_AAD -authorizationEndpoint "" -tokenEndpoint "" -clientID yourclientid -clientSecret yourclientsecret -CertEndpoint "" -userNameField preferred_username -allowedAlgorithms HS256 RS256 RS512 -Attribute1 email -Attribute2 family_name -Attribute3 given_name -Attribute4 upn -Attribute5 groups 
add authentication Policy AuthPol_OAuth_AAD -rule true -action AuthAct_OAuth_AAD

bind authentication vserver AAA_vServer_OAuth -policy lschema_OnlyUsername -priority 100 -gotoPriorityExpression END
bind authentication vserver AAA_vServer_OAuth -policy AuthPol_LDAPS_NoAuth -priority 100 -nextFactor AuthPol_OAuth_AAD -gotoPriorityExpression NEXT


If you would like to use that configuration for “Citrix Gateway” or “Adaptive Authentication” as selected IdP in your DaaS Tenant for Cloud Workspace – that’s technically not possible, see details here. NetScaler is aware of the issue and working hard on the solution. You have to use SAML to Entra ID instead of OAuth to make the Flow do the work.


I hope this post gave you some useful configuration insights about Azure AD and NetScaler when doing OAuth and may help you decide on which authentication protocol (SAML vs OAuth) your next deployment is based on.


  1. Hi Julian,
    thanks for this post. I’m testing it in my Testenvironment but status remains in INIT or CERTFETCH and doesn’t complete. May you give me some tips:-)
    In your script the priority is twice 100 shouldn’t it be different?

  2. I have this configured and seems to work as it should but after login I’m receiving the below message. When I swap back to SAML the login works and Storefront is displayed.

    Citrix Gateway
    Citrix Gateway Plug-in for Java is not supported. For further help or information, contact your help desk or system administrator.

    Any ideas?

  3. Hi Jullian,

    In our case we are not able to set consent on behalf of your organization and do the configuration over that notification that you are mentioning.

    Can you please share also App registrations -> AAA/GW -> API Permission -> Configured permissions page in your case?

    We tried to configure that according
    but we still get see The status of the OAuth Server is CERTFETCH. after configuring OAUTH in gateway.

    Thank you in advance

    1. Hi Jiri,

      sure, these are my API Permissions for the App Registration, all is MS-Graph:
      – email, Delegated, View Users email address, Admin consent required: No
      – profile, Delegated, View Users basic profile, Admin consent required: No
      – User.Read, Delegated, Sign in and read user profile, Admin consent required: No

      When OAuth Server is still on CERTFETCH, check if your NetScaler is able to do name resolution to public ( and to communicate via https to fetch the certificate from MS.

      1. Thanks a lot for sharing

        We have those API permissions in place.
        Im getting name resolution from DNS server so I get response for command:
        show dns addRec with like 8 Records and IP address.

        In our case we need to use proxy because of security. It looks like I have some issue to fetch the certificate from MS over proxy.
        Proxy is allowed based on source IP where source IP is Gateway SNIP to Microsoft pages

        when I try curl
        curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to login.

        but when you specify the configuration of the connection over our proxy server it connects
        curl -x :

        I need to ensure that authentication server will communicate over the proxy in this case
        So I tried different setting on ADC for proxy but I didn’t have luck to get this working.
        Is there something special to set in this case related to proxy?

        Thanks for the tips

        1. There is no way to configure netscaler to use proxy for outgoing communication (authentication or any other).
          I spent weeks to try to brake it using some configuration tricks and even Citrix solution architects were not able to help.
          It is possible to make it working using http proxy, but not https proxy. Point is that for https proxy there is added on extra request to setup proxy session and than everything is sent to same session with same TCP source port – Netscaler by design never reuse TCP source port for such communication at least I was not able to make it work (may be restriction of INC HA in Azure I was using).

          There are two options how to make it working:
          a) create DNS name FW rules (firewall admins hate such thing, it adds some necessary processing and complicates the configuration)
          b) create protocol extension in lua but who knows how to do it and who will pay for such effort

  4. I figured out the issue with the plug-in error. Had to define my default AAA group in the oAuth policy. Once I did that everything was working.

    Is there a way to pass the UPN to the oAuth policy. Trying to give customers the ability to enter SAM account or UPN on the username only MFA for login. I’m storing the UPN in the attribute when I do the LDAP extraction.


    1. Hi Chuck,

      as you can see in the first screenshot, the is the UPN which I entered in the first Username-Only Loginschema for LDAP group extraction on NetScaler. With OAuth, this is the default which gets prefilled at the Azure AD login.

      1. Hi Julian,
        Hi Cuck,
        i stuck now at the same point. Some of our users enter the username in SAM format in the first form (username only). In this case, the login_hint is the SAM and not the UPN. Have you found a solution to set the UPN as login_hint instead of the value the user enters into the form?
        I’ve experimented a lot with AAA_Rewite policies, but it’s not possible to bind a AAA_rewite policy in the reply direction to the AAA-v server to change the “location” header.

        1. Hi Markus,

          your requirement reminds me to the following scenario I had some times ago
          You could do it similar, change the User Expression from your username-only Loginschema which matches attributes for the correct UPN, so the User can put in SAM but the NetScaler should put your Expression from the Loginschema inside the login_hint and send to AAD.

          Let me know when you get it working

  5. Hi Julian,

    Your summary in overview chapter is not correct. Azure AD supports login_hint but as a query parameter in request url.
    It is tricky to change the ADC saml request url to add login_hint (authetication rewrite action does not allow response body mofification where the url is stored) but it is possible (a bit insane login flow involving adhoc creating of saml policies with another complication cause by the fact that ADC pair is running in Azure) .
    I made such solution, for DaaS – ADC gateway as authentication, AAD MFA (passwordless sign-in), with session SSO without FAS.

      1. Hi Jakob,

        Unfortunately I cannot share the configuration (customer’s security restrictions).
        Principle is following:
        policylabel 1 – user is authenticated using LDAP (cloud attributes extraction enabled), as well as upn
        policylabel 2 – then 3x wbeauth/httpcallout nitro resp api requests to ‘itself’/snip (with enabled management/api access) to create SAML action with login_hint= specified in SAML request URL|create policy policy filter expression user.attributes(number of upn)= |binding policy with next action policylabel 4 (I did it with explicit false result for action creation, policy creation and explicit true for policy binding in one policylabel – next action for binding is policylabel 2, number for priority is extracted from objectSID (number after last ‘-‘, it seems that when extract cloud attributes is not selected than objectSID is in binary form)
        policylabel 3 – SAML prepared during policylabel 2
        policylabel 4 – same as policylabel 2 but with unbind policy/delete policy/delete action.
        end (in my implementation I have there couple of other processing but that is some other story)

        For rest api I created user with api access only, access rule enabling only very specific actions related to requested actions (no show command enabled).

        Make it working in Azure HA/INC was terrible, I had to add check/policy filters to identify what is the active node to use RESP API requests on correct node (both nodes have their own active/different SNIP)

  6. Sorry for the noob question, but can this be used multi-tenant? And if so how could it be locked down to specific tenants?


    1. Sure, see in my script “Binding to an AAA vServer with first doing LDAPS Group Extraction (NoAuth)” you could do a filtering on LDAPS Groups (Group1 = OAuth Action1,…) or you’re checking and filtering on the UPN-Suffix like user1@domain1 is redirected to domain1 OAuth Action.

  7. Hi Julian,

    Thank for sharing valuable information.

    Last month, I have created the oAuth policy and status was coming as “Complete”.
    But, since yesterday it is showing as “OAuth Status: GRAPH”. Could you please suggest on this what can be validated in this situation.
    PS: Firewall ports openings validated.

  8. This Works! But what du you do about logoff? When logging off the storefront page the browser is still authenticated with Azure. SAML has a logout URL option..hmm

Leave a Reply

Your email address will not be published. Required fields are marked *