|
The WS-Security
specification , Addendum and related Web Services work is arguably
the most important advancement to Web Services since the formalization
of the SOAP specification. There is no question that SOAP and XML Web
Services has completely changed the business landscape in terms of interoperability
and standards.
However, until recently, there have been some gaping deficiencies in
the web services problem domain, namely, the ability to handle security,
authentication, encryption, digital signature, routing and attachments
at the message level, rather than using out of band or
OS-proprietary techniques. Because of these deficiencies, many organizations
that otherwise would have totally embraced the web services interoperability
concept and business model early on have been holding back. There just
wasn't a standardized way of doing these important things that everybody
could agree to in the same spirit that everyone agreed to the SOAP specification
for web services.
With the finalization of the WS-Security specification, vendors can get
down to the business of providing interfaces and programming toolkits
for their products that all "speak the same language", and developers
can begin to use the benefits of these efforts. Microsoft, after a period
of testing and use of its initial technology release "WSDK",
has now released Web Services Enhancements 1.0 for Microsoft .NET (WSE),
its first supported toolset for implementing security within the SOAP
message. As of December 9, 2002, you can begin using a supported toolkit
for WS-Security. You can download it HERE.
Now a SOAP message, on its own, can be authenticated, its integrity verified,
and it can be encrypted in whole or in part using the mechanisms defined
in WS-Security. The only thing really missing from the Microsoft implementation
is support for Security Assertion Markup Language (SAML), but it does
appear to be working toward some implementation of a portion of the SAML
architecture for .NET Server. Developers are of course free to implement
SAML themselves. The only real deficiency that remains is the fact that
WSDL does not yet have the ability to describe WS-Security
interfaces for clients that wish to consume a WS-Security - compliant
web service. Hopefully, this "missing link" will arrive soon.
The architectural model of WSE is built on a "pipeline" of
filters that process inbound and outbound SOAP messages. This is built
on the pre-existing SOAPExtension classes, and developers who have experimented
with SOAP Extensions for compression, encryption, logging and other purposes
will find the implementation familiar.
One of the biggest impediments to learning to use WSE is the fact that
the MS documentation and accompanying articles are sometimes difficult
to understand, even for advanced developers. In addition, there are only
a few samples implemented by authors at the time of this writing, and
the ones I've seen are very much NOT "user friendly". I've determined
to put an end to this by providing my own sample implementation
that will take you through the process of implementing username/password
authentication with the WSE. You'll be taken step-by-step through the
process and the de-mystification of WSE will be rendered complete with
a downloadable solution that will give you everything you need to be up
and running with a working example in no time. Understanding and using
WSE is not that intimidating; you just have to take it "a slice at
a time". So, let's get started!
WSE provides a Microsoft.Web.Services.SoapContext class
which gives us an interface that allows us to process the WS-Security
SOAP headers and other headers on incoming SOAP messages, and to add WS-Security
and other headers for outbound SOAP messages. A wrapper class adds the
SOAPContext (just like HttpContext) for the SOAP request and response,
and the server uses a SOAPExtension, "Microsoft.Web.Services.WebServicesExtension"
which enables us to validate incoming SOAP messages and provides the request
and response SoapContext which we can access from within
our WebMethods.
Setting up the WSE Environment
To set up the basic WSE environment, we need to configure our ASP.NET
application to use the WSE SOAP Extension. The simplest way to do this
is to add the required /configuration/system.web/webServices/soapExtensionTypes/Add
element to the web.config for your service's virtual directory.
The element should look like the following:
<webServices>
<soapExtensionTypes>
<add type="Microsoft.Web.Services.WebServicesExtension, Microsoft.Web.Services,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
priority="1" group="0" />
</soapExtensionTypes>
</webServices> |
Note that the type attribute must appear on a single line, it's been
wrapped here for readability. As far as the WSE is concerned, we are ready
to start using it. There are a few additional web.config items that I'll
cover shortly. At this point, we only need to add a reference to the Microsoft.Web.Services.dll
in the project. That's normally located at:
C:\Program Files\Microsoft WSE\v1.0.2312\Microsoft.Web.Services.dll
In order to validate a user/password in our web service
using WSE, we need to understand what WSE offers us in this regard. WS-Security
defines a UsernameToken element to provide the means
for basic username/password validation. There are three variations, and
the last two are the ones we are interested in:
<!--
Clear Text Password -->
<UsernameToken>
<Username>Bob</Username>
<Password Type="wsse:PasswordText">Abracadabra</Password>
</UsernameToken>
<!-- Digest: SHA1 hash of base64-encoded
Password -->
<UsernameToken>
<Username>user1</Username>
<Password Type="wsse:PasswordDigest">
QSMAKo67+vzYnU9TcMSqOFXy14U=
</Password>
</UsernameToken>
|
The IPasswordProvider Interface
WSE defines an interface that a class must implement in
order to register as a PasswordProvider. The interface
has one defined method, GetPassword, which accepts a
Microsoft.Web.Services.Security.UsernameToken as its input parameter.
The method returns the password for the given user. The idea is that in
this method, you are free to implement whatever custom authentication
scheme you want to use for the web service. In this example, we'll use
a modified version of the Employees table of the trusty old Northwind
database, to which we have added columns for username and password, to
do our lookup. Since the PasswordProvider interface requires that an actual
password be returned to match the password portion of the UsernameToken
object, we can't use the LogonUser API directly for custom Windows Authentication
without employing the clear text username/password option. Normally this
would simply require that we use WSE to encrypt our username and password
for transmission over the wire to the web service, but anything that has
an XML representation within the <wsse:Security> header as part
of either the Tokens or Elements collection can neither be signed or encrypted
in WSE 1.0. MS says they will support this option in a future release.
So, we would need to have a way to custom encrypt and then custom decrypt
these elements in our GetPassword method -- outside of the WSE framework
-- before calling LogonUser. Certainly an inconvenience; I wish Microsoft
had anticipated the need for this a little better. However, it's not a
concern for now, as our initial objective is to simply get to "First
Base".
WSE Settings Add-In
At this point I think it would be appropriate to introduce
one of the coolest arrivals that's been made part of the WSE: the Visual
Studio.NET WSE Settings add-in, which is a separate download you can find
at the MSDN link above. If you highlight your project in Solution Explorer
and right click on it, you'll see a new context menu addition at the bottom,
"WSE Settings", which brings up a neat tool that allows to to
set all the important configuration and other settings for using WSE:

This enables us to easily set the Password Provider Implementation
class configuration settings elements, Decryption Key Provider elements,
X.509 Certificate settings, and even custom Binary Security Tokens which
we desire to use. In addition, other tabs enable us to configure customized
input and output filters for the WSE pipeline, routing configuration,
and enable Diagnostics, which will write our input and output SOAP messages
for each request/ response to files for examination. It doesn't do everything
we need, but it's certainly a good start to promote ease-of-use for the
WSE!
The PasswordProvider security element is a child of the
parent <configuration> element in web.config, and tells WSE what
class you are using to implement the PasswordProvider interface:
<microsoft.web.services>
<security>
<!-- NAMESPACE
. CLASSNAME , ASSEMBLYNAME -->
<passwordProvider type="WSESecurity.WSEPasswordProvider,
WSESecurity" />
</security>
</microsoft.web.services>
|
So as not to lose our train of thought, let's also take
a look at the code I've implemented for it in this example:
namespace WSESecurity
{
public class WSEPasswordProvider : IPasswordProvider
{
public string GetPassword(UsernameToken token)
{
try
{
SqlConnection cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());
cn.Open();
SqlCommand cmd = new SqlCommand("SELECT Username, password from
Employees where username ='" + token.Username + "'",cn);
SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
dr.Read();
return dr["password"].ToString();
}
catch(Exception ex)
{
throw new Exception (ex.Message);
}
}
}
}
|
Obviously, we can get a lot more sophisticated, but the
above code represents correct working code to fully implement the IPasswordProvider
interface for the authentication of a user via username, password combination.
Note also that there is very little we actually have to do in terms of
writing code, all the plumbing is handled by WSE "under the hood".
Writing a WebMethod that employs WS-Security
Now that we've got all our security plumbing taken care
of, our final portion of the web service is simply to implement whatever
method(s) we want to have in our web service. Here I've implemented a
method that simply runs the CustOrderHist stored procedure in the Northwind
database, accepting a string UserID as the sole parameter, and returns
a DataSet. If the client calling this webservice can be successfully authenticated
based on the message level UsernameToken information it sends, then they
will get back their DataSet. If not, they'll get back an exception telling
them they didn't get authenticated. The beauty of the WSE is that with
only a little bit of effort on your part, all this stuff gets done for
you under the hood, so you can spend most of your development time building
a product instead of building a security mechanism!
[WebMethod]
public DataSet CustOrderHist(string CustId)
{
// Only accept SOAP formatted requests
SoapContext requestContext = HttpSoapContext.RequestContext;
if(requestContext==null)
{
throw new ApplicationException("Non-SOAP request!");
}
// check all the tokens in Tokens Collection for a UsernameToken
bool valid=false;
try
{
foreach(SecurityToken tkn in requestContext.Security.Tokens)
{
if(tkn is UsernameToken)
valid=true;
}
}
catch(Exception ex)
{
throw new Exception( ex.Message + ": " + ex.InnerException.Message);
}
if (valid==false)
throw new ApplicationException("Invalid or Missing Security
Token.");
SqlConnection cn;
SqlDataAdapter da;
DataSet ds;
cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());
cn.Open();
da = new SqlDataAdapter("custorderHist '" +CustId + "'",
cn);
ds = new DataSet();
da.Fill(ds, "CustOrderHist");
return ds;
}
|
With the above WebMethod, and an appropriate connection
string in the appSettings "SqlConn" element, we have completed
a full working implementation of username / password authentication with
WSE on the server side! Your WebMethod will need to have references
to the Microsoft.Web.Services and Microsoft.Web.Services.Security
namespaces. Now, let's complete the picture by building an ASP.NET web
- based client that is capable of sending the required SOAP headers to
be authenticated and successfully call our method.
Building the WSE ASP.NET Client
The WSE provides the Microsoft.Web.Services.WebServicesClientProtocol
class, which derives from the System.Web.Services.Protocols.SoapHttpClientProtocol
class. When you use WSDL.EXE to generate a proxy, or Add
Web Reference in Visual Studio.NET, you'll need to make sure
the proxy class derives from Microsoft.Web.Services.WebServicesClientProtocol
and not the default System.Web.Services.Protocols.SoapHttpClientProtocol
class. This derived class in WSE provides us with the new RequestSoapContext
and ResponseSoapContext properties that are used to access
the WS-Security conformant SOAP headers that our application and client
send and receive. For C# projects, if you have used the Add Web Reference
dialog, you can click the Show All Files button in Solution Explorer:

Show All Files Button
Clicking this button will expose the underlying code file,
Reference.cs, under the Web References node in Solution Explorer and allow
you to edit the file.
In order to create the correct UsernameToken to accompany
(at the message level) our proxy method call to the web service, I use
code like the following:
private void Button1_Click(object
sender, System.EventArgs e)
{
localhost.SecurityServiceWse wse=new localhost.SecurityServiceWse();
UsernameToken tkn = new UsernameToken(txtUsername.Text,txtPassword.Text,PasswordOption.SendHashed);
wse.RequestSoapContext.Security.Tokens.Add (tkn);
try
{
DataSet ds=wse.CustOrderHist(txtCustID.Text);
DataGrid1.DataSource=ds;
DataGrid1.DataBind();
}
catch(Exception ex)
{
DataGrid1.Visible=false;
lblMessages.Text=ex.Message;
}
} |
All we are doing here is taking the values from two TextBox
input fields on the client WebForm, txtUsername and txtPassword, and combining
them with the PasswordOption.SendHashed enum to create a valid UserNameToken
as shown above. When the call is made to the webservice,
the WSE SOAP extension validates the general format of the request, and
then checks the password hash against the retrieved password from our
PasswordProvider method. If it matches, we let the call go through and
the client gets back its Dataset which is then bound to the Page - level
Datagrid.
And Voila! We've now created a complete, working implementation
of SHA1 digest hashed username / password authentication against a database
with WSE! The complete working solution may be downloaded at the link
below. It includes a short SQL script that will add the required username
and password table columns to the Northwind Employees table, and populate
the table with a new record containing the username "user1"
and the password "pass1".
In the next
article in this series, we'll be covering X.509 Certificate authentication
and Digital Signatures of SOAP Messages.
Download
the code that accompanies this article
| |
| | Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform. |
|