Advanced server-side authentication for data connections, part 3

This is the final segment of my three part series. In the first part of this series I introduced the concepts involved in three tier authentication. Then I covered single sign-on with some code. Now we'll go one step further…

 

Authorization using the Web service proxy

 

InfoPath Forms Services includes a Web service proxy which can forward SOAP requests to a Web service to enable authorization for a data query.  The premise is simple – the proxy runs under an account that is trusted by the Web service.   The Web service authenticates the trusted account.  In addition, the proxy sends the identity of the calling user in the SOAP header using a WS-Security UsernameToken.  The Web service can then use the provided identity to determine what data to return from the query.

Setting up a connection to use the proxy is straightforward.  First of all, the connection has to use settings from a UDC file on the server.   The UseFormsServiceProxy attribute on the ServiceUrl element in the UDC file determines whether InfoPath and Forms Services will forward the Web service request using the proxy:

 

<udc:ServiceUrl UseFormsServiceProxy="true">

http://myserver/proxydemo/service.asmx

</udc:ServiceUrl>

Everything else on this end is automatic.  It's a little more interesting on the Web service end.

It's tempting to consider using the WSE 2.0 library to consume the UsernameToken from the Web service request.  WSE 2.0 has methods to automatically consume WS-Security headers.  However, the default behavior of WSE 2.0 is to attempt to logon using the credentials in the UsernameToken.  It is possible to override the default behavior by specifying an alternate UsernameTokenHandler.  However, there is a more straightforward approach.

First, declare an array of SoapUnknownHeader at class scope in your service:

 

public SoapUnknownHeader[] unknownHeaders;

 

Then declare a SoapHeader attribute for the web method:

 

[WebMethod]

[SoapHeader("unknownHeaders")]

public ExpenseInfo[] GetMyExpenses()

In your web method, look for the UserName element in the header array, which is returned as an XML Document.

 

if (null != unknownHeaders && unknownHeaders.Length > 0)

{

// look for a userNameToken and return the username if present

     try

     {

          strUserName = unknownHeaders[0].Element.CreateNavigator().SelectSingleNode("//*[local-name(.)='Username']").Value;

     }

     catch (Exception)

     {

     }

}

 

In the example, I'm searching for a Username element anywhere in the headers.  To be more precise, the UserName is within a UsernameToken element, which is in turn under a Security header in the WS-Security namespace.  The entire SOAP header sent by the proxy looks like this:

 

<SOAP-ENV:Header.xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">

<wsse:Security.xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis/security-secext-1.0.xsd">

<wsse:UsernameToken>

<wsse:Username>DOMAINusername</wsse:Username>

</wsse:UsernameToken>

</wsse:Security>

</SOAP-ENV:Header>

I've put the code to get the username within a utility function called GetUserName. In the function, I look first for the usernametoken. If this is not present, I get the current Windows user. In either case, I strip off the domain portion of the identifier and return the unadorned username.

private string GetUserName(SoapUnknownHeader[] unknownHeaders)

{

bool fUserFound = false;

string strUserName = string.Empty;

if (null != unknownHeaders && unknownHeaders.Length > 0)

{

// look for a userNameToken and return the username if present

try

{

strUserName = unknownHeaders[0].Element.CreateNavigator().SelectSingleNode("//*[local-name(.)='Username']").Value;

}

catch (Exception)

{

}

if (string.Empty != strUserName)

{

fUserFound = true;

}

}

if (!fUserFound)

{

// return the current Windows identity

strUserName = WindowsIdentity.GetCurrent().Name;

}

// trim off the domain string if present

int nDomainStringIndex = strUserName.IndexOf('\');

if (nDomainStringIndex > 0)

{

strUserName = strUserName.Substring(nDomainStringIndex + 1);

}

return strUserName;

}

In order to use the identity to retrieve data, I use it as part of the query against my database.  Here's the entire Webmethod. The code assumes that you have an Expense database with a table called Expenses, and that one column of the table, called Employee, contains the Windows username of the employee who filed the expense.

 

[WebMethod]

[SoapHeader("unknownHeaders")]

public ExpenseInfo[] GetMyExpenses()

{

ExpenseInfo[] oReturn = null;

DataSet oDS = new DataSet();

string strQuery = string.Format("SELECT ExpenseID, Description, Date, Amount FROM Expenses WHERE Employee = '{0}'", GetUserName(unknownHeaders));

try

{

m_Connection.Open();

m_Adapter = new SqlDataAdapter(strQuery, m_Connection);

m_Adapter.Fill(oDS, "Expenses");

int cRows = oDS.Tables["Expenses"].Rows.Count;

oReturn = new ExpenseInfo[cRows];

for (int nRowIterator = 0; nRowIterator < cRows; nRowIterator++)

{

oRow = oDS.Tables["Expenses"].Rows[nRowIterator];

oReturn[nRowIterator] = new ExpenseInfo();

oReturn[nRowIterator].Amount = oRow["Amount"].ToString();

oReturn[nRowIterator].ExpenseID = oRow["ExpenseID"].ToString();

oReturn[nRowIterator].Date = oRow["Date"].ToString();

oReturn[nRowIterator].Description = oRow["Description"].ToString();

}

}

catch (Exception ex)

{

}

if (m_Connection.State != ConnectionState.Closed)

{

m_Connection.Close();

}

return oReturn;

}

- Nick

Program Manager