Skip to content

Instantly share code, notes, and snippets.

@adityamukho
Last active December 23, 2015 10:09
Show Gist options
  • Save adityamukho/6619998 to your computer and use it in GitHub Desktop.
Save adityamukho/6619998 to your computer and use it in GitHub Desktop.
BIRT SSO with Drupal 7
public class AuthFilter implements Filter
{
private static final boolean debug = false;
// The filter configuration object we are associated with. If
// this value is null, this filter instance is not currently
// configured.
private FilterConfig filterConfig = null;
public AuthFilter ()
{
}
private boolean validateSession (HttpServletRequest request, Map<String, String> authorizedSessions)
{
Cookie[] cookies = request.getCookies ();
if (cookies != null)
{
for (int i = 0; i < cookies.length; ++i)
{
String remoteSession = cookies[i].getValue ();
if (authorizedSessions.containsKey (remoteSession))
{
String localSession = authorizedSessions.get (remoteSession);
String jSessionId = request.getSession ().getId ();
if (localSession == null)
{
authorizedSessions.put (remoteSession, jSessionId);
return true;
}
else if (localSession.equals (jSessionId) )
{
return true;
}
break;
}
}
}
return false;
}
/**
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param chain The filter chain we are processing
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet error occurs
*/
@Override
public void doFilter (ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException
{
if (debug)
{
log ("AuthFilter:doFilter()");
}
// Create wrappers for the request and response objects.
// Using these, you can extend the capabilities of the
// request and response, for example, allow setting parameters
// on the request before sending the request to the rest of the filter chain,
// or keep track of the cookies that are set on the response.
//
// Caveat: some servers do not handle wrappers very well for forward or
// include requests.
RequestWrapper wrappedRequest = new RequestWrapper ((HttpServletRequest) request);
ResponseWrapper wrappedResponse = new ResponseWrapper ((HttpServletResponse) response);
Map<String, String> authorizedSessions = (Map<String, String>) wrappedRequest.getServletContext ().getAttribute (AuthManagerServlet.class.getPackage ().getName () + "." + AuthManagerServlet.class.getName () + ".authorizedSessions");
if (authorizedSessions == null)
{
wrappedResponse.sendError (HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
if (!validateSession (wrappedRequest, authorizedSessions))
{
wrappedResponse.sendError (HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
Throwable problem = null;
try
{
chain.doFilter (wrappedRequest, wrappedResponse);
}
catch (IOException | ServletException t)
{
// If an exception is thrown somewhere down the filter chain,
// we still want to execute our after processing, and then
// rethrow the problem after that.
problem = t;
}
// If there was a problem, we want to rethrow it if it is
// a known type, otherwise log it.
if (problem != null)
{
if (problem instanceof ServletException)
{
throw (ServletException) problem;
}
if (problem instanceof IOException)
{
throw (IOException) problem;
}
sendProcessingError (problem, response);
}
}
/**
* Return the filter configuration object for this filter.
*/
public FilterConfig getFilterConfig ()
{
return (this.filterConfig);
}
/**
* Set the filter configuration object for this filter.
*
* @param filterConfig The filter configuration object
*/
public void setFilterConfig (FilterConfig filterConfig)
{
this.filterConfig = filterConfig;
}
/**
* Destroy method for this filter
*/
@Override
public void destroy ()
{
}
/**
* Init method for this filter
*/
@Override
public void init (FilterConfig filterConfig)
{
this.filterConfig = filterConfig;
if (filterConfig != null)
{
if (debug)
{
log ("AuthFilter: Initializing filter");
}
}
}
/**
* Return a String representation of this object.
*/
@Override
public String toString ()
{
if (filterConfig == null)
{
return ("AuthFilter()");
}
StringBuilder sb = new StringBuilder ("AuthFilter(");
sb.append (filterConfig);
sb.append (")");
return (sb.toString ());
}
private void sendProcessingError (Throwable t, ServletResponse response)
{
String stackTrace = getStackTrace (t);
if (stackTrace != null && !stackTrace.equals (""))
{
try
{
response.setContentType ("text/html");
try (PrintStream ps = new PrintStream (response.getOutputStream ()); PrintWriter pw = new PrintWriter (ps))
{
pw.print ("\n\nError\n\n\n"); //NOI18N
// PENDING! Localize this for next official release
pw.print ("</pre><h1>The resource did not process correctly</h1><pre>\n\n");
pw.print (stackTrace);
pw.print ("\n");
//NOI18N
}
response.getOutputStream ().close ();
}
catch (Exception ex)
{
}
}
else
{
try
{
try (PrintStream ps = new PrintStream (response.getOutputStream ()))
{
t.printStackTrace (ps);
}
response.getOutputStream ().close ();
}
catch (Exception ex)
{
}
}
}
public static String getStackTrace (Throwable t)
{
String stackTrace = null;
try
{
StringWriter sw = new StringWriter ();
PrintWriter pw = new PrintWriter (sw);
t.printStackTrace (pw);
pw.close ();
sw.close ();
stackTrace = sw.getBuffer ().toString ();
}
catch (Exception ex)
{
}
return stackTrace;
}
public void log (String msg)
{
filterConfig.getServletContext ().log (msg);
}
/**
* This request wrapper class extends the support class HttpServletRequestWrapper, which implements all the methods in the
* HttpServletRequest interface, as delegations to the wrapped request. You only need to override the methods that you need to change. You
* can get access to the wrapped request using the method getRequest()
*/
class RequestWrapper extends HttpServletRequestWrapper
{
public RequestWrapper (HttpServletRequest request)
{
super (request);
}
// You might, for example, wish to add a setParameter() method. To do this
// you must also override the getParameter, getParameterValues, getParameterMap,
// and getParameterNames methods.
protected HashMap localParams = null;
public void setParameter (String name, String[] values)
{
if (debug)
{
System.out.println ("AuthFilter::setParameter(" + name + "=" + values + ")" + " localParams = " + localParams);
}
if (localParams == null)
{
localParams = new HashMap ();
// Copy the parameters from the underlying request.
Map wrappedParams = getRequest ().getParameterMap ();
Set keySet = wrappedParams.keySet ();
for (Iterator it = keySet.iterator (); it.hasNext ();)
{
Object key = it.next ();
Object value = wrappedParams.get (key);
localParams.put (key, value);
}
}
localParams.put (name, values);
}
@Override
public String getParameter (String name)
{
if (debug)
{
System.out.println ("AuthFilter::getParameter(" + name + ") localParams = " + localParams);
}
if (localParams == null)
{
return getRequest ().getParameter (name);
}
Object val = localParams.get (name);
if (val instanceof String)
{
return (String) val;
}
if (val instanceof String[])
{
String[] values = (String[]) val;
return values[0];
}
return (val == null ? null : val.toString ());
}
@Override
public String[] getParameterValues (String name)
{
if (debug)
{
System.out.println ("AuthFilter::getParameterValues(" + name + ") localParams = " + localParams);
}
if (localParams == null)
{
return getRequest ().getParameterValues (name);
}
return (String[]) localParams.get (name);
}
@Override
public Enumeration getParameterNames ()
{
if (debug)
{
System.out.println ("AuthFilter::getParameterNames() localParams = " + localParams);
}
if (localParams == null)
{
return getRequest ().getParameterNames ();
}
return Collections.enumeration (localParams.keySet ());
}
@Override
public Map getParameterMap ()
{
if (debug)
{
System.out.println ("AuthFilter::getParameterMap() localParams = " + localParams);
}
if (localParams == null)
{
return getRequest ().getParameterMap ();
}
return localParams;
}
}
/**
* This response wrapper class extends the support class HttpServletResponseWrapper, which implements all the methods in the
* HttpServletResponse interface, as delegations to the wrapped response. You only need to override the methods that you need to change.
* You can get access to the wrapped response using the method getResponse()
*/
class ResponseWrapper extends HttpServletResponseWrapper
{
public ResponseWrapper (HttpServletResponse response)
{
super (response);
}
// You might, for example, wish to know what cookies were set on the response
// as it went throught the filter chain. Since HttpServletRequest doesn't
// have a get cookies method, we will need to store them locally as they
// are being set.
/*
* protected Vector cookies = null;
*
* // Create a new method that doesn't exist in HttpServletResponse public Enumeration getCookies() { if (cookies == null) cookies =
* new Vector(); return cookies.elements(); }
*
* // Override this method from HttpServletResponse to keep track // of cookies locally as well as in the wrapped response. public void
* addCookie (Cookie cookie) { if (cookies == null) cookies = new Vector(); cookies.add(cookie);
* ((HttpServletResponse)getResponse()).addCookie(cookie); }
*/
}
}
public class AuthManagerServlet extends HttpServlet
{
public static final String ENCRYPTION_KEY = "ENCRYPTION_KEY";
private Map<String, String> authorizedSessions = new HashMap<> ();
private String encryptionKey, initialVector = null;
private Timer timer = new Timer (true);
private enum Operations
{
LOGIN, LOGOUT
}
/**
* Processes requests for both HTTP
* <code>GET</code> and
* <code>POST</code> methods.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType ("text/html;charset=UTF-8");
PrintWriter out = response.getWriter ();
String message = null;
try
{
String encData = request.getParameter ("data");
if (encData == null)
{
message = "Operation failed. Data cannot be null.";
response.sendError (HttpServletResponse.SC_UNAUTHORIZED, message);
}
else
{
String params = Transcoder.decrypt (encData, initialVector, encryptionKey);
StringTokenizer st = new StringTokenizer (params, "&=");
final Map<String, String> paramMap = new HashMap<> ();
while (st.hasMoreTokens ())
{
String name = st.nextToken ();
String value = st.nextToken ();
paramMap.put (name, value);
}
Operations ops;
try
{
ops = Operations.valueOf (paramMap.get ("op").toUpperCase ());
switch (ops)
{
case LOGIN:
authorizedSessions.put (paramMap.get ("session_id"), null);
int timeout = Integer.parseInt (paramMap.get ("timeout"));
//Timeout is received with each auth request, but is set globally for all future sessions.
request.getServletContext ().setAttribute (getClass ().getPackage ().getName () + "." + getClass ().getName () + ".sessionTimeout", timeout);
message = "Login successful.";
break;
case LOGOUT:
authorizedSessions.remove (paramMap.get ("session_id"));
message = "Logout successful.";
break;
}
}
catch (IllegalArgumentException e)
{
message = "Operation failed. Invalid op.";
response.sendError (HttpServletResponse.SC_UNAUTHORIZED, message);
}
}
}
finally
{
out.println (message);
out.close ();
}
}
//
/**
* Handles the HTTP
* <code>GET</code> method.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
processRequest (request, response);
}
/**
* Handles the HTTP
* <code>POST</code> method.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPost (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
processRequest (request, response);
}
/**
* Returns a short description of the servlet.
*
* @return a String containing servlet description
*/
@Override
public String getServletInfo ()
{
return "Simple Authentication Manager Servlet, based on background info recieved from authorized servers.";
}//
@Override
public void init (ServletConfig config) throws ServletException
{
super.init (config);
ServletContext context = config.getServletContext ();
context.setAttribute (getClass ().getPackage ().getName () + "." + getClass ().getName () + ".authorizedSessions", authorizedSessions);
encryptionKey = context.getInitParameter (ENCRYPTION_KEY);
if (encryptionKey == null)
{
Logger.getLogger (getClass ().getName ()).log (Level.SEVERE, "Error!! Encryption key not found.");
}
try
{
initialVector = Transcoder.md5 (Transcoder.md5 (encryptionKey)).substring (0, 16);
}
catch (NoSuchAlgorithmException ex)
{
Logger.getLogger (getClass ().getName ()).log (Level.SEVERE, null, ex);
}
}
}
/**
* Snippets from the main module file. One problem with Drupal (as of 7.14) is that the session id is not available at the time the user login hook is fired.
* To get around this limitation, I need to define my own table for tracking sessions and mapping them to user ids.
* The init hook, which is one of the first to fire on every page load, looks for entries in the table with blank session ids and does the requisite post-login processing.
* As a consequence of introducing this additional table, some additional logic is required to update its data on each event.
**/
/**
* Implements hook_user_logout().
*/
function birt_reports_user_logout($account) {
db_delete('birt_reports_sessions')
->condition('uid', $account->uid)
->condition('sid', $account->sid)
->execute();
$birt_reports_auth_sessions = cache_get('birt_reports_auth_sessions');
if (!empty($birt_reports_auth_sessions)) {
unset($birt_reports_auth_sessions->data[$account->sid]);
cache_set('birt_reports_auth_sessions', $birt_reports_auth_sessions->data, 'cache', CACHE_PERMANENT);
}
_birt_reports_report_server_auth($account->sid, 'logout');
}
/**
* Implements hook_user_login().
*/
function birt_reports_user_login(&$edit, $account) {
//Session ID is not yet present. So using roundabout method.
if (user_access('access report servers', $account)) {
db_insert('birt_reports_sessions')
->fields(array('uid' => $account->uid))
->execute();
}
}
function _birt_reports_report_server_auth($sid, $op, $timeout = 600) {
$params = "session_id=$sid&op=$op&timeout=$timeout";
//Every time a field instance is created/updated, its data is also saved to the variables table for easy retrieval later on.
$encryption_keys = variable_get('birt_reports_encryption_keys', array());
$report_servers = variable_get('birt_reports_active_report_servers', array());
$session_servlets = variable_get('birt_reports_session_servlets', array());
$options = array(
'method' => 'POST',
'headers' => array('Content-Type' => 'application/x-www-form-urlencoded'),
);
//Inform all registered report servers of new event.
foreach ($report_servers as $id => $server) {
$key = $encryption_keys[$id];
if (empty($key)) {
watchdog('birt_reports', 'Encryption key not set for report server @server. Skipping authentication.', array('@server' => $server), WATCHDOG_WARNING);
continue;
}
$servlet = empty($session_servlets[$id]) ? 'smanage' : $session_servlets[$id];
$url = $server . "/$servlet";
//AES 128 bit encryption requires the initial vector to be 16 chars long.
//This is strictly enforced in Java, though not in PHP.
$iv = substr(md5(md5($key)), 0, 16);
$data = _birt_reports_encrypt($params, $iv, $key);
$options['data'] = 'data=' . urlencode($data);
// dpm ($data);
$result = drupal_http_request($url, $options);
if (isset($result->error)) {
watchdog('birt_reports', 'Error logging in/out from report server @server. Error is: @error', array('@server' => $server, '@error' => "$result->code $result->error"), WATCHDOG_ERROR);
}
else {
watchdog('birt_reports', 'Successful auth transaction with report server @server. Message is: @message', array('@server' => $server, '@message' => "$result->status_message"), WATCHDOG_INFO);
}
}
}
function _birt_reports_encrypt($message, $initialVector, $secretKey) {
return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, md5($secretKey), $message, MCRYPT_MODE_CFB, $initialVector));
}
/**
* Implements hook_init().
*/
function birt_reports_init() {
global $user;
if (user_access('access report servers')) {
$birt_reports_auth_sessions = cache_get('birt_reports_auth_sessions');
if (empty($birt_reports_auth_sessions)) {
$birt_reports_auth_sessions = new stdClass();
$birt_reports_auth_sessions->data = array();
}
if (!in_array($user->sid, $birt_reports_auth_sessions->data)) {
$result = db_select('birt_reports_sessions', 'b')
->fields('b', array('uid', 'sid'))
->condition('b.uid', $user->uid, '=')
->condition('b.sid', '0', '=')
->execute()
->fetchObject();
$uid = $result->uid;
if ($uid) {
db_update('birt_reports_sessions')
->fields(array('sid' => $user->sid))
->condition('uid', $uid)
->condition('sid', '0')
->execute();
$timeout = ini_get('session.cookie_lifetime');
if (!is_numeric($timeout) || ($timeout < 0)) { $timeout = 600; } _birt_reports_report_server_auth($user->sid, 'login', $timeout);
}
$birt_reports_auth_sessions->data[$user->sid] = $user->sid;
cache_set('birt_reports_auth_sessions', $birt_reports_auth_sessions->data, 'cache', CACHE_PERMANENT);
}
}
}
@WebListener ()
public class SessionLifecycleListener implements HttpSessionListener
{
@Override
public void sessionCreated (HttpSessionEvent hse)
{
int sessionTimeout = (int) hse.getSession ().getServletContext ().getAttribute (AuthManagerServlet.class.getPackage ().getName () + "." + AuthManagerServlet.class.getName () + ".sessionTimeout");
if (sessionTimeout <= 0)
{
sessionTimeout = 600;
}
hse.getSession ().setMaxInactiveInterval (sessionTimeout);
}
@Override
public void sessionDestroyed (HttpSessionEvent hse)
{
Map<String, String> authorizedSessions = (Map<String, String>) hse.getSession ().getServletContext ().getAttribute (AuthManagerServlet.class.getPackage ().getName () + "." + AuthManagerServlet.class.getName () + ".authorizedSessions");
String jSessionId = hse.getSession ().getId ();
Collection values = authorizedSessions.values ();
for (Iterator i = values.iterator (); i.hasNext ();)
{
String value = i.next ();
if (jSessionId.equals (value))
{
authorizedSessions.remove (value);
break;
}
}
}
}
public class Transcoder
{
private Transcoder ()
{
}
public static String md5 (String input) throws NoSuchAlgorithmException
{
MessageDigest md = MessageDigest.getInstance ("MD5");
byte[] messageDigest = md.digest (input.getBytes ());
BigInteger number = new BigInteger (1, messageDigest);
return number.toString (16);
}
public static String decrypt (String encryptedData, String initialVectorString, String secretKey)
{
String decryptedData = null;
try
{
SecretKeySpec skeySpec = new SecretKeySpec (md5 (secretKey).getBytes (), "AES");
IvParameterSpec initialVector = new IvParameterSpec (initialVectorString.getBytes ());
Cipher cipher = Cipher.getInstance ("AES/CFB8/NoPadding");
cipher.init (Cipher.DECRYPT_MODE, skeySpec, initialVector);
byte[] encryptedByteArray = (new org.apache.commons.codec.binary.Base64 ()).decode (encryptedData.getBytes ());
byte[] decryptedByteArray = cipher.doFinal (encryptedByteArray);
decryptedData = new String (decryptedByteArray, "UTF-8");
}
catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | UnsupportedEncodingException e)
{
Logger.getLogger (Transcoder.class.getName ()).log (Level.SEVERE, "Problem decrypting the data", e);
}
return decryptedData;
}
}
<!--
Report resources directory for preview. Defaults to ${birt home}
-->
<context-param>
<param-name>BIRT_VIEWER_WORKING_FOLDER</param-name>
<param-value>report</param-value>
</context-param>
<context-param>
<description>The secret key used to encrypt/decrypt informations between servers and browsers. Must never be transmitted!</description>
<param-name>ENCRYPTION_KEY</param-name>
<param-value>{your encryption key}</param-value>
</context-param>
<filter>
<filter-name>AuthFilter</filter-name>
<filter-class>{package.path}.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<servlet-name>EngineServlet</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<servlet-name>ViewerServlet</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>AuthManagerServlet</servlet-name>
<servlet-class>{package.path}.AuthManagerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AuthManagerServlet</servlet-name>
<url-pattern>/smanage</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>10</session-timeout>
</session-config>
<listener>
<listener-class>{package.path}.SessionLifecycleListener</listener-class>
</listener>
@adityamukho
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment