NAM and Intune: Device Authentication

0 Likes
2 months ago

The scenario

Microfocus Access Manager grants SSO to all services. There is the need to extend the current Authentication mechanism to handle Multifactor authentication for specific applications.

Microfocus Advanced Authentication Framework handles the 2nd factor

eDirectory as Authentication Directory, Access Manager for SSO/Risk based Access policies and Advanced Authentication for Multifactor handling. Office365 federated with Access Manager and Office365 users created via Microfocus Identity Manager Driver for Office365

The goal: achieving adaptive MFA using Azure device security posture

 

Grant granular access policies based on Risk Based Access control policies, trying to merge them with all the data we can gather from devices.

 

Make use Microsoft Intune Mobile Device Management device posture to implement our Risk Based Access Policies

 

 

High level architecture

 

 

This solution take inspiration from the great work done by Patryk Krolikowski, A Forgerock solutions architect who developed an authentication node for ForgeRock solution

https://backstage.forgerock.com/marketplace/api/catalog/entries/AXBGauVAnnbgOG9zoUN4

 

Final Solution

 

Automatic User Identification and Device Authentication: 

Link to the video

 

Azure Configuration

Basically, the idea is to put a X509 certificate on every known / managed device. This certificate is crafted with the Device Id as Common Name, in order to be able from Microfocus to retrieve device information before trying to authenticate the user

 

To provide via Intune a valid certificate we prepared the Azure environment like that

 

  1. Create account in Azure

 

We defined an Azure service user with the following roles

 

 


  1. Configure a third party Certification Authority

 

 

 

We chose to add from the Azure marketplace the free community edition of SCEPman

 

We added a resource group in an Azure Subscriptions with an enabled administrative user

 

Resource group: scepman

 

Here we can deploy the scepman app. To deploy the scepman app we need to define an App registration in Azure

  

Click New registration and enter a name, e.g. SCEPman

 

 

And we save the autogenerated client-ID

 

Then click on Certificates & secrets

 

 

And add a new secret

 

 

Copy the secret and write it down in a secure place.

 

Finally, define the API permissions

 

 

 

 

  1. Deploy SCEPman app

 

Download the app from azure marketplace the SCEPman Community Edition

 

 

Define basic information

 

 

 

 

 

Then click create

 

Inside the resource group you will find

 

 

 

 

 

 

 

  1. Create SCEPman root certificate

 

  1. Navigate to App Services.
  2. Choose the SCEPman application and click on Browse to see the SCEPman website.
  3. When everything works as intended Vault, Intune and Graph are set as connected.
  4. The option click here to start creates the Azure Key Vault RootCA certificate. The initial root certificate should be created only once in a farm. 5. Select I have read the documentation[...] and click Create First Node. 6. After some seconds / minutes you can refresh the page. Now you should see that the root certificate is available

 

  1. Define Configuration policies for devices in endpoint.microsoft.com

 

 

 

You have to define a configuration profile to send the CA public key to devices. To do this download the CA cert from your defined app

And then define a configuration profile which sends the CA to your devices

If you have to support multiple platforms do more than configuration profile to send the device cert

 

The Intune Configuration is ready

 

  1. Transform the device x509 certificate into an header for your Identity Provider

 

With an SSL Terminator Reverse Proxy in front of your Identity Provider, in our case BigIP F5, you can define a rule to obtain a header having the certificate common name as a value. To do this you have to define a Client SSL profile asking for certificates released from your CA

 

 

And a Rule able to extract from the certificate the CN to put it on a header

 

when HTTP_REQUEST_SEND {

   # Need to force the host header replacement and HTTP:: commands into the clientside context

   # as the HTTP_REQUEST_SEND event is in the serverside context

   clientside {

      if {[SSL::cert 0] ne ""}{

        set tmpcn [X509::subject [SSL::cert 0]]

        set cn [findstr $tmpcn "CN=" 3]

        HTTP::header replace x-intune $cn

      } else {

        HTTP::header remove x-intune

      }

  }

}

 

This will ensure the presence of your device ID in a header

 

Microfocus Configuration

 

First of all, we need an authentication class responsible in parsing the header, read the device data / ownership and write this information inside the current session

 

I called this class “ReadDeviceClass”

With a set of properties to interact with AzureAD

 

 

The class is Used by a method not set to identify the user

 

 

With its own Read_Device.jsp login page, which we will see after

 

The class is defined in this way (comments inside)

package com.customer.nidp.authentication.local;

 

import org.apache.http.HttpEntity;

import org.apache.http.HttpHeaders;

import org.apache.http.Header;

import org.apache.http.HttpResponse;

import org.apache.http.NameValuePair;

import org.apache.http.client.HttpClient;

import org.apache.http.client.entity.UrlEncodedFormEntity;

import org.apache.http.client.methods.HttpGet;

import org.apache.http.client.methods.HttpPost;

import org.apache.http.impl.client.HttpClientBuilder;

import org.apache.http.client.protocol.HttpClientContext;

import org.apache.http.impl.client.CloseableHttpClient;

import org.apache.http.impl.client.HttpClients;

import org.apache.http.message.BasicNameValuePair;

import org.apache.http.util.EntityUtils;

import org.apache.http.entity.StringEntity;

import java.util.*;

import java.io.IOException;

import javax.security.auth.callback.Callback;

import javax.security.auth.callback.CallbackHandler;

import javax.security.auth.callback.UnsupportedCallbackException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;

import org.eclipse.higgins.sts.api.ISecurityInformation;

import org.eclipse.higgins.sts.api.IUsernameToken;

import com.novell.nidp.authentication.local.LocalAuthenticationClass;

import com.novell.nidp.authentication.local.STSAuthenticationClass;

import com.novell.nidp.authentication.local.CallbackAuthentication;

import com.novell.nidp.authentication.local.BasicClass;

import com.novell.nidp.authentication.local.PageToShow;

import com.novell.nidp.authentication.local.PasswordValidationCallback;

import org.apache.commons.lang.StringEscapeUtils;

import com.novell.nidp.NIDPConstants;

import com.novell.nidp.NIDPException;

import com.novell.nidp.NIDPPrincipal;

import com.novell.nidp.NIDPSession;

import com.novell.nidp.NIDPSessionData;

import com.novell.nidp.authentication.AuthnConstants;

import com.novell.nidp.resource.strings.NIDPMainResDesc;

import com.novell.nidp.common.authority.PasswordExpiredException;

import com.novell.nidp.common.authority.PasswordExpiringException;

import com.novell.nidp.common.authority.UserAuthority;

import com.novell.nidp.logging.NIDPLog;

import com.novell.nidp.saml.SAMLAuthMethods;

import com.netiq.nam.common.security.InputSanitizer;

import com.novell.nidp.NIDPContext;

import com.novell.nidp.common.authority.ldap.LDAPPrincipal;

import com.novell.nidp.servlets.NIDPServletContext;

import com.netiq.nam.common.security.InputSanitizer;

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.io.OutputStream;

import java.net.URL;

import java.net.URLEncoder;

import org.json.*;

import javax.servlet.http.Cookie;

 

public class ReadDeviceClass extends LocalAuthenticationClass implements STSAuthenticationClass, CallbackAuthentication {

 

    private String customeruser;

    private String username;

    private String debug;

    private static IntuneConfig config;

    private static String deviceId;

    private static String access_token;

    private static ArrayList<String> devApps = new ArrayList<>();

    private static String complianceResult;

    private static JSONObject deviceProperties;

    private Set<String> appsBlackList;

 

    LocalAuthenticationClass basicClass = null;

 

    /**

     * Constructor for form based authentication

     * 

     * @param props

     *            Properties associated with the implementing class

     * @param uStores

     *            List of ordered user stores to authenticate against

     */

    public customerReadDeviceClass(Properties props, ArrayList<UserAuthority> uStores) {

        super(props, uStores);

      this.debug = this.getProperty("DEBUG");

      if ("".equals(this.debug)) this.debug = null;

      if(null != this.debug) {

         this.debug = InputSanitizer.getSanitizedStr(this.debug);

      }

      //reda the class configuration properties

      config = new IntuneConfig(props);

 

    }

 

    /**

     * Get the authentication type this class implements

     * 

     * @return returns the authentication type represented by this class

     */

    public String getType() {

        return AuthnConstants.OTHER;

    }

 

    public void initializeRequest(HttpServletRequest request, HttpServletResponse response, NIDPSession session, NIDPSessionData data, boolean following, String url) {

    if (debug != null) java.lang.System.out.println("initializeRequestCalled");

        super.initializeRequest(request, response, session, data, following, url);

        if (basicClass != null)

            basicClass.initializeRequest(request, response, session, data, following, url);

    }

 

    protected int doAuthenticate() {

 

    //Save the target url if present

    String targetUrl = this.m_Session.getTargetUrl();

    if (targetUrl == null && getAuthnRequest() != null && getAuthnRequest().getTarget() != null)

            targetUrl = getAuthnRequest().getTarget();

    if (debug != null) java.lang.System.out.println("customerDeviceReadClass target: " + targetUrl);

    this.m_Session.setTargetUrl(targetUrl);

 

 

    if (debug != null) java.lang.System.out.println("customerDeviceReadClass Started do Authenticate");

 

    //Look if the post data comes from the attached login page Read_Device.jsp, in that case forward ahead to the risk policies

    String sendAuth = m_Request.getParameter("sendAuth");

    if (debug != null) java.lang.System.out.println("customer ReadDevice sendAuth: " + sendAuth);

    if (sendAuth != null && sendAuth.equals("true")) return AUTHENTICATED;

 

    //read the intune header

    boolean deviceIdInHeader = m_Request.getHeader(config.inTuneHeader().toString()) != null;

 

    try {

        if (deviceIdInHeader) {

            //if the header was read by the reverse proxy in front of IDPs

            deviceId = m_Request.getHeader(config.inTuneHeader().toString()).replaceAll("CN=", "");            

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: deviceID: " + deviceId);

            //read the data from Intune Device

            roGrant();

            //read the compliance state

            checkCompliance();

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Device Properties Length: " + deviceProperties.length());

 

            //if our device has the property "deviceId"

            if (deviceProperties != null && deviceProperties.length() > 0

                    && deviceId.equals(deviceProperties.get("deviceId"))) {

 

                //write the device properties inside the tomcat session, since we do not have for now a valid m_Session where to store this information

                //Pay attention: this solution works only when you are making use of sticky sessions on your load balancer

                JSONObject jsonObject = new JSONObject();

                jsonObject.put("IntuneInfo",deviceProperties);

                HttpSession hts = m_Request.getSession();

                if (hts == null) {

                        m_Response.setStatus(500);

                        return NOT_AUTHENTICATED;

                }

                hts.setAttribute("IntuneInfo", jsonObject.toString());

 

                if (debug != null) java.lang.System.out.println("[customerReadDevice]: Writing back cookie");

 

                //write to a cookie into the browser to mark the presence of a good session, so we would be able to know what to check during the RBA policy

                Cookie respcookie = new Cookie("deviceSession","present");

                respcookie.setHttpOnly(true);

                respcookie.setPath("/nidp");

                respcookie.setSecure(true);

                //respcookie.setMaxAge(604800);

                m_Response.addCookie(respcookie);

                String jsp = getProperty(AuthnConstants.PROPERTY_JSP);

                if (jsp == null || jsp.length() == 0)

                    jsp = NIDPConstants.JSP_LOGIN;

                m_PageToShow = new PageToShow(jsp);

                m_PageToShow.addAttribute(NIDPConstants.ATTR_URL,(getReturnURL() != null? getReturnURL():m_Request.getRequestURL().toString()));

                //Show the Read_Device.jsp, this will set our deviceSession cookie and autosubmit the method

                return SHOW_JSP;

 

            } else {

                if (debug != null) java.lang.System.out.println("[customerReadDevice]: Device not found");

            }

 

        } else {

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: No device ID found");

        }

 

    } catch (Exception e) {

        if (debug != null) java.lang.System.out.println("[customerReadDevice]: Error: " + e.getLocalizedMessage());

    }

 

    //If we do not find out Azure Device or the header is missing, empty the cookie marking the presence of a device on Azure

    Cookie respcookie2 = new Cookie("deviceSession","");

    respcookie2.setHttpOnly(true);

    respcookie2.setPath("/nidp");

    respcookie2.setSecure(true);

    respcookie2.setMaxAge(0);

    m_Response.addCookie(respcookie2);

    if (debug != null) java.lang.System.out.println("customer ReadDevice ended");

    return AUTHENTICATED;

    

    }

 

    public NIDPPrincipal handleSTSAuthentication(ISecurityInformation securityInformation) {

        IUsernameToken usernameToken = (IUsernameToken) securityInformation.getFirst(IUsernameToken.class);

 

        if (null != usernameToken) {

            try {

                if (authenticateWithPassword(usernameToken.getUsername(), usernameToken.getPassword()))

                    return getPrincipal();

            } catch (PasswordExpiringException pe) {

                return getPrincipal();

            } catch (PasswordExpiredException pe) {

            }

        }

        return null;

    }

 

    @Override

    public NIDPPrincipal cbAuthenticate(CallbackHandler cbHandler) {

        PasswordValidationCallback pwdCallback = new PasswordValidationCallback();

        Callback[] callbacks = new Callback[] { pwdCallback };

 

        NIDPPrincipal principal = null;

        try {

            cbHandler.handle(callbacks);

            if (pwdCallback.getUsername() != null) {

              

                String query = getProperty(AuthnConstants.PROPERTY_QUERY);

                String ldapQuery = null;

                boolean status = false;

                if (query != null)

                {

                  ldapQuery = getLDAPQueryString(query,pwdCallback.getUsername());

                  if (findPrincipalsByQuery(ldapQuery).length == 1)

                    status = true;

                }

                else if (findPrincipals(pwdCallback.getUsername()).length == 1)

                    status = true;

              

              if ( status == true ) {

                    principal = getPrincipal();

                    principal.setAuthMethod(SAMLAuthMethods.PASSWORD);

                    return principal;

                }

            }

 

        } catch (IOException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        } catch (UnsupportedCallbackException e) {

            if(NIDPLog.isLoggableWSTrustFine())

                NIDPLog.logWSTrustFine("The caller doesn't support password callback: " + e.getMessage());

        }

        return null;

    }

 

    //Obtain an Azure Session

    private void roGrant() {

        HttpClient httpClient = HttpClientBuilder.create().build();

        HttpPost post = new HttpPost(

                "https://login.microsoftonline.com/" + config.azureTenantId() + "/oauth2/v2.0/token");

        try

 

        {

 

            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(1);

            nameValuePairs.add(new BasicNameValuePair("client_id", config.appRegistrationClientId()));

            nameValuePairs.add(new BasicNameValuePair("client_secret", config.appRegistrationClientSecret()));

            nameValuePairs.add(new BasicNameValuePair("grant_type", "password"));

            nameValuePairs.add(new BasicNameValuePair("username", config.userName()));

            nameValuePairs.add(new BasicNameValuePair("password", config.userPassword()));

            nameValuePairs.add(new BasicNameValuePair("scope", "DeviceManagementManagedDevices.Read.All"));

 

            post.setEntity(new UrlEncodedFormEntity(nameValuePairs));

            HttpResponse response = httpClient.execute(post);

 

            BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));

            HttpEntity entity = response.getEntity();

            String content = EntityUtils.toString(entity);

 

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Access request content: " + content);

 

            JSONObject jsonObject = new JSONObject(content);

            access_token = jsonObject.getString("access_token");

 

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Access_token: " + access_token);

 

        } catch (IOException | JSONException e)

 

        {

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Something went wrong while getting an access_token: " + e);

            e.printStackTrace();

        }

    }

 

    //read device data

    private void checkCompliance() {

        HttpClient httpClient = HttpClientBuilder.create().build();

 

        try {

            HttpGet request = new HttpGet(

                    "https://graph.microsoft.com/beta/deviceManagement/manageddevices/" + deviceId);

            String bearerHeader = "Bearer " + access_token;

            request.setHeader(HttpHeaders.AUTHORIZATION, bearerHeader);

            HttpResponse response = httpClient.execute(request);

            HttpEntity entity = response.getEntity();

            String content = EntityUtils.toString(entity);

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Device Status: " + content);

 

            JSONObject jsonObject = new JSONObject(content);

 

            /**

             * Extract some of device properties returned by Intune.

             */

 

            // General

            String deviceId = jsonObject.getString("id");

            ; // device ID in Intune

            String deviceName = jsonObject.getString("deviceName");

            String deviceType = jsonObject.getString("deviceType");

            String model = jsonObject.getString("model");

            String manufacturer = jsonObject.getString("manufacturer");

            String serialNumber = jsonObject.getString("serialNumber");

 

            // OS

            String operatingSystem = jsonObject.getString("operatingSystem");

            String osVersion = jsonObject.getString("osVersion");

 

            // Management

            String deviceManagementState = jsonObject.getString("managementState");

            String deviceRegistrationState = jsonObject.getString("deviceRegistrationState");

            String isSupervised = jsonObject.getString("isSupervised");

            String deviceEnrollmentType = jsonObject.getString("deviceEnrollmentType");

            String managedDeviceOwnerType = jsonObject.getString("managedDeviceOwnerType");

 

            // Compliance & security

            String intuneComplianceState = jsonObject.getString("complianceState");

            String jailBroken = jsonObject.getString("jailBroken");

            String lostModeState = jsonObject.getString("lostModeState");

            String isEncrypted = jsonObject.getString("isEncrypted");

 

            // User or Owner related

            String userPrincipalName = jsonObject.getString("userPrincipalName");

            String userDisplayName = jsonObject.getString("userDisplayName");

 

            /**

             * Build new json out of these properties

             */

            deviceProperties = new JSONObject().put("deviceId", deviceId).put("deviceName", deviceName)

                    .put("deviceType", deviceType).put("model", model).put("manufacturer", manufacturer)

                    .put("serialNumber", serialNumber).put("operatingSystem", operatingSystem)

                    .put("osVersion", osVersion).put("deviceRegistrationState", deviceRegistrationState)

                    .put("deviceManagementState", deviceManagementState).put("isSupervised", isSupervised)

                    .put("deviceEnrollmentType", deviceEnrollmentType)

                    .put("managedDeviceOwnerType", managedDeviceOwnerType).put("ComplianceState", intuneComplianceState)

                    .put("jailBroken", jailBroken).put("lostModeState", lostModeState).put("isEncrypted", isEncrypted)

                    .put("userPrincipalName", userPrincipalName).put("userDisplayName", userDisplayName);

 

            if (intuneComplianceState.equals("compliant")) {

                complianceResult = "compliant";

            } else if (deviceManagementState.equals("noncompliant")) {

                complianceResult = "noncompiant";

            } else if (deviceManagementState.equals("inGracePeriod")) {

                complianceResult = "inGracePeriod";

            } else if (deviceManagementState.equals("unknown")) {

                complianceResult = "unknown";

            } else if (deviceManagementState.equals("conflict")) {

                complianceResult = "conflict";

            } else if (deviceManagementState.equals("error")) {

                complianceResult = "error";

            } else if (deviceManagementState.equals("configManager")) {

                complianceResult = "configManager";

            }

        } catch (IOException | JSONException e) {

            complianceResult = "error";

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Something went wrong while inspecting device status endpoint: " + e);

        }

    }

 

    //extract installed Apps

    private void extractApps() {

        HttpClient httpClient = HttpClientBuilder.create().build();

 

        try {

            HttpGet request = new HttpGet(

                    "https://graph.microsoft.com/beta/deviceManagement/manageddevices/" + deviceId + "/detectedApps");

            String bearerHeader = "Bearer " + access_token;

            request.setHeader(HttpHeaders.AUTHORIZATION, bearerHeader);

            HttpResponse response = httpClient.execute(request);

            HttpEntity entity = response.getEntity();

            String content = EntityUtils.toString(entity);

            JSONObject jsonObject = new JSONObject(content);

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Device apps content: " + content);

            /**

             * Extract list of apps with versions.

             */

            JSONArray jsonArray = jsonObject.getJSONArray("value");

 

            for (int i = 0; i < jsonArray.length(); i++) {

                // Store JSON objects in an array

                // Get the index of the JSON object and print the value per index

                JSONObject valueContents = (JSONObject) jsonArray.get(i);

                String displayName = (String) valueContents.get("displayName");

                devApps.add(displayName);

            }

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Device Array: " + devApps);

 

        } catch (IOException | JSONException e) {

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Something went wrong while extracting apps: " + e);

        }

    }

 

    //extract 

    private boolean blacklistedAppsPresent(ArrayList jsonArray) throws JSONException {

        appsBlackList = config.appsBlackList();

        if (debug != null) java.lang.System.out.println("[customerReadDevice]: Blacklisted apps search started");

        if (debug != null) java.lang.System.out.println("[customerReadDevice]: current list: " + config.appsBlackList().toString());

        if (!Collections.disjoint(jsonArray, appsBlackList)) {

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: Blacklisted app found");

            return true;

        } else {

            if (debug != null) java.lang.System.out.println("[customerReadDevice]: NO Blacklisted app found");

            return false;

        }

    }

 

    private class IntuneConfig {

 

        boolean extractApps;

        String inTuneHeader;

        String idpBaseUrl;

        String username;

        String userPassword;

        String azureTenantId;

        String appRegistrationClientSecret;

        Set<String> appsBlackList;

        String appRegistrationClientId;

 

        public IntuneConfig(Properties configProps) {

            idpBaseUrl = configProps.getProperty("idpBaseUrl", "https://yourIDPurl.com/");

            extractApps = Boolean.parseBoolean(configProps.getProperty("extractApps", "false"));

            inTuneHeader = configProps.getProperty("inTuneHeader", "x-intune");

            username = configProps.getProperty("username", "");

            userPassword = configProps.getProperty("userPassword", "");

            azureTenantId = configProps.getProperty("azureTenantId", "");

            appRegistrationClientSecret = configProps.getProperty("appRegistrationClientSecret", "");

            appsBlackList = new HashSet<String>(

                    Arrays.asList(configProps.getProperty("appsBlackList", ",").split(",")));

            appRegistrationClientId = configProps.getProperty("appRegistrationClientId", "");

 

        }

 

        public boolean extractApps() {

            return extractApps;

        }

 

        public Object inTuneHeader() {

            return inTuneHeader;

        }

 

        public String idpBaseUrl() {

            return idpBaseUrl;

        }

 

        public String userName() {

            return username;

        }

 

        public String azureTenantId() {

            return azureTenantId;

        }

 

        public String userPassword() {

            return userPassword;

        }

 

        public String appRegistrationClientSecret() {

            return appRegistrationClientSecret;

        }

 

        public Set<String> appsBlackList() {

            return appsBlackList;

        }

 

        public String appRegistrationClientId() {

            return appRegistrationClientId;

        }

 

    }

 

}
 

The Authentication Method will be inserted on a chain as first method, just to extract information from Azure without trying to identify or authenticate the user

The JSP page attached to the ReadDevice method, named “Read_Device.jsp”, is shown on the following lines

 

  <body onload="document.forms[0].submit()">

  <div id="loginForm" class="login-page">

    <div class="form">

      <h4><span class="icon icon-key"></span><span class="title">Secure Logon</span></h4>   

      <div id="formcontainer">

        <form class="login-form" name="authForm" enctype="application/x-www-form-urlencoded" method="POST" action="<%= (String) request.getAttribute("url")%>" autocomplete="off">

          <input id="sendAuth" type="hidden" name="sendAuth" value="true"/>

        </form>

      </div>

    </div>

  </body>
 

The JSP is shown only to ensure the Set-Cooke instructions specified on the java class are executed. The login page is auto submitted by the onload javascript function, letting the user to continue with the Authentication Contract

 

So far, we set achieved the following goals

  • Set a cookie explaining the if we have information about the device on Azure or not
  • Save on the current tomcat session the device data (for this reason it is mandatory having sticky sessions on the load balancer)

 

Let’s continue in looking how we can use this information. We created two subsequent methods on the contract just to use two RBA policies

 

 

The first Class “tst_RBA_preauthenticationVerifyDeviceRisk” is a PreAuthRiskBasedAuthenticationClass class with a Cookie Risk based Rule that verifies if the cookie is present

 

 

 

If the cookie is present (Low Risk), we try to authenticate leveraging our decisions using the device data inside the tomcat session, otherwise we proceed with a normal login

 

When the Risk level is Low, an authentication Method AuthDevice extract the information from the tomcat session and tries to ask for login using the device owner

 

The authentication Class is defined in this way

 

package com.nidp.authentication.local;

 

import java.io.IOException;

import java.util.ArrayList;

import java.util.Properties;

import java.util.Iterator;

import javax.security.auth.callback.Callback;

import javax.security.auth.callback.CallbackHandler;

import javax.security.auth.callback.UnsupportedCallbackException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;

import com.novell.nidp.authentication.local.PageToShow;

 

import org.eclipse.higgins.sts.api.ISecurityInformation;

import org.eclipse.higgins.sts.api.IUsernameToken;

 

import com.novell.nidp.authentication.local.LocalAuthenticationClass;

import com.novell.nidp.authentication.local.STSAuthenticationClass;

import com.novell.nidp.authentication.local.CallbackAuthentication;

import com.novell.nidp.authentication.local.BasicClass;

import com.novell.nidp.authentication.local.PageToShow;

import com.novell.nidp.authentication.local.PasswordValidationCallback;

 

import org.apache.commons.lang.StringEscapeUtils;

 

import com.novell.nidp.NIDPConstants;

import com.novell.nidp.NIDPException;

import com.novell.nidp.NIDPPrincipal;

import com.novell.nidp.NIDPSession;

import com.novell.nidp.NIDPSessionData;

import com.novell.nidp.authentication.AuthnConstants;

import com.novell.nidp.resource.strings.NIDPMainResDesc;

import com.novell.nidp.common.authority.PasswordExpiredException;

import com.novell.nidp.common.authority.PasswordExpiringException;

import com.novell.nidp.common.authority.UserAuthority;

import com.novell.nidp.common.protocol.AuthnRequest;

import com.novell.nidp.liberty.wsc.cache.WSCCacheEntry;

import com.novell.nidp.logging.NIDPLog;

import com.novell.nidp.saml.SAMLAuthMethods;

import com.sun.xml.wss.impl.callback.UsernameCallback;

import com.netiq.nam.common.security.InputSanitizer;

import com.novell.nidp.NIDPContext;

import com.novell.nidp.common.authority.ldap.LDAPPrincipal;

import com.novell.nidp.resource.NIDPResourceManager;

import com.novell.nidp.servlets.NIDPServletContext;

import java.util.HashMap;

import java.util.Map;

import java.util.Properties;

import org.json.JSONObject;

import org.json.JSONTokener;

import org.json.*;

import javax.servlet.http.Cookie;

 

public class AuthDeviceClass extends LocalAuthenticationClass implements STSAuthenticationClass, CallbackAuthentication {

   private String m_Error;

   protected ArrayList<UserAuthority> userStores;

   //where we load temporarily the user name

   private String user;

   private String sendAuthEnabled;

   private String debug;

    // for NRL

    LocalAuthenticationClass basicClass = null;

 

    /**

     * Constructor for form based authentication

     * 

     * @param props

     *            Properties associated with the implementing class

     * @param uStores

     *            List of ordered user stores to authenticate against

     */

    public AuthDeviceClass(Properties props, ArrayList<UserAuthority> uStores) {

        super(props, uStores);

      userStores = uStores;

 

      this.debug = this.getProperty("DEBUG");

      if ("".equals(this.debug)) this.debug = null;

      if(null != this.debug) {

         this.debug = InputSanitizer.getSanitizedStr(this.debug);

      }

          

      this.sendAuthEnabled = this.getProperty("SENDAUTH");

      if ("".equals(this.sendAuthEnabled)) this.sendAuthEnabled = null;

      if(null != this.sendAuthEnabled) {

         this.sendAuthEnabled = InputSanitizer.getSanitizedStr(this.sendAuthEnabled);

      }

 

    }

 

    /**

     * Get the authentication type this class implements

     * 

     * @return returns the authentication type represented by this class

     */

    public String getType() {

        return AuthnConstants.OTHER;

    }

 

    public void initializeRequest(HttpServletRequest request, HttpServletResponse response, NIDPSession session, NIDPSessionData data, boolean following, String url) {

    if (debug != null) java.lang.System.out.println("initializeRequestCalled");

        super.initializeRequest(request, response, session, data, following, url);

        if (basicClass != null)

            basicClass.initializeRequest(request, response, session, data, following, url);

    }

 

    /**

     * Perform form based authentication. This method gets called on each

     * response during authentication process

     * 

     * @return returns the status of the authentication process which is one of

     *         AUTHENTICATED, NOT_AUTHENTICATED, CANCELLED, HANDLED_REQUEST,

     *         PWD_EXPIRING, PWD_EXPIRED

     */

    protected int doAuthenticate() {

        

        //first of all, when called for the first time the class will attempt to look for an already recognized NIDP Principal

        String id="";

        NIDPPrincipal nidpid = (NIDPPrincipal)this.m_Properties.get("Principal");

 

        HttpSession hts = m_Request.getSession();

        //if there is not already a NIDP Prinipal recognized for this session and we have data on the tomcat session

        if (hts != null && nidpid == null) {

            //Looke for intune gathered information

            String intuneinfo = (String) hts.getAttribute("IntuneInfo");

            if (debug != null) java.lang.System.out.println(" AuthDevice intuneinfo: " + intuneinfo);

            if (intuneinfo != null){

                //when intune data are available

                try{

                    //parse the previoushly gathered information

                    JSONObject jsonIntuneInfo = new JSONObject(intuneinfo).getJSONObject("IntuneInfo");

                    //Look for the device owner from the Intune Data

                    String userFromDevice = jsonIntuneInfo.getString("userPrincipalName").split("@")[0].replaceAll("_", " ");

                    if (debug != null) java.lang.System.out.println(" AuthDevice userFromDevice: " + userFromDevice);

 

                    NIDPPrincipal[] users = null;

                    for (int i=0; i< userStores.size(); i++){

                        //Look for the user inside the user stores set for this Method

                        users = userStores.get(i).searchUserByName(userFromDevice); //users = ua.searchUser(ldapFilter);

                    }

                    if (users.length ==1) //if the user is found on a userstore, load it as new NIDP Principal

                        nidpid = users[0];

                }

                catch (Exception e){

                if (debug != null) java.lang.System.out.println(" AuthDevice exception: " + e.getMessage());

                nidpid=null;

                }

            }

        }

        //if we found the username from the device informations

        if (debug != null && nidpid != null) java.lang.System.out.println(" AuthDevice LDAP Principal: " + ((LDAPPrincipal) nidpid).getDN().toString());  

        //If we recognized a user from the NIDP session or from the tomcat Session

        if (nidpid != null){

            try{

                //collect just the user CN

                id= ((LDAPPrincipal) nidpid).getDN().toString().split(",")[0].split("=")[1];

                if (debug != null && nidpid != null) java.lang.System.out.println(" AuthDevice Id: "+ id);

                //put the ldap principal inside the tomcat session

                hts.setAttribute("LdapDn", ((LDAPPrincipal) nidpid).getDN().toString());

                            Cookie respcookie = new Cookie("deviceSession","used");

                            respcookie.setHttpOnly(true);

                            respcookie.setPath("/nidp");

                            respcookie.setSecure(true);

                            m_Response.addCookie(respcookie);

                //save the CN inside the user variable, to pass it to the JSP frontend

                user = id;

                if (debug != null && nidpid != null) java.lang.System.out.println(" AuthDevice principal set");

            }catch (Exception e){java.lang.System.out.println(" AuthDevice exception: ");  e.printStackTrace();}

        }

 

        //Check if this class is meant to behave with a two step interaction, and our is

        String sendAuth = m_Request.getParameter("sendDeviceAuth");

        if (debug != null && nidpid != null) java.lang.System.out.println("sendAuth: " + sendAuth);

 

        //Read the attached login page from Method configuration

        String jsp = getProperty(AuthnConstants.PROPERTY_JSP);

        if (jsp == null || jsp.length() == 0)

            jsp = NIDPConstants.JSP_LOGIN;

        m_PageToShow = new PageToShow(jsp);

 

        //Look if the post data comes from the attached login page auth_device.jsp 

        //when a user confirm the ownership of the device, in that return AUTHENTICATED

        if (sendAuth != null && sendAuth.equals("true")) return AUTHENTICATED;

 

        //change user if the user click on "Change User" or handle uknown devices

        if (sendAuth != null && sendAuth.equals("false")) {

            //check if info about the device is not available

            Cookie[] cookies = m_Request.getCookies();

            String deviceSessionUnknown="";

            if (cookies != null) 

            {

            for (Cookie cookie : cookies)

            {

                if (cookie.getName().equals("deviceSession"))

                    deviceSessionUnknown = cookie.getValue();

            }

            }

            //return authenticated without selecting any user. This is the outcome of the authentication class 

            //when we do not find a corresponding device on azure OR when a user click on "change user" and the authentication

            //class already removed info about any device

            if (deviceSessionUnknown.equals("unknown")){

                return AUTHENTICATED;

            }

 

            //The following code runs when the user decides to change user

 

            //Look into tomcat session for our matched DN and remove it

            hts.setAttribute("LdapDn", "unknown");

            //then clear the cookie and ask to autosubmit the page

            Cookie respcookie = new Cookie("deviceSession","unknown");

            respcookie.setHttpOnly(true);

            respcookie.setPath("/nidp");

            respcookie.setSecure(true);

            m_Response.addCookie(respcookie);   

            m_PageToShow.addAttribute("autosubmit","true");

        }

 

        //if available, pass to the frontend the recognized user

        m_PageToShow.addAttribute("user", user);

 

        if (m_Error != null) {

                //setErrorMsg(NIDPMainResDesc.LOGIN_FAILED, "User not found",null);

                m_PageToShow.addAttribute(NIDPConstants.ATTR_LOGIN_ERROR, m_Error);

                if (debug != null) java.lang.System.out.println(" AuthDevice Setting error message " + m_Error);

        }

 

        String returnUrl = (getReturnURL() != null? getReturnURL():m_Request.getRequestURL().toString());

        if (debug != null) java.lang.System.out.println(" AuthDevice returnURL " + returnUrl);

        m_PageToShow.addAttribute(NIDPConstants.ATTR_URL,(getReturnURL() != null? getReturnURL():m_Request.getRequestURL().toString())); 

 

        String target = "";

        if (getAuthnRequest() != null && getAuthnRequest().getTarget() != null){

                target = StringEscapeUtils.escapeHtml(getAuthnRequest().getTarget());

                m_PageToShow.addAttribute("target", getAuthnRequest().getTarget());

            }

        if (debug != null) java.lang.System.out.println(" AuthDevice target " + target);

 

        //Show the frontend

        return SHOW_JSP;

    

    }

 

    public NIDPPrincipal handleSTSAuthentication(ISecurityInformation securityInformation) {

        IUsernameToken usernameToken = (IUsernameToken) securityInformation.getFirst(IUsernameToken.class);

 

        if (null != usernameToken) {

            try {

                if (authenticateWithPassword(usernameToken.getUsername(), usernameToken.getPassword()))

                    return getPrincipal();

            } catch (PasswordExpiringException pe) {

                return getPrincipal();

            } catch (PasswordExpiredException pe) {

            }

        }

        return null;

    }

 

    @Override

    public NIDPPrincipal cbAuthenticate(CallbackHandler cbHandler) {

        PasswordValidationCallback pwdCallback = new PasswordValidationCallback();

        Callback[] callbacks = new Callback[] { pwdCallback };

 

        NIDPPrincipal principal = null;

        try {

            cbHandler.handle(callbacks);

            if (pwdCallback.getUsername() != null) {

              

                String query = getProperty(AuthnConstants.PROPERTY_QUERY);

                String ldapQuery = null;

                boolean status = false;

                if (query != null)

                {

                  ldapQuery = getLDAPQueryString(query,pwdCallback.getUsername());

                  if (findPrincipalsByQuery(ldapQuery).length == 1)

                    status = true;

                }

                else if (findPrincipals(pwdCallback.getUsername()).length == 1)

                    status = true;

              

              if ( status == true ) {

                    principal = getPrincipal();

                    principal.setAuthMethod(SAMLAuthMethods.PASSWORD);

                    return principal;

                }

            }

 

        } catch (IOException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        } catch (UnsupportedCallbackException e) {

            if(NIDPLog.isLoggableWSTrustFine())

                NIDPLog.logWSTrustFine("The caller doesn't support password callback: " + e.getMessage());

        }

        return null;

    }

}
 

The attached login page auth_device.jsp looks like:

 

<%@ page import="com.novell.nidp.*" %>

<%@ page import="com.novell.nidp.servlets.*" %>

<%@ page import="com.novell.nidp.resource.*" %>

<%@ page import="com.novell.nidp.resource.jsp.*" %>

<%@ page import="com.novell.nidp.saml2.protocol.SAML2AuthnRequest"%>

<%@ page import="com.novell.nidp.common.protocol.AuthnRequest"%>

<%@ page import="org.apache.commons.lang.StringEscapeUtils" %>

<%@ page import="com.novell.nidp.ui.*" %>

 

<% String err = (String) request.getAttribute(NIDPConstants.ATTR_LOGIN_ERROR);

    String redirectURL = "";

    String query = request.getQueryString();

    String autosubmit = (String) request.getAttribute("autosubmit");

    //read the username from device

    String username = (String) request.getAttribute("user");

%>

<html lang="en">

 

<head>

    <title>MFA Conditional Device Page</title>

 

    <meta http-equiv="Content-Language" content="en">

    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">

    <meta http-equiv="Pragma" content="no-cache">

    <meta http-equiv="Expires" content="0">

    <meta http-equiv="content-type" content="text/html;charset=utf-8">

    <meta http-equiv="X-UA-Compatible" content="IE=edge">

 

    <!-- Apple IOS -->

    <meta name="apple-mobile-web-app-capable" content="yes">

    <meta name="apple-mobile-web-app-status-bar-style" content="black">

 

    <!-- Google Android -->

    <meta name="mobile-web-app-capable" content="yes">

    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-cover=fit">

 

    <link rel="stylesheet" href="../css/reset.css">

    <link rel="stylesheet" href="../css/new_Style.css">

 

    <script type="text/javascript" src="<%= request.getContextPath() %>/images/showhide_2.js"></script>

 

    <script>

    function checkSubmit(){

   <%

        if(autosubmit!=null)  {

    %>            

        document.getElementById('sendDeviceAuth').value='false';document.forms[0].submit();

    <%

        }

    %>

    }

    </script>

 

    <meta charset="utf-8">

    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

</head>

    <body onload="checkSubmit()">

    <h1><img src="<%= request.getContextPath() %>/images/logo.svg" alter="img" class="logo"></h1>

    <p><%=err%></p>

    <div id="loginForm" class="login-page">

            <div class="form" style="word-wrap: break-word !important; line-height: 1.5 !important; max-width: 760px !important;">

                <h4 style="margin-bottom: 16px;"><span class="icon icon-key"></span><span class="title">Secure Logon for Enterprise Portal</span></h4>  

 

            <div id="formcontainer" style="display: inline-block; max-width: 360px !important; align: center !important"><form class="login-form" name="enrollForm" enctype="application/x-www-form-urlencoded" method="POST" action="<%= (String) request.getAttribute("url")%>" autocomplete="off">

                    <input class="inputRedesign" id="sendDeviceAuth" type="hidden" name="sendDeviceAuth" value="true"/>

                    <input class="btn btn-block credentials_input_submit" id="continueButton" value="Continue To Login as <%= (String) request.getAttribute("user")%>" type="submit" style="line-height: normal; margin: 8px; margin-left: 0px !important;"/>

                    <input class="btn btn-block credentials_input_submit" id="enrollButton" value="Change User" onclick="document.getElementById('sendDeviceAuth').value='false';document.forms[0].submit()" " type="button" style="line-height: normal; margin: 8px; margin-left: 0px !important; white-space:normal;"/>

            </form>

            </div>

        </div>

    </div>

    </body>

</html>

 

Finally, lets see the last method “tst_RBA_preauthIsUserContextPresent” where NAM retrieves the user and implement a second factor or for unknow devices requires the username and password

 

The authentication Class

 

The risk policy

 

The policy “IsUserContextPresent” is a custom Risk based access policy aimed to extract the result of the device authentication. If the user is already present on session (cooke deviceSession “used”), the Risk would be evaluated Low

 

public boolean evaluate(HTTPContext httpContext, LocationContext lContext, Devicecontext dContext, UserContext uContext, ResponseObject rspObject) {

      if(this.m_ruleEnabled) {

        RiskLog.debug("isUserContextPresentstarted");

        String knownCookieValue = httpContext.getCookieValue("deviceSession");

        RiskLog.debug("isUserContextPresent: " +  knownCookieValue); 

        if (knownCookieValue!=null & knownCookieValue.equals("used")) {

                return true;

        }

      } 

         return false;

}

 

The ConfirmAuthDeviceMethod simply identifies the user saved on the tomcat session during te doAuthenticate() method

    

protected int doAuthenticate() {

     HttpSession hts = m_Request.getSession();

     NIDPPrincipal[] users = null;

     for (int i=0; i< userStores.size(); i++){

       users = userStores.get(i).searchUserByName(hts.getAttribute("LdapDn").toString().split(",")[0].split("=")[1]); 

     }

     if (users.length ==1){

        this.setPrincipal(users[0]);

     }

     return AUTHENTICATED;  

    }

 

 

 

 

Conclusions

 

The entire logic relies on a client-side cookie named “deviceSession” because the impossibility to write the state of the authentication directly with NAM facilities. Studying deeper the NAM NIDPSession object, it would e possible to move this logic server side.

Beside that, even if we use a client cookie to understand the state of the authentication, maintaining all the relevant information on the tomcat session, the implementation is safe from any client-side tampering. Even if someone would play with its cookie, he would never be able to change the security posture.

Labels:

How To-Best Practice
Other
Comment List
Anonymous
Related Discussions
Recommended