Skip to Content

How to develop Sap Portal custom login module (based on NTLM protocol). Portal Active directory SSO

In this article I will describe how to develop custom portal login module based on NTLM to identify user.

0. Introduction.

NT LAN Manager (NTLM) is a suite of Microsoft security protocols that provides authentication, integrity, and confidentiality to users.

NTLM is a challenge-response authentication protocol which uses three messages to authenticate a client in a connection oriented environment (connectionless is similar), and a fourth additional message if integrity is desired.

1. the client establishes a network path to the server and sends a NEGOTIATE_MESSAGE advertising its capabilities.

2. the server responds with CHALLENGE_MESSAGE which is used to establish the identity of the client.

3. the client responds to the challenge with an AUTHENTICATE_MESSAGE.

More info provided via this link: https://en.wikipedia.org/wiki/NT_LAN_Manager

So we need to set http header  WWW-Authenticate: NTLM and HTTP/1.1 401 Unauthorized if we will find empty Authorization header on logon page jsp in portal logon module. Everything seems simple but portal server will erase WWW-Authenticate header and client will nott recieve it. So here we come to idea tha we need to develop custom portal login module. Here is good manual how to develop project itself http://help.sap.com/saphelp_nw73ehp1/helpdata/en/48/99a22e7f020e27e10000000a421937/content.htm?frameset=/en/48/99a22e7f020e27e10000000a421937/frameset.htm&current_toc=/en/74/8ff534d56846e2abc61fe5612927bf/plain.htm&node_id=16&show_children=false

I have to add one thing, you have to export ear as SAP EAR file (because if you try to deploy it from NWDS or use CTS to transport to target system, LoginModuleConfiguration.xml will not be deployed) and place LoginModuleConfiguration.xml into root folder of result ear. Here is exemplary content of such an xml:


<login-modules>
<!-- holds all login modules -->
  <login-module>
  <!-- describes one login module -->
  <display-name>NtlmLoginModule</display-name>
  <!-- holds the display name of the login module -->
  <class-name>com.vendor.logon.modules.ntlm.NtlmLoginModule</class-name>
  <!-- holds the full path to the login module class -->
  <description>NtlmLoginModule extends AbstractLoginModule</description>
  <!-- holds the description of the login module -->
  <options>
  <!-- holds all the options of the login module -->
  <option>
  <!-- holds the name/value pair of an option -->
  <name>jndiBeanName</name>
  <value>vendor.com/portal~logon~logic~ear/LOCAL/LogonFacadeBean/com.vendor.logon.beans.LogonFacadeBeanLocal</value>
  </option>
  </options>
  </login-module>
</login-modules>

0. Developing NTLM custom login module.

Here is some example code to create NTLM login module:


package com.vendor.logon.modules.ntlm;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import javax.naming.InitialContext;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import jcifs.ntlmssp.Type3Message;
import com.vendor.logon.beans.LogonFacadeBeanLocal;
import com.vendor.logon.entity.SearchParameters;
import com.vendor.logon.entity.UserCredentials;
import com.sap.engine.interfaces.security.auth.AbstractLoginModule;
import com.sap.engine.lib.security.Principal;
import com.sap.engine.lib.security.http.HttpGetterCallback;
import com.sap.engine.lib.security.http.HttpSetterCallback;
import com.sap.tc.logging.Location;
public class NtlmLoginModule extends AbstractLoginModule {
  private String BEAN_JNDI_NAME = "vendor.com/portal~logon~logic~ear/LOCAL/LogonFacadeBean/com.vendor.logon.beans.LogonFacadeBeanLocal";
  private com.sap.engine.lib.security.Principal principal;
  private int state = 1;
  private Location logger = null;
  private CallbackHandler callbackHandler = null;
  private Subject subject = null;
  private Map sharedState = null;
  @SuppressWarnings("unchecked")
  private Map options = null;
  private String msgStage2 = "";
  **
    * The method initialize the login module with the
    * relevant authentication and state information.
    */
  @Override
  public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
  /*This method cames frome manual and simply inits our login module*/
  // Initializing the values of the variables
  logger = Location.getLocation(this.getClass());
  this.subject = subject;
  this.callbackHandler = callbackHandler;
  this.sharedState = sharedState;
  this.options = options;
  /*This part is retrieving logon logic bean jndi name from NWA config */
  String BEAN_JNDI_NAME = (String) options.get("jndiBeanName");
  if (null != BEAN_JNDI_NAME && !BEAN_JNDI_NAME.isEmpty()) {
  this.BEAN_JNDI_NAME = BEAN_JNDI_NAME;
  }
  }
  /*
  getHttpAuthorizationHeader retrieves Authorization HttpHeader from client
  */
  private String getHttpAuthorizationHeader() throws LoginException {
  //Creating HttpGetterCallback to get Authorization HttpHeader
  HttpGetterCallback httpGetterCallback = new HttpGetterCallback();
  httpGetterCallback.setType((byte) 1);
  httpGetterCallback.setName("Authorization");
  try {
  this.callbackHandler.handle(new Callback[] { httpGetterCallback });
  } catch (Exception e) {
  logger.traceThrowableT(500, "Could not get authorization header from request.", e);
  failedAuthenticationException("Could not get authorization header from request. Reason: " + e.getMessage());
  }
  String authzHeader = (String) httpGetterCallback.getValue();
  if (logger.beDebug()) {
  logger.debugT("Authorization header [{0}] read from HTTP request: {1}", authzHeader);
  }
  return authzHeader;
  }
  private void failedAuthenticationException(String errorMessage) throws LoginException {
  this.state = 3;
  throw new LoginException(errorMessage);
  }
  private void fallbackStateException() throws LoginException {
  this.state = 5;
  throw new LoginException("NTLM authentication has failed during previous attempt.");
  }
  private void initialStateException() throws LoginException {
  this.state = 1;
  throw new LoginException("Trigger NTLM authentication. Send key to client.");
  }
  private void sendBackNtlmKeyException() throws LoginException {
  this.state = -1;
  throw new LoginException("Trigger NTLM authentication. Get key to client");
  }
  /*
  existPreventAutologonCookie check is there any cookie that we created while logging out
  to prevent autologon, I created JS code in AFP framework page that puts preventautologon coockie
  with value true and lifetime about one minute, so user will have opportunity to logon manually
  */
  public static boolean existPreventAutologonCookie(CallbackHandler callbackHandler, Location logger) {
  String cookie = getOriginalURLCookie(callbackHandler, logger);
  return cookie != null && !cookie.isEmpty() && cookie.equalsIgnoreCase("true");
  }
  /*
  getOriginalURLCookie gets cookie with name preventautologon
  */
  public static String getOriginalURLCookie(CallbackHandler callbackHandler, Location logger) {
  HttpGetterCallback sgc = new HttpGetterCallback();
  sgc.setType((byte) 2);
  // sgc.setName("com.sap.engine.security.authentication.original_application_url");
  sgc.setName("preventautologon");
  try {
  callbackHandler.handle(new Callback[] { sgc });
  } catch (Exception e) {
  logger.traceThrowableT(500, "Could not get original URL cookie from request.", e);
  return null;
  }
  String cookie_value = (String) sgc.getValue();
  logger.debugT("getOriginalURLCookie cookie_value " + cookie_value);
  return cookie_value;
  }
  /**
  * Retrieves the user credentials and checks them. This is
  * the first part of the authentication process.
  */
  @Override
  public boolean login() throws LoginException {
  String authHeader = getHttpAuthorizationHeader();
  if (authHeader != null) {
  // if client responds with Authorization HttpHeader
  try {
  processAuthorizationHeader(authHeader);
  } catch (IOException e) {
  fallbackStateException();
  }
  } else if (existPreventAutologonCookie(this.callbackHandler, logger)) {
  logger.debugT("Authorization header not received. Prevent autologon cookie found in request. NTLM running in fallback mode.");
  fallbackStateException();
  } else {
  logger.debugT("Authorization header not received. Prevent autologon cookie not found in request. Triggering NTLM authentication.");
  initialStateException();
  }
  return true;
  }
  /*
  processAuthorizationHeader recieves authHeader and check what NTLM phase is it
  */
  @SuppressWarnings("unchecked")
  private void processAuthorizationHeader(String authHeader) throws LoginException, IOException {
  logger.debugT("authHeader " + authHeader);
  if (authHeader.startsWith("NTLM ")) {
  //First phase, client answered that it can work over NTLM
  byte[] msg = new sun.misc.BASE64Decoder().decodeBuffer(authHeader.substring(5));
  if (msg[8] == 1) {
  byte z = 0;
  byte[] msg1 = { (byte) 'N', (byte) 'T', (byte) 'L', (byte) 'M', (byte) 'S', (byte) 'S', (byte) 'P', z, (byte) 2, z, z, z, z, z, z, z,
  (byte) 40, z, z, z, (byte) 1, (byte) 130, z, z, z, (byte) 2, (byte) 2, (byte) 2, z, z, z, z, z, z, z, z, z, z, z, z };
  msgStage2 = "NTLM " + new sun.misc.BASE64Encoder().encodeBuffer(msg1);
  //sending server response with encrypted key
  sendBackNtlmKeyException();
  } else if (msg[8] == 3) {
  // Did Authentication Succeed?
  Type3Message type3 = new Type3Message(msg);
  if (logger.beDebug()) {
  logger.debugT("osUser: " + type3.getUser());
  logger.debugT("osRemoteHost: + " + type3.getWorkstation());
  logger.debugT("osDomain: " + type3.getDomain());
  }
  try {
  InitialContext ic = new InitialContext();
  LogonFacadeBeanLocal facade = (LogonFacadeBeanLocal) ic.lookup(BEAN_JNDI_NAME);
  UserCredentials userCredentials = facade.getUserCredentials(new SearchParameters().setAdAccountName(type3.getUser().trim())
  .setAdDomainName(type3.getDomain().trim()));
  //Executing some logic to retrieve user ID by OS user credentials
  if (null != userCredentials) {
  logger.debugT("AbstractLoginModule.PRINCIPAL: "+AbstractLoginModule.PRINCIPAL);
  logger.debugT("userCredentials: " + userCredentials);
  String userName = userCredentials.getCisWithEs();
  if (sharedState.get(AbstractLoginModule.NAME) == null) {
  /*
  If the authentication is successful, put the user name in a shared state.
  One and only one login module from the stack must put the user name in the shared state to
  represent the authenticated user. For example, if the login is successful, method getRemoteUser()
  of the HTTP request will retrieve this name. In the example shown below,
  we also set the variable successful to true and return the value true for the login module.
  */
  sharedState.put(AbstractLoginModule.NAME, userName);
  }
  this.principal = new Principal(userName);
  this.state = 2;
  } else {
  logger.debugT("Didnt find UserCredentials for header: " + authHeader);
  fallbackStateException();
  }
  } catch (Exception e) {
  logger.catching(e);
  fallbackStateException();
  }
  }
  } else {
  logger.warningT("Authorization header received is not NTLM token: " + authHeader);
  failedAuthenticationException("Authorization header received is not NTLM token: " + authHeader);
  }
  }
  /*
  In this method you commit the log on.
  This is the second part of the authentication process.
  If a user's name has been stored by the login() method,
  then this user name is added to the subject of a new principal.
  */
  @SuppressWarnings("unchecked")
  @Override
  public boolean commit() throws LoginException {
  logger.debugT("commit state: " + state);
  if (this.state == 4) {
  return false;
  }
  if (this.state == 2) {
  Set<java.security.Principal> principals = this.subject.getPrincipals();
  if (null != principals && logger.beDebug()) {
  logger.debugT("principals size :" + principals.size());
  }
  principals.add(principal);
  sharedState.put(AbstractLoginModule.PRINCIPAL, this.principal);
  }
  return true;
  }
  private void triggerNTLMAuthentication(String msg) {
  HttpSetterCallback httpSetterResponseCode = new HttpSetterCallback();
  httpSetterResponseCode.setType((byte) 8);
  httpSetterResponseCode.setValue("401");
  HttpSetterCallback httpSetterWWWAuthnHeader = new HttpSetterCallback();
  httpSetterWWWAuthnHeader.setType((byte) 1);
  httpSetterWWWAuthnHeader.setName("WWW-Authenticate");
  if (null == msg || msg.isEmpty()) {
  msg = "NTLM";
  }
  httpSetterWWWAuthnHeader.setValue(msg);
  try {
  this.callbackHandler.handle(new Callback[] { httpSetterResponseCode, httpSetterWWWAuthnHeader });
  } catch (Exception e) {
  logger.traceThrowableT(500, "Could not set 401 response code and WWW-Authenticate: Negotiate header. Msg: " + msg, e);
  }
  }
  /*
  This method is used for aborting the authentication process:
  */
  @Override
  public boolean abort() throws LoginException {
  logger.debugT("abort state " + state);
  switch (this.state) {
  case 1:
  triggerNTLMAuthentication(null);
  break;
  case -1:
  triggerNTLMAuthentication(msgStage2);
  break;
  case 2:
  Set<java.security.Principal> principals = this.subject.getPrincipals();
  principals.remove(principal);
  break;
  case 4:
  if (logger.bePath()) {
  logger.exiting("abort", new Boolean(false));
  }
  return false;
  }
  logger.exiting("abort", new Boolean(true));
  return true;
  }
  /*
  the method logs out the user and also removes the principals and destroys or removes the credentials that were associated with the user during the commit phase.
  */
  @Override
  public boolean logout() throws LoginException {
  logger.debugT("logout");
  if (logger.bePath()) {
  logger.entering("logout");
  }
  subject.getPrincipals(Principal.class).clear();
  if (logger.bePath()) {
  logger.exiting("logout", new Boolean(true));
  }
  logger.entering("logout");
  return true;
  }
}
Be the first to leave a comment
You must be Logged on to comment or reply to a post.