Sunday, September 25, 2011

Alfresco 4.0 Share single sign on (SSO)

As mentioned earlier, Alfresco has many built-in authorization mechanisms. However, Alfresco has not native support for transparent authentication (SSO, single-sign-on). Therefore, the challenge is: to create an authorization subsystem that enables users to enter Alfresco without a password using NTLM domain authentication. This subsystem must be running Windows 7, Vista, XP, be safe and secure. Read more to know how this problem was solved.


The proposed scheme of transparent SSO authentication in Alfresco is shown in Figure:

As the working mechanism of Alfresco authorization you should select External. For this in file C:\Alfresco\tomcat\shared\classes\alfresco-global.properties you should set the following properties:

authentication.chain=external1:external,alfrescoNtlm1:alfrescoNtlm 
external.authentication.enabled=true 
external.authentication.proxyHeader=X-Alfresco-Remote-User
external.authentication.proxyUserName=

To set the interaction between the applications Alfresco and Share to be hold by cookies, in a file you should write:

<config evaluator="string-compare" condition="Remote">
  <remote>  
     <endpoint>
        <id>alfresco-noauth</id>
        <name>Alfresco - unauthenticated access</name>
        <description>Access to Alfresco Repository WebScripts that do 
        not require authentication</description>
        <connector-id>alfresco</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
        <identity>none</identity>
     </endpoint>         
     <connector>
        <id>alfrescoCookie</id>
        <name>Alfresco Connector</name>
        <description>Connects to an Alfresco instance using cookie-
        based authentication</description>
        <class>org.springframework.extensions.webscripts.connector.AlfrescoConnector</class>
     </connector>         
     <endpoint>
        <id>alfresco</id>
        <name>Alfresco - user access</name>
        <description>Access to Alfresco Repository WebScripts that 
        require user authentication</description>
        <connector-id>alfrescoCookie</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/wcs</endpoint-url>
        <identity>user</identity>
        <external-auth>true</external-auth>
     </endpoint>
  </remote>
</config>

If you do not, Share can not communicate with the repository of Alfresco.

Now, Alfresco will trust the transferred to it server variable RemoteUser. To generate this variable, create a subsystem of the Apache modules. As the unit responsible for the domain transparent user authentication, choose sspi_auth_module.

To plug it in, write in Apache's configuration file (httpd.conf):

LoadModule sspi_auth_module modules/mod_auth_sspi.so

At the same time, register in the httpd.conf configuration that will be useful in the future:

LoadFile "C:/Perl/bin/perl512.dll"
LoadModule perl_module modules/mod_perl.so
Listen 10.0.20.245:80
ServerName vm-lysenko

<VirtualHost 10.0.20.245:80>
  ServerName vm-lysenko
  DocumentRoot "C:\alf"
  UseCanonicalName Off

  PerlModule Apache2::RequestRec

  RedirectMatch ^/$ /share/
  RedirectMatch ^/alfresco$ /alfresco/
  RedirectMatch ^/share$ /share/
  RedirectMatch ^/login$ /login/
  
  <Proxy ajp://127.0.0.1:8009/login/>
    Order deny,allow
    Deny from none
    Allow from all
  </Proxy>

  <Proxy ajp://127.0.0.1:8009/alfresco/>
    Order deny,allow
    Deny from none
    Allow from all
  </Proxy>

  <Proxy ajp://127.0.0.1:8009/share/>
    Order deny,allow
    Deny from none
    Allow from all
    PerlFixupHandler Apache2::SetRemoteUser
  </Proxy>
  
  ProxyPass /login/ ajp://127.0.0.1:8009/login/
  ProxyPassReverse /login/ ajp://127.0.0.1:8009/login/
  ProxyPass /alfresco/ ajp://127.0.0.1:8009/alfresco/
  ProxyPassReverse /alfresco/ ajp://127.0.0.1:8009/alfresco/
  ProxyPass /share/ ajp://127.0.0.1:8009/share/
  ProxyPassReverse /share/ ajp://127.0.0.1:8009/share/  

  <Location "/login/login.jsp">
    AuthName "Tomcat"
    AuthType SSPI
    SSPIAuth On
    SSPIPackage NTLM
    SSPIAuthoritative On
    SSPIOfferBasic Off
    SSPIOmitDomain On
    SSPIUsernameCase lower
    # SSPIPerRequestAuth Off
    # SSPIBasicPreferred
    # SSPIUsernameCase lower
    require valid-user
  </Location>  
</VirtualHost>

From the above settings it is clear, that Apache serves as a proxy, and passes through all the requests from the user to the Alfresco Share. Every time we refer to Share, we use PerlFixupHandler Apache2:: SetRemoteUser, to set the variable RemoteUser. To set this variable, use this perl-script: (in folder c:\Perl\site\lib\Apache2\ file SetRemoteUser.pm)

package Apache2::SetRemoteUser;
 
use strict;
use warnings;

use CGI::Cookie;
use Apache2::Const -compile => qw(OK);
use Apache2::Connection;
use Apache::DBI;
 
sub handler {
    my $r = shift;
            
    my $session_value = "";
    
    my $raw_cookie = $r->headers_in->{'Cookie'} || '';
    if ($raw_cookie ne '') {
      my %cookie = CGI::Cookie->parse($raw_cookie);
      if (exists $cookie{'JSESSIONID'}) {
        $session_value = $cookie{'JSESSIONID'}->value || "unknown-session";
      }
    }
    
    my $ip             = $r->connection->remote_ip() || "unknown-ip";
    my $browser        = $r->headers_in->{ 'User-Agent' } || "unknown-agent";
    my $authorization  = $r->headers_in->{ 'Authorization' } || "not-auth";    
    
    my $url      = $r->uri || "unknown-url";
    
    my $dbh = DBI->connect("DBI:mysql:sessions:localhost:3306", "session_user", "session_password", {
        PrintError => 1, # warn( ) on errors
        RaiseError => 0, # don't die on error
        AutoCommit => 1, # commit executes immediately
      }
    ) or die "Cannot connect to database: $DBI::errstr";   
    
    my $do_sql = "SELECT username FROM session WHERE unique_value = MD5('" . $session_value . $ip . $browser . "')";
    my $sth = $dbh->prepare($do_sql);
    $sth->execute( );
    
    my $result = $sth->fetchrow_hashref();
    my $u_name = $result->{'username'} || "";
    $dbh->disconnect( ); # NOOP under Apache::DBI  

    if ($u_name ne "") {
      $r->user($u_name);
    }
    return Apache2::Const::OK; 
} 
1;

This script checks if there is a registered session with the provided user name. If the session exists, the remoteUser sets up. As a place of sessions storage selected the MySQL database. The structure of sessions table is:

CREATE TABLE `session` (
  `session_ID` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) DEFAULT NULL,
  `session_value` varchar(50) DEFAULT NULL,
  `unique_value` varchar(255) DEFAULT NULL,
  `session_date` datetime DEFAULT NULL,
  PRIMARY KEY (`session_ID`)
) ENGINE=InnoDB

Perl-script only reads the data from this table. The jsp-app Login, which is mentioned in the httpd.conf settings, records there. This application must be deployed on the same tomcat server, as applications Alfresco and Share. The main part of it is the file login.jsp, which is responsible for "recognition" of users and recording sessions to the database.

<%@ page session="true" %>
<%@ page import="java.io.*"  %>
<%@ page import="java.sql.*;" %>
<% 

String remoteUser = request.getRemoteUser();
String returnPath = request.getParameter("r");

Boolean login = false;
Integer error_status = 0;

if (remoteUser != null) {
    
  String driver = "org.gjt.mm.mysql.Driver";
  Class.forName(driver).newInstance();
  
  Connection        con = null;
  ResultSet         rst = null;
  Statement         stmt = null;
  PreparedStatement pstmt = null;
  
  String found_name="";
  String sessionValue="";
  String sql="";
  
  String ip      = request.getRemoteAddr();
  String browser = request.getHeader("User-Agent");  
  
  Cookie cookies[] = request.getCookies();
  
  if (cookies != null) {
    for (int i = 0; i < cookies.length; i++) {
      if (((String)cookies[i].getName()).equals("JSESSIONID")) {
        sessionValue = cookies[i].getValue();
      }
    }
  } 
 
  try {
    String session_url  = "jdbc:mysql://localhost:3306/sessions";
    String alfresco_url = "jdbc:mysql://localhost:3307/alfresco";
    
    con = DriverManager.getConnection(alfresco_url, "root", "root");
    sql = "SELECT authority FROM alf_authority WHERE authority = '" +  remoteUser + "'"; 
    stmt = con.createStatement();
    rst = stmt.executeQuery(sql);
    while(rst.next()){
      found_name = rst.getString(1);
    }
    con.close();
    stmt.close();
    
    if (sessionValue.equals("")) {
      error_status = 1;
    }
    
    if (!(found_name.equals(remoteUser) && !found_name.equals(""))) {
      error_status = 2;
    }

    if (error_status.equals(0)) {
      con=DriverManager.getConnection(session_url, "session_user", "session_password");
      sql = "DELETE FROM session WHERE (session_value = '" + sessionValue + "') OR (username = '" + remoteUser + "')";    
      pstmt = con.prepareStatement(sql);
      pstmt.executeUpdate();    

      sql = "INSERT INTO session SET session_value = '" + sessionValue + "', username = '" + remoteUser + "', session_date = NOW(), unique_value = MD5('" + sessionValue + ip + browser + "')";    
      pstmt = con.prepareStatement(sql);
      pstmt.executeUpdate();
      
      login = true; 
    } else {      
      login = false;
    }    
    
    rst.close();
    pstmt.close();
    con.close();  
  } catch(Exception e){
    System.out.println(e.getMessage());
  }
  
  if (login) {
    if (returnPath != null) {
      response.sendRedirect(returnPath);
    } else {
      response.sendRedirect("/share/page/user/" + remoteUser + "/dashboard");
    }
  } else {
    if (error_status.equals(1)) {
      response.sendRedirect("login.jsp");
    } else {
      response.sendRedirect("could-not-login.jsp");
    }
  }
}
%>

Login.jsp checks if there is a specific user in the database of Alfresco. This is necessary in order to allow only the domain users that are registered in Alfresco. Also, this script writes to the table a unique user sessions data that are necessary for unambiguously identifying: browser, IP, etc.

Each time the Share application is not identified the user, it produces a page with a login and password form. In this page we need to implement a handler that instead of issuing a login form will redirect to our domain login script. To do this, you need to add the line in file slingshot-login.ftl:

<script>
 window.location.href = "/login/login.jsp?r=" + window.location.href;
</script>

The r parameter is needed in order to return the user to the page, that it wanted to refer.

Now that you have installed all the additional subsystems, you must correctly configure Tomcat. In the server.xml file should be registered two connectors with the following parameters:

<Connector port="8080" URIEncoding="UTF-8" protocol="HTTP/1.1" 
           connectionTimeout="20000" 
           redirectPort="8443" tomcatAuthentication="false"
           emptySessionPath="true" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" 
           tomcatAuthentication="false"
           emptySessionPath="true" />

Connector on port 8009 is required for Apache proxy. For security purposes, you must close the ports 8080 and 8009 from outside access, only local server must have access to them. All external requests takes on Apache on port 80. If you do not close the mentioned ports, an attacker could fool the system and log into any desired user.

Parameter tomcatAuthentication = "false" need to turn off the built-in Tomcat authentication mechanisms, because we do not need them. Parameter emptySessionPath = "true" is needed to ensure that the session, formed in one application, will be acted in another. If this option is not set, the session of Share will not operate in the Alfresco application, to say nothing of our Login application. Described procedures for the authorization required to Login application could freely transfer session to Share application.

0 comments:

Post a Comment