Application Delivery Management
Application Modernization & Connectivity
CyberRes
IT Operations Management
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
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
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
Automatic User Identification and Device Authentication:
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
We defined an Azure service user with the following roles
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
Download the app from azure marketplace the SCEPman Community Edition
Define basic information
Then click create
Inside the resource group you will find
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
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
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
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; }
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.