Tuesday, October 27, 2015

Writing a custom NTLM grant handler and a sample client for API Manager with handshake support

NTLM is a challange/response based authentication protocol which is proprietary for Microsoft. If you are new to NTLM and need to have a basic idea what is happening you can read my previous blog post [1].

This blog explains how to write a custom grant handler to WSO2 API Manager to support NTLM grant type.

If you are going to try this please note you should be in a Windows environment. If you are trying this on a single machine (Client and server user accounts on a same computer), you would be able to try this without any issues (I have tried on a Windows 7/8 environments). But if you need to try an actual production environment (clients and server are different computers) , first of all you need to setup a Active Directory Domain. You can follow link [2] to setup that.

In this process we need to deal with Windows Platform related APIs. For that we can use Java Native Access (JNA) [3] library which is a simple way of native access without requiring to write JNI (Java Native Interface) code. There is another library which is built on top of JNA which is Waffle [4] which encapsulates all functionality you need to implement user authentication.

Following Maven dependencies can be used for setting up JNA and Waffle respectively.

<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>com.github.dblock.waffle</groupId>
<artifactId>waffle-jna</artifactId>
<version>1.8.0</version>
</dependency>
view raw jna_mvn.xml hosted with ❤ by GitHub


First step of the NTLM handshake is to create the Type1 token by the client.

String securityPackage = "Negotiate";
WindowsSecurityContextImpl clientContext = null;
IWindowsCredentialsHandle clientCredentials = null;
// client credentials handle
clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
clientCredentials.initialize();
// initial client security context
clientContext = new WindowsSecurityContextImpl();
clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
System.out.println("username :" + WindowsAccountImpl.getCurrentUsername());
clientContext.setCredentialsHandle(clientCredentials.getHandle());
clientContext.setSecurityPackage(securityPackage);
clientContext.initialize(null, null, "localhost");
String clientTokenType1 = Base64.encode(clientContext.getToken());
System.out.println("Generated NTLM token Type1:" + clientTokenType1);
Generated token is then encoded into Base64 and should be send as a header.

Server (APIM) recieves the token from the request header. Then it validates the Type 1 token and generates a Type 2 token. It is sent back to the client. Then client should generate a Type 3 token based on Type 2 token. It is sent to the server as a Header.
/* client receives a Token 2 as a Header in following format
WWW-Authenticate: NTLM <token2>
*/
Header tokenHeader = response.getFirstHeader("WWW-Authenticate");
String clientTokenType2 = tokenHeader.getValue().trim().split(" ")[1];
System.out.println("Recieved NTLM token Type2 From Server:" + clientTokenType2);
byte[] decodedToken2 = Base64.decode(clientTokenType2);
Sspi.SecBufferDesc continueToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, decodedToken2);
clientContext.initialize(clientContext.getHandle(), continueToken, "localhost");
byte[] clientTokenType3 = clientContext.getToken();
String encodedToken3 = Base64.encode(clientTokenType3);
System.out.println("Generated NTLM token Type3:" + encodedToken3);
//send the token 3 to server
response = invokeNTLMTokenEndpoint(encodedToken3);
HttpEntity entity = response.getEntity();
String responseString = EntityUtils.toString(entity, "UTF-8");
System.out.println(responseString);
Once server receives a token, it first determines the type of it. Type is the value of the 8th byte of the Base64 decoded NTLM token. If the server determines it as a Type 1 token, then it validates it and generates a Type 2 token based on that.
Following function can be used to determine the type of the token.

public int getNLTMMessageType(byte[] decodedNLTMMessage) throws IdentityOAuth2Exception {
int messageType;
if (decodedNLTMMessage.length > 8) {
messageType = decodedNLTMMessage[8];
} else {
throw new IdentityOAuth2Exception(
"Cannot extract message type from NLTM Token. Decoded token length is less than 8.");
}
//NLTM token type must be one of 1,2 or 3
if (messageType < 1 || messageType > 3) {
throw new IdentityOAuth2Exception(
"Invalid NLTM message type:" + messageType + ". Should be one of 1,2 or 3.");
}
return messageType;
}
If the server determines it as a Type 1 token, then it validates it and generates a Type 2 token based on that. If it is Type 3 token, it will validate it and it will then identifies the user details communicating with the domain controller.
To write a custom grant handler we can basically follow the doc [5]. That includes a sample [6] code too. When writing our handler we need to basically extend AbstractAuthorizationGrantHandler when writing our class.
Following is the grant handler code.

package org.wso2.carbon.identity.oauth.custom.ntlm;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.ResponseHeader;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;
import org.wso2.carbon.identity.oauth2.token.handlers.grant.AbstractAuthorizationGrantHandler;
import waffle.util.Base64;
import waffle.windows.auth.IWindowsSecurityContext;
import waffle.windows.auth.impl.WindowsAuthProviderImpl;
public class NTLMAuthenticationGrantHandlerWithHandshake extends AbstractAuthorizationGrantHandler {
private static Log log = LogFactory.getLog(NTLMAuthenticationGrantHandlerWithHandshake.class);
private static WindowsAuthProviderImpl provider = new WindowsAuthProviderImpl();
private String securityPackage = "Negotiate";
public int getNLTMMessageType(byte[] decodedNLTMMessage) throws IdentityOAuth2Exception {
int messageType;
if (decodedNLTMMessage.length > 8) {
messageType = decodedNLTMMessage[8];
} else {
throw new IdentityOAuth2Exception(
"Cannot extract message type from NLTM Token. Decoded token length is less than 8.");
}
//NLTM token type must be one of 1,2 or 3
if (messageType < 1 || messageType > 3) {
throw new IdentityOAuth2Exception(
"Invalid NLTM message type:" + messageType + ". Should be one of 1,2 or 3.");
}
return messageType;
}
@Override
public boolean validateGrant(OAuthTokenReqMessageContext tokReqMsgCtx) throws IdentityOAuth2Exception {
boolean validGrant = super.validateGrant(tokReqMsgCtx);
if (!validGrant) {
return false;
}
String token = tokReqMsgCtx.getOauth2AccessTokenReqDTO().getWindowsToken();
IWindowsSecurityContext serverContext = null;
if (token != null) {
byte[] bytesToken = Base64.decode(token);
int tokenType = getNLTMMessageType(bytesToken);
if (log.isDebugEnabled()) {
log.debug("Received NTLM token Type " + tokenType + ":" + token);
}
if (tokenType == 1) {
serverContext = provider.acceptSecurityToken("server-connection", bytesToken, securityPackage);
String type2Token = Base64.encode(serverContext.getToken());
if (log.isDebugEnabled()) {
log.debug("Sent NTLM token Type 2:" + type2Token);
}
ResponseHeader[] responseHeaders = new ResponseHeader[1];
responseHeaders[0] = new ResponseHeader();
responseHeaders[0].setKey("WWW-Authenticate");
responseHeaders[0].setValue("NTLM " + type2Token);
tokReqMsgCtx.addProperty("RESPONSE_HEADERS_PROPERTY", responseHeaders);
return false;
} else if (tokenType == 3) {
serverContext = provider.acceptSecurityToken("server-connection", bytesToken, securityPackage);
String resourceOwnerUserNameWithDomain = serverContext.getIdentity().getFqn();
String resourceOwnerUserName = resourceOwnerUserNameWithDomain.split("\\\\")[1];
tokReqMsgCtx.setAuthorizedUser(resourceOwnerUserName);
return true;
} else {
log.error("Unknown NTLM token, Type " + tokenType + ":" + token);
return false;
}
} else {
if (log.isDebugEnabled()) {
log.debug("NTLM token is null");
}
throw new IdentityOAuth2Exception("NTLM token is null");
}
}
}
The link [7] consists of full code of client and server.

Once you completed writing the handler code, you can do the following configuration to enable the new custom grant type.

There is a section called "SupportedGrantTypes" in identity.xml in /repository/conf folder. (If you are using APIM 1.10.x this can be found at /repository/conf/identity folder). Add your new grant type to that section as a new SupportedGrantType. Correctly put the fully qualified class name of the custom grant handler under

<SupportedGrantType>
<GrantTypeName>iwa:ntlmhandshake</GrantTypeName>
<GrantTypeHandlerImplClass>org.wso2.carbon.identity.oauth2.token.handlers.grant.iwa.ntlm.NTLMAuthenticationGrantHandler</GrantTypeHandlerImplClass>
</SupportedGrantType>


After setting this you can start the server. You can use the provided sample client at [7] to test the new NTLM grant handler. You can follow the Readme.txt file which is added there.


[1] NTLM Authentication Basics : http://malinthaprasan.blogspot.com/2015/10/ntlm-authentication-basics.html
[2] Setting up an Active Directory Domain: http://roshanwijesena.blogspot.com/2015/05/ntlm-grant-type-support-with-wso2-api.html
[3] Java Native Access (JNA) Library: https://github.com/java-native-access/jna
[4] Waffle : https://github.com/dblock/waffle
[5] Writing a custom grant type : https://docs.wso2.com/display/IS500/Writing+a+Custom+OAuth+2.0+Grant+Type
[6] Custom grant type sample code : https://svn.wso2.org/repos/wso2/people/asela/oauth/custom-grant/
[7] Sample NTLM grant handler with handshake support and a sample client code: https://drive.google.com/open?id=0B1jOwGWdNiSNMEw3UFJ1R3Z4WFU

No comments:

Post a Comment