Building a Single Sign-On Module for the BIRT Report Viewer – Part 3-1

This is the third (actually first part of the third) post of the BIRT SSO Series wherein I describe the implementation of a single sign-on module for the Eclipse BIRT Report Viewer. This post deals with the technical details of the module itself. The introduction and server configuration are covered in Part 1 and Part 2 respectively. It is recommended that you read them first in order to get acquainted with the background and the premises on which this solution is built. This post is split into two to keep the individual post lengths sane.

Part 3: The Module

There are actually two parts to the module - one which runs on the Drupal application and listens to user login and logout events, and communicates these events to the reporting component; the other sits in the report viewer webapp, listens for requests and session lifecycle events, and manages user authentication. You will have to design the first part specifically for the platform on which your main application runs, using my Drupal example as a reference. The second part, which runs in the report viewer, can be used as is. For the sake of brevity, I will refer to them hereon as the Drupal Module and the BIRT Module respectively.

3.1: The Drupal Module

For those of you familiar with the Drupal 7 API (and in particular the Field API), it may be of interest to know that this module defines its own field type, which can be attached to any entity type. All configuration options (listed below) are defined at a field instance level. There are two display modes: Embedded Report and External Link. The embedded report may be useful when you want to show the report (in an iframe) when the node page is loaded. The external link mode is more suitable for lists and tables.

However, letting too many of the Field API-specific details into this post would make it nearly impossible to follow for those who do not come from a Drupal programming background. I will therefore try to keep things as generic as possible.

For any implementation of this scheme to work, the following events should be generated by the underlying platform, and our module should be able to latch on to them to do its own stuff:

  1. User login
  2. User logout
  3. Session timeout (optional)
  4. Server shutdown (optional)

My implementations of these event hooks are shown in the code listings below. There is some configuration that needs to be done though, to inform this module about some details of the reporting component, like its location, servlet mappings, shared encryption key etc. In my case, it is possible to define them at an individual field instance level. You may choose to define them once globally if you like:

  1. Report Server URL: The fully qualified URL of the server where the BIRT engine webapp is running.
  2. Report Server Subfolder (Optional): If BIRT report files are being placed (see note below for how they can be made accessible to both Drupal and the reporting webapp) into a subfolder of _BIRT_VIEWER_WORKINGFOLDER (web.xml of BIRT webapp), specify that here.
  3. Session Management Servlet: User sessions for authenticating users to the reporting server are automatically managed by Drupal behind the scenes. If for some reason you need to map your session management servlet to something other than the default (smanage in web.xml of BIRT webapp), you can tell the BIRT Reports module about it here.
  4. Report Viewer Servlet: The name of the servlet used to run and display the live reports. Most users would want to set this to frameset.
  5. Encryption Key: The secret key used for encrypting data transmitted to the report server. This key must be set in the report server configuration as well.

Note: The report design files must be accessible to the reporting server (if the Drupal frontend is being used to upload them, it becomes important to get this right). If Drupal and the reporting server are running on the same machine, this is easily achieved by creating a symbolic link pointing from _BIRT_VIEWER_WORKINGFOLDER to the (preferably) subfolder inside the Drupal files folder where the reports are getting saved. If the servers are running on different machines, then the files must be made commonly accessible to both servers using NFS mounts or some other technique, the setup of which is beyond the scope of this article.

birt_reports.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* 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);
}
}
}

What this code is doing is basically encrypting a message string of the form _session_id={sessionid}&op={login|logout}&timeout={timeout} and sending this over to the reporting server. The reporting server will decrypt this using the same key that was used to encrypt it, and use whatever parameters it needs.

The (MySQL) ‘create statement’ for the database table used for mapping session ids to uids is given below:

1
2
3
4
5
6
7
8
delimiter $$

CREATE TABLE `birt_reports_sessions` (
`uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'User’s Uid',
`sid` varchar(255) NOT NULL DEFAULT '0' COMMENT 'Session ID',
PRIMARY KEY (`uid`,`sid`),
KEY `sid_idx` (`sid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Tracks authenticated sessions to be authorized with...'$$

Part 3-2 of this series describes the Java Module.