Thursday, September 18, 2014

Using Shibboleth Attributes from Javascript


When you gain access to a location in Apache that has been protected using Shibboleth, Shibboleth makes the session's attributes available via Apache environment variables.  These are easy to access using a server-side programming language like PHP.  Javascript is another story.  Javascript, of course, cannot see these variables, because it runs on the client.  Using WSO2's Enterprise Service Bus (wso2esb), however, and a little bit of clever configuration, we can make web service calls from Javascript that make use of the attributes Shibboleth exposes to the Apache web server, even though Javascript can't really see them.

In brief, this is what we're going to do.  We will set up two Shibboleth protected locations in Apache (let's call them /html and /services).  Our /html location will serve the html page containing a Javascript function that will make a web service call.  Our Javascript function won't call the actual web service directly, however, but will rather proxy its call through /services, our second Shibboleth protected Apache location.  Our /services location is configured as a reverse proxy to our WSO2 ESB.  We also make use of Apache's mod_headers' RequestHeader directive to set an HTTP header equal to the value of the Apache environment variable we want to send to the enterprise service bus.  We will use the 'Enrich' mediator on the ESB to grab the value of this header, and insert it into the body of the SOAP request it makes to the final service endpoint.  All clear?

Apache Configuration


Let's start with our Apache configuration.  It looks like this:

RequestHeader set X-EMPID "%{employeeNumber}e"
<location /html>
  AuthType shibboleth  
  ShibRequireSession on
  require valid-user
  ShibUseEnvironment on
  Order allow,deny
  Allow from all
</location>
<location /services>
  AuthType shibboleth
  ShibRequireSession on
  require valid-user
  ShibUseEnvironment on
  ProxyPass http://localhost:8282/services/
  ProxyPassReverse http://localhost:8282/services/
</location>

The RequestHeader directive sets the value of X-EMPID equal to the value of the Apache environment variable employeeNumber, which is something my Shibboleth setup sends along when I authenticate.

The /html location will serve up the html page containing our Javascript.  We'll get to that in a minute.  The /services location is configured as a reverse proxy, pointing at our ESB.  (Note: 8282 is not the standard port on which the WSO2 ESB presents web services.  I'm running multiple WSO2 services on the same server, so I offset the port number.)

Note that both locations are protected by Shibboleth.  Web services are typically accessed non-interactively.  If we removed the Shibboleth directives from the /services location, we could send our SOAP payload using curl, for example.  When the location is protected by Shibboleth, non-interactive access is not possible.  We must have an existing session.

Create a Web Service


So Apache is going to proxy web service calls to an enterprise service bus.  The enterprise service bus, in turn, is going to modify the body of the web service call before itself proxying the request along to the actual service.  Let's skip over the ESB for the moment, and create a Web Service endpoint we can use.  We're going to do this using PostgreSQL and WSO2's Data Services Server (wso2dss).

Create a Database


First we're going to create a simple database.  It's going to contain one table, with a little bit of data, and a stored procedure.  The SQL to create this database looks like this:

CREATE TABLE folks (
  user_id
    VARCHAR(64)
    NOT NULL
    PRIMARY KEY,
  name_first
    VARCHAR(64)
    NOT NULL,
  name_last
    VARCHAR(64)
    NOT NULL
);

CREATE OR REPLACE FUNCTION
get_user ( INOUT userid TEXT,
           OUT lastname TEXT,
           OUT firstname TEXT )
AS $$
DECLARE
BEGIN
    SELECT user_id, name_last, name_first
    INTO userid, lastname, firstname
    FROM folks
    WHERE user_id = userid;
END;
$$
LANGUAGE PLPGSQL;

INSERT INTO
  folks ( user_id, name_first, name_last )
VALUES
  ( '000001', 'road', 'runner' ),
  ( '000002', 'wile', 'coyote' );

If we call save this in a file called 'init.sql', then we can use the following simple bash script to create the database (assuming we've enabled ident auth for the 'postgres' user):

#! /bin/bash

psql -U postgres <<EOF
drop database profile;
drop user profile;
create role profile login password 'passwordhere';
create database profile owner profile;
EOF

psql -U profile -d profile < init.sql

Convert SQL to Web Service with WSO2 Data Services Server


Plop the following configuration (call it profile.dbs) into ${WSO2DSS}/repository/deployment/server/dataservices/, and voila!  You have a web service! 

<data name="profiles">
   <config id="profile">
      <property name="driverClassName">org.postgresql.Driver</property>
      <property name="url">jdbc:postgresql://localhost/profile</property>
      <property name="username">profile</property>
      <property name="password">passwordhere</property>
   </config>

   <query id="get_account_q" useConfig="profile">
     <sql>select userid, lastname, firstname from get_user( ? );</sql>
     <param name="userid" ordinal="1" sqlType="STRING"/>
     <result singleRow="true"
             element="profiles"
             rowname="profile"
             defaultNamespace="http://ws.wso2.org/dataservice">
         <element name="userid" column="userid" xsdType="xs:string"/>
         <element name="lastname" column="lastname" xsdType="xs:string"/>
         <element name="firstname" column="firstname" xsdType="xs:string"/>
     </result>
  </query>
  <operation name="get_account">
     <call-query href="get_account_q">
        <with-param name="userid" query-param="userid"/>
     </call-query>
  </operation>
</data>

I'm not going to explain all of the details of how the WSO2 Data Services Server works here.  The main thing you need to know is how to access this as a web service.  Here's an example using curl.

#! /bin/bash
/usr/bin/curl \
    --verbose \
    --request POST \
    --data @getaccount.soap.xml \
    --header "Content-Type: application/soap+xml; charset=UTF-8; action=\"urn:get_account\"" \
    http://wso2dss.p:9763/services/profiles.SOAP12Endpoint/ \
    | /usr/bin/xmllint --format -

Where getaccount.soap.xml looks like this:

<?xml version="1.0"?>
<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope">
  <soapenv:Body>
    <p:get_account xmlns:p="http://ws.wso2.org/dataservice">
      <xs:userid xmlns:xs="http://ws.wso2.org/dataservice">000001</xs:userid>
    </p:get_account>
  </soapenv:Body>
</soapenv:Envelope>

Of course, your endpoint will have a different URL.  Adjust as required.  You may have noticed I'm calling an http endpoint.  You can set up https easily enough, but let's not get bogged down in TLS/SSL issues for now.

You don't need to use curl to test your web service.  The data services server has its own built-in testing mechanism.  I just thought it would be illustrative to show what the actual call looks like.  The server also has a built-in SOAP tracer, so you can see what the messages look like as they go through.

There's also a very handy utility in the server's /bin directory called 'tcpmon.sh' that you can use to monitor http requests and responses.  Very useful, as it shows header information as well as the SOAP messages.

Configuring the WSO2 Enterprise Service Bus


OK, so now we have a web service.  We also have an Apache configuration that will proxy a web service call from Javascript to the ESB.  We know what the web service call will look like, so let's defer showing how to make the call from Javascript for a moment, and focus on the ESB configuration.  The ESB is going to accept a web service call from Apache.  This call is going to include an HTTP header called X-EMPID.  The ESB is going to update the value of the userid element of our SOAP message with the value of this header, before sending the request along to our data services server.  We can send our message with an empty userid value - the ESB is going to fill it out for us.

Here's what the ESB configuration looks like:

<?xml version="1.0" encoding="UTF-8"?>
<proxy xmlns="http://ws.apache.org/ns/synapse"
       name="getaccount"
       transports="https,http"
       statistics="disable"
       trace="disable"
       startOnLoad="true">
   <target>
      <inSequence>
         <property name="empid"
                   expression="$trp:X-EMPID"
                   scope="default"
                   type="STRING"/>
         <enrich>
            <source type="property" clone="true" property="empid"/>
            <target xmlns:xs="http://ws.wso2.org/dataservice"
                    xpath="//xs:get_account/xs:userid"/>
         </enrich>
      </inSequence>
      <outSequence>
         <property name="messageType"
                   value="application/json"
                   scope="axis2"
                   type="STRING"/>
         <send/>
      </outSequence>
      <endpoint>
         <address uri="http://10.2.8.1:9763/services/profiles"/>
      </endpoint>
   </target>
   <publishWSDL uri="http://10.2.8.1:9763/services/profiles?wsdl2"/>
   <description/>
</proxy>

The in sequence of this proxy service sets a property called 'empid' to the value of the X-EMPID http header.  It gets the value using an xpath expression (the $trp part is a special WSO2 Synapse xpath variable.)  Then the 'enrich' mediator replaces the value of our SOAP request's userid element (referred to using xpath), with the value of this property.  It's easy to find the endpoint addresses for the web service we created in the data services server using the wso2dss administrative console.

One other nicety.  Since we're consuming the results in Javascript, we set the 'messageType' property of our response to 'application/json'.  This (magic!) converts our web service's SOAP response into JSON, which is a lot easier for Javascript to consume than XML.  This is super cool!

Wrap it up with some Javascript


OK, everything is in place.  Now we can create a web page that will call our web service.  Remember, we need to put this under our /html location, protected by Shibboleth.  If we're not already authenticated, we'll be directed to our IdP (possibly via a Discovery Service), and once authenticated, we'll be directed back to the resource we requested.  At that point, our Javascript function will be able to access the Shib protected /services location.  Here's a simple example:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Test AJAX web service call</title>
  <script type="text/javascript" src="/js/vkbeautify/vkbeautifyxml.js"></script>
  <script type="text/javascript" src="/js/vkbeautify/vkbeautify.js"></script>
  <script type="text/javascript">
function prettyPrintXML(xml) {
    var pre = document.createElement('pre');
    var prettyXML = vkbeautifyxml( xml );
    var prettyXML_node = document.createTextNode( prettyXML );
    pre.appendChild(prettyXML_node);
    document.body.appendChild(pre);
}
function prettyPrintJSON(json) {
    var pre = document.createElement('pre');
    var prettyJSON = vkbeautify.json( json );
    var prettyJSON_node = document.createTextNode( prettyJSON );
    pre.appendChild(prettyJSON_node);
    document.body.appendChild(pre);
}
function getProfile() {

// Completely irrelevant what we put here, because ESB is
// going to overwrite our userid value with the value of our
// X-EMPID header.
var userid = document.getElementById('userid').value;

var xmlhttp = new XMLHttpRequest();

xmlhttp.onreadystatechange=function() {
  if (xmlhttp.readyState==4 && xmlhttp.status==200){
    var profileData = xmlhttp.responseText;
    // Print our JSON response
    prettyPrintJSON( profileData );
  }
}

// SOAP request
var sr =
  '<?xml version="1.0"?>' +
  '<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope">' +
    '<soapenv:Body>' +
      '<p:get_account xmlns:p="http://ws.wso2.org/dataservice">' +
        '<xs:userid xmlns:xs="http://ws.wso2.org/dataservice">' + userid + '</xs:userid>' +
      '</p:get_account>' +
    '</soapenv:Body>' +
  '</soapenv:Envelope>';

// Print our SOAP request
prettyPrintXML( sr );

var rh = "application/soap+xml; charset=UTF-8; action=\"urn:get_account\"";
xmlhttp.open('POST','https://www.mydomain.com/services/getaccount.getaccountHttpSoap12Endpoint',true);
xmlhttp.setRequestHeader('Content-Type', rh);

xmlhttp.send( sr );
}
  </script>
</head>
<body>
  <h1>AJAX Proxy Example</h1>
  <form name="getuser" action="javascript:getProfile()">
    User ID: <input type="text" id="userid" value="000001"/>
    <input type="submit" value="Submit"/>
  </form>
</body>
</html>

A couple of functions to pretty print XML and JSON.  Our SOAP request goes into a variable called 'sr'.  We ask for a userid in our little form.  It makes no difference at all what you put here, because the ESB is going to overwrite this value.

The only expectation here is that Shibboleth is going to set an attribute called 'employeeNumber' that will match the value of one of the entries in our SQL table.

Postscript


OK, that's really cool.  Or at least I think so.  So on to the next thing - OpenID Connect.  Seems to be the new hotness in federated authentication.  If I manage to get my head around that, and have anything interesting to say, maybe that will be my next post.

No comments:

Post a Comment