TOC
Introduction
Web Services are thought of to be a means to provide easily accessible services over a network. They should be simply usable regardless of the underlying network structure or configuration, operating system, communication mechanism or implementing language.
While there are different possibilities to communicate with a Web Services, SOAP is regarded to be the de-facto standard. SOAP messages are being sent to service endpoints identified by URIs, commonly with the endpoint's service processing some action and sending a SOAP response containing results or error codes. This can simply be SOAP over HTTP or even a SOAP message packed in an e-mail, transferred by SMTP.
To be successful in business scenarios, Web Services have to be suitable for secure communication. Yet the original SOAP specification contains no solutions to solve the security problem. Other techniques as SSL or IPSec provide standard transport security. The problem: a SOAP connection from one endpoint to another can be seen as a logical connection, abstracting from the physical infrastructure beyond. Logically being an end-to-end connection, the physical layer can comprise of diverse intermediaries forwarding SOAP messages. So during this process of receiving and forwarding messages, security information defined on transport level, the way i.e. SSL works, can easily get lost. Thus any recipient had to rely on the security handling of his physical connection point predecessor, as well as its handling of the data integrity and confidentiality. A way out is to specify security information on message level.
Some big players as Microsoft and IBM built a group that dealed with the security problem, finally offering several specifications. The most important, and foundation of the others, is Web Service-Security (WS-Security or WSS).
WS-Security
WS-Security defines SOAP extensions to implement client authentication, message integrity and message confidentiality on the message level.
Thereby it's not the goal of WS-Security to invent new techniques, but to show how to use existing security solutions with SOAP and Web Service communication. It specifies rules for authentication, signatures and encryption mechanisms.
One benefit: WS-Security works in conjunction with other Web Service extensions.
Authentication
Authentication solves questions as "Who is the caller?" and "How does he prove his identity?". If these questions can be answered, it's the recipient's task to clarify the caller is to be trusted.
Authentication specifically prevents:
- Masquerade attacks: Users must prove their identity, so it is more difficult to masquerade as another.
- Replay attacks: When useing timestamps, it is difficult to reuse stolen authentication information.
- Identity interception: When exchanges are additionally encrypted, intercepted identities are useless.
Note that authentication is only half the part of the security task. Once you know who the user is, you have to determine which resources the user is allowed to access. That's what authorization is for.
Integrity
Message integrity ensures the recipient that the data he receives has not been altered during transit. WS-Security tries to ensure integrity using the XML Signature specification, which defines a methodology for cryptographically signing XML. The signatures are defined using a <Signature>
element and accompanying sub-elements as part of a security header.
The signature itself is computed based on the SOAP message content and a security token. The message receiver can check the validity of the message using an according decoding algorithm.
Confidentiality
Message confidentiality is to make the user sure that the data can't be read during transit, by means of message encryption. Here, the XML Encryption specification is the basis to encrypt portions of the SOAP messages. Any portions of SOAP messages, including headers, body blocks, and substructures, may be encrypted.
The encryption is realized using either symmetric keys shared by the sender and the receiver of the message or a key carried in the message in an encrypted form.
Since signatures and message encryption aren't within the scope of this article, please refer to the second part of this series. It will handle both topics in detail.
The <Security> Header
The entry-point to WS-Security is a SOAP header element, called <Security>
. It contains the security-related data and information needed to implement mechanisms like security tokens, signatures or encryption. This element can be present multiple times to enable targeting different receivers (a so called SOAP role). The receiver can either be the ultimate message receiver or an intermediary. The target of a <Security>
header is announced by use of the <role>
element. To target security information for different recipients you must implement these information in different header blocks, each specifying a different<role>
value. It's important that no two headers can have the same <role>
value or omit the <role>
. A header without a <role>
value can be consumed by anyone.
Recognize the fact that no two security headers can use the same role. But an intermediary is not restricted to one security header - he can in fact consume multiple headers.
Below a SOAP message skeleton using the <Security>
header:
<SOAP:Envelope xmlns:SOAP="...">
<SOAP:Header>
<wsse:Security SOAP:role="..." SOAP:mustUnderstand="...">
<wsse:UsernameToken>
..
</wsse:UsernameToken>
...
</wsse:Security>
</SOAP:Header>
<SOAP:Body Id="MsgBody">
<!�- SOAP Body data -->
</SOAP:Body>
</SOAP:Envelope>
So as we can see only the header element of the SOAP message is altered to add WS-Security. All security elements are placed inside the <Security>
element. The body remains as is.
Security Tokens
Identity and it's proof is the fist topic we are interested in. Without question most service providers see the importance to know who is talking to them, and whether to allow the message sender access to their services. And also the other way, the client to be sure of the service provider's authenticity, is of interest. But the latter is not within the scope of this article.
Authentication can be done using security tokens. WS-Security allows us to use any security token we like to use. Explicitly defined are three different options: username/password authentication in case of custom authentication and binary authentication tokens in the form of Kerberos tickets or X.509 certificates. In addition, custom binary security tokens can be applied.
The first option is to rely on custom authentication using username and password validation only. WSS defines an element called <UsernameToken>
which provides support of this purpose.
<UsernameToken>
among other things comprise the following sub-elements:
/Username
username associated with this token
/Password
password for the username associated with this token
/Password/@Type
type of the password provided; 2 pre-defined types:
PasswordText
clear text password
PasswordDigest
digest of the password, base64-encoded SHA1 hash value of the UTF8-encoded password
/Nonce
nonce for the token
/Created
date and time of token creation
The sub-elements above are the elements important for the understanding of this article.
Username/Password Scenarios
There are different ways of using the <UsernameToken>
element, dependent on the way the <Password>
element is used.. The easiest way of identification would be just to convey a username and omit the password. A snippet of an according SOAP message can be seen below.
<UsernameToken>
<Username>MyName</Username>
</UsernameToken>
It's clear that this mechanism alone would be quite insecure. Without any prove of identity, it's only useful in situations where some other authentication mechanism like SSL is used, with the user name only being a basic identification means.
But WS-Security gives us the means to implement "better" security. Conveying the <password>
element as a part of the <UsernameToken>
element, the first approach to (an admittedly weak) secure authentication would be done, since an identity could now be proven. A SOAP snippet would look at follows:
<UsernameToken>
<Username>MyName</Username>
<Password Type="PasswordText">MyPass</Password>
</UsernameToken>
The Type
attribute indicates that the password is given as clear text. Thus, if someone intercepts the message he can easily figure out the password and authenticate himself. To prevent this, the password should ever be sent as a hashed value, making it impossible for an intermediary to see the real password. The hashing is done following the SHA-1 algorithm, then transmitting the hashed password base64-encoded.
<UsernameToken>
<Username>MyName</Username>
<Password Type="PasswordDigest">fm6SuM0RpIIhBQFgmESjdim/yj0=</Password>
</UsernameToken>
Now, the problem shifts from the danger of the password being clearly readable to the case that someone intercepts the message and uses the hashed password in his own message to authenticate himself. In this case it doesn't help that the malicious interceptor doesn't know the user's password itself - he just doesn't need to.
Even here WS-Security provides some additional means to make authentication safer: the password isn't transmitted as the hash value of the clear text, but the hash of a combination of the real text password, a Nonce and the creation time of the security token:
Password_Digest = Base64(SHA-1(Nonce + Created + Password))
A Nonce is a unique, random string that now identifies the password. In case it is correctly used and newly created for every SOAP message sent, no password hashes would be the same.
Taken this, the <UsernameToken>
could look like this:
<UsernameToken>
<Username>MyName</Username>
<Password Type="PasswordDigest">fm6SuM0RpIIhBQFgmESjdim/yj0=</Password>
<Nonce>Pj+EzE2y5ckMDx5ovEvzWw==</Nonce>
<Created>2004-05-11T12:05:16Z</Created>
</UsernameToken>
In this scenario, the client creates the password and transmits the hashed value. The server uses the built-in mechanism to retrieve the password related to the given username, calculates the hash value and compares it to the received one. Access is granted if the hash values are identical.
But just handling the password like explained is not really secure. An evil user could take someone's whole UsernameToken and put it in his own request.
A first approach to prevent this could be to specify a timeout value for the token, thus a request with an expired timestamp wouldn't be accepted by the server. If the sender sets a timestamp of 60 seconds and the server receives the message later then 60 seconds after the given <Created>
value, it simply rejects the whole request. This is easy to implement, but can also have some problems, like expired messages being accepted due to clock synchronisation issues on the server.
Regarding time synchronisation issues, WS-Security provides the <Timestamp>
header. It can be used to express creation and expiration time of a message. This can be very useful for message creation, receipt and processing. As with the <Security>
header, multiple <Timestamp>
elements can be specified and targeted to different roles. The schema outline for the <Timestamp>
element is as follows:
<Timestamp>
<Created>...</Created>
<Expires>...</Expires>
</Timestamp>
Note that the order of the sub-elements is fixed and thus has to be preserved by intermediaries. It's remarkable that while the sub-elements as <Created>
are defined to be used with the <Timestamp>
header, they can be used anywhere within the header or body when time-related markers are needed. Remember that we've already seen the <Created>
element being used within the <UsernameToken>
security token.
A next and probably better method to prevent UsernameToken replay is to keep a history of Nonce values from recently received requests on the server. Requests with a Nonce already used before would not be accepted. It's clear that the Nonce values have to be hold only for the time the request's timestamp propagates, then they can be deleted. In case several messages with similar Nonce values are received, all should be rejected since the interceptor could have delayed the original message and sent his own first.
Even with that many precautions made, one can not be sure to have a secure communication. Still a malicious person could stop a whole message from being delivered and use the UsernameToken to authenticate an own message. One needs to add a digital signature to his message to be secure against such situations, since this ensures the sender's authenticity.
Namespaces required
Notice that the xml snippets above don't have the namespace prefix included.
Important namespaces related to WS-Security and their associated prefixes are:
Prefix |
Namespace |
Some related elements |
wsse |
http://schemas.xmlsoap.org/ws/2002/07/secext |
<Security>, <UsernameToken>, <Username>, <Password>, <Nonce> |
wsu |
http://schemas.xmlsoap.org/ws/2002/07/utility |
<Timestamp>, <Created>, <Expires> |
ds |
http://www.w3.org/2000/09/xmldsig# |
<Signature>, <SignedInfo>, <SignatureValue>, <KeyInfo> |
xenc |
http://www.w3.org/2001/04/xmlenc# |
<EncryptedData>, <CipherData> |
Setting up the WSE environment
The most important class WSE provides for our purposes is Microsoft.Web.Services.SoapContext
. It gives us an interface to process the WS-Security header as well as other headers for incoming SOAP messages and to add WS-Security and other headers for outgoing SOAP messages. A wrapper class adds a SoapContext for the SOAP request and response. On the server side, a SOAP extension, Microsoft.Web.Services.Web ServicesExtension
, validates incoming messages and provides SOAPContext for both request and response accessible from within our WebMethods.
First step is to set up the .NET application to use the WSE SOAP extension. This can be done machine-wide by adding an entry to machine.config, or with a scope restricted to the virtual directory of our service by adding the entry to its Web.config file. We choose the latter way since we don't want WSE support for all our applications. So just add a /configuration/system.web/webServices/soapExtensionTypes/Add element by typing the following lines:
<webServices>
<soapExtensionTypes>
<add type="Microsoft.Web.Services.Web ServicesExtension,
Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" priority="1" group="0"
/>
</soapExtensionTypes>
</webServices>
Note that the type
attribute has to be single-lined, and it's wrapped here for readability purposes.
Implementation
Now we start implementing a simple client authentication scenario. At the end of this article, we'll have a Web Service providing a simple method. This method is to be called by a client who adds WS-Security entries in form of the <security>
header to his SOAP request. On the server side, the header is first gonna be evaluated and the client is granted access if authentication succeeded. Not until then the service' method will be called.
Password Provider
We know that the caller of a service has to add a UsernameToken
to his SOAP request, and the recipient has to process the security token to validate the sender's user name and password. Thus given a user name, there has to be some mechanism to check a password against that user. The WSE provide a mechanism called a Password Provider to process this task.
To register a Password Provider, one needs to create a class implementing the interface Microsoft.Web.Services.Security.IPasswordProvider
. The interface has one function called GetPassword
, taking a Microsoft.Web.Services.Security.UsernameToken
as an input argument. The task of GetPassword
is to return the password related to the user name given in the UsernameToken. It's up to you what mechanism to use to retrieve the password. Most likely it would be a database-related solution.
My example Password Provider uses a very simple username/password association mechanism. Given a user name it searches a database for the associated password being returned by GetPassword
. To have a (nearly) ready-to-go example, I used a simple text file accessed by ODBC as a data source. You'll probably need
to get the sample work. Having installed these packages the ODBC connection should work without problems.
Then create a simple .txt file (I named it pwd.txt) containing your username-password combinations similar to the following schema.
The first line is just the header and can be left out. The entries are separated by tabs, but it's up to you to choose another mechanism('|' for instance). Place the file in a folder accessible from your Password Provider - in my test scenario I simply placed it in the bin directory part of the IIS directory containing our Web Service.
Implementing GetPassword
is all coding work to be done for a Password Provider. So create a new managed C++ project (ok, you can use C#, too) named PwdProvider. Call the namespace WS_Security
and create a class MyPasswordProvider
derived from IPasswordProvider
. Add the function GetPassword
as shown below.
String* GetPassword(UsernameToken __gc* token)
{
if( token == 0 )
throw new ArgumentNullException();
Object *odbcPath = System::Configuration::
ConfigurationSettings::AppSettings->get_Item("odbcPath");
String *connString = String::Format(S"Driver={0};DBQ={1}",
S"{Microsoft Text Driver (*.txt; *.csv)}", odbcPath);
OdbcConnection *conn = new OdbcConnection(connString);
conn->Open();
String *odbcFile = System::Configuration::
ConfigurationSettings::AppSettings->get_Item("odbcFile")->ToString();
String *cmdString = String::Format(S"SELECT Username,
Password FROM {0} WHERE Username='{1}'",
odbcFile, token->Username);
OdbcCommand *cmd = new OdbcCommand(cmdString, conn);
OdbcDataReader *dr = cmd->ExecuteReader(CommandBehavior::CloseConnection);
if( dr->Read() )
return (String*)dr->get_Item(S"Password");
else
throw new ApplicationException("Unable to retrieve password");
}
The only interesting line is
return (String*)dr->get_Item(S"Password");
where the return of the password associated with the given user takes place. The other lines only suite for ODBC access actions to retrieve the password.
Check that the application configuration contains entries called odbcPath
and odbcFile
pointing to the location of your text file. A simple way is to add the values to the Web.config of our Web Service. We'll figure that out later.
A Web Service
Now that we've implemented the Password Provider let's build a simple Web Service.
First inform the WSE about the existence of your Password Provider. This is done by adding some entries to your Web Service application configuration file. The first entry specifies a WSE class that understands the configuration entry announcing the Password Provider. It's placed at /configuration in your Web.config file.
<configSections>
<section name="microsoft.web.services"
type="Microsoft.Web.Services.Configuration.Web ServicesConfiguration,
Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
</configSections>
Again, the type
attribute has to appear at one line. Having introduced the microsoft.web.services
element, the element that announces your Password Provider, /configuration/microsoft.web.services/security/passwordProvider, has to be added:
<microsoft.web.services>
<security>
<passwordProvider type="WS_Security.MyPasswordProvider, PwdProvider" />
</security>
</microsoft.web.services>
The type
attribute of the <passwordProvider>
element tells WSE about your class implementing the Password Provider. In my case, the class is called MyPasswordProvider
. It's defined in the WS_Security
namespace and located in an Assembly called PwdProvider
. Thus, the general form of this attribute is: NAMESPACE.CLASS, ASSEMBLY.
Since we currently deal with configuration issues, lets add a second element to .../microsoft.web.services: the diagnostics element. Especially for test and debugging purposes it's always helpful to have the SOAP messages received and sent by the Web Service logged. Now modify the security element as follows:
<microsoft.web.services>
<security>
<passwordProvider type="WS_Security.MyPasswordProvider, PwdProvider" />
</security>
<diagnostics>
<trace enabled="true" input="inputTrace.config"
output="outputTrace.config" />
</diagnostics>
</microsoft.web.services>
These lines enable tracing of the SOAP messages, where incoming messages are stored in inputTrace.config and outgoing messages in outputTrace.config, both in the folder your Web Service Dll is located in.
The Web Method
Now we add a web method to the service. It's the obligatory HelloWorld
, simply returning a string when called.
String __gc* Class1::HelloWorld()
{
SoapContext* sc = HttpSoapContext::RequestContext;
if( sc == 0 )
throw new ApplicationException(S"Only SOAP-requests allowed!");
bool valid = false;
SecurityToken *st = 0;
IEnumerator *ie = sc->Security->Tokens->GetEnumerator();
while( ie->MoveNext() ) {
st = (SecurityToken *)ie->get_Current();
if( st != 0 && st->GetType()->Equals(__typeof(UsernameToken)) )
{
valid = true;
break;
}
}
if( valid == false )
throw new ApplicationException(S"Invalid or missing security token");
UsernameToken *ut = (UsernameToken*)st;
return ut->Username;
}
More important is what happens inside this method.
SoapContext* sc = HttpSoapContext::RequestContext
if( sc == 0 )
throw new ApplicationException(S"Only SOAP-requests allowed!");
is used to retrieve the context for the SOAP request and to verify that a actually a SOAP message was received. Now we have access to the WS-Security features of the SOAP message.
Next step is to iterate through the security tokens part of the SOAP context. All security tokens associated with the SOAP message are contained in the Tokens collection that is a member of the Security class. This class represents the security header added to the SOAP message. It is accessed through the SoapContext we retrieved:
IEnumerator *ie = sc->Security->Tokens->GetEnumerator();
Our aim is to find a UsernameToken
that contains the user name and password to validate. When found we simply return the sender's username.
Now there's one action to perform left. We still have to add the entries for our ODBC data source to a config file. For simplicity let's just take the Web Service' Web.config file. Insert a <appSettings>
element and add a <add>
element for odbcPath
and odbcFile
:
<configuration>
...
<appSettings>
<add key="odbcPath" value="YOUR_FOLDER_PATH" />
<add key="odbcFile" value="YOUR_FILE" />
</appSettings>
...
</configuration>
Now build the Web Service and let it add to the IIS web publishing folder. Also make PwdProvider.dll available to the service, just by adding is to the bin subfolder of the Web Service.
A Client Application
To finish the scenario let's build a client making use of the user validation implemented on the server-side in form of the Password Provider and our WebService.
In order to call our Web Service, we use the built-in capabilities of Visual Studio to create a proxy hiding the actual communication. This can be easy done by selecting Project->Add Web Reference, which calls wsdl.exe to create a source file for the Web Service proxy. Then wsdl.exe creates a .cs file containing the class that makes all Web Service calls. Here is the point where we have to hook into. Wsdl.exe derives the generated class from System.Web.Services.Protocols.SoapHttpClientProtocol
by default. To be able to use the WSS methods to access the security headers, the proxy class has to inherit Microsoft.Web.Services.WebServicesClientProtocol
. This gives us access to the RequestSoapContext
and ResponseSoapContext
, allowing us to process the WS-Security headers of the SOAP message.
Now create a method to call the Web Service. The main task herein is to create a new UsernameToken
containing our username and password.
UsernameToken *ut = new UsernameToken("Otto",
"Meier", PasswordOption::SendHashed);
These lines create a <UsernameToken>
element as a part of the security header and add the <Username>
and <Password>
fields to it. WSE automatically appends the <Nonce>
and <Created>
elements and computes the password's hash value using the latter three elements.
Afterwards the token is added to the proxy's RequestSoapContext
property.
ws->RequestSoapContext->Security->Tokens->Add(ut);
The example assumes that the WebService class is called SimpleWebService
. Below you can see the method as a whole.
SimpleWebService *ws = new SimpleWebService();
UsernameToken *ut = new UsernameToken("Otto",
"Meier", PasswordOption::SendHashed);
ws->RequestSoapContext->Security->Tokens->Add(ut);
Console::WriteLine(ws->HelloWorld());
Now remember what was said about the security issues (interceptors using the token and authenticating their own messages), and to use the <Created>
element to limit the time a request is valid. As I said, this can be easily implemented - simply add the following line to above code:
ws->RequestSoapContext->Timestamp->Ttl = 60000;
Now if the server receives the message 60 seconds after it was created, the request is automatically rejected by the WSE.
Further Information
If you're interested in this topic and are looking for further information try these sites:
Installing the Examples
To test the examples included, you need an IIS running on your system. Then, build the Visual Studio projects. If configured properly, the Web Service will be automatically added to your IIS' web folder. PwdProvider.dll and Pwd.txt have to be in its bin folder to be found by the WSE. Note that the file WebService.dll needs to reside in the same folder as your WSClient.exe to be found by the Web Service Client. Then, simply start WSClient.exe, and if everything worked as expected "Otto" should be returned by the service.
Perspective
This article dealt with the basic authentication mechanism WS-Security provides, the username/password client authentication. The next part of this article series will go a little further and handle X.509 certificates and digital signing of SOAP messages, as a whole or in parts. Possibly, some other things will also play a role - that depends on my free time during the next days ... or weeks :)